Repository navigation
Expand file tree
/
Copy pathSetupUsageWidget.js
More file actions
53 lines (53 loc) · 3.89 KB
/
Copy pathSetupUsageWidget.js
File metadata and controls
53 lines (53 loc) · 3.89 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
// SetupUsageWidget · ScriptableTokenWidgets · MIT
// Paste this entire script into Scriptable, then run it once.
let saved = {};
try { saved = JSON.parse(Keychain.get("usage-bridge-v1")) || {}; } catch {}
const alert = new Alert();
alert.title = "连接你的用量";
alert.message = "先连接 Tailscale。输入 PC 上生成的服务地址与只读令牌。无需填写模型 API Key。";
alert.addTextField("http://你的 Tailscale IP:8799", typeof saved.baseURL === "string" ? saved.baseURL : "");
alert.addSecureTextField("只读访问令牌", typeof saved.token === "string" ? saved.token : "");
alert.addAction("验证并安装"); alert.addCancelAction("取消");
if (await alert.presentAlert() !== -1) {
const baseURL = alert.textFieldValue(0).trim().replace(/\/+$/, "");
const token = alert.textFieldValue(1).trim();
try {
if (!/^https?:\/\/[^/?#\s]+$/i.test(baseURL) || /@/.test(baseURL)) throw new Error("请输入完整服务地址,不含路径或登录信息。");
const host = baseURL.replace(/^https?:\/\//i, "").split(":")[0].toLowerCase();
const parts = host.split(".").map(Number);
const tailnet = parts.length === 4 && parts[0] === 100 && parts[1] >= 64 && parts[1] <= 127 && parts.every(n => Number.isInteger(n) && n >= 0 && n <= 255);
if (/^http:/i.test(baseURL) && !tailnet && host !== "localhost" && host !== "127.0.0.1" && !/\.ts\.net$/.test(host)) {
throw new Error("HTTP 地址必须是 Tailscale 地址或本机回环地址。");
}
if (token.length < 32) throw new Error("令牌不完整,请复制 PC 上生成的完整只读令牌。");
const probe = new Request(baseURL + "/api/v1/overview");
probe.headers = { Authorization: "Bearer " + token }; probe.timeoutInterval = 12;
const data = await probe.loadJSON();
if (probe.response && probe.response.statusCode === 401) throw new Error("只读令牌无效。");
if (data.schemaVersion !== 1) throw new Error("未识别到兼容服务,请检查地址与令牌。");
const download = new Request(baseURL + "/scriptable/UsageWidget.js"); download.timeoutInterval = 12;
const source = await download.loadString();
if (!source.startsWith("// UsageWidget · ScriptableTokenWidgets") || !source.includes("Script.setWidget") || source.length > 100000) throw new Error("组件下载校验失败。");
let fm;
try { fm = FileManager.iCloud(); fm.documentsDirectory(); }
catch { fm = FileManager.local(); }
const target = fm.joinPath(fm.documentsDirectory(), "UsageWidget.js");
if (fm.fileExists(target)) {
if (fm.isFileStoredIniCloud(target)) await fm.downloadFileFromiCloud(target);
if (!fm.readString(target).startsWith("// UsageWidget · ScriptableTokenWidgets")) throw new Error("已有同名脚本,请先重命名原脚本再安装。");
}
fm.writeString(target, source);
Keychain.set("usage-bridge-v1", JSON.stringify({ baseURL, token }));
const done = new Alert();
done.title = "已连接";
done.message = "UsageWidget 已安装。长按桌面添加 Scriptable 中号或大号小组件,选择 UsageWidget。每页最多六个订阅,布局随数量自动调整。\n\n中号和大号参数留空即可;page=2 查看第二页,used 改显已用,可组合 page=2|used。小号填 codex、minimax 或 glm,可加 |used;同一个厂商登录了多个账号时,填 codex@账号名 指定其中一个(如 codex@liuzq),只填 codex 表示该厂商的第一个账号。再次运行本安装器可更新组件。";
done.addAction("预览组件"); done.addCancelAction("完成");
if (await done.presentAlert() !== -1) Safari.open("scriptable:///run?scriptName=UsageWidget");
} catch (error) {
const failed = new Alert();
failed.title = "连接未完成";
failed.message = String(error.message || "请检查 PC 服务和 Tailscale,再重试。");
failed.addAction("知道了"); await failed.presentAlert();
}
}
Script.complete();