智谱 GLM Coding Plan 用量查询 - iOS小组件
myluzh 发布于 阅读:131 VibeCdoing
前言
有GLM订阅的朋友们都知道,智谱官方手册给了一个GLM用量查询的插件,但是每次需要在命令行调用,所以我就寻思做一个苹果小组件,这样直接就可以看到。
GLM 用量统计 Widget
关于
将智谱官方的 GLM 用量查询功能做成 iOS 主屏幕小组件,无需手动查询,实时掌握配额使用情况。
小组件如下图:
主要功能
自动刷新:每分钟自动更新用量数据
多尺寸支持:适配 2×2 和 2×4 小组件
智能配色:
根据用量百分比动态变色
- 🟢 0-30%(绿色)
- 🔵 30-60%(蓝色)
- 🟡 60-80%(黄色)
- 🔴 80%+(红色)
显示内容 :
- Token 使用百分比及具体数值
- MCP Tool 调用次数
- Token 配额重置时间(2×4 显示剩余时间)
- 数据更新时间
使用方法
1、在AppStore下载scriptable
2、在 Scriptable 中导入脚本,配置你的 API Key,添加到主屏幕即可使用
记得把apikey换成你自己的
// GLM 用量 Widget - Scriptable
var CONFIG = {
baseURL: "https://api.z.ai",
apiKey: "xxxxxxxxxxxxxxxx"
};
// 传统 pad 函数,替代 padStart
function padNum(n) {
if (n < 10) {
return "0" + n;
}
return String(n);
}
// 格式化数字
function fmtNum(n) {
if (n >= 1000000) {
return (n / 1000000).toFixed(1) + "M";
} else if (n >= 1000) {
return (n / 1000).toFixed(1) + "K";
}
return String(n);
}
// 获取状态颜色
function getStatusColor(tokenUsage) {
if (tokenUsage < 30) return new Color("#34C759");
if (tokenUsage < 60) return new Color("#32D74B");
if (tokenUsage < 80) return new Color("#FFD60A");
return new Color("#FF453A");
}
async function createWidget(widgetFamily) {
var tokenUsage = 0, tokenUsed = 0, tokenTotal = 0;
var mcpUsage = 0, mcpUsed = 0, mcpTotal = 0;
var resetTime = "未知";
var resetDate = null;
try {
var quotaReq = new Request(CONFIG.baseURL + "/api/monitor/usage/quota/limit");
quotaReq.headers = {
"Authorization": CONFIG.apiKey,
"Content-Type": "application/json"
};
var quotaResp = await quotaReq.loadJSON();
var limits = (quotaResp.data && quotaResp.data.limits) ? quotaResp.data.limits : [];
for (var i = 0; i < limits.length; i++) {
var limit = limits[i];
if (limit.type === "TOKENS_LIMIT") {
tokenUsage = limit.percentage || 0;
tokenUsed = limit.currentValue || 0;
tokenTotal = limit.usage || 0;
resetDate = new Date(limit.nextResetTime);
resetTime = padNum(resetDate.getMonth() + 1) + "/" + padNum(resetDate.getDate()) + " " + padNum(resetDate.getHours()) + ":" + padNum(resetDate.getMinutes());
} else if (limit.type === "TIME_LIMIT") {
mcpUsage = limit.percentage || 0;
mcpUsed = limit.currentValue || 0;
mcpTotal = limit.usage || 0;
}
}
} catch (error) {
console.log("错误: " + error);
}
var widget = new ListWidget();
widget.backgroundColor = new Color("#000000");
var statusColor = getStatusColor(tokenUsage);
var showRemaining = widgetFamily !== "small";
var isWide = widgetFamily !== "small";
// 顶部内边距
widget.addSpacer(0);
// 标题栏
var headerStack = widget.addStack();
headerStack.layoutHorizontally();
var titleText = isWide ? "GLM 用量统计(GLM Coding Plan)" : "GLM 用量统计";
var title = headerStack.addText(titleText);
title.font = Font.boldSystemFont(14);
title.textColor = Color.white();
headerStack.addSpacer();
// 圆形图标
var dot = headerStack.addText("●");
dot.font = Font.systemFont(20);
dot.textColor = statusColor;
widget.addSpacer(10);
// 主内容区:左侧百分比 + 右侧详情
var mainStack = widget.addStack();
mainStack.layoutHorizontally();
// 左侧大百分比
var percentText = mainStack.addText(Math.floor(tokenUsage) + "%");
percentText.font = Font.boldSystemFont(32);
percentText.textColor = statusColor;
mainStack.addSpacer(6);
// 右侧详情:根据尺寸决定布局
if (isWide) {
// 2x4 或更大:Token 和 MCP 各占一行(标签和数值在同一行)
var rightStack = mainStack.addStack();
rightStack.layoutVertically();
// Token 行
var tokenRow = rightStack.addStack();
tokenRow.layoutHorizontally();
var tokenLabel = tokenRow.addText("Token");
tokenLabel.font = Font.systemFont(12);
tokenLabel.textColor = new Color("#8E8E93");
tokenRow.addSpacer(4);
var tokenValue = tokenRow.addText(fmtNum(tokenUsed) + "/" + fmtNum(tokenTotal));
tokenValue.font = Font.systemFont(12);
tokenValue.textColor = Color.white();
rightStack.addSpacer(6);
// MCP 行
var mcpRow = rightStack.addStack();
mcpRow.layoutHorizontally();
var mcpLabel = mcpRow.addText("MCP");
mcpLabel.font = Font.systemFont(12);
mcpLabel.textColor = new Color("#8E8E93");
mcpRow.addSpacer(4);
var mcpValue = mcpRow.addText(mcpUsed + "/" + mcpTotal);
mcpValue.font = Font.systemFont(12);
mcpValue.textColor = Color.white();
} else {
// 2x2:4 行布局
var rightStack = mainStack.addStack();
rightStack.layoutVertically();
// Token 标签
var tokenLabel = rightStack.addText("Token");
tokenLabel.font = Font.systemFont(8);
tokenLabel.textColor = new Color("#8E8E93");
// Token 数值
var tokenValue = rightStack.addText(fmtNum(tokenUsed) + "/" + fmtNum(tokenTotal));
tokenValue.font = Font.systemFont(9);
tokenValue.textColor = Color.white();
rightStack.addSpacer(4);
// MCP 标签
var mcpLabel = rightStack.addText("MCP");
mcpLabel.font = Font.systemFont(8);
mcpLabel.textColor = new Color("#8E8E93");
// MCP 数值
var mcpValue = rightStack.addText(mcpUsed + "/" + mcpTotal);
mcpValue.font = Font.systemFont(9);
mcpValue.textColor = Color.white();
}
widget.addSpacer(10);
// 分隔线(简化版)
widget.addSpacer(8);
// 重置时间
var resetTextStr = "重置:" + resetTime;
if (showRemaining && resetDate) {
var now = new Date();
var diffMs = resetDate - now;
if (diffMs > 0) {
var diffHours = Math.floor(diffMs / (1000 * 60 * 60));
var diffMins = Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60));
if (diffHours > 0) {
var minsText = diffMins > 0 ? diffMins + "m" : "";
resetTextStr += " (剩" + diffHours + "h" + minsText + ")";
} else {
resetTextStr += " (剩" + diffMins + "m)";
}
}
}
var resetText = widget.addText(resetTextStr);
resetText.font = Font.systemFont(9);
resetText.textColor = new Color("#8E8E93");
widget.addSpacer(4);
// 刷新时间
var now = new Date();
var updateTime = "刷新:" + padNum(now.getMonth() + 1) + "/" + padNum(now.getDate()) + " " + padNum(now.getHours()) + ":" + padNum(now.getMinutes());
var updateText = widget.addText(updateTime);
updateText.font = Font.systemFont(9);
updateText.textColor = new Color("#3C3C43");
// 底部内边距
widget.addSpacer(4);
// 设置每分钟刷新
var nextRefresh = new Date(Date.now() + 60 * 1000);
widget.refreshAfterDate = nextRefresh;
return widget;
}
async function run() {
var widgetFamily = (typeof config !== 'undefined' && config.widgetFamily) ? config.widgetFamily : "small";
var widget = await createWidget(widgetFamily);
if (typeof config !== 'undefined' && config.runsInWidget) {
Script.setWidget(widget);
} else {
widget.presentSmall();
}
Script.complete();
}
run();