dsh-codearts 0.5.1 → 0.5.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/lib/bin.mjs CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
- import { C as isBenefitModel, E as codeartsAccountsPath, n as codeartsUsagePath, r as discoverModels, t as CodeArtsUsageTracker, u as fetchCallerIdentity, w as CodeArtsAccountStore, x as FALLBACK_CODEARTS_MODELS } from "./usage-B4vQb1sP.mjs";
2
+ import { A as isBenefitModel, N as codeartsAccountsPath, j as CodeArtsAccountStore, n as codeartsUsagePath, r as discoverModels, t as CodeArtsUsageTracker, u as fetchCallerIdentity } from "./usage-Djtou78p.mjs";
3
3
  import { existsSync, statSync } from "node:fs";
4
4
  //#region src/version.ts
5
- const CODEARTS_VERSION = "0.5.1";
5
+ const CODEARTS_VERSION = "0.5.3";
6
6
  //#endregion
7
7
  //#region src/bin.ts
8
8
  /**
@@ -38,7 +38,7 @@ const CODEARTS_VERSION = "0.5.1";
38
38
  * @module dsh-codearts/bin
39
39
  */
40
40
  /** Bumped when a `--json` document changes shape (5: identity backfill). */
41
- const JSON_SCHEMA_VERSION = 5;
41
+ const JSON_SCHEMA_VERSION = 6;
42
42
  /**
43
43
  * 一句话描述一个账号,供人类可读输出用。
44
44
  *
@@ -70,11 +70,7 @@ async function status(jsonOutput) {
70
70
  version: CODEARTS_VERSION,
71
71
  signedIn: accounts.length > 0,
72
72
  signable: accounts.some((account) => account.signable),
73
- accounts,
74
- models: FALLBACK_CODEARTS_MODELS.map((m) => ({
75
- id: m.id,
76
- name: m.name
77
- }))
73
+ accounts
78
74
  };
79
75
  if (cred !== void 0) {
80
76
  if (cred.label !== void 0) out.label = cred.label;
@@ -91,15 +87,14 @@ async function status(jsonOutput) {
91
87
  for (const account of accounts) process.stdout.write(` - ${describeAccount(account)}\n`);
92
88
  if (!out.signable) process.stdout.write("warning: no account carries AK/SK, so requests cannot be signed and chats will be rejected\n");
93
89
  } else process.stdout.write("not signed in (no account stored; sign in from the CodeArts settings card)\n");
94
- process.stdout.write(`models (${out.models.length}):\n`);
95
- for (const m of out.models) process.stdout.write(` - ${m.id} ${m.name}\n`);
90
+ process.stdout.write("models: CLI 不缓存目录,跑 `dsh-codearts models` 现问上游\n");
96
91
  return out.signedIn ? 0 : 1;
97
92
  }
98
93
  /**
99
94
  * 动态发现当前可用模型(三路:内置 / 限时福利 / agent-center)。
100
95
  *
101
- * 这是排查「模型选择器里没有某个模型」的第一手段:它打印的是上游真实返回,
102
- * 而不是插件里那份手维护的种子清单。未登录时退回种子清单并说明原因。
96
+ * 这是排查「模型选择器里没有某个模型」的第一手段:它打印的是上游真实返回。
97
+ * 未登录时就是没有目录,直接说明原因 —— 不打印手维护的种子清单冒充上游结果。
103
98
  */
104
99
  async function models(jsonOutput) {
105
100
  if ((await store().list()).length === 0) {
@@ -108,16 +103,9 @@ async function models(jsonOutput) {
108
103
  version: CODEARTS_VERSION,
109
104
  discovered: false,
110
105
  reason: "not signed in",
111
- models: FALLBACK_CODEARTS_MODELS.map((m) => ({
112
- id: m.id,
113
- name: m.name,
114
- benefit: isBenefitModel(m.id)
115
- }))
106
+ models: []
116
107
  }, null, 2) + "\n");
117
- else {
118
- process.stdout.write("not signed in — showing the built-in seed catalog\n");
119
- for (const m of FALLBACK_CODEARTS_MODELS) process.stdout.write(` - ${m.id} ${m.name}${isBenefitModel(m.id) ? " [benefit]" : ""}\n`);
120
- }
108
+ else process.stdout.write("not signed in — 没有目录可展示(不会用种子清单冒充上游返回)\n");
121
109
  return 1;
122
110
  }
123
111
  const outcome = await discoverModels(store());
@@ -34,7 +34,14 @@ interface CodeArtsSettingsKey {
34
34
  optionalModelsAllHint: string;
35
35
  selectedCount: string;
36
36
  selectAll: string;
37
- free: string;
37
+ benefit: string;
38
+ benefitBlocked: string;
39
+ benefitBlockedHint: string;
40
+ refreshModels: string;
41
+ refreshing: string;
42
+ refreshed: string;
43
+ refreshStale: string;
44
+ refreshFailed: string;
38
45
  saveModels: string;
39
46
  saving: string;
40
47
  saved: string;
@@ -7,8 +7,23 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
7
  let react = require("react");
8
8
  let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
9
9
  let react_jsx_runtime = require("react/jsx-runtime");
10
- //#region \0dsh-css:F:\centos\dsh-codearts\src\client\CodeArtsPluginCard.module.css.mjs
11
- const css = ".DZPfya_card{border:.5px solid var(--dsw-alias-settings-card-stroke);border-radius:var(--dsw-radius-lg);background:var(--dsw-alias-settings-card-fill);max-width:720px}.DZPfya_header{align-items:flex-start;gap:12px;padding:14px 16px;display:flex}.DZPfya_headText{flex-direction:column;flex:1;gap:6px;min-width:0;display:flex}.DZPfya_name{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;line-height:22px}.DZPfya_desc{color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:20px}.DZPfya_statusLine{color:var(--dsw-alias-label-primary);align-items:center;gap:6px;font-size:14px;font-weight:500;line-height:22px;display:inline-flex}.DZPfya_meta{color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums;font-size:12px;line-height:18px}.DZPfya_body{border-top:.5px solid var(--dsw-alias-border-l2);margin:0 16px;padding:4px 0 16px}.DZPfya_section{margin-top:16px}.DZPfya_sectionHead{justify-content:space-between;align-items:baseline;gap:12px;display:flex}.DZPfya_sectionLabel{color:var(--dsw-alias-label-tertiary);font-size:11px;font-weight:500}.DZPfya_headActions{flex:none;align-items:center;gap:8px;display:flex}.DZPfya_count{color:var(--dsw-alias-label-secondary);font-variant-numeric:tabular-nums;flex:none;font-size:12px}.DZPfya_hint{color:var(--dsw-alias-label-secondary);margin:8px 0 0;font-size:13px;line-height:20px}.DZPfya_error{color:var(--dsw-alias-state-error-primary);margin:10px 0 0;font-size:13px;line-height:20px}.DZPfya_actions{flex-wrap:wrap;align-items:center;gap:8px;margin-top:12px;display:flex}.DZPfya_danger{color:var(--dsw-alias-state-error-primary)}.DZPfya_link{appearance:none;color:var(--dsw-alias-brand-text);font:inherit;text-align:left;word-break:break-all;cursor:pointer;background:0 0;border:none;padding:0;font-size:12px}.DZPfya_link:hover{text-decoration:underline}.DZPfya_choices{flex-direction:column;gap:2px;margin-top:8px;display:flex}.DZPfya_choiceRow{border-radius:var(--dsw-radius-sm);align-items:center;gap:8px;margin:0 -6px;padding:5px 6px;display:flex}.DZPfya_choiceRow:hover{background:var(--dsw-alias-interactive-bg-hover)}.DZPfya_check{flex:1;min-width:0}.DZPfya_check>span{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.DZPfya_manual{border:.5px solid var(--dsw-alias-border-l3);border-radius:var(--dsw-radius-md);margin-top:12px;padding:0 12px}.DZPfya_manual[open]{padding-bottom:12px}.DZPfya_manualSummary{color:var(--dsw-alias-label-secondary);cursor:pointer;align-items:center;gap:6px;padding:9px 0;font-size:13px;list-style:none;display:flex}.DZPfya_manualSummary::-webkit-details-marker{display:none}.DZPfya_field{flex-direction:column;gap:5px;margin-top:10px;display:flex}.DZPfya_fieldLabel{color:var(--dsw-alias-label-tertiary);font-size:12px}.DZPfya_area{box-sizing:border-box;resize:vertical;border:.5px solid var(--dsw-alias-border-l4);border-radius:var(--dsw-radius-md);background:var(--dsw-alias-bg-layer-1);width:100%;color:var(--dsw-alias-label-primary);padding:6px 8px;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:13px;line-height:20px}.DZPfya_area::placeholder{color:var(--dsw-alias-label-dimmed)}.DZPfya_area:focus{border-color:var(--dsw-alias-state-business-primary);outline:none}";
10
+ //#region src/catalog.ts
11
+ /**
12
+ * 剥掉模型名尾巴上的标记:` · x0.18`、` · x0.00 · 福利`、`(福利)`、`(免费)`、
13
+ * `(x0.18)` 等(兼容旧格式)。
14
+ */
15
+ function stripModelSuffixes(name) {
16
+ let base = name;
17
+ for (;;) {
18
+ const next = base.replace(/\s*·\s*(?:x\d+(?:\.\d+)?|(?:限时)?(?:免费|福利)(?:\s+x\d+(?:\.\d+)?)?)\s*$/, "");
19
+ if (next === base) break;
20
+ base = next;
21
+ }
22
+ return base.replace(/\s*[((]\s*(?:(?:免费|福利)(?:\s+x\d+(?:\.\d+)?)?|x\d+(?:\.\d+)?)\s*[))]\s*$/, "").trim();
23
+ }
24
+ //#endregion
25
+ //#region \0dsh-css:/home/runner/work/dsh-codearts/dsh-codearts/src/client/CodeArtsPluginCard.module.css.mjs
26
+ const css = ".xoziZG_card{border:.5px solid var(--dsw-alias-settings-card-stroke);border-radius:var(--dsw-radius-lg);background:var(--dsw-alias-settings-card-fill);max-width:720px}.xoziZG_header{align-items:flex-start;gap:12px;padding:14px 16px;display:flex}.xoziZG_headText{flex-direction:column;flex:1;gap:6px;min-width:0;display:flex}.xoziZG_name{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;line-height:22px}.xoziZG_desc{color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:20px}.xoziZG_statusLine{color:var(--dsw-alias-label-primary);align-items:center;gap:6px;font-size:14px;font-weight:500;line-height:22px;display:inline-flex}.xoziZG_meta{color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums;font-size:12px;line-height:18px}.xoziZG_body{border-top:.5px solid var(--dsw-alias-border-l2);margin:0 16px;padding:4px 0 16px}.xoziZG_section{margin-top:16px}.xoziZG_sectionHead{justify-content:space-between;align-items:baseline;gap:12px;display:flex}.xoziZG_sectionLabel{color:var(--dsw-alias-label-tertiary);font-size:11px;font-weight:500}.xoziZG_headActions{flex:none;align-items:center;gap:8px;display:flex}.xoziZG_count{color:var(--dsw-alias-label-secondary);font-variant-numeric:tabular-nums;flex:none;font-size:12px}.xoziZG_hint{color:var(--dsw-alias-label-secondary);margin:8px 0 0;font-size:13px;line-height:20px}.xoziZG_error{color:var(--dsw-alias-state-error-primary);margin:10px 0 0;font-size:13px;line-height:20px}.xoziZG_actions{flex-wrap:wrap;align-items:center;gap:8px;margin-top:12px;display:flex}.xoziZG_danger{color:var(--dsw-alias-state-error-primary)}.xoziZG_link{appearance:none;color:var(--dsw-alias-brand-text);font:inherit;text-align:left;word-break:break-all;cursor:pointer;background:0 0;border:none;padding:0;font-size:12px}.xoziZG_link:hover{text-decoration:underline}.xoziZG_choices{flex-direction:column;gap:2px;margin-top:8px;display:flex}.xoziZG_choiceRow{border-radius:var(--dsw-radius-sm);align-items:center;gap:8px;margin:0 -6px;padding:5px 6px;display:flex}.xoziZG_choiceRow:hover{background:var(--dsw-alias-interactive-bg-hover)}.xoziZG_check{flex:1;min-width:0}.xoziZG_check>span{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.xoziZG_manual{border:.5px solid var(--dsw-alias-border-l3);border-radius:var(--dsw-radius-md);margin-top:12px;padding:0 12px}.xoziZG_manual[open]{padding-bottom:12px}.xoziZG_manualSummary{color:var(--dsw-alias-label-secondary);cursor:pointer;align-items:center;gap:6px;padding:9px 0;font-size:13px;list-style:none;display:flex}.xoziZG_manualSummary::-webkit-details-marker{display:none}.xoziZG_field{flex-direction:column;gap:5px;margin-top:10px;display:flex}.xoziZG_fieldLabel{color:var(--dsw-alias-label-tertiary);font-size:12px}.xoziZG_area{box-sizing:border-box;resize:vertical;border:.5px solid var(--dsw-alias-border-l4);border-radius:var(--dsw-radius-md);background:var(--dsw-alias-bg-layer-1);width:100%;color:var(--dsw-alias-label-primary);padding:6px 8px;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:13px;line-height:20px}.xoziZG_area::placeholder{color:var(--dsw-alias-label-dimmed)}.xoziZG_area:focus{border-color:var(--dsw-alias-state-business-primary);outline:none}";
12
27
  const tagId = "dsh-codearts/CodeArtsPluginCard.module.css";
13
28
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
14
29
  const tag = document.createElement("style");
@@ -18,32 +33,32 @@ if (typeof document !== "undefined" && document.querySelector("style[data-plugin
18
33
  document.head.appendChild(tag);
19
34
  }
20
35
  var CodeArtsPluginCard_module_css_default = {
21
- "choiceRow": "DZPfya_choiceRow",
22
- "manual": "DZPfya_manual",
23
- "name": "DZPfya_name",
24
- "sectionLabel": "DZPfya_sectionLabel",
25
- "section": "DZPfya_section",
26
- "danger": "DZPfya_danger",
27
- "field": "DZPfya_field",
28
- "area": "DZPfya_area",
29
- "fieldLabel": "DZPfya_fieldLabel",
30
- "headText": "DZPfya_headText",
31
- "sectionHead": "DZPfya_sectionHead",
32
- "card": "DZPfya_card",
33
- "statusLine": "DZPfya_statusLine",
34
- "manualSummary": "DZPfya_manualSummary",
35
- "header": "DZPfya_header",
36
- "headActions": "DZPfya_headActions",
37
- "actions": "DZPfya_actions",
38
- "link": "DZPfya_link",
39
- "check": "DZPfya_check",
40
- "meta": "DZPfya_meta",
41
- "hint": "DZPfya_hint",
42
- "count": "DZPfya_count",
43
- "body": "DZPfya_body",
44
- "error": "DZPfya_error",
45
- "choices": "DZPfya_choices",
46
- "desc": "DZPfya_desc"
36
+ "manualSummary": "xoziZG_manualSummary",
37
+ "headActions": "xoziZG_headActions",
38
+ "error": "xoziZG_error",
39
+ "area": "xoziZG_area",
40
+ "headText": "xoziZG_headText",
41
+ "danger": "xoziZG_danger",
42
+ "body": "xoziZG_body",
43
+ "name": "xoziZG_name",
44
+ "actions": "xoziZG_actions",
45
+ "statusLine": "xoziZG_statusLine",
46
+ "card": "xoziZG_card",
47
+ "check": "xoziZG_check",
48
+ "field": "xoziZG_field",
49
+ "sectionHead": "xoziZG_sectionHead",
50
+ "meta": "xoziZG_meta",
51
+ "choiceRow": "xoziZG_choiceRow",
52
+ "section": "xoziZG_section",
53
+ "desc": "xoziZG_desc",
54
+ "manual": "xoziZG_manual",
55
+ "header": "xoziZG_header",
56
+ "sectionLabel": "xoziZG_sectionLabel",
57
+ "choices": "xoziZG_choices",
58
+ "fieldLabel": "xoziZG_fieldLabel",
59
+ "hint": "xoziZG_hint",
60
+ "count": "xoziZG_count",
61
+ "link": "xoziZG_link"
47
62
  };
48
63
  //#endregion
49
64
  //#region src/client/CodeArtsPluginCard.tsx
@@ -55,10 +70,9 @@ function formatStored(ts) {
55
70
  return String(ts);
56
71
  }
57
72
  }
58
- /** 上游给的 name 自带「 (免费)」尾巴,已经用 Tag 表达了就别重复一遍。 */
59
- const TRAILING_FREE = /\s*[((]\s*免费\s*[))]\s*$/;
73
+ /** 目录里的 name 自带「 · x0.18 · 福利」尾巴,这里已经用 Tag 表达了就别重复。 */
60
74
  function modelName(choice) {
61
- return choice.free === true ? choice.name.replace(TRAILING_FREE, "") : choice.name;
75
+ return stripModelSuffixes(choice.name);
62
76
  }
63
77
  /** 官方用量明细页。 */
64
78
  const OFFICIAL_USAGE_URL = "https://codearts.huaweicloud.com/portal/settings/personal-usage";
@@ -306,6 +320,8 @@ function CodeArtsPluginCard({ t }) {
306
320
  const [savingModels, setSavingModels] = (0, react.useState)(false);
307
321
  const [savedModels, setSavedModels] = (0, react.useState)(false);
308
322
  const [modelError, setModelError] = (0, react.useState)(null);
323
+ const [refreshing, setRefreshing] = (0, react.useState)(false);
324
+ const [refreshMsg, setRefreshMsg] = (0, react.useState)(null);
309
325
  /** 账号身份是否展开:默认遮蔽(false = 显示 *******)。 */
310
326
  const [accountsRevealed, setAccountsRevealed] = (0, react.useState)(false);
311
327
  const [claiming, setClaiming] = (0, react.useState)(false);
@@ -409,6 +425,7 @@ function CodeArtsPluginCard({ t }) {
409
425
  }
410
426
  }
411
427
  const all = selection?.choices.length ?? 0;
428
+ const blockedCount = selection?.choices.filter((c) => c.blocked === true).length ?? 0;
412
429
  const wire = draft.length === all ? [] : draft;
413
430
  const savedIds = selection?.choices.filter((c) => c.enabled).map((c) => c.id) ?? [];
414
431
  const dirty = savedIds.length !== draft.length || savedIds.some((id) => !draft.includes(id));
@@ -443,6 +460,35 @@ function CodeArtsPluginCard({ t }) {
443
460
  setSavingModels(false);
444
461
  }
445
462
  }
463
+ /**
464
+ * 手动重拉一次模型目录。
465
+ *
466
+ * 存在的理由:限时福利名单会轮换(模型被移出、被转正),后台每小时才拉一次,
467
+ * 干等或重启都不是办法。顺带清掉「福利今日不可用」标记 —— 活动回来时这是唯一
468
+ * 不用等第二天的出口。
469
+ */
470
+ async function refreshCatalog() {
471
+ setRefreshing(true);
472
+ setRefreshMsg(null);
473
+ try {
474
+ const res = await fetch("/api/codearts/refresh-models", { method: "POST" });
475
+ const data = await res.json().catch(() => null);
476
+ if (!res.ok) throw new Error(data?.message ?? `HTTP ${res.status}`);
477
+ if (data?.selection !== void 0) {
478
+ setSelection(data.selection);
479
+ setDraft(data.selection.choices.filter((c) => c.enabled).map((c) => c.id));
480
+ setSavedModels(true);
481
+ }
482
+ setRefreshMsg(data?.refreshed === false ? t("refreshStale", { n: data.models ?? 0 }) : t("refreshed", {
483
+ n: data?.models ?? 0,
484
+ benefit: data?.benefit ?? 0
485
+ }));
486
+ } catch (err) {
487
+ setRefreshMsg(t("refreshFailed", { message: err instanceof Error ? err.message : String(err) }));
488
+ } finally {
489
+ setRefreshing(false);
490
+ }
491
+ }
446
492
  async function startOAuthLogin() {
447
493
  setBusy(true);
448
494
  setMsg(null);
@@ -721,35 +767,56 @@ function CodeArtsPluginCard({ t }) {
721
767
  children: [
722
768
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
723
769
  className: CodeArtsPluginCard_module_css_default.sectionHead,
724
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
725
- className: CodeArtsPluginCard_module_css_default.sectionLabel,
726
- children: t("modelsTitle")
727
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
728
- className: CodeArtsPluginCard_module_css_default.count,
729
- children: t("selectedCount", {
730
- n: draft.length,
731
- total: all
770
+ children: [
771
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
772
+ className: CodeArtsPluginCard_module_css_default.sectionLabel,
773
+ children: t("modelsTitle")
774
+ }),
775
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
776
+ className: CodeArtsPluginCard_module_css_default.count,
777
+ children: t("selectedCount", {
778
+ n: draft.length,
779
+ total: all
780
+ })
781
+ }),
782
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
783
+ variant: "outline",
784
+ size: "sm",
785
+ disabled: refreshing,
786
+ onClick: () => void refreshCatalog(),
787
+ children: refreshing ? t("refreshing") : t("refreshModels")
732
788
  })
733
- })]
789
+ ]
734
790
  }),
735
791
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
736
792
  className: CodeArtsPluginCard_module_css_default.hint,
737
793
  children: selection.restricted ? t("optionalModelsHint") : t("optionalModelsAllHint")
738
794
  }),
795
+ blockedCount > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
796
+ className: CodeArtsPluginCard_module_css_default.hint,
797
+ children: t("benefitBlockedHint", { n: blockedCount })
798
+ }),
739
799
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
740
800
  className: CodeArtsPluginCard_module_css_default.choices,
741
801
  children: selection.choices.map((choice) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
742
802
  className: CodeArtsPluginCard_module_css_default.choiceRow,
743
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Checkbox, {
744
- className: CodeArtsPluginCard_module_css_default.check,
745
- checked: draft.includes(choice.id),
746
- disabled: !selection.writable || savingModels,
747
- onChange: (on) => setModel(choice.id, on),
748
- label: modelName(choice)
749
- }), choice.free === true && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tag, {
750
- tone: "success",
751
- children: t("free")
752
- })]
803
+ children: [
804
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Checkbox, {
805
+ className: CodeArtsPluginCard_module_css_default.check,
806
+ checked: draft.includes(choice.id),
807
+ disabled: !selection.writable || savingModels,
808
+ onChange: (on) => setModel(choice.id, on),
809
+ label: modelName(choice)
810
+ }),
811
+ choice.benefit === true && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tag, {
812
+ tone: choice.blocked === true ? "warning" : "success",
813
+ children: choice.blocked === true ? t("benefitBlocked") : t("benefit")
814
+ }),
815
+ choice.ratio !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tag, {
816
+ tone: "quiet",
817
+ children: choice.ratio
818
+ })
819
+ ]
753
820
  }, choice.id))
754
821
  }),
755
822
  selection.writable && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -924,7 +991,14 @@ const zh = {
924
991
  optionalModelsAllHint: "所有模型默认都出现在选择器里;取消勾选不想用的即可。",
925
992
  selectedCount: "已选 {n} / {total}",
926
993
  selectAll: "全选",
927
- free: "免费",
994
+ benefit: "福利",
995
+ benefitBlocked: "福利今日不可用",
996
+ benefitBlockedHint: "{n} 个福利模型今天被上游判定没有权益(活动领完或渠道未开),发出去会直接报 benefit not found。",
997
+ refreshModels: "刷新模型列表",
998
+ refreshing: "刷新中…",
999
+ refreshed: "已刷新:{n} 个模型,其中 {benefit} 个走福利路由",
1000
+ refreshStale: "上游没回应,仍是旧名单({n} 个模型)",
1001
+ refreshFailed: "刷新失败:{message}",
928
1002
  saveModels: "保存模型选择",
929
1003
  saving: "保存中…",
930
1004
  saved: "已保存",
@@ -1000,7 +1074,14 @@ const en = {
1000
1074
  optionalModelsAllHint: "All models show in the picker by default; uncheck the ones you don’t want.",
1001
1075
  selectedCount: "{n} of {total} selected",
1002
1076
  selectAll: "Select all",
1003
- free: "Free",
1077
+ benefit: "Benefit",
1078
+ benefitBlocked: "Benefit unavailable today",
1079
+ 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.",
1080
+ refreshModels: "Refresh models",
1081
+ refreshing: "Refreshing…",
1082
+ refreshed: "Refreshed: {n} models, {benefit} on the benefit route",
1083
+ refreshStale: "Upstream did not answer; still showing the old {n} models",
1084
+ refreshFailed: "Refresh failed: {message}",
1004
1085
  saveModels: "Save selection",
1005
1086
  saving: "Saving…",
1006
1087
  saved: "Saved",
package/lib/index.d.mts CHANGED
@@ -7,18 +7,18 @@ import { AttachmentStore } from "@deepseek-ai/dsh-attachment";
7
7
  /**
8
8
  * CodeArts model catalog.
9
9
  *
10
- * 目录有两条来源:
11
- * 1. **动态发现**(`src/discovery.ts` + `upstream.fetchModels`):启动与每小时
12
- * 各拉一次,取内置 / 限时福利 / agent-center 三路的并集。这是正常路径。
13
- * 2. **静态种子**(`FALLBACK_CODEARTS_MODELS`):仅在发现失败或尚未登录时兜底。
10
+ * **目录只放上游给的东西。** 唯一来源是动态发现(`src/discovery.ts` +
11
+ * `upstream.fetchModels`):启动、每小时、以及卡片上「刷新模型列表」各拉一次,
12
+ * 取内置 / 限时福利 / agent-center 三路的并集。上游没返回的模型就是没有,
13
+ * 未登录或三路全失败时目录是空的 —— 不拿一份历史名单冒充「可用模型」。
14
14
  *
15
- * 种子的来龙去脉(诚实标注,别把它当权威):
16
- * - 福利模型的 id 已与真实账号的 `gateway/config` 对齐;
17
- * - 商业模型的 id 可信(来自 codearts2api 的 `seedKnownModels`),但
18
- * `contextWindow` / `maxTokens` / `supportsImages` 是**占位值**,发现成功时
19
- * 会被上游真实值覆盖 —— 这正是 `fillFromSeed` 只补缺、不覆盖的原因。
20
- * 写错的 contextWindow 只影响 DSH 的上下文计量与截断余量,不影响路由;
21
- * 写错的 maxTokens 会直接让请求失败(见 `MEASURED_MAX_TOKENS`)。
15
+ * `FALLBACK_CODEARTS_MODELS` 因此**不再往目录里塞模型**,只剩一个按 id 的参数表:
16
+ * 上游有时不给 `contextWindow` / `maxTokens`,而写错的 `maxTokens` 会让每个请求
17
+ * 直接失败(见 `MEASURED_MAX_TOKENS`)。它只给发现到的模型补参数,别的什么都不管。
18
+ *
19
+ * 福利路由同理:只有 `gateway/config` 亲口给出的模型才算福利(见 `isBenefitModel`)。
20
+ * 这条渠道 2026-10-21 起整体不可用,于是福利模型当天就从目录里消失 —— 那是事实,
21
+ * 不是故障,硬撑只会把能正常用的积分模型推到一条不存在的渠道上。
22
22
  *
23
23
  * @module dsh-codearts/catalog
24
24
  */
@@ -29,6 +29,12 @@ interface CodeArtsModelInfo {
29
29
  contextWindow: number;
30
30
  maxTokens: number;
31
31
  supportsImages: boolean;
32
+ /**
33
+ * 积分消耗倍率(上游 `credit[].ratio_display` 的原值,如 `0.18x`)。
34
+ *
35
+ * 来自模型发现;福利模型由展示层强制成 `x0.00`,不依赖上游是否给 credit。
36
+ */
37
+ ratio?: string;
32
38
  /**
33
39
  * 该模型走限时福利(免费套餐)路由。
34
40
  *
@@ -38,12 +44,11 @@ interface CodeArtsModelInfo {
38
44
  benefit?: boolean;
39
45
  }
40
46
  /**
41
- * Static seed roster. Ids are the *upstream registered* spellings (case
42
- * matters — `canonicalModel` in upstream.ts passes these through verbatim).
43
- * Updated 2026-09 from codearts2api's known model set.
47
+ * 参数表(不是模型名单):只给**发现到的**模型补 `contextWindow` / `maxTokens` /
48
+ * `supportsImages`,绝不会因为这里有条目就把模型塞进目录。
44
49
  *
45
50
  * maxTokens 注记(2026-09-30 实测,scripts/param-probe.mjs 直连上游二分验证):
46
- * 免费福利 DeepSeek 模型的 max_tokens 服务端硬上限是 **65536**,更大的值
51
+ * 福利 DeepSeek 模型的 max_tokens 服务端硬上限是 **65536**,更大的值
47
52
  * (73728 / 98304 / 131072 / 384000 / 393216)一律流内报
48
53
  * InferHub.001001005.400 "The request param is invalid, Please check it"。
49
54
  * 此前的 384000/393216 是占位值:DSH 会把 catalog 的 maxTokens 作为
@@ -52,6 +57,7 @@ interface CodeArtsModelInfo {
52
57
  declare const FALLBACK_CODEARTS_MODELS: readonly CodeArtsModelInfo[];
53
58
  /** Mutable catalog shared by the shim's `/v1/models` and the adapter. */
54
59
  declare class CodeArtsCatalog {
60
+ /** 空目录起步:没发现过就是没有模型,不拿历史名单冒充。 */
55
61
  private models;
56
62
  current(): readonly CodeArtsModelInfo[];
57
63
  set(models: readonly CodeArtsModelInfo[]): void;
package/lib/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { C as isBenefitModel, D as codeartsAuthPath, E as codeartsAccountsPath, O as startLogin, S as filterEnabledModels, T as CodeArtsCredentialStore, _ as pickDailyWelfareCampaign, a as SNAP_HOST, b as CodeArtsCatalog, c as claimDailyWelfare, d as fetchModels, f as fetchWelfareCampaigns, g as maxTokensCeilingFor, h as isWelfareClaimedStatus, i as refreshCatalog, l as degradeTokenCeiling, m as isParamInvalidFrame, n as codeartsUsagePath, o as canonicalModel, p as isConcurrencyCap, r as discoverModels, s as chatStream, t as CodeArtsUsageTracker, u as fetchCallerIdentity, v as prepareChatBody, w as CodeArtsAccountStore, x as FALLBACK_CODEARTS_MODELS, y as signedGet } from "./usage-B4vQb1sP.mjs";
1
+ import { A as isBenefitModel, C as upstreamErrorMessage, D as creditRatioLabel, E as clearBenefitBlocked, F as startLogin, M as CodeArtsCredentialStore, N as codeartsAccountsPath, O as filterEnabledModels, P as codeartsAuthPath, S as signedGet, T as FALLBACK_CODEARTS_MODELS, _ as isWelfareClaimedStatus, a as SNAP_HOST, b as prepareChatBody, c as claimDailyWelfare, d as fetchModels, f as fetchWelfareCampaigns, g as isTpmLimit, h as isQueueableRate, i as refreshCatalog, j as CodeArtsAccountStore, k as isBenefitBlocked, l as degradeTokenCeiling, m as isParamInvalidFrame, n as codeartsUsagePath, o as canonicalModel, p as isConcurrencyCap, r as discoverModels, s as chatStream, t as CodeArtsUsageTracker, u as fetchCallerIdentity, v as maxTokensCeilingFor, w as CodeArtsCatalog, x as rateLimitFrame, y as pickDailyWelfareCampaign } from "./usage-Djtou78p.mjs";
2
2
  import z from "@deepseek-ai/schemastery";
3
3
  import { randomBytes, timingSafeEqual } from "node:crypto";
4
4
  import { createServer } from "node:http";
@@ -380,7 +380,8 @@ var CodeArtsCreditCache = class {
380
380
  * models and points pi-ai at the shim's OpenAI-compatible endpoint.
381
381
  *
382
382
  * MVP simplifications vs dsh-codebuddy-cli:
383
- * - no billing rate in the model name (per-request cost not exposed by the API)
383
+ * - only the default-slot credit multiplier goes into the model name (`(x0.18)`);
384
+ * per-request cost is not modelled (subscription quota, cost reported as zero)
384
385
  * - no per-model reasoning ladder (models are surfaced as plain chat models)
385
386
  *
386
387
  * @module dsh-codearts/adapter
@@ -655,21 +656,6 @@ const SERVER_COOLDOWN_MS = 3e4;
655
656
  /** 零散错误累计到阈值后冷却该账号。 */
656
657
  const ERROR_COOLDOWN_MS = 6e4;
657
658
  const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
658
- /**
659
- * 在**同一个账号**上重发,直到不再撞上限或预算耗尽。
660
- *
661
- * 只在没有别的账号可选时使用 —— 等 52s 是最后手段,能换号就换号。
662
- * 命中上限不甩给 pi-ai:429 会被重试 5 次,每次再去要一个会话,越试越满。
663
- */
664
- async function openStream(credential, prepared, signal) {
665
- const until = Date.now() + CAP_WAIT_LIMIT;
666
- for (;;) {
667
- const result = await chatStream(credential, prepared, signal);
668
- if (result.ok) return result;
669
- if (!(isConcurrencyCap(result) && !signal.aborted && Date.now() + CAP_RETRY_GAP <= until)) return result;
670
- await sleep(CAP_RETRY_GAP);
671
- }
672
- }
673
659
  function createCodeArtsShim(options) {
674
660
  const { store, catalog } = options;
675
661
  const logger = options.logger;
@@ -775,6 +761,21 @@ function createCodeArtsShim(options) {
775
761
  const reader = result.stream.getReader();
776
762
  const first = await reader.read();
777
763
  const head = first.done ? void 0 : first.value;
764
+ const rate = head === void 0 ? void 0 : rateLimitFrame(new TextDecoder().decode(head));
765
+ if (rate !== void 0) {
766
+ reader.releaseLock();
767
+ await result.stream.cancel().catch(() => {});
768
+ logger?.warn(`dsh-codearts: ${prepared.model} 首帧被判限流(${rate.code}),进入本地排队`);
769
+ return {
770
+ prepared,
771
+ result: {
772
+ ok: false,
773
+ kind: "soft_rate",
774
+ status: 429,
775
+ message: `${rate.code}: ${rate.message}`
776
+ }
777
+ };
778
+ }
778
779
  const shouldHeal = heal < MAX_CEILING_HEALS && head !== void 0 && isParamInvalidFrame(new TextDecoder().decode(head));
779
780
  reader.releaseLock();
780
781
  if (!shouldHeal) return {
@@ -795,6 +796,39 @@ function createCodeArtsShim(options) {
795
796
  logger?.warn(`dsh-codearts: ${prepared.model} 被判参数非法,max_tokens 降档到 ${maxTokensCeilingFor(prepared.model)} 后重发`);
796
797
  }
797
798
  }
799
+ /**
800
+ * 在**同一个账号**上重发,直到不再被限流或预算耗尽。
801
+ *
802
+ * 并发槽位是账号级的:有别的号就该换号,等 52s 是最后手段。
803
+ * TPM 是模型级配额:换号只是换个账号撞同一堵墙,只能原地等窗口滑过去 ——
804
+ * 间隔逐次翻倍(3s → 6s → 12s → 24s,封顶 30s),避免在配额已经满的时候
805
+ * 把重试本身也打成一波流量。
806
+ *
807
+ * 命中限流不甩给 pi-ai:429 会被重试 5 次,每次再撞一次限流,越试越满。
808
+ */
809
+ async function openStream(credential, raw, contextWindow, signal) {
810
+ const started = Date.now();
811
+ const until = started + CAP_WAIT_LIMIT;
812
+ let gap = CAP_RETRY_GAP;
813
+ for (;;) {
814
+ const opened = await openWithCeilingHeal(credential, raw, contextWindow, signal);
815
+ const waitedMs = Date.now() - started;
816
+ if (opened.result.ok || signal.aborted) return {
817
+ ...opened,
818
+ waitedMs
819
+ };
820
+ if (!isQueueableRate(opened.result)) return {
821
+ ...opened,
822
+ waitedMs
823
+ };
824
+ if (isTpmLimit(opened.result)) gap = Math.min(gap * 2, 3e4);
825
+ if (Date.now() + gap > until) return {
826
+ ...opened,
827
+ waitedMs
828
+ };
829
+ await sleep(gap);
830
+ }
831
+ }
798
832
  /** 先交回降档判定读过的那一帧(它是流的开头,丢了就少一段),再接着读剩下的。 */
799
833
  async function* drainFrames(reader, head) {
800
834
  if (head !== void 0) yield head;
@@ -838,6 +872,8 @@ function createCodeArtsShim(options) {
838
872
  pool.sync(ids);
839
873
  const tried = /* @__PURE__ */ new Set();
840
874
  let cappedAccount;
875
+ let tpmAccount;
876
+ let queueWaitedMs = 0;
841
877
  let lastKind;
842
878
  let lastStatus = 0;
843
879
  let lastMessage = "";
@@ -874,6 +910,10 @@ function createCodeArtsShim(options) {
874
910
  cappedAccount = id;
875
911
  continue;
876
912
  }
913
+ if (isTpmLimit(result)) {
914
+ tpmAccount = id;
915
+ break;
916
+ }
877
917
  if (result.kind === "session_dead") {
878
918
  pool.disable(id, result.message);
879
919
  continue;
@@ -889,39 +929,41 @@ function createCodeArtsShim(options) {
889
929
  }
890
930
  }
891
931
  if (controller.signal.aborted) return;
892
- if (cappedAccount !== void 0) {
893
- if (pool.acquire(cappedAccount)) {
894
- const prepared = prepareChatBody(raw, contextWindow);
895
- try {
896
- const credential = await store.resolve(cappedAccount);
897
- if (credential !== void 0) {
898
- const result = await openStream(credential, prepared, controller.signal);
899
- if (controller.signal.aborted) {
900
- if (result.ok) await result.stream.cancel().catch(() => {});
901
- return;
902
- }
903
- if (result.ok) {
904
- pool.noteSuccess(cappedAccount);
905
- await pipeToResponse(res, result.stream, controller.signal, {
906
- accountId: cappedAccount,
907
- model: prepared.model
908
- });
909
- return;
910
- }
911
- lastKind = result.kind;
912
- lastStatus = result.status;
913
- lastMessage = result.message;
932
+ const queueAccount = cappedAccount ?? tpmAccount;
933
+ if (queueAccount !== void 0) {
934
+ if (pool.acquire(queueAccount)) try {
935
+ const credential = await store.resolve(queueAccount);
936
+ if (credential !== void 0) {
937
+ const opened = await openStream(credential, raw, contextWindow, controller.signal);
938
+ const { result } = opened;
939
+ queueWaitedMs = opened.waitedMs;
940
+ if (controller.signal.aborted) {
941
+ if (result.ok) await result.stream.cancel().catch(() => {});
942
+ return;
914
943
  }
915
- } finally {
916
- pool.release(cappedAccount);
944
+ if (result.ok) {
945
+ pool.noteSuccess(queueAccount);
946
+ await pipeToResponse(res, result.stream, controller.signal, {
947
+ accountId: queueAccount,
948
+ model: opened.prepared.model
949
+ }, opened.head);
950
+ return;
951
+ }
952
+ lastKind = result.kind;
953
+ lastStatus = result.status;
954
+ lastMessage = result.message;
917
955
  }
956
+ } finally {
957
+ pool.release(queueAccount);
918
958
  }
919
959
  }
920
960
  if (lastKind === void 0) {
921
961
  writeOpenAIError(res, 429, "soft_rate", "codearts 当前没有可用账号(全部被禁用或冷却中),请稍后重试");
922
962
  return;
923
963
  }
924
- writeOpenAIError(res, KIND_STATUS[lastKind], lastKind, `codearts upstream ${lastKind} (http ${lastStatus}): ${lastMessage.slice(0, 400)}`);
964
+ const explanation = upstreamErrorMessage(lastMessage, lastStatus);
965
+ const waited = queueWaitedMs >= 1e3 ? `(本地已排队重试 ${Math.round(queueWaitedMs / 1e3)} 秒)` : "";
966
+ writeOpenAIError(res, KIND_STATUS[lastKind], lastKind, `${explanation}${waited}`);
925
967
  }
926
968
  /**
927
969
  * 记下一帧里的上游流内错误。判据宽松(帧文本含 "error" 才解析),解析不出来就忽略 ——
@@ -1014,11 +1056,13 @@ const CODEARTS_LOGIN_STATUS_PATH = "/api/codearts/login/status";
1014
1056
  const CODEARTS_ACCOUNTS_PATH = "/api/codearts/accounts";
1015
1057
  /** POST /api/codearts/claim — 领一次福利中心的每日签到积分(幂等由上游按天去重)。 */
1016
1058
  const CODEARTS_CLAIM_PATH = "/api/codearts/claim";
1059
+ /** POST /api/codearts/refresh-models — 立刻重拉一次模型目录,并清掉「福利今日不可用」标记。 */
1060
+ const CODEARTS_REFRESH_MODELS_PATH = "/api/codearts/refresh-models";
1017
1061
  /**
1018
1062
  * 由完整目录 + 已存 enabled 计算 selection。
1019
1063
  * - enabled 为空/未存 => 全部勾选(restricted=false)
1020
1064
  * - enabled 为非空列表 => 仅勾选其中的(restricted=true),缺的按未勾选
1021
- * 福利模型用 catalog 的 benefit 标记(这里靠 model.free 字段,由调用方注入)。
1065
+ * 福利模型用 catalog 的 benefit 标记(这里靠 model.benefit 字段,由调用方注入)。
1022
1066
  */
1023
1067
  function selectionOf(models, enabled, writable) {
1024
1068
  const allowed = new Set(enabled ?? []);
@@ -1027,13 +1071,29 @@ function selectionOf(models, enabled, writable) {
1027
1071
  choices: models.map((m) => ({
1028
1072
  id: m.id,
1029
1073
  name: m.name,
1030
- free: m.free,
1074
+ benefit: m.benefit,
1075
+ blocked: m.blocked,
1076
+ ratio: m.ratio,
1031
1077
  enabled: restricted ? allowed.has(m.id) : true
1032
1078
  })),
1033
1079
  restricted,
1034
1080
  writable
1035
1081
  };
1036
1082
  }
1083
+ /** 目录 -> 勾选 UI 的选项(福利标记、今日不可用标记与倍率都在这里算)。 */
1084
+ function choicesOf(deps) {
1085
+ return deps.models().map((m) => {
1086
+ const benefit = isBenefitModel(m.id);
1087
+ const ratio = creditRatioLabel(m.ratio, benefit);
1088
+ return {
1089
+ id: m.id,
1090
+ name: m.name,
1091
+ benefit,
1092
+ blocked: isBenefitBlocked(m.id),
1093
+ ...ratio === void 0 ? {} : { ratio }
1094
+ };
1095
+ });
1096
+ }
1037
1097
  function json(res, status, body) {
1038
1098
  const payload = JSON.stringify(body);
1039
1099
  res.writeHead(status, {
@@ -1225,11 +1285,7 @@ function modelsSelectionHandler(deps) {
1225
1285
  return;
1226
1286
  }
1227
1287
  if (req.method === "GET") {
1228
- const all = deps.models().map((m) => ({
1229
- id: m.id,
1230
- name: m.name,
1231
- free: isBenefitModel(m.id)
1232
- }));
1288
+ const all = choicesOf(deps);
1233
1289
  const writable = deps.setEnabledModels !== void 0;
1234
1290
  json(res, 200, { selection: selectionOf(all, deps.enabledModels?.(), writable) });
1235
1291
  return;
@@ -1254,11 +1310,7 @@ function modelsSelectionHandler(deps) {
1254
1310
  json(res, 403, { error: "selection-not-writable" });
1255
1311
  return;
1256
1312
  }
1257
- json(res, 200, { selection: selectionOf(deps.models().map((m) => ({
1258
- id: m.id,
1259
- name: m.name,
1260
- free: isBenefitModel(m.id)
1261
- })), deps.enabledModels?.(), true) });
1313
+ json(res, 200, { selection: selectionOf(choicesOf(deps), deps.enabledModels?.(), true) });
1262
1314
  } catch (error) {
1263
1315
  json(res, 500, { error: safeMessage(error) });
1264
1316
  }
@@ -1375,6 +1427,43 @@ function claimHandler(deps) {
1375
1427
  }
1376
1428
  };
1377
1429
  }
1430
+ /**
1431
+ * POST /api/codearts/refresh-models — 立刻重拉目录,并清掉「福利今日不可用」标记。
1432
+ *
1433
+ * 清标记放在拉目录**之前**:福利回来时这是唯一的自救出口,不能让用户等到明天。
1434
+ * 代价是「今天确实领完了」的那种缺失也会被一并清掉,再点一次就重新记上。
1435
+ */
1436
+ function refreshModelsHandler(deps) {
1437
+ return async (req, res) => {
1438
+ if (req.method !== "POST") {
1439
+ json(res, 405, { error: "method not allowed" });
1440
+ return;
1441
+ }
1442
+ if (!loopbackRequest(req)) {
1443
+ json(res, 403, { error: "request-not-trusted" });
1444
+ return;
1445
+ }
1446
+ if (deps.refreshModels === void 0) {
1447
+ json(res, 404, { error: "refresh not available" });
1448
+ return;
1449
+ }
1450
+ try {
1451
+ clearBenefitBlocked();
1452
+ const counted = await deps.refreshModels();
1453
+ const selection = selectionOf(choicesOf(deps), deps.enabledModels?.(), deps.setEnabledModels !== void 0);
1454
+ json(res, 200, {
1455
+ ok: true,
1456
+ ...counted,
1457
+ selection
1458
+ });
1459
+ } catch (error) {
1460
+ json(res, 500, {
1461
+ ok: false,
1462
+ message: safeMessage(error)
1463
+ });
1464
+ }
1465
+ };
1466
+ }
1378
1467
  /** Mount the status / models / token routes. */
1379
1468
  function registerCodeArtsStatusRoute(ctx, deps) {
1380
1469
  ctx.effect(() => {
@@ -1421,6 +1510,11 @@ function registerCodeArtsStatusRoute(ctx, deps) {
1421
1510
  path: CODEARTS_CLAIM_PATH,
1422
1511
  handler: claimHandler(deps)
1423
1512
  });
1513
+ const disposeRefresh = ctx.webServer.register({
1514
+ kind: "exact",
1515
+ path: CODEARTS_REFRESH_MODELS_PATH,
1516
+ handler: refreshModelsHandler(deps)
1517
+ });
1424
1518
  return () => {
1425
1519
  disposeStatus();
1426
1520
  disposeModels();
@@ -1430,6 +1524,7 @@ function registerCodeArtsStatusRoute(ctx, deps) {
1430
1524
  disposeLoginStatus();
1431
1525
  disposeAccounts();
1432
1526
  disposeClaim();
1527
+ disposeRefresh();
1433
1528
  for (const s of activeLogins.values()) s.close();
1434
1529
  activeLogins.clear();
1435
1530
  };
@@ -1650,6 +1745,8 @@ function apply(ctx, rawConfig) {
1650
1745
  await settings.update(settingsCompatNamespace(ctx), { enabledModels: [...ids] });
1651
1746
  return true;
1652
1747
  };
1748
+ /** 手动「刷新模型列表」的预算:三路来源各打一次,比每小时的后台刷新更该给足时间。 */
1749
+ const MANUAL_REFRESH_MS = 6e4;
1653
1750
  ctx.inject(["webServer"], (webCtx) => registerCodeArtsStatusRoute(webCtx, {
1654
1751
  store,
1655
1752
  models: () => catalog.current(),
@@ -1692,6 +1789,15 @@ function apply(ctx, rawConfig) {
1692
1789
  message: error instanceof Error ? error.message : String(error)
1693
1790
  };
1694
1791
  }
1792
+ },
1793
+ refreshModels: async () => {
1794
+ const outcome = await refreshCatalog(store, catalog, AbortSignal.timeout(MANUAL_REFRESH_MS));
1795
+ const models = catalog.current();
1796
+ return {
1797
+ models: models.length,
1798
+ benefit: models.filter((model) => isBenefitModel(model.id)).length,
1799
+ refreshed: outcome?.anySucceeded === true
1800
+ };
1695
1801
  }
1696
1802
  }));
1697
1803
  installSettingsSectionCompat(ctx, CODEARTS_SETTINGS_NS, Config, config, {
@@ -830,12 +830,11 @@ var CodeArtsAccountStore = class {
830
830
  //#endregion
831
831
  //#region src/catalog.ts
832
832
  /**
833
- * Static seed roster. Ids are the *upstream registered* spellings (case
834
- * matters — `canonicalModel` in upstream.ts passes these through verbatim).
835
- * Updated 2026-09 from codearts2api's known model set.
833
+ * 参数表(不是模型名单):只给**发现到的**模型补 `contextWindow` / `maxTokens` /
834
+ * `supportsImages`,绝不会因为这里有条目就把模型塞进目录。
836
835
  *
837
836
  * maxTokens 注记(2026-09-30 实测,scripts/param-probe.mjs 直连上游二分验证):
838
- * 免费福利 DeepSeek 模型的 max_tokens 服务端硬上限是 **65536**,更大的值
837
+ * 福利 DeepSeek 模型的 max_tokens 服务端硬上限是 **65536**,更大的值
839
838
  * (73728 / 98304 / 131072 / 384000 / 393216)一律流内报
840
839
  * InferHub.001001005.400 "The request param is invalid, Please check it"。
841
840
  * 此前的 384000/393216 是占位值:DSH 会把 catalog 的 maxTokens 作为
@@ -900,36 +899,73 @@ const FALLBACK_CODEARTS_MODELS = [
900
899
  },
901
900
  {
902
901
  id: "deepseek-v4-flash-0731",
903
- name: "DeepSeek-V4-Flash (免费)",
902
+ name: "DeepSeek-V4-Flash (福利)",
904
903
  contextWindow: 131072,
905
904
  maxTokens: 65536,
906
905
  supportsImages: true
907
906
  },
908
907
  {
909
908
  id: "deepseek-v4-pro-0813",
910
- name: "DeepSeek-V4-Pro (免费)",
909
+ name: "DeepSeek-V4-Pro (福利)",
911
910
  contextWindow: 131072,
912
911
  maxTokens: 65536,
913
912
  supportsImages: true
914
913
  },
915
914
  {
916
915
  id: "deepseek-v4.1-flash",
917
- name: "DeepSeek-V4.1-Flash (免费)",
916
+ name: "DeepSeek-V4.1-Flash",
918
917
  contextWindow: 131072,
919
918
  maxTokens: 65536,
920
919
  supportsImages: true
921
920
  },
922
921
  {
923
922
  id: "glm-5.3-flash",
924
- name: "GLM-5.3-Flash (免费)",
923
+ name: "GLM-5.3-Flash (福利)",
925
924
  contextWindow: 128e3,
926
925
  maxTokens: 8e3,
927
926
  supportsImages: true
928
927
  }
929
928
  ];
929
+ /**
930
+ * 倍率的展示写法:福利恒为 `x0.00`,其余用上游的数值,拿不到就不显示。
931
+ *
932
+ * 福利模型不走积分,倍率显示成 0 比显示上游某个残留 credit 更接近事实。
933
+ */
934
+ function creditRatioLabel(ratio, benefit) {
935
+ if (benefit) return "x0.00";
936
+ const value = (ratio ?? "").trim().replace(/x$/i, "").trim();
937
+ return /^\d+(?:\.\d+)?$/.test(value) ? `x${Number(value).toFixed(2)}` : void 0;
938
+ }
939
+ /**
940
+ * 模型在宿主下拉里的**名字**带一个标记尾巴:` · x0.18`、` · x0.00 · 福利`。
941
+ *
942
+ * 写法照搬 dsh-codebuddy-cli 的 provider:倍率在前、上游徽章措辞在后,统一用
943
+ * ` · ` 连接(`Name · x0.71 · 限时免费`)。宿主自己的模型下拉只渲染文字,
944
+ * 插不进 Tag,所以那是用户唯一能看见标记的地方;插件卡片会反过来把尾巴剥掉、
945
+ * 改挂 Tag(见 client 的 modelName),两边都不会重复。
946
+ */
947
+ function modelDisplayName(name, benefit, ratio) {
948
+ const base = stripModelSuffixes(name);
949
+ const parts = [creditRatioLabel(ratio, benefit), benefit ? "福利" : void 0].filter((part) => part !== void 0);
950
+ return parts.length === 0 ? base : `${base} · ${parts.join(" · ")}`;
951
+ }
952
+ /**
953
+ * 剥掉模型名尾巴上的标记:` · x0.18`、` · x0.00 · 福利`、`(福利)`、`(免费)`、
954
+ * `(x0.18)` 等(兼容旧格式)。
955
+ */
956
+ function stripModelSuffixes(name) {
957
+ let base = name;
958
+ for (;;) {
959
+ const next = base.replace(/\s*·\s*(?:x\d+(?:\.\d+)?|(?:限时)?(?:免费|福利)(?:\s+x\d+(?:\.\d+)?)?)\s*$/, "");
960
+ if (next === base) break;
961
+ base = next;
962
+ }
963
+ return base.replace(/\s*[((]\s*(?:(?:免费|福利)(?:\s+x\d+(?:\.\d+)?)?|x\d+(?:\.\d+)?)\s*[))]\s*$/, "").trim();
964
+ }
930
965
  /** Mutable catalog shared by the shim's `/v1/models` and the adapter. */
931
966
  var CodeArtsCatalog = class {
932
- models = FALLBACK_CODEARTS_MODELS;
967
+ /** 空目录起步:没发现过就是没有模型,不拿历史名单冒充。 */
968
+ models = [];
933
969
  current() {
934
970
  return this.models;
935
971
  }
@@ -947,19 +983,15 @@ function filterEnabledModels(models, enabled) {
947
983
  const kept = models.filter((model) => allow.has(model.id));
948
984
  return kept.length === 0 ? models : kept;
949
985
  }
950
- const benefitLower = new Set([
951
- "deepseek-v4-flash-0731",
952
- "deepseek-v4-pro-0813",
953
- "deepseek-v4.1-flash",
954
- "glm-5.3-flash"
955
- ].map((m) => m.toLowerCase()));
956
986
  /**
957
- * 动态发现确认的福利模型(小写 id)。
987
+ * 动态发现确认的福利模型(小写 id)—— 这是「是不是福利」的**唯一**依据。
988
+ *
989
+ * 只认 `gateway/config` 给过的名单,没有静态兜底:猜错一次的代价不对称 ——
990
+ * 漏带 `maas_type: benefit` 是福利模型报 404(它本来就不可用),多带一次却是把
991
+ * 一个正常能用的积分模型推进一条不存在的渠道,直接 benefit not found。
958
992
  *
959
993
  * 与 codearts2api 的差别:那边按账号隔离目录,本插件的聊天链路(shim → upstream)
960
- * 拿不到账号上下文,所以这里取**并集**并保留种子兜底 —— 宁可多带一次
961
- * `maas_type: benefit` 头,也不要漏带导致 404。套餐轮换后模型「转正」时,
962
- * 下次发现会把它从这份集合里去掉。
994
+ * 拿不到账号上下文,所以这里取所有成功账号的并集。
963
995
  */
964
996
  const discoveredBenefit = /* @__PURE__ */ new Set();
965
997
  /** 用一次发现的结果替换动态福利集合(传空数组表示「本轮确认没有福利模型」)。 */
@@ -973,15 +1005,45 @@ function setDiscoveredBenefitModels(ids) {
973
1005
  /**
974
1006
  * 该模型是否走限时福利路由(按小写 ID 判定)。
975
1007
  *
976
- * 发现结果优先于种子:目录里明确「不是福利」的模型(已转正)不会被种子重新标成福利,
977
- * 因为发现成功时会把它从 `discoveredBenefit` 里剔除,而种子只在该集合为空时兜底。
1008
+ * 只认发现结果:`gateway/config` 没给过的模型一律不算福利,没有静态兜底。
978
1009
  */
979
1010
  function isBenefitModel(id) {
1011
+ return discoveredBenefit.has(id.trim().toLowerCase());
1012
+ }
1013
+ /**
1014
+ * 今天被上游判定「没有福利权益」的模型(小写 id -> 本地日期键)。
1015
+ *
1016
+ * 判据只有一个:聊天回 `InferHub.4004.200 benefit not found`。上游没有任何接口能
1017
+ * 提前告诉我们福利余量(`statistics/plugin` 的 `model_quota_num` 实测恒为 -1,
1018
+ * 唯一能给数的 opengw balance 也不覆盖福利池),所以只能「失败一次才知道」。
1019
+ *
1020
+ * 按本地日期存:福利是每天发的,昨天的失败不代表今天。卡片据此把模型标成
1021
+ * 「福利今日不可用」,但**不**替用户取消勾选 —— 那是改他的设置。
1022
+ */
1023
+ const benefitBlocked = /* @__PURE__ */ new Map();
1024
+ function localDayKey(at = Date.now()) {
1025
+ const d = new Date(at);
1026
+ const month = String(d.getMonth() + 1).padStart(2, "0");
1027
+ const day = String(d.getDate()).padStart(2, "0");
1028
+ return `${d.getFullYear()}-${month}-${day}`;
1029
+ }
1030
+ /** 记一次「这个模型今天没有福利权益」。 */
1031
+ function markBenefitBlocked(id) {
1032
+ const key = id.trim().toLowerCase();
1033
+ if (key !== "") benefitBlocked.set(key, localDayKey());
1034
+ }
1035
+ /** 该模型今天是否已经因为福利权益缺失失败过。 */
1036
+ function isBenefitBlocked(id) {
980
1037
  const key = id.trim().toLowerCase();
981
- if (key === "") return false;
982
- if (discoveredBenefit.has(key)) return true;
983
- if (discoveredBenefit.size > 0) return false;
984
- return benefitLower.has(key);
1038
+ const day = benefitBlocked.get(key);
1039
+ if (day === void 0) return false;
1040
+ if (day === localDayKey()) return true;
1041
+ benefitBlocked.delete(key);
1042
+ return false;
1043
+ }
1044
+ /** 清空福利缺失记录 —— 手动刷新模型列表时调,给用户一个自救出口。 */
1045
+ function clearBenefitBlocked() {
1046
+ benefitBlocked.clear();
985
1047
  }
986
1048
  /**
987
1049
  * 合并多路来源的模型清单(内置 / 限时福利 / agent-center)。
@@ -1016,12 +1078,14 @@ function richerModel(current, next) {
1016
1078
  const benefit = current.benefit === true || next.benefit === true;
1017
1079
  const keep = richerThan(next, current) ? next : current;
1018
1080
  const drop = keep === next ? current : next;
1081
+ const ratio = keep.ratio ?? drop.ratio;
1019
1082
  return {
1020
1083
  id: keep.id,
1021
1084
  name: keep.name || drop.name || keep.id,
1022
1085
  contextWindow: keep.contextWindow > 0 ? keep.contextWindow : drop.contextWindow,
1023
1086
  maxTokens: keep.maxTokens > 0 ? keep.maxTokens : drop.maxTokens,
1024
1087
  supportsImages: keep.supportsImages || drop.supportsImages,
1088
+ ...ratio === void 0 ? {} : { ratio },
1025
1089
  ...benefit ? { benefit: true } : {}
1026
1090
  };
1027
1091
  }
@@ -1032,29 +1096,26 @@ function richerThan(a, b) {
1032
1096
  return a.id !== a.id.toLowerCase() && b.id === b.id.toLowerCase();
1033
1097
  }
1034
1098
  /**
1035
- * 用种子清单给**发现结果**补缺:种子只补,不覆盖。
1099
+ * 用参数表给**发现结果**补 0 值字段。参数表里有、上游这次没返回的模型 —— 不塞进目录。
1036
1100
  *
1037
- * 为什么不能把种子一起丢进 `mergeModelLists`:那个函数按「参数更全/大小写更像注册名」
1038
- * 选记录,种子里的占位值(例如 OpenPangu 的 ctx 32000 / max 4000)会凭大小写规则
1101
+ * 为什么不能把参数表一起丢进 `mergeModelLists`:那个函数按「参数更全/大小写更像注册名」
1102
+ * 选记录,表里的占位值(例如 OpenPangu 的 ctx 32000 / max 4000)会凭大小写规则
1039
1103
  * 压过上游返回的真实值(ctx 524288 / max 131072)。所以顺序必须是
1040
- * 「先合并发现结果 → 再用种子补 0 值字段与缺失条目」。
1104
+ * 「先合并发现结果 → 再用参数表补 0 值字段」。
1041
1105
  */
1042
- function fillFromSeed(discovered, seed = FALLBACK_CODEARTS_MODELS) {
1106
+ function backfillParams(discovered, params = FALLBACK_CODEARTS_MODELS) {
1043
1107
  const byId = /* @__PURE__ */ new Map();
1044
1108
  for (const model of discovered) byId.set(model.id.toLowerCase(), { ...model });
1045
- for (const fallback of seed) {
1046
- const key = fallback.id.toLowerCase();
1047
- const current = byId.get(key);
1048
- if (current === void 0) {
1049
- byId.set(key, { ...fallback });
1050
- continue;
1051
- }
1052
- byId.set(key, {
1109
+ for (const fallback of params) {
1110
+ const current = byId.get(fallback.id.toLowerCase());
1111
+ if (current === void 0) continue;
1112
+ byId.set(fallback.id.toLowerCase(), {
1053
1113
  id: current.id,
1054
1114
  name: current.name || fallback.name,
1055
1115
  contextWindow: current.contextWindow > 0 ? current.contextWindow : fallback.contextWindow,
1056
1116
  maxTokens: current.maxTokens > 0 ? current.maxTokens : fallback.maxTokens,
1057
1117
  supportsImages: current.supportsImages || fallback.supportsImages,
1118
+ ...current.ratio === void 0 ? {} : { ratio: current.ratio },
1058
1119
  ...current.benefit === true ? { benefit: true } : {}
1059
1120
  });
1060
1121
  }
@@ -1161,10 +1222,12 @@ function signRequest(req, body, cred) {
1161
1222
  * @module dsh-codearts/upstream
1162
1223
  */
1163
1224
  var upstream_exports = /* @__PURE__ */ __exportAll({
1225
+ BENEFIT_MISSING_CODE: () => BENEFIT_MISSING_CODE,
1164
1226
  CONCURRENCY_CAP_CODE: () => CONCURRENCY_CAP_CODE,
1165
1227
  MAX_TOKENS_CEILING: () => MAX_TOKENS_CEILING,
1166
1228
  PARAM_INVALID_CODE: () => PARAM_INVALID_CODE,
1167
1229
  SNAP_HOST: () => SNAP_HOST,
1230
+ TPM_LIMIT_CODE: () => TPM_LIMIT_CODE,
1168
1231
  canonicalModel: () => canonicalModel,
1169
1232
  chatStream: () => chatStream,
1170
1233
  claimBenefit: () => claimBenefit,
@@ -1174,13 +1237,18 @@ var upstream_exports = /* @__PURE__ */ __exportAll({
1174
1237
  fetchCallerIdentity: () => fetchCallerIdentity,
1175
1238
  fetchModels: () => fetchModels,
1176
1239
  fetchWelfareCampaigns: () => fetchWelfareCampaigns,
1240
+ isBenefitMissing: () => isBenefitMissing,
1177
1241
  isConcurrencyCap: () => isConcurrencyCap,
1178
1242
  isParamInvalidFrame: () => isParamInvalidFrame,
1243
+ isQueueableRate: () => isQueueableRate,
1244
+ isTpmLimit: () => isTpmLimit,
1179
1245
  isWelfareClaimedStatus: () => isWelfareClaimedStatus,
1180
1246
  maxTokensCeilingFor: () => maxTokensCeilingFor,
1181
1247
  pickDailyWelfareCampaign: () => pickDailyWelfareCampaign,
1182
1248
  prepareChatBody: () => prepareChatBody,
1183
- signedGet: () => signedGet
1249
+ rateLimitFrame: () => rateLimitFrame,
1250
+ signedGet: () => signedGet,
1251
+ upstreamErrorMessage: () => upstreamErrorMessage
1184
1252
  });
1185
1253
  const SNAP_HOST = "https://snap-access.cn-north-4.myhuaweicloud.com";
1186
1254
  const EP_CHAT = "/api/v2/chat/completions";
@@ -1448,6 +1516,85 @@ const CONCURRENCY_CAP_CODE = "TM.00001041";
1448
1516
  function isConcurrencyCap(result) {
1449
1517
  return !result.ok && result.message.includes("TM.00001041");
1450
1518
  }
1519
+ /**
1520
+ * 模型级 TPM(tokens per minute)配额超限。2026-10-21 实测:它和并发上限不同,
1521
+ * 是**流内首帧**(HTTP 200)回的,所以照样绕过了 classify;换号也没用(配额按
1522
+ * 模型共享),能做的只有本地排队等窗口滑过去。
1523
+ *
1524
+ * 上游的码并不固定:同一小时内见过 `InferHub.ModelArts.81111.429`("The model TPM
1525
+ * limit has been significantly exceeded.")和 `InferHub.ModelArts.81114.429`
1526
+ * ("Too many requests, the rate limit is 50000000 tokens per minute.")。所以按
1527
+ * 形状认(ModelArts.*.429)而不是按某一个常量认。
1528
+ */
1529
+ const TPM_LIMIT_CODE = "InferHub.ModelArts.81111.429";
1530
+ const MODEL_RATE_CODE = /InferHub\.ModelArts\.\d+\.429/;
1531
+ const MODEL_RATE_TEXT = /tpm limit|rate limit|too many requests/i;
1532
+ function isTpmLimit(result) {
1533
+ if (result.ok) return false;
1534
+ return MODEL_RATE_CODE.test(result.message) || MODEL_RATE_TEXT.test(result.message);
1535
+ }
1536
+ /** 撞到这两类限流都值得原地等,而不是直接甩给 pi-ai 重试。 */
1537
+ function isQueueableRate(result) {
1538
+ return isConcurrencyCap(result) || isTpmLimit(result);
1539
+ }
1540
+ /**
1541
+ * 从一帧已转成 OpenAI 形状的 SSE 里认出「可排队限流」。
1542
+ *
1543
+ * 只在**还没有任何内容产出**的首帧上使用:此时重发是安全的;一旦下游已经收到
1544
+ * 字节,HTTP 头也发出去了,就只能把错误帧原样交出去。
1545
+ */
1546
+ function rateLimitFrame(text) {
1547
+ if (!text.includes("error")) return void 0;
1548
+ for (const line of text.split("\n")) {
1549
+ const trimmed = line.trim();
1550
+ if (!trimmed.startsWith("data:")) continue;
1551
+ const payload = trimmed.slice(5).trim();
1552
+ if (payload === "" || payload === "[DONE]") continue;
1553
+ let ev;
1554
+ try {
1555
+ ev = JSON.parse(payload);
1556
+ } catch {
1557
+ continue;
1558
+ }
1559
+ const code = String(ev.error?.code ?? ev.error_code ?? "");
1560
+ const message = String(ev.error?.message ?? ev.error_msg ?? "");
1561
+ const probe = {
1562
+ ok: false,
1563
+ kind: "soft_rate",
1564
+ status: 429,
1565
+ message: `${code}: ${message}`
1566
+ };
1567
+ if (code === "TM.00001041" || isTpmLimit(probe)) return {
1568
+ code: code === "" ? TPM_LIMIT_CODE : code,
1569
+ message
1570
+ };
1571
+ }
1572
+ }
1573
+ /**
1574
+ * 把上游错误翻译成用户能看懂的一句话。
1575
+ *
1576
+ * 这些错误都发生在「账号、插件都正常」的时候 —— 原样把英文码甩给用户,只会
1577
+ * 让人以为是自己配错了。默认分支仍保留原始信息,便于排查。
1578
+ */
1579
+ function upstreamErrorMessage(message, status = 0) {
1580
+ if (MODEL_RATE_CODE.test(message) || MODEL_RATE_TEXT.test(message)) return "华为侧该模型当前请求过多(共享 TPM 超限),不是你的账号或插件问题。稍后重试,或临时改用 GLM-5.2 / GLM-5.3。";
1581
+ if (message.includes("TM.00001041")) return "CodeArts 免费档同时只允许 3 个会话,当前槽位一直占满(被打断的会话约 1 分钟才释放)。请稍后重试。";
1582
+ if (isBenefitMissing("", message)) return "这个模型属于限时福利,当前福利通道不可用(不是账号问题)。可以刷新模型列表,改用非福利模型。";
1583
+ if (message.toLowerCase().includes("model is not registered")) return "上游没有注册这个模型,它可能已经下线;请刷新模型列表后重选。";
1584
+ return `codearts 上游错误 (http ${status}): ${message.slice(0, 400)}`;
1585
+ }
1586
+ /**
1587
+ * 「这个账号当前没有该模型的限时福利权益」。2026-10-21 实测:上游回 HTTP 200,
1588
+ * 错误在**流内帧**里(不在状态码上),所以它既不走 retry 也不走换号,用户看到的
1589
+ * 就是正文里裸裸一句 `benefit not found`。
1590
+ *
1591
+ * 这条不是请求写错了,重发也不会好:福利模型只有福利路由,去掉 `maas_type: benefit`
1592
+ * 就变成 InferHub.002002009.404 model is not registered。唯一出路是等活动回来。
1593
+ */
1594
+ const BENEFIT_MISSING_CODE = "InferHub.4004.200";
1595
+ function isBenefitMissing(code, message) {
1596
+ return code === "InferHub.4004.200" || message.toLowerCase().includes("benefit not found");
1597
+ }
1451
1598
  function classify(status, message) {
1452
1599
  const m = message.toLowerCase();
1453
1600
  if (status === 401 || status === 403) return "session_dead";
@@ -1524,6 +1671,7 @@ async function chatStream(credential, prepared, signal) {
1524
1671
  }
1525
1672
  if (!resp.ok) {
1526
1673
  const text = await resp.text().catch(() => "");
1674
+ if (isBenefitMissing("", text)) markBenefitBlocked(prepared.model);
1527
1675
  return {
1528
1676
  ok: false,
1529
1677
  kind: classify(resp.status, text),
@@ -1582,6 +1730,7 @@ async function chatStream(credential, prepared, signal) {
1582
1730
  continue;
1583
1731
  }
1584
1732
  if (ev.error_code && ev.error_code !== "0" && ev.error_code !== 0) {
1733
+ if (isBenefitMissing(String(ev.error_code), String(ev.error_msg ?? ""))) markBenefitBlocked(model);
1585
1734
  controller.enqueue(openAIData({
1586
1735
  choices: [{
1587
1736
  delta: { content: "" },
@@ -1727,6 +1876,21 @@ function asArray(value) {
1727
1876
  return Array.isArray(value) ? value : [];
1728
1877
  }
1729
1878
  /**
1879
+ * 从 `credit[]` 里取常规时段的展示倍率(原样 `0.18x`)。
1880
+ *
1881
+ * 上游给每个时段一条(`default` / `off_peak`,实测 2026-10-21)。这里只取
1882
+ * `default`:尖峰价才是用户平时付的,低峰 0.09x 属于额外优惠,拿来当标题会低估。
1883
+ * 没有 credit 的模型(如 Qwen3-VL)返回 undefined —— 界面上就不显示倍率。
1884
+ */
1885
+ function creditRatioOf(record) {
1886
+ const credits = asArray(record.credit).map(asRecord);
1887
+ if (credits.length === 0) return void 0;
1888
+ const pick = credits.find((entry) => entry.time_slot_id === "default") ?? credits.find((entry) => entry.time_slot_id === null || entry.time_slot_id === void 0) ?? credits[0];
1889
+ if (pick === void 0) return void 0;
1890
+ const match = /^\s*(\d+(?:\.\d+)?)\s*x\s*$/i.exec(firstString(pick.ratio_display));
1891
+ return match === null ? void 0 : `${match[1]}x`;
1892
+ }
1893
+ /**
1730
1894
  * 内置模型:`GET /v1/model/builtin`(`Agent-Type: PromptCenter`)。
1731
1895
  * 实测响应 `{ count, builtinModels: [{ model_id, model_name, model_desc,
1732
1896
  * enable, display_enabled, supports_images, ... }] }` —— 注意它**不带**
@@ -1742,12 +1906,14 @@ async function fetchBuiltinModels(credential, signal) {
1742
1906
  if (model.enable === false) continue;
1743
1907
  const id = firstString(model.model_id, model.model_name);
1744
1908
  if (id === "") continue;
1909
+ const ratio = creditRatioOf(model);
1745
1910
  out.push({
1746
1911
  id,
1747
1912
  name: firstString(model.model_name, id),
1748
1913
  contextWindow: positive(model.context_window),
1749
1914
  maxTokens: positive(model.max_tokens),
1750
- supportsImages: model.supports_images === true
1915
+ supportsImages: model.supports_images === true,
1916
+ ...ratio === void 0 ? {} : { ratio }
1751
1917
  });
1752
1918
  }
1753
1919
  return out;
@@ -1768,12 +1934,14 @@ async function fetchBenefitModels(credential, signal) {
1768
1934
  const model = asRecord(raw);
1769
1935
  const id = firstString(model.model_id, model.model_name);
1770
1936
  if (id === "") continue;
1937
+ const ratio = creditRatioOf(model);
1771
1938
  out.push({
1772
1939
  id,
1773
1940
  name: firstString(model.model_name, id),
1774
1941
  contextWindow: positive(model.context_window),
1775
1942
  maxTokens: positive(model.max_tokens),
1776
1943
  supportsImages: model.supports_images === true,
1944
+ ...ratio === void 0 ? {} : { ratio },
1777
1945
  benefit: true
1778
1946
  });
1779
1947
  }
@@ -1813,12 +1981,14 @@ async function fetchAgentModels(credential, signal) {
1813
1981
  const params = asRecord(model.model_parameters);
1814
1982
  const id = firstString(params.model_id, model.model_id, model.model_alias, model.model_name);
1815
1983
  if (id === "") continue;
1984
+ const ratio = creditRatioOf(model) ?? creditRatioOf(params);
1816
1985
  out.push({
1817
1986
  id,
1818
1987
  name: firstString(model.model_name, model.model_alias, id),
1819
1988
  contextWindow: positive(params.context_window),
1820
1989
  maxTokens: positive(params.max_tokens),
1821
- supportsImages: model.supports_images === true
1990
+ supportsImages: model.supports_images === true,
1991
+ ...ratio === void 0 ? {} : { ratio }
1822
1992
  });
1823
1993
  }
1824
1994
  return out;
@@ -1874,7 +2044,7 @@ async function fetchModels(credential, signal) {
1874
2044
  discovered.push(item.models);
1875
2045
  }
1876
2046
  if (sources.length === 0) throw new Error(`model discovery failed on every source: ${errors.map((e) => `${e.source}: ${e.message}`).join("; ")}`);
1877
- const merged = fillFromSeed(mergeModelLists(discovered));
2047
+ const merged = backfillParams(mergeModelLists(discovered));
1878
2048
  if (merged.length === 0) throw new Error("model discovery returned an empty catalog");
1879
2049
  return {
1880
2050
  models: merged.map((model) => ({
@@ -2181,8 +2351,14 @@ async function discoverModels(store, signal) {
2181
2351
  async function refreshCatalog(store, catalog, signal) {
2182
2352
  const outcome = await discoverModels(store, signal);
2183
2353
  if (!outcome.anySucceeded || outcome.models.length === 0) return outcome;
2184
- catalog.set(outcome.models);
2185
2354
  setDiscoveredBenefitModels(outcome.models.filter((model) => model.benefit === true).map((model) => model.id));
2355
+ catalog.set(outcome.models.map((model) => {
2356
+ const benefit = isBenefitModel(model.id);
2357
+ return {
2358
+ ...model,
2359
+ name: modelDisplayName(model.name, benefit, model.ratio)
2360
+ };
2361
+ }));
2186
2362
  return outcome;
2187
2363
  }
2188
2364
  //#endregion
@@ -2404,4 +2580,4 @@ function sumOf(store) {
2404
2580
  return out;
2405
2581
  }
2406
2582
  //#endregion
2407
- export { isBenefitModel as C, codeartsAuthPath as D, codeartsAccountsPath as E, startLogin as O, filterEnabledModels as S, CodeArtsCredentialStore as T, pickDailyWelfareCampaign as _, SNAP_HOST as a, CodeArtsCatalog as b, claimDailyWelfare as c, fetchModels as d, fetchWelfareCampaigns as f, maxTokensCeilingFor as g, isWelfareClaimedStatus as h, refreshCatalog as i, degradeTokenCeiling as l, isParamInvalidFrame as m, codeartsUsagePath as n, canonicalModel as o, isConcurrencyCap as p, discoverModels as r, chatStream as s, CodeArtsUsageTracker as t, fetchCallerIdentity as u, prepareChatBody as v, CodeArtsAccountStore as w, FALLBACK_CODEARTS_MODELS as x, signedGet as y };
2583
+ export { isBenefitModel as A, upstreamErrorMessage as C, creditRatioLabel as D, clearBenefitBlocked as E, startLogin as F, CodeArtsCredentialStore as M, codeartsAccountsPath as N, filterEnabledModels as O, codeartsAuthPath as P, signedGet as S, FALLBACK_CODEARTS_MODELS as T, isWelfareClaimedStatus as _, SNAP_HOST as a, prepareChatBody as b, claimDailyWelfare as c, fetchModels as d, fetchWelfareCampaigns as f, isTpmLimit as g, isQueueableRate as h, refreshCatalog as i, CodeArtsAccountStore as j, isBenefitBlocked as k, degradeTokenCeiling as l, isParamInvalidFrame as m, codeartsUsagePath as n, canonicalModel as o, isConcurrencyCap as p, discoverModels as r, chatStream as s, CodeArtsUsageTracker as t, fetchCallerIdentity as u, maxTokensCeilingFor as v, CodeArtsCatalog as w, rateLimitFrame as x, pickDailyWelfareCampaign as y };
package/package.json CHANGED
@@ -2,7 +2,8 @@
2
2
  "name": "dsh-codearts",
3
3
  "displayName": "DSH CodeArts Connect",
4
4
  "description": "把华为云 CodeArts 接入 DeepSeek Harness —— 内置 OAuth2 登录(PKCE + DPoP),登录后即可在 DSH 对话窗口使用 CodeArts 的模型。",
5
- "version": "0.5.1",
5
+ "version": "0.5.3",
6
+ "packageManager": "pnpm@11.24.0",
6
7
  "author": "elegant01",
7
8
  "keywords": [
8
9
  "dsh-plugin",