opencode-claude-code-ua 0.3.6 → 0.3.9
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.
- package/README.md +94 -90
- package/index.mjs +2 -6
- package/lib/ua-protocol.mjs +8 -0
- package/package.json +1 -1
- package/setup.mjs +1 -1
- package/src/account-usage.mjs +33 -0
- package/src/accounts.mjs +96 -23
- package/src/key-usage.mjs +73 -0
- package/src/newapi.mjs +106 -13
- package/src/plugin.mjs +4 -34
- package/src/scheduler.mjs +2 -2
- package/src/setup-server.mjs +16 -3
- package/src/ua-plugin.mjs +57 -14
- package/src/ua-runtime.mjs +1 -1
- package/src/ua-service.mjs +38 -6
- package/src/ua-store.mjs +4 -1
- package/src/ua-usage.mjs +64 -0
- package/ui/accounts.css +1 -1
- package/ui/accounts.html +21 -11
- package/ui/accounts.js +230 -26
- package/ui/pro.css +1 -0
- package/ui/pro.html +10 -11
- package/ui/pro.js +82 -12
package/ui/accounts.js
CHANGED
|
@@ -4,25 +4,118 @@ export function formatAmount(value, signed = false) {
|
|
|
4
4
|
return signed && value > 0 ? `+${text}` : text;
|
|
5
5
|
}
|
|
6
6
|
|
|
7
|
+
function formatConnectionTime(value) {
|
|
8
|
+
if (!value) return '尚未查询';
|
|
9
|
+
const date = new Date(value);
|
|
10
|
+
return Number.isNaN(date.getTime()) ? '尚未查询' : date.toLocaleString('zh-CN');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Classify a connection reading without ever borrowing site-account totals. */
|
|
14
|
+
export function connectionBalanceSource(result) {
|
|
15
|
+
if (!result) return 'unknown';
|
|
16
|
+
const usage = result.usage && typeof result.usage === 'object' ? result.usage : null;
|
|
17
|
+
if (!usage) return result.error ? 'error' : 'unknown';
|
|
18
|
+
if (typeof usage.error === 'string' && usage.error.trim()) return 'error';
|
|
19
|
+
if (typeof usage.balance === 'string' && usage.balance.trim()) return 'key';
|
|
20
|
+
if (Array.isArray(usage.windows) && usage.windows.some(window => typeof window?.display === 'string' && window.display.trim())) return 'key';
|
|
21
|
+
return 'unknown';
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Format one configured connection row; bindings are shown, never auto-selected. */
|
|
25
|
+
export function connectionBalanceView(connection = {}, result = null, accounts = []) {
|
|
26
|
+
const usage = result?.usage && typeof result.usage === 'object' ? result.usage : null;
|
|
27
|
+
const windows = Array.isArray(usage?.windows) ? usage.windows : [];
|
|
28
|
+
const windowsText = windows
|
|
29
|
+
.map(item => typeof item?.display === 'string' ? item.display.trim() : '')
|
|
30
|
+
.filter(Boolean)
|
|
31
|
+
.join(' ');
|
|
32
|
+
const balance = typeof usage?.balance === 'string' && usage.balance.trim() ? usage.balance.trim() : '余额未知';
|
|
33
|
+
const source = connectionBalanceSource(result);
|
|
34
|
+
const accountId = connection.newApiAccountId ?? '';
|
|
35
|
+
const bound = accountId ? (Array.isArray(accounts) ? accounts : []).find(account => account.id === accountId) : null;
|
|
36
|
+
const boundAccount = !accountId
|
|
37
|
+
? '未绑定签到账号'
|
|
38
|
+
: bound ? `签到账号:${bound.name}` : '绑定的签到账号已失效';
|
|
39
|
+
return {
|
|
40
|
+
name: String(connection.name ?? '').trim() || '未命名连接',
|
|
41
|
+
root: String(connection.baseURL ?? '').trim() || '—',
|
|
42
|
+
balance,
|
|
43
|
+
windows: windowsText,
|
|
44
|
+
source,
|
|
45
|
+
sourceLabel: source === 'key' ? '已配置连接 Key 用量' : source === 'error' ? '查询失败' : '尚未查询',
|
|
46
|
+
boundAccount,
|
|
47
|
+
lastAt: formatConnectionTime(result?.at),
|
|
48
|
+
error: typeof usage?.error === 'string' && usage.error.trim()
|
|
49
|
+
? usage.error
|
|
50
|
+
: typeof result?.error === 'string' ? result.error : '',
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** A reading is valid only for the exact id, Key revision and address it came from. */
|
|
55
|
+
export function connectionResultMatches(connection = {}, result = null) {
|
|
56
|
+
return Boolean(connection && result
|
|
57
|
+
&& result.connectionId === connection.id
|
|
58
|
+
&& result.revision === connection.revision
|
|
59
|
+
&& result.baseURL === connection.baseURL);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Drop readings after a list reload changes the Key revision or address. */
|
|
63
|
+
export function pruneConnectionResults(connections = [], results = {}) {
|
|
64
|
+
const kept = {};
|
|
65
|
+
for (const connection of Array.isArray(connections) ? connections : []) {
|
|
66
|
+
const result = results?.[connection.id];
|
|
67
|
+
if (connectionResultMatches(connection, result)) kept[connection.id] = result;
|
|
68
|
+
}
|
|
69
|
+
return kept;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Explicit same-origin refresh; the network is never touched by the initial list load. */
|
|
73
|
+
export async function requestConnectionUsage(fetcher, connection = {}) {
|
|
74
|
+
let response;
|
|
75
|
+
try {
|
|
76
|
+
response = await fetcher('./api/accounts/connection-usage', {
|
|
77
|
+
method: 'POST',
|
|
78
|
+
headers: { 'Content-Type': 'application/json' },
|
|
79
|
+
body: JSON.stringify({ connectionId: connection.id, confirmNetwork: true }),
|
|
80
|
+
});
|
|
81
|
+
} catch {
|
|
82
|
+
return { ok: false, error: 'Key 用量查询未完成,请检查站点地址与网络' };
|
|
83
|
+
}
|
|
84
|
+
let data = null;
|
|
85
|
+
try { data = await response.json(); } catch { data = null; }
|
|
86
|
+
if (!response.ok) {
|
|
87
|
+
return { ok: false, error: data && typeof data.error === 'string' && data.error ? data.error : 'Key 用量查询未完成' };
|
|
88
|
+
}
|
|
89
|
+
if (!data || typeof data !== 'object' || data.connectionId !== connection.id
|
|
90
|
+
|| !data.usage || typeof data.usage !== 'object') {
|
|
91
|
+
return { ok: false, error: 'Key 用量响应无法确认,请稍后重试' };
|
|
92
|
+
}
|
|
93
|
+
return { ok: true, data };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
|
|
7
97
|
export function accountValues(account = {}) {
|
|
8
98
|
return {
|
|
9
|
-
id: account.id, name: account.name ?? '',
|
|
10
|
-
|
|
99
|
+
id: account.id, name: account.name ?? '', siteType: account.siteType === 'agentrouter' ? 'agentrouter' : 'newapi',
|
|
100
|
+
siteUrl: account.siteUrl ?? '', userId: account.userId ?? '', username: account.username ?? '',
|
|
101
|
+
authMode: account.authMode ?? 'token', accessToken: '', cookie: '', password: '',
|
|
11
102
|
quotaPerUnit: account.quotaPerUnit ?? 500000, unit: account.unit ?? 'USD',
|
|
12
103
|
enabled: account.enabled ?? true, checkinEnabled: account.checkinEnabled ?? true,
|
|
13
104
|
scheduleEnabled: account.schedule?.enabled ?? false,
|
|
14
105
|
scheduleTime: account.schedule?.time ?? '09:00',
|
|
15
106
|
scheduleTimeZone: account.schedule?.timeZone ?? Intl.DateTimeFormat().resolvedOptions().timeZone ?? 'UTC',
|
|
16
|
-
clearAccessToken: false, clearCookie: false,
|
|
107
|
+
clearAccessToken: false, clearCookie: false, clearPassword: false,
|
|
17
108
|
};
|
|
18
109
|
}
|
|
19
110
|
|
|
20
111
|
export function accountPayload(values) {
|
|
21
|
-
const { clearAccessToken, clearCookie, scheduleEnabled = false, scheduleTime = '09:00', scheduleTimeZone = 'UTC', ...raw } = values;
|
|
22
|
-
const account = { ...raw, quotaPerUnit: Number(raw.quotaPerUnit), schedule: { enabled: scheduleEnabled, time: scheduleTime, timeZone: scheduleTimeZone } };
|
|
112
|
+
const { clearAccessToken, clearCookie, clearPassword, scheduleEnabled = false, scheduleTime = '09:00', scheduleTimeZone = 'UTC', ...raw } = values;
|
|
113
|
+
const account = { ...raw, siteType: raw.siteType === 'agentrouter' ? 'agentrouter' : 'newapi', username: String(raw.username ?? '').trim(), quotaPerUnit: Number(raw.quotaPerUnit), schedule: { enabled: scheduleEnabled, time: scheduleTime, timeZone: scheduleTimeZone } };
|
|
114
|
+
if (!account.password) delete account.password;
|
|
23
115
|
const clearSecrets = [];
|
|
24
116
|
if (clearAccessToken) { clearSecrets.push('accessToken'); account.accessToken = ''; }
|
|
25
117
|
if (clearCookie) { clearSecrets.push('cookie'); account.cookie = ''; }
|
|
118
|
+
if (clearPassword) { clearSecrets.push('password'); account.password = ''; }
|
|
26
119
|
if (!account.id) delete account.id;
|
|
27
120
|
return { account, clearSecrets };
|
|
28
121
|
}
|
|
@@ -40,7 +133,7 @@ export function accountSummary(account) {
|
|
|
40
133
|
deltaRequests: snapshot?.deltaRequests == null ? '—' : String(snapshot.deltaRequests),
|
|
41
134
|
state: !account.enabled ? '已停用' : account.status?.stale ? '缓存读数' : snapshot ? '已更新' : '尚未查询',
|
|
42
135
|
error: account.status?.error ?? '',
|
|
43
|
-
checkin: labels[account.checkin?.state] ?? account.checkin?.state ?? '尚未签到',
|
|
136
|
+
checkin: account.checkin?.state === 'unknown' && account.checkin.lastAttemptAt ? '结果未确认' : labels[account.checkin?.state] ?? account.checkin?.state ?? '尚未签到',
|
|
44
137
|
schedule: !account.schedule?.enabled ? '自动签到未开启' : !account.enabled || !account.checkinEnabled
|
|
45
138
|
? '自动签到已暂停(账号或签到已停用)' : `每天 ${account.schedule.time} · ${account.schedule.timeZone}`,
|
|
46
139
|
reward: amount(account.checkin?.reward), reset: snapshot?.reset ?? false,
|
|
@@ -65,8 +158,10 @@ export function normalizedAccountDraft(values = {}, secrets = {}) {
|
|
|
65
158
|
return {
|
|
66
159
|
id: text(values.id),
|
|
67
160
|
name: text(values.name),
|
|
161
|
+
siteType: values.siteType === 'agentrouter' ? 'agentrouter' : 'newapi',
|
|
68
162
|
siteUrl: text(values.siteUrl),
|
|
69
163
|
userId: text(values.userId),
|
|
164
|
+
username: text(values.username),
|
|
70
165
|
authMode: values.authMode === 'cookie' ? 'cookie' : 'token',
|
|
71
166
|
quotaPerUnit: number(values.quotaPerUnit),
|
|
72
167
|
unit: text(values.unit),
|
|
@@ -77,17 +172,38 @@ export function normalizedAccountDraft(values = {}, secrets = {}) {
|
|
|
77
172
|
scheduleTimeZone: text(values.scheduleTimeZone),
|
|
78
173
|
accessToken: secretIntent(values, 'accessToken', 'clearAccessToken', Boolean(secrets.accessToken)),
|
|
79
174
|
cookie: secretIntent(values, 'cookie', 'clearCookie', Boolean(secrets.cookie)),
|
|
175
|
+
password: secretIntent(values, 'password', 'clearPassword', Boolean(secrets?.password)),
|
|
80
176
|
};
|
|
81
177
|
}
|
|
82
178
|
|
|
179
|
+
/** Client-side field validation; the backend repeats it authoritatively. */
|
|
180
|
+
export function accountFieldErrors(values = {}, secrets = {}) {
|
|
181
|
+
const text = (value) => value === null || value === undefined ? '' : String(value).trim();
|
|
182
|
+
const errors = {};
|
|
183
|
+
if (!text(values.name)) errors.name = '请填写账号名称';
|
|
184
|
+
if (!text(values.siteUrl)) errors.siteUrl = '请填写站点地址';
|
|
185
|
+
const siteType = values.siteType === 'agentrouter' ? 'agentrouter' : 'newapi';
|
|
186
|
+
const userId = text(values.userId);
|
|
187
|
+
if (siteType === 'newapi') {
|
|
188
|
+
if (!/^[1-9]\d*$/.test(userId)) errors.userId = '请填写正整数用户 ID';
|
|
189
|
+
} else {
|
|
190
|
+
if (userId && !/^[1-9]\d*$/.test(userId)) errors.userId = '用户 ID 必须为正整数,或留空由登录结果确定';
|
|
191
|
+
if (!text(values.username)) errors.username = '请填写 AgentRouter 登录用户名';
|
|
192
|
+
if (!text(values.password) && !values.clearPassword && !secrets?.password) {
|
|
193
|
+
errors.password = '请填写登录密码;已保存的密码可留空保留';
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return errors;
|
|
197
|
+
}
|
|
198
|
+
|
|
83
199
|
export function accountDraftDirty(baseline, draft) {
|
|
84
200
|
if (!baseline || !draft) return false;
|
|
85
201
|
return JSON.stringify(baseline) !== JSON.stringify(draft);
|
|
86
202
|
}
|
|
87
203
|
|
|
88
|
-
export function bulkEligibility(accounts = []) {
|
|
204
|
+
export function bulkEligibility(accounts = [], connections = []) {
|
|
89
205
|
const list = Array.isArray(accounts) ? accounts : [];
|
|
90
|
-
const refresh = list.some((account) => account?.enabled === true);
|
|
206
|
+
const refresh = list.some((account) => account?.enabled === true) || (Array.isArray(connections) && connections.some(connection => connection?.enabled === true));
|
|
91
207
|
const checkin = list.some((account) => account?.enabled === true && account?.checkinEnabled === true);
|
|
92
208
|
return {
|
|
93
209
|
refresh,
|
|
@@ -141,15 +257,18 @@ export async function initializeAccounts() {
|
|
|
141
257
|
const get = (id) => document.getElementById(id);
|
|
142
258
|
const editor = get('account-editor');
|
|
143
259
|
const history = get('account-history');
|
|
144
|
-
const controls = { name: 'account-name', siteUrl: 'site-url', userId: 'user-id', authMode: 'auth-mode', accessToken: 'access-token', cookie: 'account-cookie', quotaPerUnit: 'quota-per-unit', unit: 'unit', enabled: 'account-enabled', checkinEnabled: 'checkin-enabled', scheduleEnabled: 'schedule-enabled', scheduleTime: 'schedule-time', scheduleTimeZone: 'schedule-timezone', clearAccessToken: 'clear-token', clearCookie: 'clear-cookie' };
|
|
145
|
-
const checks = new Set(['enabled', 'checkinEnabled', 'scheduleEnabled', 'clearAccessToken', 'clearCookie']);
|
|
146
|
-
const fieldIds = { name: 'account-name', siteUrl: 'site-url', userId: 'user-id',
|
|
147
|
-
accessToken: 'access-token', cookie: 'account-cookie',
|
|
260
|
+
const controls = { name: 'account-name', siteType: 'site-type', siteUrl: 'site-url', userId: 'user-id', username: 'agent-username', authMode: 'auth-mode', accessToken: 'access-token', cookie: 'account-cookie', password: 'agent-password', quotaPerUnit: 'quota-per-unit', unit: 'unit', enabled: 'account-enabled', checkinEnabled: 'checkin-enabled', scheduleEnabled: 'schedule-enabled', scheduleTime: 'schedule-time', scheduleTimeZone: 'schedule-timezone', clearAccessToken: 'clear-token', clearCookie: 'clear-cookie', clearPassword: 'clear-password' };
|
|
261
|
+
const checks = new Set(['enabled', 'checkinEnabled', 'scheduleEnabled', 'clearAccessToken', 'clearCookie', 'clearPassword']);
|
|
262
|
+
const fieldIds = { name: 'account-name', siteUrl: 'site-url', userId: 'user-id', username: 'agent-username',
|
|
263
|
+
authMode: 'auth-mode', accessToken: 'access-token', cookie: 'account-cookie', password: 'agent-password',
|
|
264
|
+
quotaPerUnit: 'quota-per-unit', unit: 'unit',
|
|
148
265
|
'schedule.enabled': 'schedule-enabled', 'schedule.time': 'schedule-time', 'schedule.timeZone': 'schedule-timezone' };
|
|
149
266
|
const actionStatus = { refresh: '正在刷新账号余额…', checkin: '正在签到并刷新余额…',
|
|
150
267
|
bind: '正在更新当前连接绑定…', remove: '正在删除账号…' };
|
|
151
268
|
let accounts = [];
|
|
152
269
|
let binding = null;
|
|
270
|
+
let connections = [];
|
|
271
|
+
let connectionResults = {};
|
|
153
272
|
let editing;
|
|
154
273
|
let running = false;
|
|
155
274
|
let unknownOutcome = null;
|
|
@@ -217,10 +336,11 @@ export async function initializeAccounts() {
|
|
|
217
336
|
return window.confirm('有未保存的账号修改。确定放弃修改并继续吗?选择“取消”可返回继续编辑。');
|
|
218
337
|
}
|
|
219
338
|
function syncControls() {
|
|
220
|
-
const bulk = bulkEligibility(accounts);
|
|
339
|
+
const bulk = bulkEligibility(accounts, connections);
|
|
221
340
|
const blocked = running || Boolean(unknownOutcome);
|
|
222
341
|
get('refresh-all').disabled = blocked || loading || !bulk.refresh;
|
|
223
342
|
get('checkin-all').disabled = blocked || loading || !bulk.checkin;
|
|
343
|
+
get('connection-refresh-all').disabled = blocked || loading || connections.length === 0;
|
|
224
344
|
get('account-new').disabled = blocked;
|
|
225
345
|
get('accounts-done').disabled = blocked;
|
|
226
346
|
get('account-save').disabled = blocked;
|
|
@@ -287,8 +407,11 @@ export async function initializeAccounts() {
|
|
|
287
407
|
syncControls();
|
|
288
408
|
}
|
|
289
409
|
function credentialFields() {
|
|
290
|
-
|
|
291
|
-
get('
|
|
410
|
+
const agentRouter = get('site-type').value === 'agentrouter';
|
|
411
|
+
get('agentrouter-fields').hidden = !agentRouter;
|
|
412
|
+
get('user-id').required = !agentRouter;
|
|
413
|
+
get('token-field').hidden = get('auth-mode').value !== 'token' && !editing?.secrets?.accessToken;
|
|
414
|
+
get('cookie-field').hidden = get('auth-mode').value !== 'cookie' && !editing?.secrets?.cookie;
|
|
292
415
|
}
|
|
293
416
|
function scheduleFields() {
|
|
294
417
|
get('schedule-settings').hidden = !get('schedule-enabled').checked;
|
|
@@ -302,8 +425,9 @@ export async function initializeAccounts() {
|
|
|
302
425
|
else get(id).value = values[key];
|
|
303
426
|
}
|
|
304
427
|
get('editor-title').textContent = account ? '编辑站点账号' : '添加站点账号';
|
|
305
|
-
get('token-state').textContent = account?.secrets
|
|
306
|
-
get('cookie-state').textContent = account?.secrets
|
|
428
|
+
get('token-state').textContent = account?.secrets?.accessToken ? '已保存;留空保留原令牌' : '尚未保存系统访问令牌';
|
|
429
|
+
get('cookie-state').textContent = account?.secrets?.cookie ? '已保存;留空保留原 Cookie' : '尚未保存 Cookie';
|
|
430
|
+
get('agent-password-state').textContent = account?.secrets?.password ? '已保存;留空保留原密码' : '尚未保存登录密码';
|
|
307
431
|
error('', true);
|
|
308
432
|
clearFieldErrors();
|
|
309
433
|
credentialFields(); scheduleFields();
|
|
@@ -339,15 +463,81 @@ export async function initializeAccounts() {
|
|
|
339
463
|
return;
|
|
340
464
|
}
|
|
341
465
|
if (!result.ok) error(result.error);
|
|
342
|
-
busy(false);
|
|
343
466
|
await load();
|
|
467
|
+
try {
|
|
468
|
+
if (action === 'refresh' && !id) await refreshConnections();
|
|
469
|
+
} finally {
|
|
470
|
+
busy(false);
|
|
471
|
+
}
|
|
344
472
|
}
|
|
345
473
|
function statusSummary() {
|
|
346
474
|
if (accounts.length === 0) return '尚未添加站点账号。添加后可刷新余额或签到。';
|
|
347
|
-
return `${accounts.length} 个账号,${accounts.filter((account) => account.enabled).length} 个启用。${binding ? `当前连接 ${binding.account};绑定需点击完成后保存。` : '
|
|
475
|
+
return `${accounts.length} 个账号,${accounts.filter((account) => account.enabled).length} 个启用。${binding ? `当前连接 ${binding.account};绑定需点击完成后保存。` : '签到账号绑定请在连接与模型选择中操作。'}`;
|
|
476
|
+
}
|
|
477
|
+
function renderConnections() {
|
|
478
|
+
connectionResults = pruneConnectionResults(connections, connectionResults);
|
|
479
|
+
const list = get('connection-balance-list');
|
|
480
|
+
const empty = connections.length === 0;
|
|
481
|
+
list.hidden = empty;
|
|
482
|
+
get('connection-balance-rows').replaceChildren(...connections.map((connection) => {
|
|
483
|
+
const result = connectionResults[connection.id] ?? null;
|
|
484
|
+
const view = connectionBalanceView(connection, result, accounts);
|
|
485
|
+
const row = element('tr');
|
|
486
|
+
const identity = element('td');
|
|
487
|
+
identity.append(element('strong', view.name), element('div', view.root, 'detail'), element('div', view.boundAccount, 'detail'));
|
|
488
|
+
const balance = element('td');
|
|
489
|
+
balance.append(element('div', view.balance, 'primary-value'), element('div', view.sourceLabel, 'detail'));
|
|
490
|
+
const usage = element('td');
|
|
491
|
+
usage.append(element('div', view.windows || '—', 'detail'));
|
|
492
|
+
if (view.error && !view.windows.includes(view.error)) usage.append(element('div', view.error, 'detail'));
|
|
493
|
+
const last = element('td');
|
|
494
|
+
last.append(element('div', view.lastAt, 'detail'));
|
|
495
|
+
for (const [cell, label] of [[identity, '连接 / 站点'], [balance, 'Key 余额'], [usage, 'Key 用量'], [last, '最近查询']]) cell.dataset.label = label;
|
|
496
|
+
row.append(identity, balance, usage, last);
|
|
497
|
+
return row;
|
|
498
|
+
}));
|
|
499
|
+
if (empty) {
|
|
500
|
+
get('connection-balance-status').textContent = '尚未读取到已配置连接;请先在连接与模型选择中保存连接。';
|
|
501
|
+
} else if (Object.keys(connectionResults).length === 0) {
|
|
502
|
+
get('connection-balance-status').textContent = `已读取 ${connections.length} 个连接,Key 余额尚未查询;请手动刷新。`;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
async function refreshConnections() {
|
|
506
|
+
const targets = connections.slice();
|
|
507
|
+
if (targets.length === 0) {
|
|
508
|
+
get('connection-balance-status').textContent = '尚未读取到已配置连接;没有可刷新的 Key 用量。';
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
get('connection-balance-status').textContent = `正在刷新 ${targets.length} 个连接的 Key 用量…`;
|
|
512
|
+
let completed = 0;
|
|
513
|
+
let failed = 0;
|
|
514
|
+
let discarded = 0;
|
|
515
|
+
for (const target of targets) {
|
|
516
|
+
const current = connections.find((connection) => connection.id === target.id);
|
|
517
|
+
if (!current || current.revision !== target.revision || current.baseURL !== target.baseURL) continue;
|
|
518
|
+
const result = await requestConnectionUsage(fetch.bind(globalThis), current);
|
|
519
|
+
const latest = connections.find((connection) => connection.id === current.id);
|
|
520
|
+
if (!latest || latest.revision !== current.revision || latest.baseURL !== current.baseURL) { discarded++; continue; }
|
|
521
|
+
if (result.ok && result.data.revision !== current.revision) { discarded++; continue; }
|
|
522
|
+
connectionResults[current.id] = result.ok
|
|
523
|
+
? { connectionId: current.id, revision: current.revision, baseURL: current.baseURL, usage: result.data.usage, at: result.data.at || new Date().toISOString() }
|
|
524
|
+
: { connectionId: current.id, revision: current.revision, baseURL: current.baseURL, error: result.error, at: new Date().toISOString() };
|
|
525
|
+
if (result.ok) completed++;
|
|
526
|
+
else failed++;
|
|
527
|
+
renderConnections();
|
|
528
|
+
get('connection-balance-status').textContent = `正在刷新连接 Key 用量…(${completed + failed}/${targets.length})`;
|
|
529
|
+
}
|
|
530
|
+
renderConnections();
|
|
531
|
+
get('connection-balance-status').textContent = discarded
|
|
532
|
+
? `已刷新 ${completed + failed} 个连接;${discarded} 个结果因 Key 或站点已变化而丢弃,请重新读取列表。`
|
|
533
|
+
: failed
|
|
534
|
+
? `已刷新 ${completed + failed} 个连接;${failed} 个查询失败,详见各行站点返回的安全提示。`
|
|
535
|
+
: `已刷新 ${completed} 个连接的 Key 用量;不与站点账号汇总合并。`;
|
|
348
536
|
}
|
|
349
537
|
function render(data) {
|
|
350
538
|
accounts = Array.isArray(data.accounts) ? data.accounts : [];
|
|
539
|
+
connections = Array.isArray(data.connections) ? data.connections : [];
|
|
540
|
+
connectionResults = pruneConnectionResults(connections, connectionResults);
|
|
351
541
|
binding = data.binding ?? null;
|
|
352
542
|
const empty = accounts.length === 0;
|
|
353
543
|
get('account-empty').hidden = !empty;
|
|
@@ -355,7 +545,7 @@ export async function initializeAccounts() {
|
|
|
355
545
|
get('accounts-table').hidden = empty;
|
|
356
546
|
get('accounts-status').textContent = statusSummary();
|
|
357
547
|
get('account-totals').replaceChildren(...Object.entries(data.totals ?? {}).map(([unit, total]) => {
|
|
358
|
-
const node = element('section', undefined, 'unit-total'); node.append(element('h2', `${unit}
|
|
548
|
+
const node = element('section', undefined, 'unit-total'); node.append(element('h2', `${unit} 站点账号汇总`));
|
|
359
549
|
const metrics = element('div', undefined, 'total-values');
|
|
360
550
|
for (const [name, value] of [['剩余额度', total.remaining], ['累计消耗', total.used], ['消耗增量', total.deltaUsed]]) {
|
|
361
551
|
const metric = element('div', undefined, 'total-metric'); metric.append(element('small', name), element('strong', formatAmount(value))); metrics.append(metric);
|
|
@@ -364,7 +554,7 @@ export async function initializeAccounts() {
|
|
|
364
554
|
}));
|
|
365
555
|
get('account-list').replaceChildren(...accounts.map((account) => {
|
|
366
556
|
const row = element('tr'); const view = accountSummary(account);
|
|
367
|
-
const identity = element('td'); identity.append(element('strong', account.name), element('div', `${account.siteUrl} · ID ${account.userId}`, 'detail'), element('span', view.state, `account-state${account.status?.stale ? ' stale' : ''}`));
|
|
557
|
+
const identity = element('td'); identity.append(element('strong', account.name), element('div', `${account.siteType === 'agentrouter' ? 'AgentRouter' : 'New API'} · ${account.siteUrl} · ID ${account.userId || (account.siteType === 'agentrouter' ? '登录后确定' : '—')}`, 'detail'), element('span', view.state, `account-state${account.status?.stale ? ' stale' : ''}`));
|
|
368
558
|
if (view.error) identity.append(element('div', view.error, 'detail'));
|
|
369
559
|
if (data.binding?.selected === account.id) identity.append(element('span', '已选为当前连接', 'account-state'));
|
|
370
560
|
const remaining = element('td'); remaining.append(element('div', view.remaining, 'primary-value'), element('div', view.at, 'detail'));
|
|
@@ -390,6 +580,7 @@ export async function initializeAccounts() {
|
|
|
390
580
|
for (const [cell, label] of [[identity, '账号 / 站点'], [remaining, '剩余额度'], [used, '累计消耗'], [delta, '相较上次成功刷新'], [checkin, '签到'], [actions, '操作']]) cell.dataset.label = label;
|
|
391
581
|
return row;
|
|
392
582
|
}));
|
|
583
|
+
renderConnections();
|
|
393
584
|
syncControls();
|
|
394
585
|
}
|
|
395
586
|
async function load() {
|
|
@@ -434,10 +625,14 @@ export async function initializeAccounts() {
|
|
|
434
625
|
get('accounts-retry').hidden = false;
|
|
435
626
|
if (!hasLoaded) {
|
|
436
627
|
accounts = [];
|
|
628
|
+
connections = [];
|
|
629
|
+
connectionResults = {};
|
|
437
630
|
get('account-empty').hidden = true;
|
|
438
631
|
get('accounts-table-scroll').hidden = true;
|
|
439
632
|
get('accounts-table').hidden = true;
|
|
440
633
|
get('account-totals').replaceChildren();
|
|
634
|
+
renderConnections();
|
|
635
|
+
get('connection-balance-status').textContent = '连接列表尚未读取;请先重新读取账号列表。';
|
|
441
636
|
}
|
|
442
637
|
} finally {
|
|
443
638
|
loading = false;
|
|
@@ -458,6 +653,7 @@ export async function initializeAccounts() {
|
|
|
458
653
|
});
|
|
459
654
|
get('history-close').addEventListener('click', () => history.close());
|
|
460
655
|
get('auth-mode').addEventListener('change', credentialFields);
|
|
656
|
+
get('site-type').addEventListener('change', credentialFields);
|
|
461
657
|
get('schedule-enabled').addEventListener('change', scheduleFields);
|
|
462
658
|
get('accounts-retry').addEventListener('click', () => { if (!running && !loading) void load(); });
|
|
463
659
|
get('editor-recheck').addEventListener('click', () => { if (!running && !loading) void load(); });
|
|
@@ -465,6 +661,17 @@ export async function initializeAccounts() {
|
|
|
465
661
|
get('editor-ack').addEventListener('click', acknowledgeUnknown);
|
|
466
662
|
get('refresh-all').addEventListener('click', () => perform('refresh'));
|
|
467
663
|
get('checkin-all').addEventListener('click', () => perform('checkin'));
|
|
664
|
+
get('connection-refresh-all').addEventListener('click', async () => {
|
|
665
|
+
if (running || unknownOutcome) return;
|
|
666
|
+
busy(true); error('');
|
|
667
|
+
try {
|
|
668
|
+
await refreshConnections();
|
|
669
|
+
} catch {
|
|
670
|
+
error('连接 Key 用量刷新未完成,请稍后重试。');
|
|
671
|
+
} finally {
|
|
672
|
+
busy(false);
|
|
673
|
+
}
|
|
674
|
+
});
|
|
468
675
|
get('account-form').addEventListener('input', (event) => {
|
|
469
676
|
const target = event.target;
|
|
470
677
|
if (target && typeof target.matches === 'function' && target.matches('[aria-invalid]')) {
|
|
@@ -526,12 +733,9 @@ export async function initializeAccounts() {
|
|
|
526
733
|
event.preventDefault();
|
|
527
734
|
if (running || unknownOutcome) return;
|
|
528
735
|
const values = fields();
|
|
529
|
-
const missing = {};
|
|
530
|
-
if (!values.name.trim()) missing.name = true;
|
|
531
|
-
if (!values.siteUrl.trim()) missing.siteUrl = true;
|
|
532
|
-
if (!/^[1-9]\d*$/.test(values.userId)) missing.userId = true;
|
|
736
|
+
const missing = accountFieldErrors(values, editing?.secrets ?? {});
|
|
533
737
|
if (Object.keys(missing).length) {
|
|
534
|
-
error('
|
|
738
|
+
error('请补充或修正标记的字段。', true);
|
|
535
739
|
markFieldErrors(missing);
|
|
536
740
|
return;
|
|
537
741
|
}
|
package/ui/pro.css
CHANGED
package/ui/pro.html
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
<body>
|
|
14
14
|
<div id="pro-shell" class="pro-shell" data-ready="false">
|
|
15
15
|
<aside class="pro-side">
|
|
16
|
-
<a class="pro-brand" href="#overview" aria-label="UA PRO 概览"><span class="pro-mark" aria-hidden="true"><img src="./icon.svg" width="40" height="40" alt=""></span><span class="pro-brand-text"><span class="pro-eyebrow">MAGPIE</span><strong>UA PRO</strong><small>Claude Code · Codex 出站兼容</small></span></a>
|
|
16
|
+
<a class="pro-brand" href="#overview" data-view-link="overview" aria-label="UA PRO 概览"><span class="pro-mark" aria-hidden="true"><img src="./icon.svg" width="40" height="40" alt=""></span><span class="pro-brand-text"><span class="pro-eyebrow">MAGPIE</span><strong>UA PRO</strong><small>Claude Code · Codex 出站兼容</small></span></a>
|
|
17
17
|
<p class="pro-side-copy">配置连接,选择模板,<br>用真实模型验证请求。</p>
|
|
18
18
|
<nav id="pro-nav" class="pro-nav" data-nav aria-label="UA PRO 分区">
|
|
19
19
|
<a href="#overview" data-view-link="overview">概览</a>
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
</nav>
|
|
26
26
|
<dl class="pro-side-meta">
|
|
27
27
|
<div class="pro-local-note"><dt>本地保存</dt><dd>密钥只存本机,不会显示在页面上。</dd></div>
|
|
28
|
+
<div><dt>运行版本</dt><dd id="pro-version">—</dd></div>
|
|
28
29
|
<div><dt>配置 generation</dt><dd id="pro-generation" class="mono">—</dd></div>
|
|
29
30
|
<div><dt>运行</dt><dd id="pro-runtime">读取中</dd></div>
|
|
30
31
|
</dl>
|
|
@@ -33,7 +34,7 @@
|
|
|
33
34
|
<main class="pro-main" id="pro-main">
|
|
34
35
|
<header class="pro-head">
|
|
35
36
|
<div class="pro-head-text"><h1 id="pro-title">概览</h1><p id="pro-status" role="status">正在读取 UA PRO 状态…</p></div>
|
|
36
|
-
<div class="pro-head-actions"><button id="pro-reload" type="button" class="ghost">重新读取</button><button id="pro-close" type="button" class="ghost">完成并关闭</button></div>
|
|
37
|
+
<div class="pro-head-actions"><button id="host-sync" type="button" class="ghost" hidden>同步到宿主</button><button id="pro-reload" type="button" class="ghost">重新读取</button><button id="pro-close" type="button" class="ghost">完成并关闭</button></div>
|
|
37
38
|
</header>
|
|
38
39
|
<div id="pro-error" class="pro-alert" role="alert" tabindex="-1" hidden></div>
|
|
39
40
|
<div id="pro-pending" class="pro-pending" role="status" hidden>
|
|
@@ -49,10 +50,10 @@
|
|
|
49
50
|
<button type="button" class="primary" data-action="connection-new">添加连接</button>
|
|
50
51
|
</div>
|
|
51
52
|
<div class="panel">
|
|
52
|
-
<div class="panel-head"><h2>连接与生效状态</h2><a href="#connections" class="link">管理连接</a></div>
|
|
53
|
+
<div class="panel-head"><h2>连接与生效状态</h2><a id="overview-manage-connections" href="#connections" data-view-link="connections" class="link">管理连接</a></div>
|
|
53
54
|
<div id="overview-connections" class="overview-list"></div>
|
|
54
55
|
</div>
|
|
55
|
-
<p class="note"
|
|
56
|
+
<p class="note">普通连接位于 Magpie 的 UA PRO 供应商账号列表;勾选独立显示后才会有单独供应商。注册状态与实际请求记录分别显示,页面测试不会自动代表宿主已载入。</p>
|
|
56
57
|
</section>
|
|
57
58
|
|
|
58
59
|
<section id="view-connections" data-view="connections" aria-labelledby="pro-title" hidden>
|
|
@@ -104,8 +105,12 @@
|
|
|
104
105
|
<fieldset class="group">
|
|
105
106
|
<legend>站点与绑定</legend>
|
|
106
107
|
<label class="check"><input id="conn-enabled" type="checkbox" checked>启用此连接</label>
|
|
107
|
-
<label class="check"><input id="conn-independent" type="checkbox"
|
|
108
|
+
<label class="check"><input id="conn-independent" type="checkbox">作为独立供应商显示(不勾选时,位于 UA PRO 的账号列表)</label>
|
|
108
109
|
<label class="field" id="conn-site-field">站点 ID<input id="conn-site" maxlength="64" spellcheck="false" placeholder="main"></label>
|
|
110
|
+
<p class="hint">新增独立供应商保存后需重载 UA PRO 插件,再到 Magpie 供应商列表查看。</p>
|
|
111
|
+
<label class="field" id="conn-site-account-field">余额与签到账号<select id="conn-site-account"><option value="">不绑定签到账号(仅查询 Key 用量)</option></select></label>
|
|
112
|
+
<p id="conn-site-account-hint" class="hint">绑定只影响余额与签到,不复制凭据,也不改变模型请求;保存时按站点根地址校验。</p>
|
|
113
|
+
<p class="hint"><a id="conn-accounts-link" href="./accounts">管理签到账号</a>:补 Cookie、用户名或密码等凭据;旧绑定失效时会保留选项并提示重新选择。</p>
|
|
109
114
|
<label class="field" id="conn-binding-field">初始模板绑定<select id="conn-binding"></select></label>
|
|
110
115
|
<p id="conn-binding-note" class="hint" hidden>已有连接的模板绑定只能通过“应用模板”更改,保存连接不会改变它。</p>
|
|
111
116
|
</fieldset>
|
|
@@ -296,12 +301,6 @@
|
|
|
296
301
|
<p class="hint">在独立页面管理站点账号、余额与签到,打开前会确认未保存的草稿。</p>
|
|
297
302
|
<div class="link-row"><a href="./accounts" class="btn ghost" data-leave="accounts">余额与签到</a></div>
|
|
298
303
|
</div>
|
|
299
|
-
<div class="panel wide">
|
|
300
|
-
<h2>迁移旧 Claude 配置</h2>
|
|
301
|
-
<p class="hint">把本机旧 Claude 独立配置复制为 UA PRO 新连接,并绑定 14 头自定义模板。不修改、不停用原配置和原插件;写入前先预览映射。</p>
|
|
302
|
-
<div class="link-row"><button id="migration-preview" type="button" class="ghost">预览迁移</button><button id="migration-apply" type="button" class="primary" hidden>确认迁移</button></div>
|
|
303
|
-
<div id="migration-result" class="migration-result"></div>
|
|
304
|
-
</div>
|
|
305
304
|
<div class="panel wide">
|
|
306
305
|
<h2>本次配置会话</h2>
|
|
307
306
|
<dl id="session-info" class="kv"></dl>
|