dsh-codearts 1.2.1 → 1.2.3

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,39 @@
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.3] - 2026-11-02
7
+
8
+ ### 新增
9
+
10
+ - **模型列表里给「上游说不可用」的模型挂「异常」Tag**:某个模型被上游回
11
+ `InferHub.ModelArts.81003.401`(模型不存在 / 本账号无权限)后,设置卡片的勾选区
12
+ 会挂一个「异常」Tag,悬浮显示上游原始原因。**只标记,不替用户取消勾选**,也
13
+ **不随「刷新模型列表」清掉** —— 刷新只重拉上游清单,而清单里照样有它(2026-11-02
14
+ 实测),复活不了;标记按本地日期自然过期,第二天自动重试。多账号下这个错误只有在
15
+ 所有号都试过之后才会浮上来,所以按模型记全局。
16
+ - **勾选的模型可以点「测试」实测一次**:模型行上多一个「测试」按钮(只对已勾选的
17
+ 模型出现),点一下就用一句 `ping`、`max_tokens: 16` 真发一次请求,逐账号轮着试 ——
18
+ 清单里有 ≠ 能调通,只有实测才知道。成功显示「通过」并**清掉该模型的「异常」标记**;
19
+ 失败显示「失败」,悬浮可看上游原因。同一时间只测一个,不占满上游并发槽位。
20
+ - **点开积分弹层也能看到每个账号今天用了多少 token**:那张逐号明细表多了「今日 Token」
21
+ 一列,数字口径与设置卡片「用量」区块一致(输入 + 输出,按人类习惯缩写成 12.3k),
22
+ 悬浮还能看到输入/输出拆分与请求数。只统计本机经插件发出的请求,不是账号总额度。
23
+
24
+ ## [1.2.2] - 2026-11-02
25
+
26
+ ### 修复
27
+
28
+ - **`invalid_iam_token` 只会干等**:这类错误以**流内首帧**(HTTP 200)回来,既绕过
29
+ 错误分类、也不会触发换号;而 `ensureFresh()` 只在距 `expiration` 不足 5 分钟时才
30
+ 续期,token 被提前作废(另一个客户端续期 / 上游轮换)后每条消息都会撞同一堵墙。
31
+ 现在首帧识别 `invalid_iam_token` / `Incorrect IAM authentication` / `APIG.0301`
32
+ 等措辞,**强制续期一次**并让同一个账号重试;续期仍失败就冷却该账号,不再把
33
+ 原始英文串甩给用户。HTTP 400 响应体里的同类错误走同一分支。
34
+ - **上游流内的「模型不存在 / 无权限」和「不吃图」都翻成人话**:
35
+ `InferHub.ModelArts.81003.401` 与 `InferHub.001001020.406` 以前会原样透传,
36
+ 用户只看到两句英文。现在分别提示「模型不存在或当前账号没有权限(换号没用)」
37
+ 与「该模型不支持图片输入」。识图能力本身仍以上游发现结果为准,不做本地覆盖。
38
+
6
39
  ## [1.2.1] - 2026-10-28
7
40
 
8
41
  ### 新增
package/lib/bin.mjs CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
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-05PgtUIP.mjs";
2
+ import { R as isBenefitModel, V as CodeArtsAccountStore, W as codeartsAccountsPath, d as fetchCallerIdentity, n as codeartsUsagePath, r as discoverModels, t as CodeArtsUsageTracker } from "./usage-CzJjkqOp.mjs";
3
3
  import { existsSync, statSync } from "node:fs";
4
4
  //#region src/version.ts
5
- const CODEARTS_VERSION = "1.2.1";
5
+ const CODEARTS_VERSION = "1.2.3";
6
6
  //#endregion
7
7
  //#region src/bin.ts
8
8
  /**
@@ -41,6 +41,11 @@ interface CodeArtsSettingsKey {
41
41
  benefitBlocked: string;
42
42
  benefitBlockedHint: string;
43
43
  modelImages: string;
44
+ modelError: string;
45
+ testModel: string;
46
+ testingModel: string;
47
+ testOk: string;
48
+ testFailed: string;
44
49
  refreshModels: string;
45
50
  refreshing: string;
46
51
  refreshed: string;
@@ -91,6 +96,8 @@ interface CodeArtsSettingsKey {
91
96
  creditsDockRefreshing: string;
92
97
  creditsDockColAccount: string;
93
98
  creditsDockColBalance: string;
99
+ creditsDockColTokens: string;
100
+ creditsDockNoTokens: string;
94
101
  creditsDockEmpty: string;
95
102
  creditsDockExpiring: string;
96
103
  creditsDockUpdated: 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
- "modelChart": "xoziZG_modelChart",
37
- "accountRowActions": "xoziZG_accountRowActions",
38
- "field": "xoziZG_field",
39
- "statusLine": "xoziZG_statusLine",
40
- "meta": "xoziZG_meta",
41
- "manualForm": "xoziZG_manualForm",
42
- "usageBar": "xoziZG_usageBar",
43
- "check": "xoziZG_check",
44
- "tabs": "xoziZG_tabs",
45
- "modelTrack": "xoziZG_modelTrack",
36
+ "accountNameRow": "xoziZG_accountNameRow",
37
+ "balanceRow": "xoziZG_balanceRow",
46
38
  "modelFill": "xoziZG_modelFill",
47
- "balance": "xoziZG_balance",
48
- "creditLink": "xoziZG_creditLink",
49
- "actions": "xoziZG_actions",
50
- "accountName": "xoziZG_accountName",
51
- "area": "xoziZG_area",
52
- "sectionHead": "xoziZG_sectionHead",
53
- "accountHint": "xoziZG_accountHint",
39
+ "danger": "xoziZG_danger",
54
40
  "expiryRow": "xoziZG_expiryRow",
55
- "bar": "xoziZG_bar",
56
- "accountCard": "xoziZG_accountCard",
57
- "accountRow": "xoziZG_accountRow",
58
- "balanceMeta": "xoziZG_balanceMeta",
59
- "sectionLabel": "xoziZG_sectionLabel",
41
+ "accountEntry": "xoziZG_accountEntry",
42
+ "accountHint": "xoziZG_accountHint",
43
+ "manualSummary": "xoziZG_manualSummary",
44
+ "body": "xoziZG_body",
60
45
  "barFill": "xoziZG_barFill",
61
- "link": "xoziZG_link",
62
- "manual": "xoziZG_manual",
63
- "accountNameRow": "xoziZG_accountNameRow",
64
- "fieldLabel": "xoziZG_fieldLabel",
65
- "creditFooter": "xoziZG_creditFooter",
66
- "creditBlock": "xoziZG_creditBlock",
67
- "error": "xoziZG_error",
68
- "creditRow": "xoziZG_creditRow",
69
- "modelRow": "xoziZG_modelRow",
70
46
  "updated": "xoziZG_updated",
71
- "creditHint": "xoziZG_creditHint",
72
- "balanceRow": "xoziZG_balanceRow",
73
- "expiryLabel": "xoziZG_expiryLabel",
74
- "count": "xoziZG_count",
47
+ "error": "xoziZG_error",
48
+ "avatar": "xoziZG_avatar",
49
+ "balanceMeta": "xoziZG_balanceMeta",
50
+ "desc": "xoziZG_desc",
51
+ "name": "xoziZG_name",
52
+ "tabPanel": "xoziZG_tabPanel",
75
53
  "accountInfo": "xoziZG_accountInfo",
54
+ "card": "xoziZG_card",
76
55
  "balanceUnit": "xoziZG_balanceUnit",
77
- "danger": "xoziZG_danger",
56
+ "sectionHead": "xoziZG_sectionHead",
78
57
  "choiceRow": "xoziZG_choiceRow",
79
- "body": "xoziZG_body",
80
- "claimAllRow": "xoziZG_claimAllRow",
58
+ "balance": "xoziZG_balance",
59
+ "creditLink": "xoziZG_creditLink",
60
+ "accountRow": "xoziZG_accountRow",
81
61
  "usageFill": "xoziZG_usageFill",
82
- "header": "xoziZG_header",
83
- "tabPanel": "xoziZG_tabPanel",
84
- "desc": "xoziZG_desc",
85
- "card": "xoziZG_card",
86
- "accountEntry": "xoziZG_accountEntry",
87
- "hint": "xoziZG_hint",
88
- "accountToolbar": "xoziZG_accountToolbar",
62
+ "modelTrack": "xoziZG_modelTrack",
63
+ "area": "xoziZG_area",
64
+ "check": "xoziZG_check",
89
65
  "headText": "xoziZG_headText",
66
+ "accountRowActions": "xoziZG_accountRowActions",
67
+ "creditHint": "xoziZG_creditHint",
68
+ "actions": "xoziZG_actions",
69
+ "accountCard": "xoziZG_accountCard",
70
+ "claimAllRow": "xoziZG_claimAllRow",
71
+ "link": "xoziZG_link",
72
+ "modelTokens": "xoziZG_modelTokens",
73
+ "sectionLabel": "xoziZG_sectionLabel",
74
+ "usageBar": "xoziZG_usageBar",
90
75
  "section": "xoziZG_section",
91
76
  "modelName": "xoziZG_modelName",
92
- "name": "xoziZG_name",
93
- "creditRows": "xoziZG_creditRows",
94
- "modelShare": "xoziZG_modelShare",
95
- "modelRequests": "xoziZG_modelRequests",
77
+ "meta": "xoziZG_meta",
78
+ "bar": "xoziZG_bar",
79
+ "header": "xoziZG_header",
80
+ "fieldLabel": "xoziZG_fieldLabel",
96
81
  "choices": "xoziZG_choices",
97
- "manualSummary": "xoziZG_manualSummary",
98
- "avatar": "xoziZG_avatar",
99
- "modelTokens": "xoziZG_modelTokens"
82
+ "creditBlock": "xoziZG_creditBlock",
83
+ "creditRow": "xoziZG_creditRow",
84
+ "field": "xoziZG_field",
85
+ "hint": "xoziZG_hint",
86
+ "count": "xoziZG_count",
87
+ "accountName": "xoziZG_accountName",
88
+ "tabs": "xoziZG_tabs",
89
+ "manualForm": "xoziZG_manualForm",
90
+ "creditFooter": "xoziZG_creditFooter",
91
+ "modelRow": "xoziZG_modelRow",
92
+ "manual": "xoziZG_manual",
93
+ "modelChart": "xoziZG_modelChart",
94
+ "modelRequests": "xoziZG_modelRequests",
95
+ "accountToolbar": "xoziZG_accountToolbar",
96
+ "statusLine": "xoziZG_statusLine",
97
+ "creditRows": "xoziZG_creditRows",
98
+ "expiryLabel": "xoziZG_expiryLabel",
99
+ "modelShare": "xoziZG_modelShare"
100
100
  };
101
101
  //#endregion
102
102
  //#region src/client/CodeArtsPluginCard.tsx
@@ -115,7 +115,7 @@ function modelName(choice) {
115
115
  /** 官方用量明细页。 */
116
116
  const OFFICIAL_USAGE_URL = "https://codearts.huaweicloud.com/portal/settings/personal-usage";
117
117
  /** token 数按人类习惯缩写:12345 -> 12.3k。 */
118
- function formatTokens(value) {
118
+ function formatTokens$1(value) {
119
119
  if (!Number.isFinite(value)) return "0";
120
120
  if (value >= 1e6) return `${(value / 1e6).toFixed(1)}M`;
121
121
  if (value >= 1e3) return `${(value / 1e3).toFixed(1)}k`;
@@ -375,8 +375,8 @@ function UsageBars({ rows, t }) {
375
375
  const value = valueOf(row);
376
376
  const share = sum > 0 ? value / sum * 100 : 0;
377
377
  const detail = [t("usageRequests", { n: formatCount(row.totals.requests) }), t("usageTokens", {
378
- prompt: formatTokens(row.totals.promptTokens),
379
- completion: formatTokens(row.totals.completionTokens)
378
+ prompt: formatTokens$1(row.totals.promptTokens),
379
+ completion: formatTokens$1(row.totals.completionTokens)
380
380
  })].join(" · ");
381
381
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
382
382
  className: CodeArtsPluginCard_module_css_default.modelRow,
@@ -395,7 +395,7 @@ function UsageBars({ rows, t }) {
395
395
  }),
396
396
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
397
397
  className: CodeArtsPluginCard_module_css_default.modelTokens,
398
- children: tokenMode ? formatTokens(value) : formatCount(value)
398
+ children: tokenMode ? formatTokens$1(value) : formatCount(value)
399
399
  }),
400
400
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
401
401
  className: CodeArtsPluginCard_module_css_default.modelShare,
@@ -465,8 +465,8 @@ function UsageSection({ usage, accounts, t }) {
465
465
  t("usageRequests", { n: formatCount(total) }),
466
466
  " · ",
467
467
  t("usageTokens", {
468
- prompt: formatTokens(usage.today.promptTokens),
469
- completion: formatTokens(usage.today.completionTokens)
468
+ prompt: formatTokens$1(usage.today.promptTokens),
469
+ completion: formatTokens$1(usage.today.completionTokens)
470
470
  })
471
471
  ]
472
472
  })]
@@ -482,8 +482,8 @@ function UsageSection({ usage, accounts, t }) {
482
482
  t("usageRequests", { n: formatCount(usage.last30.requests) }),
483
483
  " · ",
484
484
  t("usageTokens", {
485
- prompt: formatTokens(usage.last30.promptTokens),
486
- completion: formatTokens(usage.last30.completionTokens)
485
+ prompt: formatTokens$1(usage.last30.promptTokens),
486
+ completion: formatTokens$1(usage.last30.completionTokens)
487
487
  })
488
488
  ]
489
489
  })]
@@ -548,6 +548,10 @@ function CodeArtsPluginCard({ t }) {
548
548
  const [modelError, setModelError] = (0, react.useState)(null);
549
549
  const [refreshing, setRefreshing] = (0, react.useState)(false);
550
550
  const [refreshMsg, setRefreshMsg] = (0, react.useState)(null);
551
+ /** 正在实测的模型 id;同一时间只测一个,避免把上游并发槽位占满。 */
552
+ const [testingId, setTestingId] = (0, react.useState)(null);
553
+ /** 逐模型的测试结果(通过/失败 + 原因)。 */
554
+ const [testResults, setTestResults] = (0, react.useState)({});
551
555
  /** 正在领取的账号 id;批量领取时是 CLAIM_ALL。null 表示没有在飞。 */
552
556
  const [claimingId, setClaimingId] = (0, react.useState)(null);
553
557
  /** 逐账号的领取结果文案。 */
@@ -763,6 +767,51 @@ function CodeArtsPluginCard({ t }) {
763
767
  }
764
768
  }
765
769
  /**
770
+ * 实测某个模型现在能不能调用。
771
+ *
772
+ * 上游的清单会列出调不动的模型(81003.401),刷新救不了;只有真发一次最小请求
773
+ * 才知道。测试成功会清掉后端记的「异常」标记,所以结束后重拉一次 selection。
774
+ */
775
+ async function testModel(id) {
776
+ setTestingId(id);
777
+ setTestResults((current) => {
778
+ const next = { ...current };
779
+ delete next[id];
780
+ return next;
781
+ });
782
+ try {
783
+ const res = await fetch("/api/codearts/test-model", {
784
+ method: "POST",
785
+ headers: {
786
+ "Content-Type": "application/json",
787
+ Accept: "application/json"
788
+ },
789
+ body: JSON.stringify({ id })
790
+ });
791
+ const data = await res.json().catch(() => null);
792
+ if (!res.ok) throw new Error(data?.message ?? `HTTP ${res.status}`);
793
+ setTestResults((current) => ({
794
+ ...current,
795
+ [id]: {
796
+ ok: data?.ok === true,
797
+ message: data?.message ?? "",
798
+ ms: data?.ms
799
+ }
800
+ }));
801
+ await refreshSelection();
802
+ } catch (err) {
803
+ setTestResults((current) => ({
804
+ ...current,
805
+ [id]: {
806
+ ok: false,
807
+ message: err instanceof Error ? err.message : String(err)
808
+ }
809
+ }));
810
+ } finally {
811
+ setTestingId(null);
812
+ }
813
+ }
814
+ /**
766
815
  * 手动重拉一次模型目录。
767
816
  *
768
817
  * 存在的理由:限时福利名单会轮换(模型被移出、被转正),后台每小时才拉一次,
@@ -1143,30 +1192,55 @@ function CodeArtsPluginCard({ t }) {
1143
1192
  }),
1144
1193
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1145
1194
  className: CodeArtsPluginCard_module_css_default.choices,
1146
- children: selection.choices.map((choice) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1147
- className: CodeArtsPluginCard_module_css_default.choiceRow,
1148
- children: [
1149
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Checkbox, {
1150
- className: CodeArtsPluginCard_module_css_default.check,
1151
- checked: draft.includes(choice.id),
1152
- disabled: !selection.writable || savingModels,
1153
- onChange: (on) => setModel(choice.id, on),
1154
- label: modelName(choice)
1155
- }),
1156
- choice.benefit === true && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tag, {
1157
- tone: choice.blocked === true ? "warning" : "success",
1158
- children: choice.blocked === true ? t("benefitBlocked") : t("benefit")
1159
- }),
1160
- choice.ratio !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tag, {
1161
- tone: "quiet",
1162
- children: choice.ratio
1163
- }),
1164
- choice.supportsImages === true && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tag, {
1165
- tone: "quiet",
1166
- children: t("modelImages")
1167
- })
1168
- ]
1169
- }, choice.id))
1195
+ children: selection.choices.map((choice) => {
1196
+ const result = testResults[choice.id];
1197
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1198
+ className: CodeArtsPluginCard_module_css_default.choiceRow,
1199
+ children: [
1200
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Checkbox, {
1201
+ className: CodeArtsPluginCard_module_css_default.check,
1202
+ checked: draft.includes(choice.id),
1203
+ disabled: !selection.writable || savingModels,
1204
+ onChange: (on) => setModel(choice.id, on),
1205
+ label: modelName(choice)
1206
+ }),
1207
+ choice.benefit === true && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tag, {
1208
+ tone: choice.blocked === true ? "warning" : "success",
1209
+ children: choice.blocked === true ? t("benefitBlocked") : t("benefit")
1210
+ }),
1211
+ choice.error !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1212
+ title: choice.error,
1213
+ style: { cursor: "help" },
1214
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tag, {
1215
+ tone: "warning",
1216
+ children: t("modelError")
1217
+ })
1218
+ }),
1219
+ choice.ratio !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tag, {
1220
+ tone: "quiet",
1221
+ children: choice.ratio
1222
+ }),
1223
+ choice.supportsImages === true && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tag, {
1224
+ tone: "quiet",
1225
+ children: t("modelImages")
1226
+ }),
1227
+ draft.includes(choice.id) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
1228
+ variant: "outline",
1229
+ size: "sm",
1230
+ disabled: testingId !== null,
1231
+ onClick: () => void testModel(choice.id),
1232
+ children: testingId === choice.id ? t("testingModel") : t("testModel")
1233
+ }),
1234
+ result !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1235
+ title: result.ms === void 0 ? result.message : `${result.message}(${result.ms} ms)`,
1236
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tag, {
1237
+ tone: result.ok ? "success" : "warning",
1238
+ children: result.ok ? t("testOk") : t("testFailed")
1239
+ })
1240
+ })
1241
+ ]
1242
+ }, choice.id);
1243
+ })
1170
1244
  }),
1171
1245
  selection.writable && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1172
1246
  className: CodeArtsPluginCard_module_css_default.actions,
@@ -1443,6 +1517,13 @@ function isCodeArts(selection) {
1443
1517
  function formatNumber(value) {
1444
1518
  return new Intl.NumberFormat().format(Math.round(value));
1445
1519
  }
1520
+ /** 与设置卡片「用量」区块同一口径:12345 -> 12.3k。 */
1521
+ function formatTokens(value) {
1522
+ if (!Number.isFinite(value)) return "0";
1523
+ if (value >= 1e6) return `${(value / 1e6).toFixed(1)}M`;
1524
+ if (value >= 1e3) return `${(value / 1e3).toFixed(1)}k`;
1525
+ return String(Math.round(value));
1526
+ }
1446
1527
  function formatClock(ts) {
1447
1528
  if (ts === void 0 || !Number.isFinite(ts)) return "";
1448
1529
  try {
@@ -1473,11 +1554,13 @@ function rowsOf(data) {
1473
1554
  creditsError: data.creditsError
1474
1555
  } : void 0);
1475
1556
  const snapshot = entry?.credits;
1557
+ const usage = data.usage?.byAccountToday?.[account.id];
1476
1558
  return {
1477
1559
  id: account.id,
1478
1560
  name: accountName(account),
1479
1561
  remain: snapshot?.credits.total.remain ?? null,
1480
1562
  expiring: snapshot?.credits.total.expiring ?? 0,
1563
+ ...usage === void 0 ? {} : { usage },
1481
1564
  ...snapshot?.credits.fetchedAt === void 0 ? {} : { fetchedAt: snapshot.credits.fetchedAt },
1482
1565
  ...entry?.creditsError === void 0 ? {} : { error: entry.creditsError }
1483
1566
  };
@@ -1760,7 +1843,7 @@ function CodeArtsCreditDock({ useProjection, t }) {
1760
1843
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1761
1844
  style: {
1762
1845
  display: "grid",
1763
- gridTemplateColumns: "minmax(0, 1fr) auto",
1846
+ gridTemplateColumns: "minmax(0, 1fr) auto auto",
1764
1847
  gap: "4px 12px",
1765
1848
  alignItems: "center",
1766
1849
  borderTop: "0.5px solid var(--dsw-alias-border-l2, rgba(127, 127, 127, 0.22))",
@@ -1769,6 +1852,10 @@ function CodeArtsCreditDock({ useProjection, t }) {
1769
1852
  },
1770
1853
  children: [
1771
1854
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("creditsDockColAccount") }),
1855
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1856
+ style: { justifySelf: "end" },
1857
+ children: t("creditsDockColTokens")
1858
+ }),
1772
1859
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1773
1860
  style: { justifySelf: "end" },
1774
1861
  children: t("creditsDockColBalance")
@@ -1777,33 +1864,50 @@ function CodeArtsCreditDock({ useProjection, t }) {
1777
1864
  style: { gridColumn: "1 / -1" },
1778
1865
  children: t("creditsDockEmpty")
1779
1866
  }),
1780
- rows.map((row) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1781
- style: { display: "contents" },
1782
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1783
- style: {
1784
- display: "flex",
1785
- alignItems: "center",
1786
- gap: 6,
1787
- minWidth: 0
1788
- },
1789
- 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", {
1790
- style: {
1791
- overflow: "hidden",
1792
- textOverflow: "ellipsis",
1793
- whiteSpace: "nowrap",
1794
- color: "var(--dsw-alias-label-primary)"
1795
- },
1796
- title: row.error === void 0 ? row.name : row.error,
1797
- children: row.name
1798
- })]
1799
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1800
- style: {
1801
- justifySelf: "end",
1802
- fontVariantNumeric: "tabular-nums"
1803
- },
1804
- children: row.remain === null ? "—" : formatNumber(row.remain)
1805
- })]
1806
- }, row.id))
1867
+ rows.map((row) => {
1868
+ const detail = row.usage === void 0 ? "" : [t("usageTokens", {
1869
+ prompt: formatTokens(row.usage.promptTokens),
1870
+ completion: formatTokens(row.usage.completionTokens)
1871
+ }), t("usageRequestCount", { n: formatNumber(row.usage.requests) })].join(" · ");
1872
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1873
+ style: { display: "contents" },
1874
+ children: [
1875
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1876
+ style: {
1877
+ display: "flex",
1878
+ alignItems: "center",
1879
+ gap: 6,
1880
+ minWidth: 0
1881
+ },
1882
+ 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", {
1883
+ style: {
1884
+ overflow: "hidden",
1885
+ textOverflow: "ellipsis",
1886
+ whiteSpace: "nowrap",
1887
+ color: "var(--dsw-alias-label-primary)"
1888
+ },
1889
+ title: row.error === void 0 ? row.name : row.error,
1890
+ children: row.name
1891
+ })]
1892
+ }),
1893
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1894
+ style: {
1895
+ justifySelf: "end",
1896
+ fontVariantNumeric: "tabular-nums"
1897
+ },
1898
+ title: row.usage === void 0 ? t("creditsDockNoTokens") : detail,
1899
+ children: row.usage === void 0 ? "—" : formatTokens(row.usage.totalTokens)
1900
+ }),
1901
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1902
+ style: {
1903
+ justifySelf: "end",
1904
+ fontVariantNumeric: "tabular-nums"
1905
+ },
1906
+ children: row.remain === null ? "—" : formatNumber(row.remain)
1907
+ })
1908
+ ]
1909
+ }, row.id);
1910
+ })
1807
1911
  ]
1808
1912
  }),
1809
1913
  rows.filter((row) => row.expiring > 0).map((row) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
@@ -1868,6 +1972,11 @@ const zh = {
1868
1972
  benefitBlocked: "福利今日不可用",
1869
1973
  benefitBlockedHint: "{n} 个福利模型今天被上游判定没有权益(活动领完或渠道未开),发出去会直接报 benefit not found。",
1870
1974
  modelImages: "识图",
1975
+ modelError: "异常",
1976
+ testModel: "测试",
1977
+ testingModel: "测试中…",
1978
+ testOk: "通过",
1979
+ testFailed: "失败",
1871
1980
  refreshModels: "刷新模型列表",
1872
1981
  refreshing: "刷新中…",
1873
1982
  refreshed: "已刷新:{n} 个模型,其中 {benefit} 个走福利路由",
@@ -1918,6 +2027,8 @@ const zh = {
1918
2027
  creditsDockRefreshing: "刷新中…",
1919
2028
  creditsDockColAccount: "账号",
1920
2029
  creditsDockColBalance: "剩余积分",
2030
+ creditsDockColTokens: "今日 Token",
2031
+ creditsDockNoTokens: "今天还没有经插件发出的请求",
1921
2032
  creditsDockEmpty: "暂时没有积分数据",
1922
2033
  creditsDockExpiring: "{account}:{expiring} 积分将在本周期结束时作废",
1923
2034
  creditsDockUpdated: "最近刷新 {time}",
@@ -1984,6 +2095,11 @@ const en = {
1984
2095
  benefitBlocked: "Benefit unavailable today",
1985
2096
  benefitBlockedHint: "{n} benefit model(s) were told they have no entitlement today (campaign exhausted or channel not opened); sending to them fails with benefit not found.",
1986
2097
  modelImages: "Images",
2098
+ modelError: "Unavailable",
2099
+ testModel: "Test",
2100
+ testingModel: "Testing…",
2101
+ testOk: "Passed",
2102
+ testFailed: "Failed",
1987
2103
  refreshModels: "Refresh models",
1988
2104
  refreshing: "Refreshing…",
1989
2105
  refreshed: "Refreshed: {n} models, {benefit} on the benefit route",
@@ -2034,6 +2150,8 @@ const en = {
2034
2150
  creditsDockRefreshing: "Refreshing…",
2035
2151
  creditsDockColAccount: "Account",
2036
2152
  creditsDockColBalance: "Credits left",
2153
+ creditsDockColTokens: "Tokens today",
2154
+ creditsDockNoTokens: "No requests through the plugin today yet",
2037
2155
  creditsDockEmpty: "No credit data yet",
2038
2156
  creditsDockExpiring: "{account}: {expiring} credits expire when this cycle ends",
2039
2157
  creditsDockUpdated: "Last updated {time}",
package/lib/index.d.mts CHANGED
@@ -870,7 +870,7 @@ interface OAuthToken {
870
870
  //#endregion
871
871
  //#region src/upstream.d.ts
872
872
  declare const SNAP_HOST = "https://snap-access.cn-north-4.myhuaweicloud.com";
873
- type UpstreamErrorKind = 'hard_credit' | 'soft_rate' | 'session_dead' | 'not_found' | 'server' | 'client';
873
+ type UpstreamErrorKind = 'hard_credit' | 'soft_rate' | 'session_dead' | 'iam_stale' | 'not_found' | 'server' | 'client';
874
874
  interface ChatResultOk {
875
875
  ok: true;
876
876
  /** 已转成 OpenAI 增量 SSE 的 ReadableStream(文本,data: ...\n\n)。 */
@@ -1117,6 +1117,13 @@ declare class CodeArtsAccountStore {
1117
1117
  clear(): Promise<void>;
1118
1118
  /** 给 shim 用:解析某账号的签名材料(内部含续期)。 */
1119
1119
  resolve(id: string): Promise<ResolvedCredential | undefined>;
1120
+ /**
1121
+ * 上游报 IAM token 无效时强制续期。
1122
+ *
1123
+ * `seenToken` 是这次请求实际用的 token:若别的请求已经换过凭证,就别再刷一次
1124
+ * (refresh_token 会轮换,白刷一次就多一次失败风险)。
1125
+ */
1126
+ forceRefresh(id: string, seenToken?: string): Promise<CodeArtsCredential | undefined>;
1120
1127
  }
1121
1128
  //#endregion
1122
1129
  //#region src/account-pool.d.ts
package/lib/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { A as filterEnabledModels, C as rateLimitFrame, D as FALLBACK_CODEARTS_MODELS, E as CodeArtsCatalog, F as MANUAL_ACCOUNT_ID, I as codeartsAccountsPath, L as codeartsAuthPath, M as isBenefitModel, N as CodeArtsAccountStore, O as clearBenefitBlocked, P as CodeArtsCredentialStore, R as startLogin, S as prepareChatBody, T as upstreamErrorMessage, _ as isQueueableRate, a as SNAP_HOST, b as maxTokensCeilingFor, c as claimDailyWelfare, d as fetchCallerIdentity, f as fetchModels, g as isParamInvalidFrame, h as isConcurrencyCap, i as refreshCatalog, j as isBenefitBlocked, k as creditRatioLabel, l as clearQueueStatus, m as fetchWelfareCampaigns, n as codeartsUsagePath, o as canonicalModel, p as fetchQueueStatus, r as discoverModels, s as chatStream, t as CodeArtsUsageTracker, u as degradeTokenCeiling, v as isTpmLimit, w as signedGet, x as pickDailyWelfareCampaign, y as isWelfareClaimedStatus } from "./usage-05PgtUIP.mjs";
1
+ import { A as upstreamErrorMessage, B as modelUnavailableReason, C as isWelfareClaimedStatus, D as prepareChatBody, E as pickDailyWelfareCampaign, F as filterEnabledModels, G as codeartsAuthPath, H as CodeArtsCredentialStore, I as forgetModelUnavailable, K as startLogin, L as isBenefitBlocked, M as FALLBACK_CODEARTS_MODELS, N as clearBenefitBlocked, O as rateLimitFrame, P as creditRatioLabel, R as isBenefitModel, S as isTpmLimit, T as modelUnavailableReasonText, U as MANUAL_ACCOUNT_ID, V as CodeArtsAccountStore, W as codeartsAccountsPath, _ as isIamTokenError, a as SNAP_HOST, b as isParamInvalidFrame, c as claimDailyWelfare, d as fetchCallerIdentity, f as fetchModels, g as isConcurrencyCap, h as firstFrameError, i as refreshCatalog, j as CodeArtsCatalog, k as signedGet, l as clearQueueStatus, m as fetchWelfareCampaigns, n as codeartsUsagePath, o as canonicalModel, p as fetchQueueStatus, r as discoverModels, s as chatStream, t as CodeArtsUsageTracker, u as degradeTokenCeiling, v as isModelNotAccessible, w as maxTokensCeilingFor, x as isQueueableRate, y as isNotMultimodal, z as markModelUnavailable } from "./usage-CzJjkqOp.mjs";
2
2
  import { i as lazyApi, r as createProvider } from "./models-CgKCibpk.mjs";
3
3
  import z from "@deepseek-ai/schemastery";
4
4
  import { randomBytes, timingSafeEqual } from "node:crypto";
@@ -853,6 +853,7 @@ const KIND_STATUS = {
853
853
  hard_credit: 402,
854
854
  soft_rate: 429,
855
855
  session_dead: 401,
856
+ iam_stale: 401,
856
857
  not_found: 502,
857
858
  server: 502,
858
859
  client: 400
@@ -1091,7 +1092,8 @@ function createCodeArtsShim(options) {
1091
1092
  const reader = result.stream.getReader();
1092
1093
  const first = await reader.read();
1093
1094
  const head = first.done ? void 0 : first.value;
1094
- const rate = head === void 0 ? void 0 : rateLimitFrame(new TextDecoder().decode(head));
1095
+ const headText = head === void 0 ? void 0 : new TextDecoder().decode(head);
1096
+ const rate = headText === void 0 ? void 0 : rateLimitFrame(headText);
1095
1097
  if (rate !== void 0) {
1096
1098
  reader.releaseLock();
1097
1099
  await result.stream.cancel().catch(() => {});
@@ -1106,7 +1108,27 @@ function createCodeArtsShim(options) {
1106
1108
  }
1107
1109
  };
1108
1110
  }
1109
- const shouldHeal = heal < MAX_CEILING_HEALS && head !== void 0 && isParamInvalidFrame(new TextDecoder().decode(head));
1111
+ const upstreamError = headText === void 0 ? void 0 : firstFrameError(headText);
1112
+ if (upstreamError !== void 0) {
1113
+ const detail = `${upstreamError.code}: ${upstreamError.message}`.replace(/^: /, "");
1114
+ if (isIamTokenError(detail) || isModelNotAccessible(detail) || isNotMultimodal(detail)) {
1115
+ reader.releaseLock();
1116
+ await result.stream.cancel().catch(() => {});
1117
+ logger?.warn(`dsh-codearts: ${prepared.model} 首帧报上游错误(${detail.slice(0, 160)})`);
1118
+ if (isModelNotAccessible(detail)) markModelUnavailable(prepared.model, modelUnavailableReasonText(detail));
1119
+ const kind = isIamTokenError(detail) ? "iam_stale" : isModelNotAccessible(detail) ? "not_found" : "client";
1120
+ return {
1121
+ prepared,
1122
+ result: {
1123
+ ok: false,
1124
+ kind,
1125
+ status: KIND_STATUS[kind],
1126
+ message: detail
1127
+ }
1128
+ };
1129
+ }
1130
+ }
1131
+ const shouldHeal = heal < MAX_CEILING_HEALS && headText !== void 0 && isParamInvalidFrame(headText);
1110
1132
  reader.releaseLock();
1111
1133
  if (!shouldHeal) return {
1112
1134
  prepared,
@@ -1202,6 +1224,8 @@ function createCodeArtsShim(options) {
1202
1224
  }
1203
1225
  pool.sync(ids);
1204
1226
  const tried = /* @__PURE__ */ new Set();
1227
+ /** 已经为它强制续期过一次的账号:同一个号只重试一次,避免把 refresh_token 连烧。 */
1228
+ const refreshed = /* @__PURE__ */ new Set();
1205
1229
  let cappedAccount;
1206
1230
  let tpmAccount;
1207
1231
  let queueWaitedMs = 0;
@@ -1257,6 +1281,18 @@ function createCodeArtsShim(options) {
1257
1281
  tpmAccount = id;
1258
1282
  break;
1259
1283
  }
1284
+ if (result.kind === "iam_stale") {
1285
+ if (!refreshed.has(id)) {
1286
+ refreshed.add(id);
1287
+ logger?.warn(`dsh-codearts: ${id} 的 IAM token 被判失效,强制续期后重试`);
1288
+ if (await store.forceRefresh(id, credential.token) !== void 0) {
1289
+ tried.delete(id);
1290
+ continue;
1291
+ }
1292
+ }
1293
+ pool.cooldown(id, SERVER_COOLDOWN_MS, result.message);
1294
+ continue;
1295
+ }
1260
1296
  if (result.kind === "session_dead") {
1261
1297
  pool.disable(id, result.message);
1262
1298
  continue;
@@ -1402,6 +1438,13 @@ const CODEARTS_ACCOUNTS_PATH = "/api/codearts/accounts";
1402
1438
  const CODEARTS_CLAIM_PATH = "/api/codearts/claim";
1403
1439
  /** POST /api/codearts/refresh-models — 立刻重拉一次模型目录,并清掉「福利今日不可用」标记。 */
1404
1440
  const CODEARTS_REFRESH_MODELS_PATH = "/api/codearts/refresh-models";
1441
+ /**
1442
+ * POST /api/codearts/test-model — 用最小请求实测某个模型能不能调通。
1443
+ *
1444
+ * 存在理由:上游的模型清单会列出调不动的模型(`InferHub.ModelArts.81003.401`),
1445
+ * 刷新救不了;只有真发一次请求才知道。成功会清掉该模型的「异常」标记。
1446
+ */
1447
+ const CODEARTS_TEST_MODEL_PATH = "/api/codearts/test-model";
1405
1448
  /** GET /api/codearts/queue — 上游排队位置(只在等开流期间有值,底部积分条旁边展示)。 */
1406
1449
  const CODEARTS_QUEUE_PATH = "/api/codearts/queue";
1407
1450
  /**
@@ -1428,6 +1471,7 @@ function selectionOf(models, enabled, writable) {
1428
1471
  blocked: m.blocked,
1429
1472
  ratio: m.ratio,
1430
1473
  supportsImages: m.supportsImages,
1474
+ ...m.error === void 0 ? {} : { error: m.error },
1431
1475
  enabled: restricted ? allowed.has(m.id) : true
1432
1476
  })),
1433
1477
  restricted,
@@ -1439,13 +1483,15 @@ function choicesOf(deps) {
1439
1483
  return deps.models().map((m) => {
1440
1484
  const benefit = isBenefitModel(m.id);
1441
1485
  const ratio = creditRatioLabel(m.ratio, benefit);
1486
+ const error = modelUnavailableReason(m.id);
1442
1487
  return {
1443
1488
  id: m.id,
1444
1489
  name: m.name,
1445
1490
  benefit,
1446
1491
  blocked: isBenefitBlocked(m.id),
1447
1492
  ...ratio === void 0 ? {} : { ratio },
1448
- ...m.supportsImages === true ? { supportsImages: true } : {}
1493
+ ...m.supportsImages === true ? { supportsImages: true } : {},
1494
+ ...error === void 0 ? {} : { error }
1449
1495
  };
1450
1496
  });
1451
1497
  }
@@ -1933,6 +1979,131 @@ function refreshModelsHandler(deps) {
1933
1979
  }
1934
1980
  };
1935
1981
  }
1982
+ /**
1983
+ * 逐账号发一次最小聊天,验证模型**真的能调用**(清单里有 ≠ 能调)。
1984
+ *
1985
+ * 只发一句 ping、max_tokens 16:目的是让上游过一遍权限与路由检查,不是拿内容。
1986
+ * 必须逐账号试 —— 池里的正常请求也是这么轮的;只测第一个号会把「这个号没权限」
1987
+ * 误报成「模型不可用」。
1988
+ */
1989
+ async function probeModel(deps, id, signal) {
1990
+ const ids = await deps.store.ids();
1991
+ if (ids.length === 0) return {
1992
+ ok: false,
1993
+ message: "没有已登录的账号,请先在设置卡片登录。"
1994
+ };
1995
+ const raw = JSON.stringify({
1996
+ model: id,
1997
+ stream: true,
1998
+ max_tokens: 16,
1999
+ messages: [{
2000
+ role: "user",
2001
+ content: "ping"
2002
+ }]
2003
+ });
2004
+ let last = "没有可用的账号";
2005
+ for (const accountId of ids.slice(0, 3)) {
2006
+ if (signal.aborted) break;
2007
+ const credential = await deps.store.resolve(accountId);
2008
+ if (credential === void 0) {
2009
+ last = "凭据不可用,请重新登录";
2010
+ continue;
2011
+ }
2012
+ const result = await chatStream(credential, prepareChatBody(raw), signal);
2013
+ if (!result.ok) {
2014
+ last = upstreamErrorMessage(result.message, result.status);
2015
+ if (result.kind === "iam_stale") await deps.store.forceRefresh(accountId, credential.token).catch(() => void 0);
2016
+ continue;
2017
+ }
2018
+ const reader = result.stream.getReader();
2019
+ try {
2020
+ for (let frame = 0; frame < 6; frame += 1) {
2021
+ const next = await reader.read();
2022
+ if (next.done) return {
2023
+ ok: true,
2024
+ message: "上游已响应",
2025
+ accountId
2026
+ };
2027
+ const text = new TextDecoder().decode(next.value);
2028
+ const error = firstFrameError(text);
2029
+ if (error !== void 0) {
2030
+ const detail = `${error.code}: ${error.message}`.replace(/^: /, "");
2031
+ if (isModelNotAccessible(detail)) markModelUnavailable(id, modelUnavailableReasonText(detail));
2032
+ return {
2033
+ ok: false,
2034
+ message: upstreamErrorMessage(detail),
2035
+ accountId
2036
+ };
2037
+ }
2038
+ if (text.includes("\"choices\"")) return {
2039
+ ok: true,
2040
+ message: "上游正常响应",
2041
+ accountId
2042
+ };
2043
+ }
2044
+ return {
2045
+ ok: true,
2046
+ message: "上游已开流",
2047
+ accountId
2048
+ };
2049
+ } finally {
2050
+ await reader.cancel().catch(() => void 0);
2051
+ }
2052
+ }
2053
+ return {
2054
+ ok: false,
2055
+ message: last
2056
+ };
2057
+ }
2058
+ /**
2059
+ * POST /api/codearts/test-model — 实测一个模型现在能不能调用。
2060
+ *
2061
+ * 成功会清掉它的「异常」标记(测试就是重新验证一次);`81003.401` 会重新记上。
2062
+ * 失败也可能只是撞了并发上限/限流,所以除模型级错误外不动标记。
2063
+ */
2064
+ function testModelHandler(deps) {
2065
+ return async (req, res) => {
2066
+ if (req.method !== "POST") {
2067
+ json(res, 405, { error: "method not allowed" });
2068
+ return;
2069
+ }
2070
+ if (!loopbackRequest(req)) {
2071
+ json(res, 403, { error: "request-not-trusted" });
2072
+ return;
2073
+ }
2074
+ let id = "";
2075
+ try {
2076
+ const body = JSON.parse(await readBody(req));
2077
+ if (typeof body.id === "string") id = body.id.trim();
2078
+ } catch {
2079
+ json(res, 400, { error: "expected {\"id\": string}" });
2080
+ return;
2081
+ }
2082
+ if (id === "") {
2083
+ json(res, 400, { error: "expected {\"id\": string}" });
2084
+ return;
2085
+ }
2086
+ if (!deps.models().some((model) => model.id === id)) {
2087
+ json(res, 404, { error: `unknown model: ${id}` });
2088
+ return;
2089
+ }
2090
+ const started = Date.now();
2091
+ try {
2092
+ const result = await probeModel(deps, id, AbortSignal.timeout(45e3));
2093
+ if (result.ok) forgetModelUnavailable(id);
2094
+ json(res, 200, {
2095
+ ...result,
2096
+ ms: Date.now() - started
2097
+ });
2098
+ } catch (error) {
2099
+ json(res, 200, {
2100
+ ok: false,
2101
+ message: safeMessage(error),
2102
+ ms: Date.now() - started
2103
+ });
2104
+ }
2105
+ };
2106
+ }
1936
2107
  /** GET /api/codearts/queue — 排队位置快照(无跟踪器/无排队时给空数组)。 */
1937
2108
  function queueHandler(deps) {
1938
2109
  return async (req, res) => {
@@ -2012,6 +2183,11 @@ function registerCodeArtsStatusRoute(ctx, deps) {
2012
2183
  path: CODEARTS_REFRESH_MODELS_PATH,
2013
2184
  handler: refreshModelsHandler(deps)
2014
2185
  });
2186
+ const disposeTestModel = ctx.webServer.register({
2187
+ kind: "exact",
2188
+ path: CODEARTS_TEST_MODEL_PATH,
2189
+ handler: testModelHandler(deps)
2190
+ });
2015
2191
  const disposeQueue = ctx.webServer.register({
2016
2192
  kind: "exact",
2017
2193
  path: CODEARTS_QUEUE_PATH,
@@ -2037,6 +2213,7 @@ function registerCodeArtsStatusRoute(ctx, deps) {
2037
2213
  disposeAccounts();
2038
2214
  disposeClaim();
2039
2215
  disposeRefresh();
2216
+ disposeTestModel();
2040
2217
  disposeQueue();
2041
2218
  disposeDock();
2042
2219
  disposeDebug();
@@ -561,6 +561,19 @@ var AccountSlot = class {
561
561
  if (!cred.refreshToken || !cred.dpopPrivateKey || !cred.clientId) return cred;
562
562
  const exp = cred.expiration ? Date.parse(cred.expiration) : NaN;
563
563
  if (!(Number.isNaN(exp) ? false : exp - Date.now() < REFRESH_SKEW_MS)) return cred;
564
+ return this.forceRefresh();
565
+ }
566
+ /**
567
+ * 不管还剩多久,立刻续期一次。
568
+ *
569
+ * 给上游回 `invalid_iam_token` 时用:token 可能被提前作废(另一个客户端续期、
570
+ * 上游轮换),而 `expiration` 还写着几小时之后,`ensureFresh` 根本不会动。
571
+ * 仍然走单飞,避免并发请求把一次性的 refresh_token 烧掉。
572
+ */
573
+ async forceRefresh() {
574
+ const cred = this._cred;
575
+ if (cred.stale === true) return cred;
576
+ if (!cred.refreshToken || !cred.dpopPrivateKey || !cred.clientId) return cred;
564
577
  return this._inflight ??= this.refresh(cred, {
565
578
  refreshToken: cred.refreshToken,
566
579
  dpopPrivateKey: cred.dpopPrivateKey,
@@ -815,6 +828,19 @@ var CodeArtsAccountStore = class {
815
828
  securityToken: cred.token
816
829
  };
817
830
  }
831
+ /**
832
+ * 上游报 IAM token 无效时强制续期。
833
+ *
834
+ * `seenToken` 是这次请求实际用的 token:若别的请求已经换过凭证,就别再刷一次
835
+ * (refresh_token 会轮换,白刷一次就多一次失败风险)。
836
+ */
837
+ async forceRefresh(id, seenToken) {
838
+ await this.loaded();
839
+ const slot = this._slots.get(id);
840
+ if (slot === void 0) return void 0;
841
+ if (seenToken !== void 0 && slot.credential.token !== seenToken) return slot.credential;
842
+ return slot.forceRefresh();
843
+ }
818
844
  };
819
845
  //#endregion
820
846
  //#region src/catalog.ts
@@ -1042,6 +1068,37 @@ function clearBenefitBlocked() {
1042
1068
  benefitBlocked.clear();
1043
1069
  }
1044
1070
  /**
1071
+ * 今天被上游明确拒绝「模型不存在 / 本账号无权限」的模型(小写 id -> 原因)。
1072
+ *
1073
+ * 判据只有聊天回的 `InferHub.ModelArts.81003.401`。上游的模型发现**照样列出它**
1074
+ * (2026-11-02 实测 `deepseek-v4.1-flash` 两路都在清单里),所以刷新模型列表救不了 ——
1075
+ * 只能等上游修好,或第二天再看。这也是「刷新清单 ≠ 验证可调用」的落点。
1076
+ *
1077
+ * 与福利缺失标记同口径:**只标记,不替用户取消勾选**。多账号下这个错误只有在
1078
+ * 所有号都试过之后才会浮上来,所以按模型记全局(不按账号)。
1079
+ */
1080
+ const unavailableModels = /* @__PURE__ */ new Map();
1081
+ /** 记一次「上游判定这个模型不可用」。 */
1082
+ function markModelUnavailable(id, reason) {
1083
+ const key = id.trim().toLowerCase();
1084
+ if (key !== "") unavailableModels.set(key, {
1085
+ day: localDayKey(),
1086
+ reason
1087
+ });
1088
+ }
1089
+ /** 该模型今天是否已被上游判定不可用;是则返回原因(卡片悬浮显示)。 */
1090
+ function modelUnavailableReason(id) {
1091
+ const key = id.trim().toLowerCase();
1092
+ const entry = unavailableModels.get(key);
1093
+ if (entry === void 0) return void 0;
1094
+ if (entry.day === localDayKey()) return entry.reason;
1095
+ unavailableModels.delete(key);
1096
+ }
1097
+ /** 单独清掉一个模型的不可用标记(手动测试成功后调)。 */
1098
+ function forgetModelUnavailable(id) {
1099
+ unavailableModels.delete(id.trim().toLowerCase());
1100
+ }
1101
+ /**
1045
1102
  * 合并多路来源的模型清单(内置 / 限时福利 / agent-center)。
1046
1103
  *
1047
1104
  * 规则对齐 codearts2api 的 `MergeModels`:
@@ -1238,14 +1295,19 @@ var upstream_exports = /* @__PURE__ */ __exportAll({
1238
1295
  fetchModels: () => fetchModels,
1239
1296
  fetchQueueStatus: () => fetchQueueStatus,
1240
1297
  fetchWelfareCampaigns: () => fetchWelfareCampaigns,
1298
+ firstFrameError: () => firstFrameError,
1241
1299
  isBenefitMissing: () => isBenefitMissing,
1242
1300
  isConcurrencyCap: () => isConcurrencyCap,
1301
+ isIamTokenError: () => isIamTokenError,
1302
+ isModelNotAccessible: () => isModelNotAccessible,
1303
+ isNotMultimodal: () => isNotMultimodal,
1243
1304
  isParamInvalidFrame: () => isParamInvalidFrame,
1244
1305
  isQueueableRate: () => isQueueableRate,
1245
1306
  isResignRefusal: () => isResignRefusal,
1246
1307
  isTpmLimit: () => isTpmLimit,
1247
1308
  isWelfareClaimedStatus: () => isWelfareClaimedStatus,
1248
1309
  maxTokensCeilingFor: () => maxTokensCeilingFor,
1310
+ modelUnavailableReasonText: () => modelUnavailableReasonText,
1249
1311
  pickDailyWelfareCampaign: () => pickDailyWelfareCampaign,
1250
1312
  prepareChatBody: () => prepareChatBody,
1251
1313
  rateLimitFrame: () => rateLimitFrame,
@@ -1597,6 +1659,9 @@ function upstreamErrorMessage(message, status = 0) {
1597
1659
  if (MODEL_RATE_CODE.test(message) || MODEL_RATE_TEXT.test(message)) return "华为侧该模型当前请求过多(共享 TPM 超限),不是你的账号或插件问题。稍后重试,或临时改用 GLM-5.2 / GLM-5.3。";
1598
1660
  if (message.includes("TM.00001041")) return "CodeArts 免费档同时只允许 3 个会话,当前槽位一直占满(被打断的会话约 1 分钟才释放)。请稍后重试。";
1599
1661
  if (isBenefitMissing("", message)) return "这个模型属于限时福利,当前福利通道不可用(不是账号问题)。可以刷新模型列表,改用非福利模型。";
1662
+ if (isIamTokenError(message)) return "上游判定本次登录凭据(IAM token)已失效。插件已尝试自动续期;若反复出现,请到设置卡片重新登录。";
1663
+ if (isModelNotAccessible(message)) return "上游说这个模型不存在或当前账号没有权限(可能已下线或改名)。请刷新模型列表后换一个模型;换号没有用,这是模型级的限制。";
1664
+ if (isNotMultimodal(message)) return "这个模型不支持图片输入(上游 406)。请移除消息里的图片,或改用支持识图的模型。";
1600
1665
  if (message.toLowerCase().includes("model is not registered")) return "上游没有注册这个模型,它可能已经下线;请刷新模型列表后重选。";
1601
1666
  return `codearts 上游错误 (http ${status}): ${message.slice(0, 400)}`;
1602
1667
  }
@@ -1613,6 +1678,65 @@ function isBenefitMissing(code, message) {
1613
1678
  return code === "InferHub.4004.200" || message.toLowerCase().includes("benefit not found");
1614
1679
  }
1615
1680
  /**
1681
+ * 上游判定本次请求的 IAM 凭据无效。
1682
+ *
1683
+ * 2026-11-02 实测:它会以**流内首帧**(HTTP 200)的形式回来,内容只有一句
1684
+ * `invalid_iam_token`,所以既不走 classify() 也不会触发换号 —— 之前只能干等到
1685
+ * 凭证自然过期(距过期不足 5 分钟才刷新),期间每条消息都撞同一堵墙。
1686
+ * 这些写法都见过:IAM 自身 / APIG 网关 / 非法 Token 三条链路各有一套措辞。
1687
+ */
1688
+ const IAM_TOKEN_ERROR = /invalid_iam_token|incorrect iam authentication|apig\.0301|iam token is invalid|iam token (?:has )?expired|decrypt token fail/i;
1689
+ function isIamTokenError(message) {
1690
+ return IAM_TOKEN_ERROR.test(message);
1691
+ }
1692
+ /**
1693
+ * 「模型不存在或当前账号没权限」。
1694
+ *
1695
+ * 上游一边把它列进模型发现(`builtin` / `agent` 两路都有),一边在聊天时回
1696
+ * `InferHub.ModelArts.81003.401`。2026-11-02 实测 `deepseek-v4.1-flash`
1697
+ * 当天就是这种状态:列表里有、调用必拒,且换号也一样 —— 所以换号无意义。
1698
+ */
1699
+ function isModelNotAccessible(message) {
1700
+ return message.includes("InferHub.ModelArts.81003.401") || message.toLowerCase().includes("the model does not exist or you do not have access to it");
1701
+ }
1702
+ /** 上游实际不吃图(模型级检查,与图片格式无关)。 */
1703
+ function isNotMultimodal(message) {
1704
+ return message.includes("InferHub.001001020.406") || message.toLowerCase().includes("the request model is not multimodal");
1705
+ }
1706
+ /** 记进目录的原因文案(卡片「异常」Tag 的悬浮说明)。 */
1707
+ function modelUnavailableReasonText(detail) {
1708
+ const trimmed = detail.trim();
1709
+ const prefix = "上游判定该模型当前不可用(模型不存在或当前账号没有权限,换号也无效)";
1710
+ return trimmed === "" ? prefix : `${prefix}:${trimmed}`;
1711
+ }
1712
+ /**
1713
+ * 从**首帧**(已转成 OpenAI 形状)里取出上游错误码与文本;没有错误返回 undefined。
1714
+ *
1715
+ * 只用于「还没产出任何内容」的判断:此时重发是安全的。帧可能被 TCP 切断,解析
1716
+ * 不出来就当没有错误,绝不影响转发。
1717
+ */
1718
+ function firstFrameError(text) {
1719
+ for (const line of text.split("\n")) {
1720
+ const trimmed = line.trim();
1721
+ if (!trimmed.startsWith("data:")) continue;
1722
+ const payload = trimmed.slice(5).trim();
1723
+ if (payload === "" || payload === "[DONE]") continue;
1724
+ let ev;
1725
+ try {
1726
+ ev = JSON.parse(payload);
1727
+ } catch {
1728
+ continue;
1729
+ }
1730
+ const code = String(ev.error?.code ?? ev.error_code ?? "");
1731
+ const message = String(ev.error?.message ?? ev.error_msg ?? "");
1732
+ if (code === "" && message === "") continue;
1733
+ return {
1734
+ code,
1735
+ message
1736
+ };
1737
+ }
1738
+ }
1739
+ /**
1616
1740
  * 本地代理「拒签」标记:billion-context 这类宿主层代理拦截出站请求做上下文压缩,
1617
1741
  * 碰到 SDK-HMAC-SHA256 签名请求要用 CodeArts 凭据重签;找不到凭据时在**本地**回
1618
1742
  * 403(根本没出网),body 里带这两个标记。这不是凭据失效 —— 归 session_dead 会把
@@ -1624,6 +1748,8 @@ function isResignRefusal(message) {
1624
1748
  function classify(status, message) {
1625
1749
  const m = message.toLowerCase();
1626
1750
  if (isResignRefusal(message)) return "client";
1751
+ if (isIamTokenError(message)) return "iam_stale";
1752
+ if (isModelNotAccessible(message)) return "not_found";
1627
1753
  if (status === 401 || status === 403) return "session_dead";
1628
1754
  if (status === 402) return "hard_credit";
1629
1755
  if (status === 429) return "soft_rate";
@@ -1727,6 +1853,7 @@ async function chatStream(credential, prepared, signal) {
1727
1853
  if (!resp.ok) {
1728
1854
  const text = await resp.text().catch(() => "");
1729
1855
  if (isBenefitMissing("", text)) markBenefitBlocked(prepared.model);
1856
+ if (isModelNotAccessible(text)) markModelUnavailable(prepared.model, modelUnavailableReasonText(text.slice(0, 200)));
1730
1857
  return {
1731
1858
  ok: false,
1732
1859
  kind: classify(resp.status, text),
@@ -1786,6 +1913,8 @@ async function chatStream(credential, prepared, signal) {
1786
1913
  }
1787
1914
  if (ev.error_code && ev.error_code !== "0" && ev.error_code !== 0) {
1788
1915
  if (isBenefitMissing(String(ev.error_code), String(ev.error_msg ?? ""))) markBenefitBlocked(model);
1916
+ const errorDetail = `${String(ev.error_code)}: ${String(ev.error_msg ?? "upstream error")}`;
1917
+ if (isModelNotAccessible(errorDetail)) markModelUnavailable(model, modelUnavailableReasonText(errorDetail));
1789
1918
  controller.enqueue(openAIData({
1790
1919
  choices: [{
1791
1920
  delta: { content: "" },
@@ -2776,4 +2905,4 @@ function sumOf(store) {
2776
2905
  return out;
2777
2906
  }
2778
2907
  //#endregion
2779
- export { filterEnabledModels as A, rateLimitFrame as C, FALLBACK_CODEARTS_MODELS as D, CodeArtsCatalog as E, MANUAL_ACCOUNT_ID as F, codeartsAccountsPath as I, codeartsAuthPath as L, isBenefitModel as M, CodeArtsAccountStore as N, clearBenefitBlocked as O, CodeArtsCredentialStore as P, startLogin as R, prepareChatBody as S, upstreamErrorMessage as T, isQueueableRate as _, SNAP_HOST as a, maxTokensCeilingFor as b, claimDailyWelfare as c, fetchCallerIdentity as d, fetchModels as f, isParamInvalidFrame as g, isConcurrencyCap as h, refreshCatalog as i, isBenefitBlocked as j, creditRatioLabel as k, clearQueueStatus as l, fetchWelfareCampaigns as m, codeartsUsagePath as n, canonicalModel as o, fetchQueueStatus as p, discoverModels as r, chatStream as s, CodeArtsUsageTracker as t, degradeTokenCeiling as u, isTpmLimit as v, signedGet as w, pickDailyWelfareCampaign as x, isWelfareClaimedStatus as y };
2908
+ export { upstreamErrorMessage as A, modelUnavailableReason as B, isWelfareClaimedStatus as C, prepareChatBody as D, pickDailyWelfareCampaign as E, filterEnabledModels as F, codeartsAuthPath as G, CodeArtsCredentialStore as H, forgetModelUnavailable as I, startLogin as K, isBenefitBlocked as L, FALLBACK_CODEARTS_MODELS as M, clearBenefitBlocked as N, rateLimitFrame as O, creditRatioLabel as P, isBenefitModel as R, isTpmLimit as S, modelUnavailableReasonText as T, MANUAL_ACCOUNT_ID as U, CodeArtsAccountStore as V, codeartsAccountsPath as W, isIamTokenError as _, SNAP_HOST as a, isParamInvalidFrame as b, claimDailyWelfare as c, fetchCallerIdentity as d, fetchModels as f, isConcurrencyCap as g, firstFrameError as h, refreshCatalog as i, CodeArtsCatalog as j, signedGet as k, clearQueueStatus as l, fetchWelfareCampaigns as m, codeartsUsagePath as n, canonicalModel as o, fetchQueueStatus as p, discoverModels as r, chatStream as s, CodeArtsUsageTracker as t, degradeTokenCeiling as u, isModelNotAccessible as v, maxTokensCeilingFor as w, isQueueableRate as x, isNotMultimodal as y, markModelUnavailable as z };
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.2.1",
5
+ "version": "1.2.3",
6
6
  "packageManager": "pnpm@11.24.0",
7
7
  "author": "elegant01",
8
8
  "keywords": [