dsh-llm-codebuddy 1.3.5 → 1.3.6

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/client.js CHANGED
@@ -17,10 +17,103 @@ window.__ModuleLoader__.load({
17
17
  color: "inherit",
18
18
  cursor: "pointer",
19
19
  whiteSpace: "nowrap",
20
+ boxSizing: "border-box",
21
+ font: "inherit",
22
+ fontSize: "13px",
23
+ fontWeight: "500",
24
+ transition: "background-color 120ms ease, border-color 120ms ease, opacity 120ms ease",
20
25
  });
21
26
  return element;
22
27
  }
23
28
 
29
+ function accountPicker() {
30
+ const element = document.createElement("select");
31
+ element.setAttribute("aria-label", "CodeBuddy 令牌账号");
32
+ Object.assign(element.style, {
33
+ minHeight: "44px",
34
+ minWidth: "180px",
35
+ maxWidth: "260px",
36
+ flex: "1 1 220px",
37
+ boxSizing: "border-box",
38
+ padding: "0 10px",
39
+ border: "1px solid var(--dsw-border-subtle, #d0d5dd)",
40
+ borderRadius: "8px",
41
+ background: "var(--dsw-surface-subtle, transparent)",
42
+ color: "inherit",
43
+ font: "inherit",
44
+ fontSize: "13px",
45
+ });
46
+ return element;
47
+ }
48
+
49
+ function textInput(type, placeholder, ariaLabel) {
50
+ const element = document.createElement("input");
51
+ element.type = type;
52
+ element.placeholder = placeholder;
53
+ element.setAttribute("aria-label", ariaLabel);
54
+ element.autocomplete = type === "password" ? "new-password" : "off";
55
+ Object.assign(element.style, {
56
+ minHeight: "44px",
57
+ minWidth: "0",
58
+ flex: "1 1 200px",
59
+ boxSizing: "border-box",
60
+ padding: "0 12px",
61
+ border: "1px solid var(--dsw-border-subtle, #d0d5dd)",
62
+ borderRadius: "8px",
63
+ background: "var(--dsw-surface-subtle, transparent)",
64
+ color: "inherit",
65
+ font: "inherit",
66
+ fontSize: "13px",
67
+ });
68
+ return element;
69
+ }
70
+
71
+ function fieldLabel(text) {
72
+ const element = document.createElement("span");
73
+ element.textContent = text;
74
+ Object.assign(element.style, {
75
+ flex: "0 0 auto",
76
+ minWidth: "92px",
77
+ fontSize: "13px",
78
+ lineHeight: "20px",
79
+ color: "var(--dsw-text-secondary, #667085)",
80
+ });
81
+ return element;
82
+ }
83
+
84
+ function row() {
85
+ const element = document.createElement("div");
86
+ Object.assign(element.style, {
87
+ display: "flex",
88
+ alignItems: "center",
89
+ gap: "8px",
90
+ flexWrap: "wrap",
91
+ width: "100%",
92
+ });
93
+ return element;
94
+ }
95
+
96
+ function section() {
97
+ const element = document.createElement("div");
98
+ Object.assign(element.style, {
99
+ display: "flex",
100
+ flexDirection: "column",
101
+ gap: "8px",
102
+ width: "100%",
103
+ boxSizing: "border-box",
104
+ padding: "10px 12px",
105
+ border: "1px solid var(--dsw-border-subtle, #d0d5dd)",
106
+ borderRadius: "10px",
107
+ background: "var(--dsw-surface-subtle, rgba(0, 0, 0, 0.02))",
108
+ });
109
+ return element;
110
+ }
111
+
112
+ function setVisible(element, visible, display = "flex") {
113
+ element.hidden = !visible;
114
+ element.style.setProperty("display", visible ? display : "none", "important");
115
+ }
116
+
24
117
  function isCodeBuddy(input) {
25
118
  const editor = input.parentElement?.parentElement;
26
119
  if (!editor) return false;
@@ -28,27 +121,122 @@ window.__ModuleLoader__.load({
28
121
  return editor.parentElement?.querySelector('select[aria-label="提供方"]')?.value === "codebuddy-cn";
29
122
  }
30
123
 
31
- function applyMode(input, keyButton, tokenButton, message, status) {
124
+ function accountText(account) {
125
+ const label = account?.accountName || account?.label || account?.account?.displayName || account?.account?.email || account?.account?.userId || "未命名账号";
126
+ const userId = account?.userId || account?.account?.userId;
127
+ return userId && userId !== label ? `${label} · ${String(userId).slice(0, 8)}` : label;
128
+ }
129
+
130
+ function accountLabel(account) {
131
+ return account?.accountName || account?.label || account?.account?.displayName || account?.account?.email || account?.account?.userId || "未命名账号";
132
+ }
133
+
134
+ function apiKeyText(key) {
135
+ const label = key?.label || (key?.kind === "environment" ? `环境变量 ${key.ref}` : "DSH 保存的 API Key");
136
+ const suffix = key?.masked ? ` · ${key.masked}` : "";
137
+ return key?.configured === false ? `${label}(不可用)` : `${label}${suffix}`;
138
+ }
139
+
140
+ function apiKeyLabel(key) {
141
+ return key?.label || (key?.kind === "environment" ? `环境变量 ${key.ref}` : "DSH 保存的 API Key");
142
+ }
143
+
144
+ function formatCredits(value) {
145
+ const number = Number(value);
146
+ return Number.isFinite(number) ? number.toLocaleString("zh-CN", { maximumFractionDigits: 2 }) : "—";
147
+ }
148
+
149
+ function applyCreditStatus(stats, status, activeAccount) {
150
+ const visible = status.mode === "token" && Boolean(activeAccount);
151
+ setVisible(stats, visible);
152
+ if (!visible) return;
153
+ const credit = stats.querySelector('[data-codebuddy-stat="credits"]');
154
+ const usage = stats.querySelector('[data-codebuddy-stat="usage"]');
155
+ credit.title = status.creditError || "";
156
+ usage.title = status.todayUsageError || "";
157
+ if (status.creditLoading) credit.textContent = "剩余积分:读取中…";
158
+ else if (status.unlimited) credit.textContent = "剩余积分:不限量";
159
+ else if (typeof status.credits === "number" && Number.isFinite(status.credits)) credit.textContent = `剩余积分:${formatCredits(status.credits)}`;
160
+ else credit.textContent = status.creditError ? "剩余积分:暂不可用" : "剩余积分:—";
161
+ const today = status.todayUsage;
162
+ if (today && today.synced === true) {
163
+ usage.textContent = `今日请求:${Number.isFinite(Number(today.count)) ? Number(today.count) : 0} 次 · 用量 ${formatCredits(today.used)} 积分`;
164
+ } else {
165
+ usage.textContent = status.creditLoading ? "今日请求:读取中…" : status.todayUsageError ? "今日请求:暂不可用" : "今日请求:—";
166
+ }
167
+ }
168
+
169
+ function applyMode(input, keyButton, tokenButton, keySection, keySourceRow, keySelect, keyHint, newKeyInput, newKeyLabelInput, saveKeyButton, removeKeyButton, tokenSection, accountRow, accountSelect, accountName, addButton, removeButton, accountStats, message, status) {
32
170
  const token = status.mode === "token";
171
+ const apiKeys = Array.isArray(status.apiKeys) ? status.apiKeys : [];
172
+ const activeApiKeyId = status.activeApiKeyId ?? apiKeys[0]?.id;
173
+ const activeApiKey = apiKeys.find((key) => key.id === activeApiKeyId);
174
+ const accounts = Array.isArray(status.accounts) ? status.accounts : [];
175
+ const activeAccountId = status.activeAccountId ?? accounts[0]?.id;
33
176
  input.disabled = token;
34
- input.placeholder = token ? "当前使用 CodeBuddy 账号令牌" : input.dataset.codebuddyPlaceholder || "输入 API 密钥";
177
+ input.placeholder = token ? "当前使用 CodeBuddy 账号令牌" : "输入新的 API Key(保存到 DSH)";
178
+ newKeyInput.disabled = token;
179
+ newKeyLabelInput.disabled = token;
35
180
  keyButton.setAttribute("aria-pressed", String(!token));
36
181
  tokenButton.setAttribute("aria-pressed", String(token));
37
182
  keyButton.style.background = !token ? "var(--dsw-accent-subtle, #eef4ff)" : "var(--dsw-surface-subtle, transparent)";
38
183
  tokenButton.style.background = token ? "var(--dsw-accent-subtle, #eef4ff)" : "var(--dsw-surface-subtle, transparent)";
39
- message.textContent = token ? status.authenticated ? "令牌已登录" : "令牌缺失,请重新登录" : "";
40
- message.style.color = token && !status.authenticated ? "var(--dsw-text-danger, #c62828)" : "var(--dsw-text-success, #2e7d32)";
184
+ setVisible(keySection, !token);
185
+ setVisible(keySourceRow, apiKeys.length > 0);
186
+ keySelect.replaceChildren(...apiKeys.map((key) => {
187
+ const option = document.createElement("option");
188
+ option.value = key.id;
189
+ option.textContent = apiKeyText(key);
190
+ option.title = apiKeyLabel(key);
191
+ return option;
192
+ }));
193
+ if (activeApiKeyId) keySelect.value = activeApiKeyId;
194
+ keyHint.textContent = token
195
+ ? ""
196
+ : activeApiKey ? `${status.apiKeyConfigured ? "当前来源" : "可选来源"}:${apiKeyText(activeApiKey)}` : "未检测到可用 API Key,可输入新 Key 保存";
197
+ keyHint.title = activeApiKey ? apiKeyLabel(activeApiKey) : "";
198
+ saveKeyButton.textContent = "添加并使用";
199
+ removeKeyButton.hidden = !activeApiKey || activeApiKey.kind !== "dsh";
200
+ setVisible(tokenSection, token);
201
+ tokenButton.textContent = token ? "令牌模式" : "令牌登录";
202
+ addButton.textContent = accounts.length ? "添加账号" : "登录 CodeBuddy";
203
+ setVisible(accountRow, token && accounts.length > 0);
204
+ accountSelect.replaceChildren(...accounts.map((account) => {
205
+ const option = document.createElement("option");
206
+ option.value = account.id;
207
+ option.textContent = accountText(account);
208
+ option.title = accountLabel(account);
209
+ return option;
210
+ }));
211
+ if (activeAccountId) accountSelect.value = activeAccountId;
212
+ const activeAccount = accounts.find((account) => account.id === activeAccountId);
213
+ accountName.textContent = activeAccount ? `当前账号:${accountText(activeAccount)}` : "";
214
+ accountName.title = activeAccount ? accountLabel(activeAccount) : "";
215
+ removeButton.hidden = !token || !activeAccount;
216
+ setVisible(accountStats, token && Boolean(activeAccount));
217
+ applyCreditStatus(accountStats, status, activeAccount);
218
+ message.textContent = token
219
+ ? status.authenticated ? `令牌已登录:${activeAccount ? accountText(activeAccount) : "当前账号"}` : "令牌缺失,请重新登录"
220
+ : status.apiKeyConfigured ? `API Key 已就绪:${activeApiKey ? apiKeyText(activeApiKey) : "当前来源"}` : "未配置 API Key,请输入后保存";
221
+ message.style.color = token
222
+ ? status.authenticated ? "var(--dsw-text-success, #2e7d32)" : "var(--dsw-text-danger, #c62828)"
223
+ : status.apiKeyConfigured ? "var(--dsw-text-success, #2e7d32)" : "var(--dsw-text-danger, #c62828)";
41
224
  }
42
225
 
43
- async function request(path) {
44
- const response = await fetch(`${ROUTE}/${path}`, { method: "POST" });
45
- const body = await response.json();
46
- if (!response.ok || !body.ok) {
47
- const error = new Error(body.message || `请求失败(${response.status})`);
226
+ async function request(path, body) {
227
+ const options = { method: "POST" };
228
+ if (body !== undefined) {
229
+ options.headers = { "content-type": "application/json" };
230
+ options.body = JSON.stringify(body);
231
+ }
232
+ const response = await fetch(`${ROUTE}/${path}`, options);
233
+ const result = await response.json();
234
+ if (!response.ok || !result.ok) {
235
+ const error = new Error(result.message || `请求失败(${response.status})`);
48
236
  error.status = response.status;
49
237
  throw error;
50
238
  }
51
- return body;
239
+ return result;
52
240
  }
53
241
 
54
242
  function mount(input) {
@@ -56,37 +244,152 @@ window.__ModuleLoader__.load({
56
244
  if (!field || field.querySelector(`[${MARKER}]`)) return;
57
245
  field.setAttribute("data-codebuddy-auth-field", "");
58
246
  input.dataset.codebuddyPlaceholder = input.placeholder;
247
+ for (const nativeNode of field.children) nativeNode.hidden = true;
248
+ Object.assign(field.style, { display: "block", width: "100%", boxSizing: "border-box" });
59
249
  const controls = document.createElement("div");
60
250
  controls.setAttribute(MARKER, "");
61
251
  controls.setAttribute("role", "group");
62
252
  controls.setAttribute("aria-label", "CodeBuddy 认证方式");
63
- Object.assign(controls.style, { display: "flex", alignItems: "center", gap: "8px", flexWrap: "wrap" });
253
+ Object.assign(controls.style, {
254
+ display: "flex",
255
+ flexDirection: "column",
256
+ alignItems: "stretch",
257
+ gap: "8px",
258
+ width: "100%",
259
+ maxWidth: "100%",
260
+ paddingTop: "4px",
261
+ boxSizing: "border-box",
262
+ color: "var(--dsw-text-primary, inherit)",
263
+ });
264
+ const modeRow = row();
265
+ const keySection = section();
266
+ const keySourceRow = row();
267
+ const keyAddRow = row();
268
+ const tokenSection = section();
269
+ const accountRow = row();
270
+ const tokenActionRow = row();
64
271
  const keyButton = button("API Key");
65
272
  const tokenButton = button("令牌登录");
273
+ const keySelect = accountPicker();
274
+ keySelect.setAttribute("aria-label", "CodeBuddy API Key 来源");
275
+ const keySourceLabel = fieldLabel("当前 API Key");
276
+ const newKeyLabel = fieldLabel("新增 API Key");
277
+ const newKeyInput = textInput("password", "粘贴新的 API Key", "新增 CodeBuddy API Key");
278
+ const newKeyLabelInput = textInput("text", "名称(可选)", "API Key 名称");
279
+ const keyHint = document.createElement("span");
280
+ Object.assign(keyHint.style, {
281
+ display: "block",
282
+ width: "100%",
283
+ minWidth: "0",
284
+ overflowWrap: "anywhere",
285
+ whiteSpace: "normal",
286
+ fontSize: "12px",
287
+ lineHeight: "18px",
288
+ color: "var(--dsw-text-secondary, #667085)",
289
+ });
290
+ const saveKeyButton = button("添加并使用");
291
+ const removeKeyButton = button("删除");
292
+ const accountSelect = accountPicker();
293
+ const accountName = document.createElement("span");
294
+ Object.assign(accountName.style, {
295
+ flex: "1 1 220px",
296
+ minWidth: "0",
297
+ overflowWrap: "anywhere",
298
+ whiteSpace: "normal",
299
+ fontSize: "13px",
300
+ color: "var(--dsw-text-secondary, #667085)",
301
+ });
302
+ const addButton = button("令牌登录");
303
+ const removeButton = button("删除账号");
304
+ const accountLabel = fieldLabel("令牌账号");
305
+ const accountStats = document.createElement("div");
306
+ accountStats.setAttribute("role", "status");
307
+ accountStats.setAttribute("aria-live", "polite");
308
+ Object.assign(accountStats.style, {
309
+ display: "flex",
310
+ alignItems: "center",
311
+ gap: "8px",
312
+ flexWrap: "wrap",
313
+ width: "100%",
314
+ minHeight: "24px",
315
+ fontSize: "13px",
316
+ color: "var(--dsw-text-secondary, #667085)",
317
+ });
318
+ const tokenHint = document.createElement("span");
319
+ tokenHint.textContent = "令牌登录后可切换账号,并查看积分与今日请求量。";
320
+ Object.assign(tokenHint.style, {
321
+ display: "block",
322
+ width: "100%",
323
+ fontSize: "12px",
324
+ lineHeight: "18px",
325
+ color: "var(--dsw-text-secondary, #667085)",
326
+ });
327
+ const creditStat = document.createElement("span");
328
+ creditStat.dataset.codebuddyStat = "credits";
329
+ const usageStat = document.createElement("span");
330
+ usageStat.dataset.codebuddyStat = "usage";
331
+ accountStats.append(creditStat, usageStat);
66
332
  const message = document.createElement("span");
67
333
  message.setAttribute("role", "status");
68
334
  message.setAttribute("aria-live", "polite");
69
- Object.assign(message.style, { fontSize: "12px", minHeight: "18px" });
70
- controls.append(keyButton, tokenButton, message);
335
+ Object.assign(message.style, { fontSize: "12px", minHeight: "18px", lineHeight: "18px" });
336
+ modeRow.append(keyButton, tokenButton);
337
+ keySourceRow.append(keySourceLabel, keySelect, removeKeyButton);
338
+ keyAddRow.append(newKeyLabel, newKeyInput, newKeyLabelInput, saveKeyButton);
339
+ keySection.append(keySourceRow, keyAddRow, keyHint);
340
+ accountRow.append(accountLabel, accountSelect, accountName, removeButton);
341
+ tokenActionRow.append(addButton);
342
+ tokenSection.append(tokenHint, accountRow, accountStats, tokenActionRow);
343
+ controls.append(modeRow, keySection, tokenSection, message);
71
344
  field.append(controls);
72
- let current = { mode: "api-key", authenticated: false };
345
+ let current = { mode: "api-key", authenticated: false, accounts: [], apiKeys: [], credits: undefined, creditLoading: false, creditError: null, todayUsage: null, todayUsageError: null };
73
346
  const render = (status) => {
74
347
  current = { ...current, ...status };
75
- applyMode(input, keyButton, tokenButton, message, current);
348
+ applyMode(input, keyButton, tokenButton, keySection, keySourceRow, keySelect, keyHint, newKeyInput, newKeyLabelInput, saveKeyButton, removeKeyButton, tokenSection, accountRow, accountSelect, accountName, addButton, removeButton, accountStats, message, current);
349
+ };
350
+ render(current);
351
+ let creditRequestId = 0;
352
+ const loadCredits = async (accountId) => {
353
+ const requestId = ++creditRequestId;
354
+ if (current.mode !== "token" || !accountId) {
355
+ render({ credits: undefined, creditLoading: false, creditError: null, todayUsage: null, todayUsageError: null });
356
+ return;
357
+ }
358
+ render({ credits: undefined, creditLoading: true, creditError: null, todayUsage: null, todayUsageError: null });
359
+ try {
360
+ const result = await request("credits", { accountId });
361
+ if (requestId !== creditRequestId || current.activeAccountId !== accountId) return;
362
+ render({ ...result, creditLoading: false });
363
+ } catch (error) {
364
+ if (requestId !== creditRequestId || current.activeAccountId !== accountId) return;
365
+ render({ credits: null, creditLoading: false, creditError: error instanceof Error ? error.message : "查询 CodeBuddy 积分失败", todayUsage: null, todayUsageError: "查询 CodeBuddy 今日请求量失败" });
366
+ }
76
367
  };
77
368
 
78
369
  const setBusy = (busy) => {
79
370
  keyButton.disabled = busy;
80
371
  tokenButton.disabled = busy;
372
+ keySelect.disabled = busy;
373
+ newKeyInput.disabled = busy || current.mode === "token";
374
+ newKeyLabelInput.disabled = busy || current.mode === "token";
375
+ saveKeyButton.disabled = busy;
376
+ removeKeyButton.disabled = busy;
377
+ accountSelect.disabled = busy;
378
+ addButton.disabled = busy;
379
+ removeButton.disabled = busy;
81
380
  keyButton.style.cursor = busy ? "progress" : "pointer";
82
381
  tokenButton.style.cursor = busy ? "progress" : "pointer";
382
+ saveKeyButton.style.cursor = busy ? "progress" : "pointer";
383
+ removeKeyButton.style.cursor = busy ? "progress" : "pointer";
384
+ addButton.style.cursor = busy ? "progress" : "pointer";
385
+ removeButton.style.cursor = busy ? "progress" : "pointer";
83
386
  };
84
387
  keyButton.addEventListener("click", async () => {
85
388
  setBusy(true);
86
389
  message.textContent = "正在切换…";
87
390
  try {
88
- render(await request("api-key"));
89
- input.focus();
391
+ render(await request("api-key", current.activeApiKeyId ? { keyId: current.activeApiKeyId } : undefined));
392
+ newKeyInput.focus();
90
393
  } catch (error) {
91
394
  message.textContent = error instanceof Error ? error.message : "切换失败";
92
395
  message.style.color = "var(--dsw-text-danger, #c62828)";
@@ -94,24 +397,126 @@ window.__ModuleLoader__.load({
94
397
  setBusy(false);
95
398
  }
96
399
  });
400
+ keySelect.addEventListener("change", async () => {
401
+ setBusy(true);
402
+ message.textContent = "正在切换 API Key…";
403
+ try {
404
+ render(await request("api-key", { keyId: keySelect.value }));
405
+ } catch (error) {
406
+ message.textContent = error instanceof Error ? error.message : "切换失败";
407
+ message.style.color = "var(--dsw-text-danger, #c62828)";
408
+ } finally {
409
+ setBusy(false);
410
+ }
411
+ });
412
+ saveKeyButton.addEventListener("click", async () => {
413
+ const value = newKeyInput.value.trim();
414
+ if (!value) {
415
+ message.textContent = "请输入新的 API Key";
416
+ message.style.color = "var(--dsw-text-danger, #c62828)";
417
+ newKeyInput.focus();
418
+ return;
419
+ }
420
+ setBusy(true);
421
+ saveKeyButton.textContent = "添加中…";
422
+ message.textContent = "正在保存 API Key…";
423
+ try {
424
+ const label = newKeyLabelInput.value.trim();
425
+ const next = await request("api-key/add", { key: value, ...(label ? { label } : {}) });
426
+ newKeyInput.value = "";
427
+ newKeyLabelInput.value = "";
428
+ render(next);
429
+ } catch (error) {
430
+ message.textContent = error instanceof Error ? error.message : "保存失败";
431
+ message.style.color = "var(--dsw-text-danger, #c62828)";
432
+ } finally {
433
+ setBusy(false);
434
+ }
435
+ });
436
+ newKeyInput.addEventListener("keydown", (event) => {
437
+ if (event.key === "Enter" && !saveKeyButton.disabled) {
438
+ event.preventDefault();
439
+ saveKeyButton.click();
440
+ }
441
+ });
442
+ removeKeyButton.addEventListener("click", async () => {
443
+ if (!current.activeApiKeyId || !window.confirm("确定删除当前 DSH 保存的 API Key 吗?")) return;
444
+ setBusy(true);
445
+ message.textContent = "正在删除 API Key…";
446
+ try {
447
+ render(await request("api-key/remove", { keyId: current.activeApiKeyId }));
448
+ } catch (error) {
449
+ message.textContent = error instanceof Error ? error.message : "删除失败";
450
+ message.style.color = "var(--dsw-text-danger, #c62828)";
451
+ } finally {
452
+ setBusy(false);
453
+ }
454
+ });
97
455
  tokenButton.addEventListener("click", async () => {
98
456
  setBusy(true);
99
- tokenButton.textContent = "等待浏览器登录…";
457
+ tokenButton.textContent = "切换中…";
458
+ message.textContent = current.accounts?.length ? "正在切换令牌账号…" : "请在浏览器中完成 CodeBuddy 中国站登录";
459
+ try {
460
+ const next = await request(current.accounts?.length ? "token" : "login");
461
+ render(next);
462
+ await loadCredits(next.activeAccountId);
463
+ } catch (error) {
464
+ message.textContent = error instanceof Error ? error.message : "登录失败";
465
+ message.style.color = "var(--dsw-text-danger, #c62828)";
466
+ } finally {
467
+ setBusy(false);
468
+ }
469
+ });
470
+ addButton.addEventListener("click", async () => {
471
+ setBusy(true);
472
+ addButton.textContent = "等待浏览器登录…";
100
473
  message.textContent = "请在浏览器中完成 CodeBuddy 中国站登录";
101
474
  try {
102
- const path = current.mode === "api-key" && current.authenticated ? "token" : "login";
103
- render(await request(path));
475
+ const next = await request("login");
476
+ render(next);
477
+ await loadCredits(next.activeAccountId);
104
478
  } catch (error) {
105
479
  message.textContent = error instanceof Error ? error.message : "登录失败";
106
480
  message.style.color = "var(--dsw-text-danger, #c62828)";
107
481
  } finally {
108
- tokenButton.textContent = "令牌登录";
482
+ setBusy(false);
483
+ }
484
+ });
485
+ accountSelect.addEventListener("change", async () => {
486
+ setBusy(true);
487
+ message.textContent = "正在切换令牌账号…";
488
+ try {
489
+ const next = await request("token", { accountId: accountSelect.value });
490
+ render(next);
491
+ await loadCredits(next.activeAccountId);
492
+ } catch (error) {
493
+ message.textContent = error instanceof Error ? error.message : "切换失败";
494
+ message.style.color = "var(--dsw-text-danger, #c62828)";
495
+ } finally {
496
+ setBusy(false);
497
+ }
498
+ });
499
+ removeButton.addEventListener("click", async () => {
500
+ if (!current.activeAccountId || !window.confirm("确定删除这个 CodeBuddy 登录账号吗?令牌将从 DSH 凭据中移除。")) return;
501
+ setBusy(true);
502
+ message.textContent = "正在删除账号…";
503
+ try {
504
+ const next = await request("remove", { accountId: current.activeAccountId });
505
+ render(next);
506
+ await loadCredits(next.activeAccountId);
507
+ } catch (error) {
508
+ message.textContent = error instanceof Error ? error.message : "删除失败";
509
+ message.style.color = "var(--dsw-text-danger, #c62828)";
510
+ } finally {
109
511
  setBusy(false);
110
512
  }
111
513
  });
112
514
  fetch(`${ROUTE}/status`, { cache: "no-store" })
113
515
  .then((response) => response.json())
114
- .then(render)
516
+ .then((status) => {
517
+ render(status);
518
+ return loadCredits(status.activeAccountId);
519
+ })
115
520
  .catch(() => {
116
521
  message.textContent = "认证状态读取失败";
117
522
  message.style.color = "var(--dsw-text-danger, #c62828)";
@@ -126,21 +531,11 @@ window.__ModuleLoader__.load({
126
531
 
127
532
  function apply(ctx) {
128
533
  ctx.effect(() => {
129
- const style = document.createElement("style");
130
- style.textContent = `
131
- [data-codebuddy-auth-field] { display: grid !important; grid-template-columns: minmax(0, 1fr) auto; gap: 8px; }
132
- [data-codebuddy-auth-field] > span:first-child { grid-column: 1 / -1; }
133
- [data-codebuddy-auth-field] > input[aria-label="API 密钥"] { grid-column: 1; min-width: 0; }
134
- [data-codebuddy-auth-field] > [data-codebuddy-auth-switch] { grid-column: 2; }
135
- @media (max-width: 640px) { [data-codebuddy-auth-field] > [data-codebuddy-auth-switch] { grid-column: 1 / -1; } }
136
- `;
137
- document.head.append(style);
138
534
  const observer = new MutationObserver(enhance);
139
535
  observer.observe(document.body, { childList: true, subtree: true });
140
536
  document.addEventListener("change", enhance, true);
141
537
  enhance();
142
538
  return () => {
143
- style.remove();
144
539
  observer.disconnect();
145
540
  document.removeEventListener("change", enhance, true);
146
541
  };