claude360 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,69 @@
1
+ import { constants } from "node:fs";
2
+ import { access, chmod, mkdir, readFile, writeFile } from "node:fs/promises";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+
6
+ export function resolveConfigPath({
7
+ platform = process.platform,
8
+ env = process.env,
9
+ homedir = os.homedir,
10
+ } = {}) {
11
+ if (platform === "win32") {
12
+ const home = env.USERPROFILE || homedir();
13
+ return path.win32.join(home, ".claude360", "config.json");
14
+ }
15
+
16
+ return path.posix.join(homedir(), ".claude360", "config.json");
17
+ }
18
+
19
+ export function createConfigStore({ configPath = resolveConfigPath() } = {}) {
20
+ return {
21
+ path: configPath,
22
+ async load() {
23
+ try {
24
+ await access(configPath, constants.F_OK);
25
+ } catch {
26
+ return {};
27
+ }
28
+
29
+ const content = await readFile(configPath, "utf8");
30
+ if (content.trim() === "") {
31
+ return {};
32
+ }
33
+ return JSON.parse(content);
34
+ },
35
+ async save(config) {
36
+ const configDir = path.dirname(configPath);
37
+ await mkdir(configDir, { recursive: true, mode: 0o700 });
38
+ await writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`, {
39
+ encoding: "utf8",
40
+ mode: 0o600,
41
+ });
42
+ if (process.platform !== "win32") {
43
+ await chmod(configDir, 0o700);
44
+ await chmod(configPath, 0o600);
45
+ }
46
+ },
47
+ };
48
+ }
49
+
50
+ export function maskSecret(value) {
51
+ if (!value) {
52
+ return "";
53
+ }
54
+ if (value.length <= 4) {
55
+ return "*".repeat(value.length);
56
+ }
57
+ if (value.length <= 8) {
58
+ return `${value.slice(0, 2)}****${value.slice(-2)}`;
59
+ }
60
+ return `${value.slice(0, 4)}**********${value.slice(-4)}`;
61
+ }
62
+
63
+ export function toDiagnosticConfig(config) {
64
+ return {
65
+ ...config,
66
+ apiKey: maskSecret(config.apiKey),
67
+ cliToken: maskSecret(config.cliToken),
68
+ };
69
+ }
@@ -0,0 +1,206 @@
1
+ import { spawn } from "node:child_process";
2
+ import { access } from "node:fs/promises";
3
+ import { constants } from "node:fs";
4
+ import os from "node:os";
5
+
6
+ export async function runDiagnostics({
7
+ config = {},
8
+ platform = defaultPlatform(),
9
+ execCommand = defaultExecCommand,
10
+ checkPathWritable = defaultCheckPathWritable,
11
+ api,
12
+ } = {}) {
13
+ const node = await commandVersion(execCommand, "node", ["--version"]);
14
+ const npm = await commandVersion(execCommand, "npm", ["--version"]);
15
+ const npmPrefix = await execCommand("npm", ["prefix", "-g"]);
16
+ const globalNpmPermission = npmPrefix.ok
17
+ ? {
18
+ ok: await checkPathWritable(npmPrefix.stdout.trim()),
19
+ detail: npmPrefix.stdout.trim(),
20
+ }
21
+ : { ok: false, detail: npmPrefix.stderr || npmPrefix.error || "npm prefix failed" };
22
+ const claudeCode = await commandVersion(execCommand, "claude", ["--version"]);
23
+ const codex = await commandVersion(execCommand, "codex", ["--version"]);
24
+
25
+ const me = await safeApiGet(api, "/api/cli/me");
26
+ const tokens = await safeApiGet(api, "/api/cli/tokens");
27
+ const topUpOptions = await safeApiGet(api, "/api/cli/topup/options");
28
+
29
+ return {
30
+ platform: {
31
+ os: platform.platform,
32
+ arch: platform.arch,
33
+ terminalQr: detectTerminalQr(platform),
34
+ },
35
+ runtime: {
36
+ node,
37
+ npm,
38
+ globalNpmPermission,
39
+ },
40
+ tools: {
41
+ claudeCode,
42
+ codex,
43
+ },
44
+ api: {
45
+ connectivity: {
46
+ ok: me.ok,
47
+ detail: me.ok ? (config.baseUrl || "connected") : me.detail,
48
+ },
49
+ },
50
+ auth: {
51
+ ok: Boolean(config.cliToken) && me.ok,
52
+ detail: config.cliToken ? "已授权" : "未授权",
53
+ },
54
+ balance: {
55
+ ok: me.ok,
56
+ detail: me.ok ? String(me.data?.quota ?? "-") : me.detail,
57
+ },
58
+ token: buildTokenStatus({ config, tokens }),
59
+ topUp: buildTopUpStatus({ topUpOptions }),
60
+ };
61
+ }
62
+
63
+ function buildTopUpStatus({ topUpOptions }) {
64
+ if (!topUpOptions.ok) {
65
+ return { ok: false, detail: topUpOptions.detail };
66
+ }
67
+ if (!topUpOptions.data?.wechat_enabled) {
68
+ return { ok: false, detail: "微信支付未启用" };
69
+ }
70
+ return { ok: true, detail: "可用" };
71
+ }
72
+
73
+ export function formatDiagnosticsSummary(report) {
74
+ return [
75
+ `OS: ${report.platform.os} ${report.platform.arch}`,
76
+ formatLine("Terminal QR", report.platform.terminalQr, "检查终端是否支持 TTY 输出,无法渲染时复制 code_url 支付"),
77
+ formatLine("Node", report.runtime.node, "安装 Node.js 18+ 后重新运行 claude360", "version"),
78
+ formatLine("npm", report.runtime.npm, "安装 npm 或修复 Node.js 安装", "version"),
79
+ formatLine("Global npm permission", report.runtime.globalNpmPermission, "修复 npm 全局目录权限,或切换到用户级 npm prefix"),
80
+ formatLine("Claude Code", report.tools.claudeCode, "安装或更新 Claude Code", "version"),
81
+ formatLine("Codex", report.tools.codex, "安装或更新 Codex", "version"),
82
+ formatLine("Claude360 API", report.api.connectivity, "检查网络连接和 Claude360 站点可用性"),
83
+ formatLine("Auth", report.auth, "重新执行浏览器授权"),
84
+ formatLine("Balance", report.balance, "授权后重试,或登录网站检查账户状态"),
85
+ formatLine("Token", report.token, "在菜单中切换或创建 API Key"),
86
+ formatLine("WeChat top-up", report.topUp, "检查后端微信支付配置或稍后重试"),
87
+ ].join("\n");
88
+ }
89
+
90
+ function formatLine(label, check, fix, valueKey = "detail") {
91
+ const line = `${label}: ${formatCheck(check, valueKey)}`;
92
+ if (check.ok) {
93
+ return line;
94
+ }
95
+ return `${line} | Fix: ${fix}`;
96
+ }
97
+
98
+ function formatCheck(check, valueKey = "detail") {
99
+ const status = check.ok ? "OK" : "FAIL";
100
+ const value = check[valueKey] || check.detail || "";
101
+ return `${status}${value ? ` ${value}` : ""}`;
102
+ }
103
+
104
+ function defaultPlatform() {
105
+ return {
106
+ platform: process.platform,
107
+ arch: process.arch,
108
+ isTTY: Boolean(process.stdout.isTTY),
109
+ env: process.env,
110
+ };
111
+ }
112
+
113
+ function detectTerminalQr(platform) {
114
+ if (!platform.isTTY) {
115
+ return { ok: false, detail: "stdout is not TTY" };
116
+ }
117
+ const term = platform.env?.TERM || "";
118
+ if (term === "dumb") {
119
+ return { ok: false, detail: "TERM=dumb" };
120
+ }
121
+ return { ok: true, detail: `TTY ${term || "unknown"}` };
122
+ }
123
+
124
+ async function commandVersion(execCommand, command, args) {
125
+ const result = await execCommand(command, args);
126
+ if (!result.ok) {
127
+ return { ok: false, detail: result.stderr || result.error || "not found" };
128
+ }
129
+ return {
130
+ ok: true,
131
+ version: result.stdout.trim(),
132
+ };
133
+ }
134
+
135
+ async function safeApiGet(api, path) {
136
+ if (!api || typeof api.get !== "function") {
137
+ return { ok: false, detail: "API client unavailable" };
138
+ }
139
+ try {
140
+ return { ok: true, data: await api.get(path) };
141
+ } catch (error) {
142
+ return { ok: false, detail: error?.message || String(error) };
143
+ }
144
+ }
145
+
146
+ function buildTokenStatus({ config, tokens }) {
147
+ if (!tokens.ok) {
148
+ return { ok: false, detail: tokens.detail };
149
+ }
150
+ const items = Array.isArray(tokens.data?.items) ? tokens.data.items : [];
151
+ if (items.length === 0) {
152
+ return { ok: false, detail: "无 API Key" };
153
+ }
154
+ const selected = items.find((token) => token.id === config.selectedTokenId) || items[0];
155
+ return { ok: true, detail: selected.name || `Token #${selected.id}` };
156
+ }
157
+
158
+ export function defaultExecCommand(command, args, { timeoutMs = 5000 } = {}) {
159
+ return new Promise((resolve) => {
160
+ let settled = false;
161
+ const child = spawn(command, args, {
162
+ stdio: ["ignore", "pipe", "pipe"],
163
+ });
164
+ const timer = setTimeout(() => {
165
+ if (settled) {
166
+ return;
167
+ }
168
+ settled = true;
169
+ child.kill("SIGTERM");
170
+ resolve({ ok: false, stdout, stderr, error: "timeout" });
171
+ }, timeoutMs);
172
+ let stdout = "";
173
+ let stderr = "";
174
+ child.stdout.on("data", (chunk) => {
175
+ stdout += chunk;
176
+ });
177
+ child.stderr.on("data", (chunk) => {
178
+ stderr += chunk;
179
+ });
180
+ child.on("error", (error) => {
181
+ if (settled) {
182
+ return;
183
+ }
184
+ settled = true;
185
+ clearTimeout(timer);
186
+ resolve({ ok: false, stdout, stderr, error: error.message });
187
+ });
188
+ child.on("close", (code) => {
189
+ if (settled) {
190
+ return;
191
+ }
192
+ settled = true;
193
+ clearTimeout(timer);
194
+ resolve({ ok: code === 0, stdout, stderr });
195
+ });
196
+ });
197
+ }
198
+
199
+ async function defaultCheckPathWritable(path) {
200
+ try {
201
+ await access(path || os.homedir(), constants.W_OK);
202
+ return true;
203
+ } catch {
204
+ return false;
205
+ }
206
+ }
@@ -0,0 +1,36 @@
1
+ export function formatGroupOption(group) {
2
+ const name = group?.name || "";
3
+ const displayName = group?.display_name || name;
4
+ const ratio = Number(group?.ratio);
5
+ const ratioText = Number.isFinite(ratio) ? `(倍率 ${formatRatio(ratio)}x)` : "";
6
+ return `${displayName} / ${name}${ratioText}`;
7
+ }
8
+
9
+ export async function loadGroups(api) {
10
+ const groups = await api.get("/api/cli/groups");
11
+ return Array.isArray(groups) ? groups : [];
12
+ }
13
+
14
+ export async function selectGroup({ groups, promptSelect }) {
15
+ if (!Array.isArray(groups) || groups.length === 0) {
16
+ throw new Error("没有可用分组");
17
+ }
18
+ if (typeof promptSelect !== "function") {
19
+ throw new Error("缺少分组选择输入");
20
+ }
21
+
22
+ const choices = groups.map((group) => ({
23
+ label: formatGroupOption(group),
24
+ value: group.name,
25
+ }));
26
+ const selectedName = await promptSelect("选择 API 分组", choices);
27
+ const selected = groups.find((group) => group.name === selectedName);
28
+ if (!selected) {
29
+ throw new Error("选择的分组无效");
30
+ }
31
+ return selected;
32
+ }
33
+
34
+ function formatRatio(ratio) {
35
+ return Number.isInteger(ratio) ? ratio.toFixed(1) : String(ratio);
36
+ }
package/src/index.js ADDED
@@ -0,0 +1,160 @@
1
+ import readline from "node:readline/promises";
2
+ import { stdin as input, stdout as output } from "node:process";
3
+ import { execFile } from "node:child_process";
4
+
5
+ import { ApiClient } from "./api-client.js";
6
+ import { authenticateWithBrowser } from "./auth.js";
7
+ import { createConfigStore } from "./config-store.js";
8
+ import { formatDiagnosticsSummary, runDiagnostics as collectDiagnostics } from "./diagnostics.js";
9
+ import { runMainMenu } from "./menu.js";
10
+ import { installOrUpdateTools as installTools } from "./tool-installer.js";
11
+ import { launchClaudeCode as startClaudeCode, launchCodex as startCodex } from "./tool-launcher.js";
12
+ import { runWechatTopUp } from "./topup.js";
13
+ import { chooseOrCreateToken, loadBalance } from "./token-manager.js";
14
+
15
+ export async function runCli({
16
+ configStore = createConfigStore(),
17
+ createApiClient = (config) => new ApiClient(config),
18
+ authenticateWithBrowser: authWithBrowser = authenticateWithBrowser,
19
+ chooseOrCreateToken: chooseToken = chooseOrCreateToken,
20
+ promptSelect = defaultPromptSelect,
21
+ promptInput = defaultPromptInput,
22
+ confirm = defaultConfirm,
23
+ openBrowser = defaultOpenBrowser,
24
+ installOrUpdateTools = installTools,
25
+ launchClaudeCode = startClaudeCode,
26
+ launchCodex = startCodex,
27
+ sleep,
28
+ writeLine = console.log,
29
+ } = {}) {
30
+ let config = await configStore.load();
31
+ const baseUrl = config.baseUrl || "https://claude360.xyz";
32
+ let api = createApiClient({ baseUrl, cliToken: config.cliToken || "" });
33
+
34
+ if (!config.cliToken) {
35
+ const cliToken = await authWithBrowser({ api, openBrowser, sleep });
36
+ config = { ...config, baseUrl, cliToken };
37
+ await configStore.save(config);
38
+ api = createApiClient({ baseUrl, cliToken });
39
+ }
40
+
41
+ if (!config.apiKey) {
42
+ const token = await chooseToken({ api, promptSelect, promptInput });
43
+ config = {
44
+ ...config,
45
+ apiKey: token.apiKey,
46
+ selectedTokenId: token.tokenId,
47
+ tokenName: token.tokenName,
48
+ group: token.group,
49
+ };
50
+ await configStore.save(config);
51
+ }
52
+
53
+ return runMainMenu({
54
+ promptSelect,
55
+ config,
56
+ writeLine,
57
+ actions: {
58
+ loadBalance: () => loadBalance(api),
59
+ runWechatTopUp: () => runWechatTopUp({ api, promptSelect, promptInput, writeLine, sleep }),
60
+ launchClaudeCode: () => launchClaudeCode({ config }),
61
+ launchCodex: () => launchCodex({ config, confirmConflict: confirm }),
62
+ installOrUpdateTools: async () => installOrUpdateTools({
63
+ targets: await selectToolTargets(promptSelect),
64
+ confirm,
65
+ }),
66
+ switchToken: async () => {
67
+ const token = await chooseToken({ api, promptSelect, promptInput });
68
+ config = {
69
+ ...config,
70
+ apiKey: token.apiKey,
71
+ selectedTokenId: token.tokenId,
72
+ tokenName: token.tokenName,
73
+ group: token.group,
74
+ };
75
+ await configStore.save(config);
76
+ },
77
+ runDiagnostics: async () => {
78
+ const report = await collectDiagnostics({ config, api });
79
+ writeLine(formatDiagnosticsSummary(report));
80
+ },
81
+ },
82
+ });
83
+ }
84
+
85
+ async function defaultPromptSelect(message, choices) {
86
+ const rl = readline.createInterface({ input, output });
87
+ try {
88
+ writeChoices(message, choices);
89
+ const answer = await rl.question("> ");
90
+ const index = Number(answer) - 1;
91
+ if (Number.isInteger(index) && choices[index]) {
92
+ return choices[index].value;
93
+ }
94
+ const matched = choices.find((choice) => choice.value === answer);
95
+ if (matched) {
96
+ return matched.value;
97
+ }
98
+ throw new Error("选择无效");
99
+ } finally {
100
+ rl.close();
101
+ }
102
+ }
103
+
104
+ async function defaultPromptInput(message, defaultValue = "") {
105
+ const rl = readline.createInterface({ input, output });
106
+ try {
107
+ const suffix = defaultValue ? ` (${defaultValue})` : "";
108
+ const answer = await rl.question(`${message}${suffix}: `);
109
+ return answer.trim() || defaultValue;
110
+ } finally {
111
+ rl.close();
112
+ }
113
+ }
114
+
115
+ async function defaultConfirm(message) {
116
+ const answer = await defaultPromptInput(`${message}\n输入 yes 确认`, "no");
117
+ return ["yes", "y", "确认", "是", "继续"].includes(answer.trim().toLowerCase());
118
+ }
119
+
120
+ function writeChoices(message, choices) {
121
+ console.log(message);
122
+ choices.forEach((choice, index) => {
123
+ console.log(`${index + 1}. ${choice.label}`);
124
+ });
125
+ }
126
+
127
+ async function selectToolTargets(promptSelect) {
128
+ const selected = await promptSelect("选择安装或更新目标", [
129
+ { label: "仅 Claude Code", value: "claude" },
130
+ { label: "仅 Codex", value: "codex" },
131
+ { label: "Claude Code 和 Codex", value: "all" },
132
+ ]);
133
+ if (selected === "claude") {
134
+ return ["claude360", "claude"];
135
+ }
136
+ if (selected === "codex") {
137
+ return ["claude360", "codex"];
138
+ }
139
+ if (selected === "all") {
140
+ return ["claude360", "claude", "codex"];
141
+ }
142
+ throw new Error("安装或更新目标无效");
143
+ }
144
+
145
+ function defaultOpenBrowser(url) {
146
+ const commands = {
147
+ win32: ["cmd", ["/c", "start", "", url]],
148
+ darwin: ["open", [url]],
149
+ linux: ["xdg-open", [url]],
150
+ };
151
+ const [command, args] = commands[process.platform] || commands.linux;
152
+ return new Promise((resolve) => {
153
+ const child = execFile(command, args, { stdio: "ignore" });
154
+ child.on("error", () => {
155
+ console.log(`无法自动打开浏览器,请访问:${url}`);
156
+ resolve();
157
+ });
158
+ child.on("close", () => resolve());
159
+ });
160
+ }
package/src/menu.js ADDED
@@ -0,0 +1,72 @@
1
+ export function createMainMenuChoices() {
2
+ return [
3
+ { label: "查看余额", value: "balance" },
4
+ { label: "微信扫码充值", value: "topup" },
5
+ { label: "启动 Claude Code", value: "launch_claude" },
6
+ { label: "启动 Codex", value: "launch_codex" },
7
+ { label: "安装或更新工具", value: "install_tools" },
8
+ { label: "切换 API Key / 分组", value: "switch_token" },
9
+ { label: "诊断", value: "diagnostics" },
10
+ { label: "退出", value: "exit" },
11
+ ];
12
+ }
13
+
14
+ export async function runMainMenu({
15
+ promptSelect,
16
+ actions = {},
17
+ config = {},
18
+ writeLine = console.log,
19
+ } = {}) {
20
+ if (typeof promptSelect !== "function") {
21
+ throw new Error("缺少菜单选择输入");
22
+ }
23
+
24
+ const selected = await promptSelect("Claude360", createMainMenuChoices());
25
+ switch (selected) {
26
+ case "balance": {
27
+ const balance = await requireAction(actions, "loadBalance")();
28
+ writeLine(formatBalance(balance, config));
29
+ break;
30
+ }
31
+ case "topup":
32
+ await requireAction(actions, "runWechatTopUp")();
33
+ break;
34
+ case "launch_claude":
35
+ await requireAction(actions, "launchClaudeCode")();
36
+ break;
37
+ case "launch_codex":
38
+ await requireAction(actions, "launchCodex")();
39
+ break;
40
+ case "install_tools":
41
+ await requireAction(actions, "installOrUpdateTools")();
42
+ break;
43
+ case "switch_token":
44
+ await requireAction(actions, "switchToken")();
45
+ break;
46
+ case "diagnostics":
47
+ await requireAction(actions, "runDiagnostics")();
48
+ break;
49
+ case "exit":
50
+ break;
51
+ default:
52
+ throw new Error("未知菜单选项");
53
+ }
54
+ return selected;
55
+ }
56
+
57
+ function requireAction(actions, name) {
58
+ if (typeof actions[name] !== "function") {
59
+ throw new Error(`缺少菜单动作:${name}`);
60
+ }
61
+ return actions[name];
62
+ }
63
+
64
+ function formatBalance(balance, config) {
65
+ return [
66
+ `账号:${balance?.username || "-"}`,
67
+ `余额:${balance?.quota ?? "-"} ${balance?.display_unit || ""}`.trim(),
68
+ `已用:${balance?.used_quota ?? "-"}`,
69
+ `当前 Key:${config.tokenName || config.selectedTokenId || "-"}`,
70
+ `当前分组:${config.group || "-"}`,
71
+ ].join("\n");
72
+ }
@@ -0,0 +1,9 @@
1
+ import os from "node:os";
2
+
3
+ export function getPlatform() {
4
+ return process.platform;
5
+ }
6
+
7
+ export function getHomeDir() {
8
+ return os.homedir();
9
+ }
@@ -0,0 +1,99 @@
1
+ import { loadGroups, selectGroup } from "./group-manager.js";
2
+ import { maskSecret } from "./config-store.js";
3
+
4
+ export async function loadBalance(api) {
5
+ return api.get("/api/cli/me");
6
+ }
7
+
8
+ export function formatTokenOption(token) {
9
+ const key = formatTokenKey(token.masked_key || token.key);
10
+ return [
11
+ token.name || `Token #${token.id}`,
12
+ key,
13
+ `group=${token.group || "-"}`,
14
+ `status=${formatStatus(token.status)}`,
15
+ `quota=${formatQuota(token)}`,
16
+ `expires=${formatExpiry(token.expired_time)}`,
17
+ ].filter(Boolean).join(" | ");
18
+ }
19
+
20
+ function formatTokenKey(key) {
21
+ if (!key) {
22
+ return "";
23
+ }
24
+ return key.includes("*") ? key : maskSecret(key);
25
+ }
26
+
27
+ export async function chooseOrCreateToken({
28
+ api,
29
+ promptSelect,
30
+ promptInput = async () => "Claude360 CLI",
31
+ } = {}) {
32
+ if (!api) {
33
+ throw new Error("缺少 API client");
34
+ }
35
+
36
+ const page = await api.get("/api/cli/tokens");
37
+ const tokens = Array.isArray(page?.items) ? page.items : [];
38
+ if (tokens.length > 0) {
39
+ if (typeof promptSelect !== "function") {
40
+ throw new Error("缺少 Token 选择输入");
41
+ }
42
+ const choices = tokens.map((token) => ({
43
+ label: formatTokenOption(token),
44
+ value: token.id,
45
+ }));
46
+ const tokenId = await promptSelect("选择 API Key", choices);
47
+ const selected = tokens.find((token) => token.id === tokenId);
48
+ if (!selected) {
49
+ throw new Error("选择的 API Key 无效");
50
+ }
51
+ const revealed = await api.post(`/api/cli/tokens/${selected.id}/reveal`);
52
+ if (!revealed?.key) {
53
+ throw new Error("未返回 API Key");
54
+ }
55
+ return {
56
+ tokenId: selected.id,
57
+ tokenName: selected.name || `Token #${selected.id}`,
58
+ apiKey: revealed.key,
59
+ group: selected.group,
60
+ created: false,
61
+ };
62
+ }
63
+
64
+ const groups = await loadGroups(api);
65
+ const group = await selectGroup({ groups, promptSelect });
66
+ const name = await promptInput("新 API Key 名称", "Claude360 CLI");
67
+ const created = await api.post("/api/cli/tokens", {
68
+ name,
69
+ group: group.name,
70
+ });
71
+ if (!created?.key) {
72
+ throw new Error("创建 API Key 失败");
73
+ }
74
+ return {
75
+ tokenId: created.id,
76
+ tokenName: created.name || name,
77
+ apiKey: created.key,
78
+ group: created.group || group.name,
79
+ created: true,
80
+ };
81
+ }
82
+
83
+ function formatStatus(status) {
84
+ return status === 1 ? "enabled" : "disabled";
85
+ }
86
+
87
+ function formatQuota(token) {
88
+ if (token.unlimited_quota) {
89
+ return "unlimited";
90
+ }
91
+ return String(token.remain_quota ?? 0);
92
+ }
93
+
94
+ function formatExpiry(expiredTime) {
95
+ if (expiredTime === -1 || expiredTime === 0 || expiredTime === undefined || expiredTime === null) {
96
+ return "never";
97
+ }
98
+ return String(expiredTime);
99
+ }