dsh-codearts 1.1.4 → 1.2.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.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,21 @@
3
3
  本文件记录本插件的重要改动。格式参考 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/),
4
4
  版本号遵循 [语义化版本](https://semver.org/lang/zh-CN/)。
5
5
 
6
+ ## [1.2.0] - 2026-10-27
7
+
8
+ ### 新增
9
+
10
+ - **对话底部那行积分可以点开看明细了**:以前只有一行聚合数字(多账号把所有号加在
11
+ 一起),看不出是哪个号还剩多少。现在那一行是个按钮,点开是逐号明细——账号名、各自
12
+ 剩余积分、哪个号有积分将在本周期结束时作废、最近刷新时间,右上角一个「刷新积分」。
13
+ 它是唯一绕开 60 秒积分缓存的出口(`GET /api/codearts/status?force=1`):点了必定打
14
+ 上游,不会拿旧数字糊弄。弹层用 fixed 定位挂在按钮上方(放不下就翻到下方),不撑宽
15
+ 宿主那条共享的 composer dock 行。
16
+ - **两个显隐开关**:设置卡片新增「显示」页,分别管输入框下方那一行的**剩余积分**与
17
+ **上游排队位置**(`showCreditDock` / `showQueueDock`)。默认都开,老配置行为不变。
18
+ 开关值随 `/status` 一起下发,底部条每 60 秒读一次,所以关掉最多要等一分钟才见效——
19
+ 这句写在设置页上,不让人自己猜。
20
+
6
21
  ## [1.1.4] - 2026-10-26
7
22
 
8
23
  ### 修复
package/lib/bin.mjs CHANGED
@@ -2,7 +2,7 @@
2
2
  import { I as codeartsAccountsPath, M as isBenefitModel, N as CodeArtsAccountStore, d as fetchCallerIdentity, n as codeartsUsagePath, r as discoverModels, t as CodeArtsUsageTracker } from "./usage-B0NfG8vZ.mjs";
3
3
  import { existsSync, statSync } from "node:fs";
4
4
  //#region src/version.ts
5
- const CODEARTS_VERSION = "1.1.4";
5
+ const CODEARTS_VERSION = "1.2.0";
6
6
  //#endregion
7
7
  //#region src/bin.ts
8
8
  /**
@@ -85,6 +85,22 @@ interface CodeArtsSettingsKey {
85
85
  creditsNeedSignIn: string;
86
86
  creditsDock: string;
87
87
  creditsDockTitle: string;
88
+ creditsDockExpand: string;
89
+ creditsDockPanelTitle: string;
90
+ creditsDockRefresh: string;
91
+ creditsDockRefreshing: string;
92
+ creditsDockColAccount: string;
93
+ creditsDockColBalance: string;
94
+ creditsDockEmpty: string;
95
+ creditsDockExpiring: string;
96
+ creditsDockUpdated: string;
97
+ tabDisplay: string;
98
+ displayTitle: string;
99
+ displayHint: string;
100
+ displayCreditsToggle: string;
101
+ displayQueueToggle: string;
102
+ displayNotWritable: string;
103
+ displayFailed: string;
88
104
  queueDock: string;
89
105
  queueDockTitle: string;
90
106
  claimButton: string;
@@ -33,70 +33,70 @@ if (typeof document !== "undefined" && document.querySelector("style[data-plugin
33
33
  document.head.appendChild(tag);
34
34
  }
35
35
  var CodeArtsPluginCard_module_css_default = {
36
- "card": "xoziZG_card",
37
- "statusLine": "xoziZG_statusLine",
38
- "tabs": "xoziZG_tabs",
39
- "actions": "xoziZG_actions",
36
+ "accountName": "xoziZG_accountName",
37
+ "modelFill": "xoziZG_modelFill",
38
+ "hint": "xoziZG_hint",
39
+ "section": "xoziZG_section",
40
+ "area": "xoziZG_area",
41
+ "updated": "xoziZG_updated",
42
+ "error": "xoziZG_error",
43
+ "headText": "xoziZG_headText",
44
+ "accountCard": "xoziZG_accountCard",
45
+ "barFill": "xoziZG_barFill",
40
46
  "choiceRow": "xoziZG_choiceRow",
41
- "modelShare": "xoziZG_modelShare",
42
- "accountRowActions": "xoziZG_accountRowActions",
43
- "sectionHead": "xoziZG_sectionHead",
44
- "creditFooter": "xoziZG_creditFooter",
45
- "danger": "xoziZG_danger",
47
+ "field": "xoziZG_field",
48
+ "body": "xoziZG_body",
49
+ "accountEntry": "xoziZG_accountEntry",
50
+ "modelChart": "xoziZG_modelChart",
46
51
  "modelRequests": "xoziZG_modelRequests",
47
- "manual": "xoziZG_manual",
48
- "fieldLabel": "xoziZG_fieldLabel",
49
- "count": "xoziZG_count",
52
+ "link": "xoziZG_link",
53
+ "actions": "xoziZG_actions",
54
+ "creditRows": "xoziZG_creditRows",
55
+ "desc": "xoziZG_desc",
56
+ "balanceRow": "xoziZG_balanceRow",
57
+ "creditLink": "xoziZG_creditLink",
50
58
  "modelName": "xoziZG_modelName",
59
+ "card": "xoziZG_card",
60
+ "bar": "xoziZG_bar",
61
+ "meta": "xoziZG_meta",
62
+ "sectionHead": "xoziZG_sectionHead",
63
+ "balanceMeta": "xoziZG_balanceMeta",
64
+ "modelTrack": "xoziZG_modelTrack",
65
+ "usageFill": "xoziZG_usageFill",
66
+ "choices": "xoziZG_choices",
67
+ "accountRowActions": "xoziZG_accountRowActions",
68
+ "tabPanel": "xoziZG_tabPanel",
69
+ "expiryRow": "xoziZG_expiryRow",
70
+ "tabs": "xoziZG_tabs",
71
+ "fieldLabel": "xoziZG_fieldLabel",
72
+ "manualSummary": "xoziZG_manualSummary",
73
+ "creditRow": "xoziZG_creditRow",
74
+ "creditHint": "xoziZG_creditHint",
75
+ "statusLine": "xoziZG_statusLine",
76
+ "danger": "xoziZG_danger",
77
+ "modelRow": "xoziZG_modelRow",
78
+ "claimAllRow": "xoziZG_claimAllRow",
79
+ "accountToolbar": "xoziZG_accountToolbar",
80
+ "accountHint": "xoziZG_accountHint",
51
81
  "expiryLabel": "xoziZG_expiryLabel",
52
82
  "name": "xoziZG_name",
53
- "barFill": "xoziZG_barFill",
83
+ "header": "xoziZG_header",
84
+ "accountRow": "xoziZG_accountRow",
85
+ "sectionLabel": "xoziZG_sectionLabel",
86
+ "count": "xoziZG_count",
54
87
  "balanceUnit": "xoziZG_balanceUnit",
55
- "section": "xoziZG_section",
56
- "usageBar": "xoziZG_usageBar",
57
- "accountHint": "xoziZG_accountHint",
58
- "modelTrack": "xoziZG_modelTrack",
59
- "accountInfo": "xoziZG_accountInfo",
60
- "balanceRow": "xoziZG_balanceRow",
61
88
  "modelTokens": "xoziZG_modelTokens",
62
89
  "accountNameRow": "xoziZG_accountNameRow",
63
- "claimAllRow": "xoziZG_claimAllRow",
64
- "modelRow": "xoziZG_modelRow",
65
- "manualSummary": "xoziZG_manualSummary",
66
- "accountName": "xoziZG_accountName",
67
- "avatar": "xoziZG_avatar",
68
- "choices": "xoziZG_choices",
69
- "creditHint": "xoziZG_creditHint",
70
- "creditRow": "xoziZG_creditRow",
71
- "field": "xoziZG_field",
72
- "usageFill": "xoziZG_usageFill",
73
- "updated": "xoziZG_updated",
74
- "expiryRow": "xoziZG_expiryRow",
75
- "accountRow": "xoziZG_accountRow",
76
- "error": "xoziZG_error",
77
- "bar": "xoziZG_bar",
78
- "header": "xoziZG_header",
79
- "tabPanel": "xoziZG_tabPanel",
80
- "meta": "xoziZG_meta",
81
- "desc": "xoziZG_desc",
82
- "balance": "xoziZG_balance",
83
- "modelFill": "xoziZG_modelFill",
84
- "creditRows": "xoziZG_creditRows",
85
- "modelChart": "xoziZG_modelChart",
86
- "headText": "xoziZG_headText",
87
- "accountToolbar": "xoziZG_accountToolbar",
90
+ "accountInfo": "xoziZG_accountInfo",
88
91
  "creditBlock": "xoziZG_creditBlock",
89
- "sectionLabel": "xoziZG_sectionLabel",
90
- "manualForm": "xoziZG_manualForm",
91
- "area": "xoziZG_area",
92
- "balanceMeta": "xoziZG_balanceMeta",
93
- "accountCard": "xoziZG_accountCard",
92
+ "balance": "xoziZG_balance",
93
+ "avatar": "xoziZG_avatar",
94
+ "manual": "xoziZG_manual",
95
+ "usageBar": "xoziZG_usageBar",
96
+ "creditFooter": "xoziZG_creditFooter",
94
97
  "check": "xoziZG_check",
95
- "creditLink": "xoziZG_creditLink",
96
- "link": "xoziZG_link",
97
- "accountEntry": "xoziZG_accountEntry",
98
- "body": "xoziZG_body",
99
- "hint": "xoziZG_hint"
98
+ "manualForm": "xoziZG_manualForm",
99
+ "modelShare": "xoziZG_modelShare"
100
100
  };
101
101
  //#endregion
102
102
  //#region src/client/CodeArtsPluginCard.tsx
@@ -142,6 +142,11 @@ function claimMessagesOf(data, accountIds, t) {
142
142
  for (const id of accountIds) out[id] = claimText(data, t);
143
143
  return out;
144
144
  }
145
+ /** 开关的兜底值:后端没这条路由(老版本)时按「都显示」渲染,与原有行为一致。 */
146
+ const DOCK_DEFAULT = {
147
+ credits: true,
148
+ queue: true
149
+ };
145
150
  /** 一个 Tab 面板;未选中的用 hidden 留在 DOM 里,草稿和滚动位置都别丢。 */
146
151
  function TabPanel({ id, labelledBy, active, children }) {
147
152
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
@@ -166,7 +171,7 @@ function claimText(data, t) {
166
171
  if (data?.state === "unavailable") return t("claimUnavailable");
167
172
  return t("claimFailed", { message: typeof data?.message === "string" && data.message !== "" ? data.message : "request failed" });
168
173
  }
169
- function formatClock(ts) {
174
+ function formatClock$1(ts) {
170
175
  if (ts === void 0 || !Number.isFinite(ts)) return "";
171
176
  try {
172
177
  return new Date(ts).toLocaleTimeString([], {
@@ -204,7 +209,7 @@ function CreditBlock({ credits, error, t, onClaim, claiming, claimResult, justCl
204
209
  used: formatCredits(c.total.used),
205
210
  total: formatCredits(c.total.total)
206
211
  });
207
- const updated = formatClock(c?.fetchedAt);
212
+ const updated = formatClock$1(c?.fetchedAt);
208
213
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
209
214
  className: CodeArtsPluginCard_module_css_default.creditBlock,
210
215
  children: [
@@ -552,6 +557,56 @@ function CodeArtsPluginCard({ t }) {
552
557
  const [manualOpen, setManualOpen] = (0, react.useState)(false);
553
558
  const [tab, setTab] = (0, react.useState)("accounts");
554
559
  const tabIdBase = (0, react.useId)();
560
+ /** 对话底部那一行的两个显隐开关(后端 = 插件 settings 里的两个布尔)。 */
561
+ const [dock, setDock] = (0, react.useState)(DOCK_DEFAULT);
562
+ const [dockWritable, setDockWritable] = (0, react.useState)(false);
563
+ const [dockSaving, setDockSaving] = (0, react.useState)(false);
564
+ const [dockError, setDockError] = (0, react.useState)(null);
565
+ async function refreshDock() {
566
+ try {
567
+ const res = await fetch("/api/codearts/dock", { headers: { Accept: "application/json" } });
568
+ if (!res.ok) return;
569
+ const data = await res.json();
570
+ if (data.dock !== void 0) setDock(data.dock);
571
+ setDockWritable(data.writable === true);
572
+ } catch {}
573
+ }
574
+ /**
575
+ * 改一个开关:先按新值渲染,写失败再退回去。
576
+ *
577
+ * 这里不像模型勾选那样攒草稿再点保存 —— 只有两个布尔,攒草稿反而让用户猜
578
+ * 「我刚才那一下到底生效没有」。
579
+ */
580
+ async function toggleDock(key, on) {
581
+ const previous = dock;
582
+ setDock({
583
+ ...dock,
584
+ [key]: on
585
+ });
586
+ setDockSaving(true);
587
+ setDockError(null);
588
+ try {
589
+ const res = await fetch("/api/codearts/dock", {
590
+ method: "POST",
591
+ headers: {
592
+ "Content-Type": "application/json",
593
+ Accept: "application/json"
594
+ },
595
+ body: JSON.stringify({ [key]: on })
596
+ });
597
+ if (!res.ok) {
598
+ const data = await res.json().catch(() => ({}));
599
+ throw new Error(typeof data.error === "string" ? data.error : `HTTP ${res.status}`);
600
+ }
601
+ const data = await res.json();
602
+ if (data.dock !== void 0) setDock(data.dock);
603
+ } catch (error) {
604
+ setDock(previous);
605
+ setDockError(error instanceof Error ? error.message : String(error));
606
+ } finally {
607
+ setDockSaving(false);
608
+ }
609
+ }
555
610
  /**
556
611
  * 领每日签到积分。给 id 就只领那个号,不给就逐个号都领一遍。
557
612
  *
@@ -620,6 +675,9 @@ function CodeArtsPluginCard({ t }) {
620
675
  (0, react.useEffect)(() => {
621
676
  refreshSelection();
622
677
  }, []);
678
+ (0, react.useEffect)(() => {
679
+ refreshDock();
680
+ }, []);
623
681
  (0, react.useEffect)(() => {
624
682
  if (status?.status === "signed-in") refreshSelection();
625
683
  }, [status?.status]);
@@ -838,12 +896,19 @@ function CodeArtsPluginCard({ t }) {
838
896
  label: t("tabUsage"),
839
897
  id: tabId("usage"),
840
898
  panelId: panelId("usage")
899
+ },
900
+ display: {
901
+ value: "display",
902
+ label: t("tabDisplay"),
903
+ id: tabId("display"),
904
+ panelId: panelId("display")
841
905
  }
842
906
  };
843
907
  const tabs = [
844
908
  tabItems.accounts,
845
909
  tabItems.models,
846
- ...usageAvailable ? [tabItems.usage] : []
910
+ ...usageAvailable ? [tabItems.usage] : [],
911
+ tabItems.display
847
912
  ];
848
913
  const activeTab = tabs.some((item) => item.value === tab) ? tab : "accounts";
849
914
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -1138,6 +1203,57 @@ function CodeArtsPluginCard({ t }) {
1138
1203
  accounts,
1139
1204
  t
1140
1205
  })
1206
+ }),
1207
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TabPanel, {
1208
+ id: panelId("display"),
1209
+ labelledBy: tabId("display"),
1210
+ active: activeTab === "display",
1211
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1212
+ className: CodeArtsPluginCard_module_css_default.section,
1213
+ children: [
1214
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1215
+ className: CodeArtsPluginCard_module_css_default.sectionHead,
1216
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1217
+ className: CodeArtsPluginCard_module_css_default.sectionLabel,
1218
+ children: t("displayTitle")
1219
+ })
1220
+ }),
1221
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1222
+ className: CodeArtsPluginCard_module_css_default.hint,
1223
+ children: t("displayHint")
1224
+ }),
1225
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1226
+ className: CodeArtsPluginCard_module_css_default.choices,
1227
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1228
+ className: CodeArtsPluginCard_module_css_default.choiceRow,
1229
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Checkbox, {
1230
+ className: CodeArtsPluginCard_module_css_default.check,
1231
+ checked: dock.credits,
1232
+ disabled: !dockWritable || dockSaving,
1233
+ onChange: (on) => void toggleDock("credits", on),
1234
+ label: t("displayCreditsToggle")
1235
+ })
1236
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1237
+ className: CodeArtsPluginCard_module_css_default.choiceRow,
1238
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Checkbox, {
1239
+ className: CodeArtsPluginCard_module_css_default.check,
1240
+ checked: dock.queue,
1241
+ disabled: !dockWritable || dockSaving,
1242
+ onChange: (on) => void toggleDock("queue", on),
1243
+ label: t("displayQueueToggle")
1244
+ })
1245
+ })]
1246
+ }),
1247
+ !dockWritable && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1248
+ className: CodeArtsPluginCard_module_css_default.hint,
1249
+ children: t("displayNotWritable")
1250
+ }),
1251
+ dockError !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1252
+ className: CodeArtsPluginCard_module_css_default.error,
1253
+ children: t("displayFailed", { message: dockError })
1254
+ })
1255
+ ]
1256
+ })
1141
1257
  })
1142
1258
  ] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1143
1259
  className: CodeArtsPluginCard_module_css_default.section,
@@ -1197,15 +1313,20 @@ function CodeArtsPluginCard({ t }) {
1197
1313
  //#endregion
1198
1314
  //#region src/client/CodeArtsCreditDock.tsx
1199
1315
  /**
1200
- * 对话底部的积分条。
1316
+ * 对话底部的积分条 + 点开后的积分弹层。
1201
1317
  *
1202
1318
  * 用户最初的诉求就是「对话底部要显示积分」—— 这一条挂在
1203
1319
  * `conversation.composer.dock` 槽位上(与宿主 session-stats strip 同位)。
1204
1320
  * 只在当前 provider 是 codearts 时才拉取并显示一行「积分 N」,别的 provider 下
1205
1321
  * 完全不渲染,不抢别人的行。
1206
1322
  *
1323
+ * 那一行是个按钮:点开就是逐号明细(多账号下聚合数字本身没意义,得能看出是哪个号)。
1324
+ * 显隐由设置卡片「显示」页那两个开关管着,开关值随 `/status` 一起下发 —— 底部条本来
1325
+ * 就在轮询它,不必为两个布尔开一条新链路。代价是关掉开关最多要等一个轮询周期(60 秒)
1326
+ * 才看得见效果,这个延迟在设置卡片上写明了。
1327
+ *
1207
1328
  * 积分数据走插件自己的 `/api/codearts/status`(带 TTL 缓存,见 src/credits.ts),
1208
- * 不会因为这条 dock 让上游多挨一次请求。
1329
+ * 不会因为这条 dock 让上游多挨一次请求;弹层上那个「刷新积分」是唯一绕缓存的出口。
1209
1330
  */
1210
1331
  /** 与 src/adapter.ts 的 CODEARTS_PROVIDER 对齐 —— client 不能 import host 模块。 */
1211
1332
  const CODEARTS_PROVIDER = "codearts";
@@ -1215,6 +1336,12 @@ const QUEUE_PATH = "/api/codearts/queue";
1215
1336
  const REFRESH_INTERVAL_MS = 6e4;
1216
1337
  /** 排队位置变得快:官方客户端也是 5 秒一次;这是本机路由,打不疼上游。 */
1217
1338
  const QUEUE_INTERVAL_MS = 5e3;
1339
+ /** 弹层宽度固定,好算它会不会顶出屏幕右边。 */
1340
+ const PANEL_WIDTH = 280;
1341
+ /** 弹层与那一行之间的缝隙。 */
1342
+ const PANEL_GAP = 8;
1343
+ /** 那一行在输入框下方,往上开才够空间;给弹层留的最小高度,不够就翻到下面。 */
1344
+ const PANEL_MIN_ABOVE = 260;
1218
1345
  function readSelection(useProjection) {
1219
1346
  if (useProjection === void 0) return void 0;
1220
1347
  try {
@@ -1226,48 +1353,118 @@ function readSelection(useProjection) {
1226
1353
  function isCodeArts(selection) {
1227
1354
  return (selection?.next ?? selection?.lastUsed)?.provider === CODEARTS_PROVIDER;
1228
1355
  }
1356
+ function formatNumber(value) {
1357
+ return new Intl.NumberFormat().format(Math.round(value));
1358
+ }
1359
+ function formatClock(ts) {
1360
+ if (ts === void 0 || !Number.isFinite(ts)) return "";
1361
+ try {
1362
+ return new Date(ts).toLocaleTimeString([], {
1363
+ hour: "2-digit",
1364
+ minute: "2-digit"
1365
+ });
1366
+ } catch {
1367
+ return "";
1368
+ }
1369
+ }
1370
+ /** 账号名与设置卡片同一套取法:备注 > 用户名 > id。 */
1371
+ function accountName(account) {
1372
+ return account.label?.trim() || account.userName?.trim() || account.id;
1373
+ }
1374
+ /**
1375
+ * 逐号明细。
1376
+ *
1377
+ * 老后端只有聚合的一份 `credits`,那时单账号就退回它 —— 别因为后端没升级就把
1378
+ * 弹层弹成一句「没有数据」。
1379
+ */
1380
+ function rowsOf(data) {
1381
+ const byId = new Map((data.creditsByAccount ?? []).map((entry) => [entry.id, entry]));
1382
+ return (data.accounts ?? []).map((account) => {
1383
+ const entry = byId.get(account.id) ?? (byId.size === 0 && (data.accounts?.length ?? 0) === 1 ? {
1384
+ id: account.id,
1385
+ credits: data.credits,
1386
+ creditsError: data.creditsError
1387
+ } : void 0);
1388
+ const snapshot = entry?.credits;
1389
+ return {
1390
+ id: account.id,
1391
+ name: accountName(account),
1392
+ remain: snapshot?.credits.total.remain ?? null,
1393
+ expiring: snapshot?.credits.total.expiring ?? 0,
1394
+ ...snapshot?.credits.fetchedAt === void 0 ? {} : { fetchedAt: snapshot.credits.fetchedAt },
1395
+ ...entry?.creditsError === void 0 ? {} : { error: entry.creditsError }
1396
+ };
1397
+ });
1398
+ }
1399
+ /** 把弹层钉在按钮边上:优先往上开(那一行本来就在输入框下面),放不下就翻到下面。 */
1400
+ function anchorOf(rect) {
1401
+ const above = rect.top > PANEL_MIN_ABOVE;
1402
+ const width = Math.min(PANEL_WIDTH, window.innerWidth - 16);
1403
+ return {
1404
+ left: Math.max(8, Math.min(rect.left, window.innerWidth - width - 8)),
1405
+ top: above ? rect.top - PANEL_GAP : rect.bottom + PANEL_GAP,
1406
+ above
1407
+ };
1408
+ }
1229
1409
  function CodeArtsCreditDock({ useProjection, t }) {
1230
1410
  const active = isCodeArts(readSelection(useProjection));
1231
1411
  const [remain, setRemain] = (0, react.useState)(null);
1232
1412
  const [total, setTotal] = (0, react.useState)(0);
1413
+ const [rows, setRows] = (0, react.useState)([]);
1414
+ const [flags, setFlags] = (0, react.useState)({
1415
+ credits: true,
1416
+ queue: true
1417
+ });
1233
1418
  const [queued, setQueued] = (0, react.useState)(null);
1419
+ const [anchor, setAnchor] = (0, react.useState)(null);
1420
+ const [refreshing, setRefreshing] = (0, react.useState)(false);
1234
1421
  const mounted = (0, react.useRef)(true);
1422
+ const triggerRef = (0, react.useRef)(null);
1423
+ const panelRef = (0, react.useRef)(null);
1235
1424
  (0, react.useEffect)(() => {
1236
1425
  mounted.current = true;
1237
1426
  return () => {
1238
1427
  mounted.current = false;
1239
1428
  };
1240
1429
  }, []);
1430
+ const loadStatus = (0, react.useCallback)(async (force) => {
1431
+ try {
1432
+ const res = await fetch(force ? `${STATUS_PATH}?force=1` : STATUS_PATH, {
1433
+ headers: { Accept: "application/json" },
1434
+ credentials: "same-origin"
1435
+ });
1436
+ if (!res.ok) return;
1437
+ const data = await res.json().catch(() => void 0);
1438
+ if (data === void 0 || !mounted.current) return;
1439
+ setFlags(data.dock ?? {
1440
+ credits: true,
1441
+ queue: true
1442
+ });
1443
+ setRows(rowsOf(data));
1444
+ const snapshots = (data.creditsByAccount ?? []).map((entry) => entry.credits).filter((value) => value !== void 0);
1445
+ const pool = snapshots.length > 0 ? snapshots : data.credits === void 0 ? [] : [data.credits];
1446
+ if (pool.length === 0) {
1447
+ setRemain(null);
1448
+ return;
1449
+ }
1450
+ setRemain(Math.round(pool.reduce((sum, item) => sum + item.credits.total.remain, 0)));
1451
+ setTotal(Math.round(pool.reduce((sum, item) => sum + item.credits.total.total, 0)));
1452
+ } catch {}
1453
+ }, []);
1241
1454
  (0, react.useEffect)(() => {
1242
1455
  if (!active) {
1243
1456
  setRemain(null);
1457
+ setRows([]);
1458
+ setAnchor(null);
1244
1459
  return;
1245
1460
  }
1246
1461
  let timer;
1247
- const refresh = async () => {
1248
- try {
1249
- const res = await fetch(STATUS_PATH, {
1250
- headers: { Accept: "application/json" },
1251
- credentials: "same-origin"
1252
- });
1253
- if (!res.ok) return;
1254
- const data = await res.json().catch(() => void 0);
1255
- if (data === void 0) return;
1256
- const snapshots = (data.creditsByAccount ?? []).map((entry) => entry.credits).filter((value) => value !== void 0);
1257
- const pool = snapshots.length > 0 ? snapshots : data.credits === void 0 ? [] : [data.credits];
1258
- if (pool.length === 0) return;
1259
- if (mounted.current) {
1260
- setRemain(Math.round(pool.reduce((sum, item) => sum + item.credits.total.remain, 0)));
1261
- setTotal(Math.round(pool.reduce((sum, item) => sum + item.credits.total.total, 0)));
1262
- }
1263
- } catch {}
1264
- };
1265
- refresh();
1266
- timer = window.setInterval(refresh, REFRESH_INTERVAL_MS);
1462
+ loadStatus(false);
1463
+ timer = window.setInterval(() => void loadStatus(false), REFRESH_INTERVAL_MS);
1267
1464
  return () => {
1268
1465
  if (timer !== void 0) window.clearInterval(timer);
1269
1466
  };
1270
- }, [active]);
1467
+ }, [active, loadStatus]);
1271
1468
  (0, react.useEffect)(() => {
1272
1469
  if (!active) {
1273
1470
  setQueued(null);
@@ -1291,16 +1488,59 @@ function CodeArtsCreditDock({ useProjection, t }) {
1291
1488
  if (timer !== void 0) window.clearInterval(timer);
1292
1489
  };
1293
1490
  }, [active]);
1294
- if (!active || remain === null && queued === null) return null;
1295
- const text = remain === null ? void 0 : t("creditsDock", { remain: new Intl.NumberFormat().format(remain) });
1296
- const title = queued !== null ? t("queueDockTitle", {
1491
+ const close = (0, react.useCallback)(() => setAnchor(null), []);
1492
+ (0, react.useEffect)(() => {
1493
+ if (anchor === null) return;
1494
+ const onPointerDown = (event) => {
1495
+ const target = event.target;
1496
+ if (target === null) return;
1497
+ if (panelRef.current?.contains(target) === true || triggerRef.current?.contains(target) === true) return;
1498
+ close();
1499
+ };
1500
+ const onKeyDown = (event) => {
1501
+ if (event.key === "Escape") close();
1502
+ };
1503
+ window.addEventListener("pointerdown", onPointerDown, true);
1504
+ window.addEventListener("keydown", onKeyDown);
1505
+ window.addEventListener("resize", close);
1506
+ window.addEventListener("scroll", close, true);
1507
+ return () => {
1508
+ window.removeEventListener("pointerdown", onPointerDown, true);
1509
+ window.removeEventListener("keydown", onKeyDown);
1510
+ window.removeEventListener("resize", close);
1511
+ window.removeEventListener("scroll", close, true);
1512
+ };
1513
+ }, [anchor, close]);
1514
+ if (!active) return null;
1515
+ const showCredits = flags.credits && remain !== null;
1516
+ const showQueue = flags.queue && queued !== null;
1517
+ if (!showCredits && !showQueue) return null;
1518
+ const queueTitle = queued === null ? void 0 : t("queueDockTitle", {
1297
1519
  model: queued.model,
1298
1520
  position: queued.position,
1299
1521
  length: queued.length
1300
- }) : t("creditsDockTitle", {
1522
+ });
1523
+ const creditsTitle = showCredits ? t("creditsDockTitle", {
1301
1524
  remain: remain ?? 0,
1302
1525
  total
1303
- });
1526
+ }) : void 0;
1527
+ const fetchedAt = rows.reduce((latest, row) => Math.max(latest, row.fetchedAt ?? 0), 0);
1528
+ const lastUpdated = fetchedAt > 0 ? formatClock(fetchedAt) : "";
1529
+ const toggle = (event) => {
1530
+ if (anchor !== null) {
1531
+ close();
1532
+ return;
1533
+ }
1534
+ setAnchor(anchorOf(event.currentTarget.getBoundingClientRect()));
1535
+ };
1536
+ const forceRefresh = async () => {
1537
+ setRefreshing(true);
1538
+ try {
1539
+ await loadStatus(true);
1540
+ } finally {
1541
+ setRefreshing(false);
1542
+ }
1543
+ };
1304
1544
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1305
1545
  style: {
1306
1546
  display: "inline-flex",
@@ -1314,8 +1554,149 @@ function CodeArtsCreditDock({ useProjection, t }) {
1314
1554
  flex: "0 0 auto",
1315
1555
  minWidth: "max-content"
1316
1556
  },
1317
- title,
1318
- children: [queued !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("queueDock", { position: queued.position }) }), text !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: text })]
1557
+ title: queueTitle ?? creditsTitle,
1558
+ children: [
1559
+ showQueue && queued !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("queueDock", { position: queued.position }) }),
1560
+ showCredits && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1561
+ ref: triggerRef,
1562
+ type: "button",
1563
+ "aria-expanded": anchor !== null,
1564
+ "aria-label": t("creditsDockExpand"),
1565
+ title: creditsTitle,
1566
+ onClick: toggle,
1567
+ style: {
1568
+ display: "inline-flex",
1569
+ alignItems: "center",
1570
+ gap: 4,
1571
+ margin: 0,
1572
+ padding: 0,
1573
+ border: 0,
1574
+ background: "none",
1575
+ color: "inherit",
1576
+ font: "inherit",
1577
+ lineHeight: "inherit",
1578
+ cursor: "pointer"
1579
+ },
1580
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("creditsDock", { remain: formatNumber(remain ?? 0) }) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1581
+ "aria-hidden": "true",
1582
+ style: {
1583
+ opacity: .6,
1584
+ fontSize: "0.8em"
1585
+ },
1586
+ children: "▾"
1587
+ })]
1588
+ }),
1589
+ anchor !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1590
+ ref: panelRef,
1591
+ role: "dialog",
1592
+ "aria-label": t("creditsDockPanelTitle"),
1593
+ style: {
1594
+ position: "fixed",
1595
+ left: anchor.left,
1596
+ top: anchor.top,
1597
+ transform: anchor.above ? "translateY(-100%)" : void 0,
1598
+ width: PANEL_WIDTH,
1599
+ maxWidth: "calc(100vw - 16px)",
1600
+ zIndex: 1e3,
1601
+ display: "flex",
1602
+ flexDirection: "column",
1603
+ gap: 8,
1604
+ padding: 12,
1605
+ boxSizing: "border-box",
1606
+ borderRadius: "var(--dsw-radius-lg, 10px)",
1607
+ border: "0.5px solid var(--dsw-alias-settings-card-stroke, rgba(127, 127, 127, 0.28))",
1608
+ background: "var(--dsw-alias-settings-card-fill, rgba(32, 32, 32, 0.96))",
1609
+ boxShadow: "0 12px 32px rgba(0, 0, 0, 0.28)",
1610
+ color: "var(--dsw-alias-label-primary)",
1611
+ fontSize: "var(--dsh-content-font-size-secondary, 13px)",
1612
+ lineHeight: "18px",
1613
+ whiteSpace: "normal"
1614
+ },
1615
+ children: [
1616
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1617
+ style: {
1618
+ display: "flex",
1619
+ alignItems: "center",
1620
+ justifyContent: "space-between",
1621
+ gap: 8
1622
+ },
1623
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1624
+ style: { fontWeight: 600 },
1625
+ children: t("creditsDockPanelTitle")
1626
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
1627
+ variant: "outline",
1628
+ size: "sm",
1629
+ disabled: refreshing,
1630
+ onClick: () => void forceRefresh(),
1631
+ children: refreshing ? t("creditsDockRefreshing") : t("creditsDockRefresh")
1632
+ })]
1633
+ }),
1634
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1635
+ style: {
1636
+ display: "grid",
1637
+ gridTemplateColumns: "minmax(0, 1fr) auto",
1638
+ gap: "4px 12px",
1639
+ alignItems: "center",
1640
+ borderTop: "0.5px solid var(--dsw-alias-border-l2, rgba(127, 127, 127, 0.22))",
1641
+ paddingTop: 8,
1642
+ color: "var(--dsw-alias-label-tertiary)"
1643
+ },
1644
+ children: [
1645
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("creditsDockColAccount") }),
1646
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1647
+ style: { justifySelf: "end" },
1648
+ children: t("creditsDockColBalance")
1649
+ }),
1650
+ rows.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1651
+ style: { gridColumn: "1 / -1" },
1652
+ children: t("creditsDockEmpty")
1653
+ }),
1654
+ rows.map((row) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1655
+ style: { display: "contents" },
1656
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1657
+ style: {
1658
+ display: "flex",
1659
+ alignItems: "center",
1660
+ gap: 6,
1661
+ minWidth: 0
1662
+ },
1663
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.StateDot, { state: row.remain === null ? "warning" : "done" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1664
+ style: {
1665
+ overflow: "hidden",
1666
+ textOverflow: "ellipsis",
1667
+ whiteSpace: "nowrap",
1668
+ color: "var(--dsw-alias-label-primary)"
1669
+ },
1670
+ title: row.error === void 0 ? row.name : row.error,
1671
+ children: row.name
1672
+ })]
1673
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1674
+ style: {
1675
+ justifySelf: "end",
1676
+ fontVariantNumeric: "tabular-nums"
1677
+ },
1678
+ children: row.remain === null ? "—" : formatNumber(row.remain)
1679
+ })]
1680
+ }, row.id))
1681
+ ]
1682
+ }),
1683
+ rows.filter((row) => row.expiring > 0).map((row) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1684
+ style: {
1685
+ margin: 0,
1686
+ color: "var(--dsw-alias-state-warn-label, var(--dsw-alias-label-secondary))"
1687
+ },
1688
+ children: t("creditsDockExpiring", {
1689
+ account: row.name,
1690
+ expiring: formatNumber(row.expiring)
1691
+ })
1692
+ }, `${row.id}-expiring`)),
1693
+ lastUpdated !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1694
+ style: { color: "var(--dsw-alias-label-tertiary)" },
1695
+ children: t("creditsDockUpdated", { time: lastUpdated })
1696
+ })
1697
+ ]
1698
+ })
1699
+ ]
1319
1700
  });
1320
1701
  }
1321
1702
  //#endregion
@@ -1405,6 +1786,22 @@ const zh = {
1405
1786
  creditsNeedSignIn: "这个账号需要重新登录",
1406
1787
  creditsDock: "积分 {remain}",
1407
1788
  creditsDockTitle: "CodeArts 剩余积分 {remain} / {total}",
1789
+ creditsDockExpand: "点开看各账号积分明细",
1790
+ creditsDockPanelTitle: "CodeArts 积分",
1791
+ creditsDockRefresh: "刷新积分",
1792
+ creditsDockRefreshing: "刷新中…",
1793
+ creditsDockColAccount: "账号",
1794
+ creditsDockColBalance: "剩余积分",
1795
+ creditsDockEmpty: "暂时没有积分数据",
1796
+ creditsDockExpiring: "{account}:{expiring} 积分将在本周期结束时作废",
1797
+ creditsDockUpdated: "最近刷新 {time}",
1798
+ tabDisplay: "显示",
1799
+ displayTitle: "对话底部显示",
1800
+ displayHint: "这两项只管输入框下方那一行,设置卡片里的积分明细不受影响。底部那条每 60 秒读一次设置,所以关掉最多要等一分钟才见效。",
1801
+ displayCreditsToggle: "显示剩余积分(点开可看各账号明细)",
1802
+ displayQueueToggle: "显示上游排队位置",
1803
+ displayNotWritable: "这个宿主没给插件开配置存储,开关只能看不能改。",
1804
+ displayFailed: "保存失败:{message}",
1408
1805
  queueDock: "排队第 {position} 位",
1409
1806
  queueDockTitle: "上游排队:{model} · 第 {position} 位 / 共 {length} 条",
1410
1807
  claimButton: "签到领积分",
@@ -1502,6 +1899,22 @@ const en = {
1502
1899
  creditsNeedSignIn: "This account needs signing in again",
1503
1900
  creditsDock: "Credits {remain}",
1504
1901
  creditsDockTitle: "CodeArts credits left {remain} / {total}",
1902
+ creditsDockExpand: "Show per-account credit details",
1903
+ creditsDockPanelTitle: "CodeArts credits",
1904
+ creditsDockRefresh: "Refresh credits",
1905
+ creditsDockRefreshing: "Refreshing…",
1906
+ creditsDockColAccount: "Account",
1907
+ creditsDockColBalance: "Credits left",
1908
+ creditsDockEmpty: "No credit data yet",
1909
+ creditsDockExpiring: "{account}: {expiring} credits expire when this cycle ends",
1910
+ creditsDockUpdated: "Last updated {time}",
1911
+ tabDisplay: "Display",
1912
+ displayTitle: "Under the chat input box",
1913
+ displayHint: "These two only control that line under the input box — the credit details in this card are unaffected. The bar re-reads settings every 60 seconds, so hiding it can take up to a minute.",
1914
+ displayCreditsToggle: "Show remaining credits (click it for per-account details)",
1915
+ displayQueueToggle: "Show the upstream queue position",
1916
+ displayNotWritable: "This host gives the plugin nowhere to persist settings, so the switches are read-only.",
1917
+ displayFailed: "Failed to save: {message}",
1505
1918
  queueDock: "Queued #{position}",
1506
1919
  queueDockTitle: "Upstream queue: {model} · position {position} of {length}",
1507
1920
  claimButton: "Claim daily credits",
package/lib/index.d.mts CHANGED
@@ -1572,10 +1572,14 @@ declare const name = "llm-codearts";
1572
1572
  declare const inject: string[];
1573
1573
  /** Settings namespace owning the configuration card. */
1574
1574
  declare const CODEARTS_SETTINGS_NS: SettingsNamespace;
1575
- /** Plugin configuration (MVP: just the model allowlist). */
1575
+ /** Plugin configuration. */
1576
1576
  interface Config {
1577
1577
  /** Allowlist of CodeArts model ids offered in the model pickers. */
1578
1578
  enabledModels?: string[];
1579
+ /** 对话输入框下方那行「积分 N」是否显示。缺省 = 显示(老配置不改行为)。 */
1580
+ showCreditDock?: boolean;
1581
+ /** 等上游开流时那行「排队第 N 位」是否显示。缺省 = 显示。 */
1582
+ showQueueDock?: boolean;
1579
1583
  }
1580
1584
  declare const Config: z<Config>;
1581
1585
  declare function apply(ctx: Context, rawConfig: Config): void;
package/lib/index.mjs CHANGED
@@ -1404,6 +1404,11 @@ const CODEARTS_CLAIM_PATH = "/api/codearts/claim";
1404
1404
  const CODEARTS_REFRESH_MODELS_PATH = "/api/codearts/refresh-models";
1405
1405
  /** GET /api/codearts/queue — 上游排队位置(只在等开流期间有值,底部积分条旁边展示)。 */
1406
1406
  const CODEARTS_QUEUE_PATH = "/api/codearts/queue";
1407
+ /**
1408
+ * GET/POST /api/codearts/dock — 对话底部那一行的显隐开关。
1409
+ * GET 给当前值与 writable,POST 收 `{credits?, queue?}` 写进 settings。
1410
+ */
1411
+ const CODEARTS_DOCK_PATH = "/api/codearts/dock";
1407
1412
  /** 最近出站请求的**形状**摘要(只有 role 名与计数,不含内容),排障用。 */
1408
1413
  const CODEARTS_DEBUG_PATH = "/api/codearts/debug";
1409
1414
  /**
@@ -1442,6 +1447,11 @@ function choicesOf(deps) {
1442
1447
  };
1443
1448
  });
1444
1449
  }
1450
+ /** 开关的默认值:没有 settings provider(或老配置没这两个键)时保持原有行为 —— 都显示。 */
1451
+ const DEFAULT_DOCK_FLAGS = {
1452
+ credits: true,
1453
+ queue: true
1454
+ };
1445
1455
  /**
1446
1456
  * 把逐账号的领取结果汇总成一条:HTTP 层只看 ok,卡片看 results 逐号展示。
1447
1457
  * 有一个号失败就算整批 ok=false,但别的号领到的结果照样在 results 里。
@@ -1563,11 +1573,15 @@ function usageOf(deps) {
1563
1573
  *
1564
1574
  * 积分是「锦上添花」:拉不到也绝不能把状态接口本身拖垮,所以失败一律降级成
1565
1575
  * `creditsError` 交给卡片显示原因(超时由注入方在自己的闭包里保证)。
1576
+ *
1577
+ * `force` 是给对话底部弹层那个「刷新积分」按钮用的:不强制的话它会命中 60 秒缓存,
1578
+ * 按钮点了数字不动 —— 那还不如不给这个按钮。
1566
1579
  */
1567
- async function creditsOf(deps) {
1568
- if (deps.credits === void 0) return {};
1580
+ async function creditsOf(deps, force) {
1581
+ const load = force ? deps.refreshCredits ?? deps.credits : deps.credits;
1582
+ if (load === void 0) return {};
1569
1583
  try {
1570
- const entries = await deps.credits();
1584
+ const entries = await load();
1571
1585
  const primary = entries.find((entry) => entry.credits !== void 0) ?? entries[0];
1572
1586
  return {
1573
1587
  ...primary?.credits === void 0 ? {} : { credits: primary.credits },
@@ -1578,7 +1592,7 @@ async function creditsOf(deps) {
1578
1592
  return { creditsError: safeMessage(error) };
1579
1593
  }
1580
1594
  }
1581
- async function buildStatus(deps) {
1595
+ async function buildStatus(deps, force = false) {
1582
1596
  const models = deps.models().map((m) => ({
1583
1597
  id: m.id,
1584
1598
  name: m.name
@@ -1586,6 +1600,7 @@ async function buildStatus(deps) {
1586
1600
  const accounts = await deps.store.list();
1587
1601
  const cred = await deps.store.current();
1588
1602
  const usage = usageOf(deps);
1603
+ const dock = deps.dock === void 0 ? {} : { dock: deps.dock() };
1589
1604
  const piAi = await probePiAiCompat();
1590
1605
  const compat = piAi === void 0 ? {} : { piAiError: piAi };
1591
1606
  if (accounts.length === 0 || cred === void 0) return {
@@ -1593,6 +1608,7 @@ async function buildStatus(deps) {
1593
1608
  models,
1594
1609
  accounts,
1595
1610
  usage,
1611
+ ...dock,
1596
1612
  ...compat
1597
1613
  };
1598
1614
  const usable = accounts.some(accountUsable);
@@ -1601,7 +1617,8 @@ async function buildStatus(deps) {
1601
1617
  ...cred.storedAt === void 0 ? {} : { storedAt: cred.storedAt },
1602
1618
  models,
1603
1619
  accounts,
1604
- usage
1620
+ usage,
1621
+ ...dock
1605
1622
  };
1606
1623
  if (!usable) return {
1607
1624
  status: "expired",
@@ -1612,7 +1629,7 @@ async function buildStatus(deps) {
1612
1629
  status: "signed-in",
1613
1630
  ...identity,
1614
1631
  ...compat,
1615
- ...await creditsOf(deps)
1632
+ ...await creditsOf(deps, force)
1616
1633
  };
1617
1634
  }
1618
1635
  /** GET /api/codearts/status */
@@ -1627,12 +1644,65 @@ function statusHandler(deps) {
1627
1644
  return;
1628
1645
  }
1629
1646
  try {
1630
- json(res, 200, await buildStatus(deps));
1647
+ json(res, 200, await buildStatus(deps, new URL(req.url ?? "", "http://localhost").searchParams.get("force") === "1"));
1631
1648
  } catch (error) {
1632
1649
  json(res, 500, { error: safeMessage(error) });
1633
1650
  }
1634
1651
  };
1635
1652
  }
1653
+ /**
1654
+ * GET/POST /api/codearts/dock — 对话底部那一行的显隐开关读写。
1655
+ *
1656
+ * 和 enabled-models 同一套规矩:GET 给当前值 + writable,POST 只收布尔、
1657
+ * 没接 settings provider 时回 403 让卡片把勾选框置灰。
1658
+ */
1659
+ function dockHandler(deps) {
1660
+ return async (req, res) => {
1661
+ if (!loopbackRequest(req)) {
1662
+ json(res, 403, { error: "request-not-trusted" });
1663
+ return;
1664
+ }
1665
+ const current = deps.dock?.() ?? DEFAULT_DOCK_FLAGS;
1666
+ if (req.method === "GET") {
1667
+ json(res, 200, {
1668
+ dock: current,
1669
+ writable: deps.setDock !== void 0
1670
+ });
1671
+ return;
1672
+ }
1673
+ if (req.method === "POST") {
1674
+ if (deps.setDock === void 0) {
1675
+ json(res, 403, { error: "dock-not-writable" });
1676
+ return;
1677
+ }
1678
+ const type = req.headers["content-type"];
1679
+ if (typeof type !== "string" || !type.trim().toLowerCase().startsWith("application/json")) {
1680
+ json(res, 415, { error: "content-type must be application/json" });
1681
+ return;
1682
+ }
1683
+ try {
1684
+ const body = JSON.parse(await readBody(req));
1685
+ const touched = ["credits", "queue"].filter((key) => body[key] !== void 0);
1686
+ if (touched.length === 0 || touched.some((key) => typeof body[key] !== "boolean")) {
1687
+ json(res, 400, { error: "expected {\"credits\"?: boolean, \"queue\"?: boolean}" });
1688
+ return;
1689
+ }
1690
+ if (!await deps.setDock(Object.fromEntries(touched.map((key) => [key, body[key]])))) {
1691
+ json(res, 403, { error: "dock-not-writable" });
1692
+ return;
1693
+ }
1694
+ json(res, 200, {
1695
+ dock: deps.dock?.() ?? DEFAULT_DOCK_FLAGS,
1696
+ writable: true
1697
+ });
1698
+ } catch (error) {
1699
+ json(res, 500, { error: safeMessage(error) });
1700
+ }
1701
+ return;
1702
+ }
1703
+ json(res, 405, { error: "method not allowed" });
1704
+ };
1705
+ }
1636
1706
  /** GET /api/codearts/models — currently offered model ids (honours allowlist). */
1637
1707
  function modelsHandler(deps) {
1638
1708
  return async (req, res) => {
@@ -1935,6 +2005,11 @@ function registerCodeArtsStatusRoute(ctx, deps) {
1935
2005
  path: CODEARTS_QUEUE_PATH,
1936
2006
  handler: queueHandler(deps)
1937
2007
  });
2008
+ const disposeDock = ctx.webServer.register({
2009
+ kind: "exact",
2010
+ path: CODEARTS_DOCK_PATH,
2011
+ handler: dockHandler(deps)
2012
+ });
1938
2013
  const disposeDebug = ctx.webServer.register({
1939
2014
  kind: "exact",
1940
2015
  path: CODEARTS_DEBUG_PATH,
@@ -1951,6 +2026,7 @@ function registerCodeArtsStatusRoute(ctx, deps) {
1951
2026
  disposeClaim();
1952
2027
  disposeRefresh();
1953
2028
  disposeQueue();
2029
+ disposeDock();
1954
2030
  disposeDebug();
1955
2031
  for (const s of activeLogins.values()) s.close();
1956
2032
  activeLogins.clear();
@@ -2115,7 +2191,11 @@ function unwrapConfig(value) {
2115
2191
  for (const [key, field] of Object.entries(value)) out[key] = unwrapVolatileValue(field);
2116
2192
  return out;
2117
2193
  }
2118
- const Config = z.object({ enabledModels: z.array(z.string()).description("Model ids offered in the model pickers (empty means every model)").volatile() });
2194
+ const Config = z.object({
2195
+ enabledModels: z.array(z.string()).description("Model ids offered in the model pickers (empty means every model)").volatile(),
2196
+ showCreditDock: z.boolean().default(true).description("Show the remaining-credits line under the chat input box").volatile(),
2197
+ showQueueDock: z.boolean().default(true).description("Show the upstream queue position under the chat input box").volatile()
2198
+ });
2119
2199
  function settingsCompatNamespace(ctx) {
2120
2200
  return resolveSettingsNamespaceCompat(ctx, ctx.get("settings"), CODEARTS_SETTINGS_NS);
2121
2201
  }
@@ -2187,6 +2267,19 @@ function apply(ctx, rawConfig) {
2187
2267
  await settings.update(settingsCompatNamespace(ctx), { enabledModels: [...ids] });
2188
2268
  return true;
2189
2269
  };
2270
+ const dock = () => ({
2271
+ credits: current().showCreditDock !== false,
2272
+ queue: current().showQueueDock !== false
2273
+ });
2274
+ const setDock = async (patch) => {
2275
+ const settings = ctx.get("settings");
2276
+ if (settings === void 0) return false;
2277
+ await settings.update(settingsCompatNamespace(ctx), {
2278
+ ...patch.credits === void 0 ? {} : { showCreditDock: patch.credits },
2279
+ ...patch.queue === void 0 ? {} : { showQueueDock: patch.queue }
2280
+ });
2281
+ return true;
2282
+ };
2190
2283
  /** 手动「刷新模型列表」的预算:三路来源各打一次,比每小时的后台刷新更该给足时间。 */
2191
2284
  const MANUAL_REFRESH_MS = 6e4;
2192
2285
  ctx.inject(["webServer"], (webCtx) => registerCodeArtsStatusRoute(webCtx, {
@@ -2200,6 +2293,12 @@ function apply(ctx, rawConfig) {
2200
2293
  queue: () => queue.snapshot(),
2201
2294
  recentRequests: () => recentRequestShapes(),
2202
2295
  credits: (signal) => loadCredits(signal),
2296
+ refreshCredits: (signal) => {
2297
+ credits.invalidate();
2298
+ return loadCredits(signal);
2299
+ },
2300
+ dock,
2301
+ setDock,
2203
2302
  claim: async (accountId) => {
2204
2303
  const ids = accountId === void 0 ? await store.ids() : [accountId];
2205
2304
  if (ids.length === 0) return {
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "dsh-codearts",
3
3
  "displayName": "DSH CodeArts Connect",
4
4
  "description": "把华为云 CodeArts 接入 DeepSeek Harness —— 内置 OAuth2 登录(PKCE + DPoP),登录后即可在 DSH 对话窗口使用 CodeArts 的模型。",
5
- "version": "1.1.4",
5
+ "version": "1.2.0",
6
6
  "packageManager": "pnpm@11.24.0",
7
7
  "author": "elegant01",
8
8
  "keywords": [