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/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 ?? '', siteUrl: account.siteUrl ?? '', userId: account.userId ?? '',
10
- authMode: account.authMode ?? 'token', accessToken: '', cookie: '',
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', authMode: 'auth-mode',
147
- accessToken: 'access-token', cookie: 'account-cookie', quotaPerUnit: 'quota-per-unit', unit: 'unit',
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
- get('token-field').hidden = get('auth-mode').value !== 'token' && !editing?.secrets.accessToken;
291
- get('cookie-field').hidden = get('auth-mode').value !== 'cookie' && !editing?.secrets.cookie;
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.accessToken ? '已保存;留空保留原令牌' : '尚未保存系统访问令牌';
306
- get('cookie-state').textContent = account?.secrets.cookie ? '已保存;留空保留原 Cookie' : '尚未保存 Cookie';
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};绑定需点击完成后保存。` : '从 Magpie 当前账号的管理入口可绑定余额与签到。'}`;
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('请填写账号名称、站点地址与正整数用户 ID', true);
738
+ error('请补充或修正标记的字段。', true);
535
739
  markFieldErrors(missing);
536
740
  return;
537
741
  }
package/ui/pro.css CHANGED
@@ -562,6 +562,7 @@ dd {
562
562
 
563
563
  .overview-item {
564
564
  display: flex;
565
+ align-items: flex-start;
565
566
  justify-content: space-between;
566
567
  gap: 15px;
567
568
  padding: 16px 0;
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">保存成功只表示配置已写入文件;正在运行的宿主是否已采用,以实际请求观察到的 generation 为准。</p>
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">在 Magpie 中独立显示此站点(需重载宿主)</label>
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>