dsh-tap 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +99 -0
- package/CHANGELOG.md +751 -0
- package/LICENSE +21 -0
- package/README.en.md +59 -0
- package/README.md +256 -0
- package/cordis.patch.yml +385 -0
- package/core/bridge.js +698 -0
- package/core/json-store.js +91 -0
- package/core/rotation.js +108 -0
- package/core/usage-meter.js +176 -0
- package/docs/diagnosis-cache-quota.md +181 -0
- package/docs/diagnosis-qoder-flash.md +67 -0
- package/docs/diagnosis-trae-3003.md +302 -0
- package/docs/goals/bridge-port-host-split.md +91 -0
- package/docs/goals/desktop-adaptation.md +106 -0
- package/docs/goals/qoder-cn-provider-design.md +308 -0
- package/docs/goals/settings-card-ux-redesign-plan.md +1680 -0
- package/docs/goals/settings-card-ux-redesign.md +162 -0
- package/docs/goals/trae-agent-v3.md +41 -0
- package/docs/goals/trae-work-cn-repair.md +44 -0
- package/docs/goals/v0.8-/351/242/235/345/272/246/345/217/257/350/247/201-/346/250/241/345/236/213/345/212/250/346/200/201/345/214/226-/345/244/232/346/234/215/345/212/241/345/225/206.md +85 -0
- package/docs/pitfalls.md +105 -0
- package/docs/reverse/trae-cloud-api.md +218 -0
- package/docs/reverse/trae-model-catalog.md +129 -0
- package/docs/reverse/traework-cn.md +520 -0
- package/docs/rules/STATE.md +198 -0
- package/docs/rules/content-moderation.md +54 -0
- package/docs/rules/dev-role-boundary.md +94 -0
- package/docs/rules/extra-providers.md +42 -0
- package/docs/rules/gateway-facts.md +91 -0
- package/docs/rules/oauth-handshake.md +76 -0
- package/docs/rules/prompt-cache.md +93 -0
- package/docs/rules/quota-signals.md +125 -0
- package/docs/rules/routing.md +84 -0
- package/docs/rules/templates/oauth-reverse-checklist.md +42 -0
- package/docs/rules/trae-surface.md +242 -0
- package/docs/rules/ua-validation.md +81 -0
- package/host-config.js +282 -0
- package/index.js +2306 -0
- package/lib/client.js +2893 -0
- package/local-scan.js +104 -0
- package/package.json +82 -0
- package/providers/ark/index.js +11 -0
- package/providers/bailian/index.js +10 -0
- package/providers/bigmodel/index.js +11 -0
- package/providers/codebuddy/agenttool.js +122 -0
- package/providers/codebuddy/catalog.js +227 -0
- package/providers/codebuddy/errors.js +44 -0
- package/providers/codebuddy/headers.js +36 -0
- package/providers/codebuddy/images.js +125 -0
- package/providers/codebuddy/index.js +123 -0
- package/providers/codebuddy/oauth.js +279 -0
- package/providers/deepseek/index.js +11 -0
- package/providers/moonshot/index.js +11 -0
- package/providers/openai-compat.js +177 -0
- package/providers/openrouter/index.js +27 -0
- package/providers/qoder/catalog.js +145 -0
- package/providers/qoder/cosy.js +419 -0
- package/providers/qoder/gateway.js +563 -0
- package/providers/qoder/index.js +116 -0
- package/providers/qoder/oauth.js +364 -0
- package/providers/qoder/qoder_auth.wasm +0 -0
- package/providers/qoder/quota.js +56 -0
- package/providers/qwen/index.js +15 -0
- package/providers/tool-pairing.js +129 -0
- package/providers/trae/catalog.js +103 -0
- package/providers/trae/errors.js +85 -0
- package/providers/trae/gateway.js +853 -0
- package/providers/trae/index.js +126 -0
- package/providers/trae/oauth.js +443 -0
- package/providers/trae/quota.js +75 -0
- package/providers/trae/remote.js +365 -0
- package/scripts/capture-cache.mjs +65 -0
- package/scripts/capture-traffic.mjs +83 -0
- package/scripts/hermes-probe-dev-role.mjs +263 -0
- package/scripts/measure-latency.mjs +253 -0
- package/scripts/probe-ark-thinking.mjs +298 -0
- package/scripts/probe-cache-decline.mjs +292 -0
- package/scripts/probe-cache-ttl.mjs +221 -0
- package/scripts/probe-cache.mjs +156 -0
- package/scripts/probe-codebuddy-efforts.mjs +355 -0
- package/scripts/probe-codebuddy-tier-wiring.mjs +244 -0
- package/scripts/probe-effort-gaps.mjs +133 -0
- package/scripts/probe-media.mjs +115 -0
- package/scripts/probe-moderation.mjs +159 -0
- package/scripts/probe-oauth.mjs +617 -0
- package/scripts/probe-qoder-attribution-arm8.mjs +92 -0
- package/scripts/probe-qoder-attribution-arm9.mjs +102 -0
- package/scripts/probe-qoder-attribution.mjs +266 -0
- package/scripts/probe-qoder-flash-confirm.mjs +94 -0
- package/scripts/probe-qoder-live.mjs +344 -0
- package/scripts/probe-qoder-matrix.mjs +274 -0
- package/scripts/probe-qoder-null-content.mjs +153 -0
- package/scripts/probe-qoder-pairing.mjs +308 -0
- package/scripts/probe-qoder-quota.mjs +162 -0
- package/scripts/probe-qoder-thinking-config.mjs +52 -0
- package/scripts/probe-qoder-thinking-efforts.mjs +269 -0
- package/scripts/probe-quota.mjs +136 -0
- package/scripts/probe-quota2.mjs +144 -0
- package/scripts/probe-routing.mjs +226 -0
- package/scripts/probe-trae-3003-diagnosis.mjs +139 -0
- package/scripts/probe-trae-agent-v3.mjs +399 -0
- package/scripts/probe-trae-efforts.mjs +149 -0
- package/scripts/probe-trae-live.mjs +147 -0
- package/scripts/probe-trae-max-effort.mjs +179 -0
- package/scripts/probe-trae-model-routing.mjs +381 -0
- package/scripts/probe-trae-thinking-scene.mjs +238 -0
- package/scripts/probe-trae-transport-outage.mjs +176 -0
- package/scripts/probe-ua.mjs +508 -0
- package/scripts/trae-model-catalog.mjs +632 -0
- package/scripts/verify-agents-md.mjs +45 -0
- package/scripts/verify-bridge.mjs +1041 -0
- package/scripts/verify-core-generic.mjs +302 -0
- package/scripts/verify-desktop-acceptance.mjs +184 -0
- package/scripts/verify-host-config.mjs +265 -0
- package/scripts/verify-models.mjs +390 -0
- package/scripts/verify-providers.mjs +255 -0
- package/scripts/verify-qoder-provider.mjs +1020 -0
- package/scripts/verify-rotation.mjs +308 -0
- package/scripts/verify-trae-model-catalog.mjs +284 -0
- package/scripts/verify-trae-provider.mjs +1451 -0
package/lib/client.js
ADDED
|
@@ -0,0 +1,2893 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser half of dsh-tap: a settings card. dsh ≥ 0.1.6 renders it in the
|
|
3
|
+
* Plugin Manager panel (slot `plugins.item`, owner props {view}); older hosts
|
|
4
|
+
* render it in Settings → 插件配置 (slot `settings.plugin.item`).
|
|
5
|
+
*
|
|
6
|
+
* Interaction model (2026-09 redesign, 0.10.0 channel accordion): the card is
|
|
7
|
+
* the only thing the host slot gives us, so intuitiveness comes from channel
|
|
8
|
+
* blocks instead of one long scroll or a flat tab bar —
|
|
9
|
+
* 1. Collapsed: the header floats a single on-demand chip (「n 项需处理」,
|
|
10
|
+
* warn/err count from attentionCount) only when something needs
|
|
11
|
+
* attention — the per-block status lines are the real source. (Legacy
|
|
12
|
+
* settings.plugin.item slot only; the Plugin Manager page view has no
|
|
13
|
+
* card header at all.)
|
|
14
|
+
* 2. Expanded: four channel blocks (CodeBuddy / TraeWork CN / Qoder CN /
|
|
15
|
+
* 通用) in fixed order; each head always shows a status line derived from
|
|
16
|
+
* buildChips (tone = the block's worst chip), so warn/err is readable
|
|
17
|
+
* without expanding.
|
|
18
|
+
* 3. Blocks lazy-mount on first open and then stay mounted hidden, so
|
|
19
|
+
* drafts, scroll positions and fetched catalogs survive both collapse
|
|
20
|
+
* and saves. Heavy work stays lazy: model-list / provider-list / trae
|
|
21
|
+
* model-list load when their block first opens; the usage poller only
|
|
22
|
+
* runs while the 通用 block is open. Long background explanations
|
|
23
|
+
* collapse into per-section <details> (HelpNote); short point-of-use
|
|
24
|
+
* hints stay inline.
|
|
25
|
+
*
|
|
26
|
+
* Every schema field in index.js SETTINGS_FIELDS has a home here, plus the
|
|
27
|
+
* two stateful surfaces that are not plain fields: per-model enable/limits
|
|
28
|
+
* (CodeBuddy + Trae) and the extra OpenAI-compatible providers registry.
|
|
29
|
+
*
|
|
30
|
+
* Talks to the host route /dsh-tap/settings:
|
|
31
|
+
* GET → { value, user, oauth, bridge, models, trae, qoder }
|
|
32
|
+
* POST → { patch } | { action: 'oauth-start'|'oauth-status'|'oauth-logout'
|
|
33
|
+
* |'model-list'|'model-sync'|'usage'|'provider-*'|'credential-*'
|
|
34
|
+
* |'trae-oauth-*'|'trae-model-*'|'qoder-oauth-*' }
|
|
35
|
+
* ('usage' → { usage, bridge, quota }: live consumption metered by the
|
|
36
|
+
* bridge from the gateway's per-request usage.credit, plus the account-side
|
|
37
|
+
* quota signals; OAuth mode gets numeric remaining quota from
|
|
38
|
+
* /billing/meter/get-user-resource (docs/rules/quota-signals.md R-Q7),
|
|
39
|
+
* api-key mode falls back to a manual-total estimate, labeled 估算.)
|
|
40
|
+
*
|
|
41
|
+
* Loaded by the dsh web client module loader via package.json
|
|
42
|
+
* exports["./client"]. No build step: plain React.createElement.
|
|
43
|
+
*
|
|
44
|
+
* Host resources this module reuses (see AGENTS.md):
|
|
45
|
+
* - `@deepseek-ai/dsh-client-ui-primitives` (Button/Input/icons) — a
|
|
46
|
+
* platform seed module, require()'d with a native-element fallback so the
|
|
47
|
+
* card still renders if the primitives table ever goes missing.
|
|
48
|
+
* - The `--dsw-alias-*` design tokens (never hard-coded colors; dark theme
|
|
49
|
+
* follows automatically via body[data-ds-dark-theme]). Two token names do
|
|
50
|
+
* NOT exist upstream (`--dsw-alias-accent`, `--dsw-alias-label-error`) —
|
|
51
|
+
* use state-business-primary / state-error-primary instead.
|
|
52
|
+
* - Styling follows the host's own protocol: one injected
|
|
53
|
+
* <style data-plugin="dsh-tap" data-plugin-css="card"> block,
|
|
54
|
+
* cbc- prefixed classes, card shell mirrored from the first-party
|
|
55
|
+
* PluginCard (border-l2, bg-layer-3 → open bg-layer-2, radius 12,
|
|
56
|
+
* padding 14/16).
|
|
57
|
+
*
|
|
58
|
+
* House rules (hard-won, see AGENTS.md pitfalls): all hooks before any
|
|
59
|
+
* conditional return; window.open must fire synchronously inside the click
|
|
60
|
+
* handler or popup blockers eat it; controlled checkboxes can fire change
|
|
61
|
+
* twice — debounce against a useRef table (a per-render object is rebuilt
|
|
62
|
+
* every render and never debounces); field editor components must live at
|
|
63
|
+
* module level or typing loses focus.
|
|
64
|
+
*/
|
|
65
|
+
window.__ModuleLoader__.load({
|
|
66
|
+
id: "dsh-tap",
|
|
67
|
+
factory: (require) => {
|
|
68
|
+
var module = { exports: {} };
|
|
69
|
+
var exports = module.exports;
|
|
70
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
71
|
+
var react = require("react");
|
|
72
|
+
var createElement = react.createElement;
|
|
73
|
+
var useState = react.useState;
|
|
74
|
+
var useEffect = react.useEffect;
|
|
75
|
+
var useRef = react.useRef;
|
|
76
|
+
var Fragment = react.Fragment;
|
|
77
|
+
|
|
78
|
+
// Platform primitives are a seed module; require defensively anyway —
|
|
79
|
+
// a missing table must degrade to native elements, not a dead card.
|
|
80
|
+
var ui = null;
|
|
81
|
+
try { ui = require("@deepseek-ai/dsh-client-ui-primitives"); } catch (e) { ui = null; }
|
|
82
|
+
// 图标命名两代共存(dsh 0.1.7 换代,实测):≤0.1.6 导出带尺寸数字后缀的
|
|
83
|
+
// IconChevronDownOutline14 / IconRefreshOutline14;0.1.7 起数字后缀**全部消失**,
|
|
84
|
+
// 改为 Artwork/Medium/Regular 尺寸变体(且基础名 IconChevronDownOutline 并不导出)。
|
|
85
|
+
// 按候选顺序取第一个存在的——两代宿主都拿到原生图标,全缺才退原生兜底。
|
|
86
|
+
function pickComponent(names) {
|
|
87
|
+
if (!ui) return null;
|
|
88
|
+
for (var i = 0; i < names.length; i++) { if (ui[names[i]]) return ui[names[i]]; }
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
var UIButton = pickComponent(["Button"]);
|
|
92
|
+
var UIInput = pickComponent(["Input"]);
|
|
93
|
+
var IconChevron = pickComponent(["IconChevronDownOutline14", "IconChevronDownOutlineRegular", "IconChevronDownOutlineMedium", "IconChevronDownOutline", "IconChevronDownOutlineArtwork"]);
|
|
94
|
+
var IconRefresh = pickComponent(["IconRefreshOutline14", "IconRefreshOutlineRegular", "IconRefreshOutlineMedium", "IconRefreshOutline", "IconRefreshOutlineArtwork"]);
|
|
95
|
+
|
|
96
|
+
var ROUTE = "/dsh-tap/settings";
|
|
97
|
+
|
|
98
|
+
// fetch 超时护栏:设置服务挂起时卡片不能永久停在"正在读取"(健壮性)。
|
|
99
|
+
// AbortError 换成带原因的中文错误——只写"(网络)"曾误导排查(踩坑 #7)。
|
|
100
|
+
function fetchWithTimeout(url, opts, ms) {
|
|
101
|
+
var ctrl = typeof AbortController !== "undefined" ? new AbortController() : null;
|
|
102
|
+
var timer = ctrl ? setTimeout(function () { ctrl.abort(); }, ms) : null;
|
|
103
|
+
var o = Object.assign({}, opts || {});
|
|
104
|
+
if (ctrl) o.signal = ctrl.signal;
|
|
105
|
+
return fetch(url, o).then(function (r) {
|
|
106
|
+
if (timer) clearTimeout(timer);
|
|
107
|
+
return r;
|
|
108
|
+
}, function (e) {
|
|
109
|
+
if (timer) clearTimeout(timer);
|
|
110
|
+
if (ctrl && e && e.name === "AbortError") throw new Error("设置服务响应超时(" + Math.round(ms / 1000) + "s,网络或服务挂起)");
|
|
111
|
+
throw e;
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ------------------------------------------------------------------
|
|
116
|
+
// Styles: one injected block, cbc- prefixed, token-backed. Idempotent.
|
|
117
|
+
// ------------------------------------------------------------------
|
|
118
|
+
var CSS_TEXT = [
|
|
119
|
+
// ---- card shell(镜像第一方 PluginCard:radius 12 / border-l2 / 层级底色)----
|
|
120
|
+
// 原生控件配色必须绑宿主主题**属性**:宿主在 html/body 上无条件写
|
|
121
|
+
// color-scheme:dark(实测两态计算值恒为 dark),而该属性决定 Chrome 给
|
|
122
|
+
// checkbox/radio 取哪套面板色 ⇒ 浅色主题下未勾选框被画成深色实心块,看着
|
|
123
|
+
// 像"已开启"(区块头常显的通道开关正处在收起态第一眼位置)。宿主主题由
|
|
124
|
+
// body[data-ds-dark-theme] 驱动,prefers-color-scheme 仿真实测零效果 ⇒ 不用
|
|
125
|
+
// @media。卡片根是全卡唯一共同根(page 视图 div.cbc-card / 旧槽 li.cbc-card;
|
|
126
|
+
// summary 视图是一行 span、无任何控件)。
|
|
127
|
+
"body[data-ds-dark-theme] .cbc-card{color-scheme:dark}",
|
|
128
|
+
"body:not([data-ds-dark-theme]) .cbc-card{color-scheme:light}",
|
|
129
|
+
".cbc-card{border:1px solid var(--dsw-alias-border-l2,rgba(128,128,128,.28));border-radius:12px;background:var(--dsw-alias-bg-layer-3,#fff);list-style:none;transition:border-color .16s}",
|
|
130
|
+
".cbc-card:hover{border-color:var(--dsw-alias-label-dimmed,rgba(128,128,128,.5))}",
|
|
131
|
+
".cbc-card.cbc-open{background:var(--dsw-alias-bg-layer-2,#fff)}",
|
|
132
|
+
".cbc-header{all:unset;display:flex;width:100%;box-sizing:border-box;padding:14px 16px;cursor:pointer;align-items:center;justify-content:space-between;gap:12px;border-radius:12px}",
|
|
133
|
+
".cbc-header:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary,#1a66ff);outline-offset:-2px}",
|
|
134
|
+
".cbc-name{font-size:15px;font-weight:600;color:var(--dsw-alias-label-primary,inherit)}",
|
|
135
|
+
".cbc-desc{font-size:13px;color:var(--dsw-alias-label-tertiary,gray);margin-top:2px}",
|
|
136
|
+
".cbc-headchips{display:flex;gap:6px;flex:0 0 auto}",
|
|
137
|
+
".cbc-chevron{display:inline-flex;color:var(--dsw-alias-label-tertiary,gray);font-size:12px;transition:transform .16s}",
|
|
138
|
+
".cbc-chevron.cbc-open{transform:rotate(180deg)}",
|
|
139
|
+
".cbc-body{border-top:1px solid var(--dsw-alias-border-l2,rgba(128,128,128,.2));margin:0 16px;padding:10px 0 12px;display:flex;flex-direction:column}",
|
|
140
|
+
// :where() 零特异性:all:unset 不再压过基类,button 只需补 cursor——消除两份全量属性拷贝的同步义务。
|
|
141
|
+
":where(button).cbc-chip{all:unset}",
|
|
142
|
+
".cbc-chip{display:inline-flex;align-items:center;gap:6px;font-size:12px;line-height:1;padding:5px 10px;border-radius:12px;border:1px solid var(--dsw-alias-border-l2,rgba(128,128,128,.3));background:var(--dsw-alias-bg-layer-1,transparent);color:var(--dsw-alias-label-secondary,inherit);white-space:nowrap;box-sizing:border-box}",
|
|
143
|
+
"button.cbc-chip{cursor:pointer}",
|
|
144
|
+
"button.cbc-chip:hover{border-color:var(--dsw-alias-label-dimmed,rgba(128,128,128,.5));color:var(--dsw-alias-label-primary,inherit)}",
|
|
145
|
+
"button.cbc-chip:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary,#1a66ff);outline-offset:1px}",
|
|
146
|
+
".cbc-dot{width:7px;height:7px;border-radius:50%;flex:0 0 auto;background:var(--dsw-alias-label-tertiary,gray)}",
|
|
147
|
+
".cbc-dot.cbc-ok{background:var(--dsw-alias-state-success-primary,#2a9d4a)}",
|
|
148
|
+
".cbc-dot.cbc-warn{background:var(--dsw-alias-state-warn-primary,#b80)}",
|
|
149
|
+
".cbc-dot.cbc-err{background:var(--dsw-alias-state-error-primary,#d33)}",
|
|
150
|
+
// idle = 「本区块没有通道运行态」(通用区块):空心环,占位不变。
|
|
151
|
+
// off 的实心灰点在三家行里读作「未启用」,借给一个有额度读数的区块是自相矛盾。
|
|
152
|
+
".cbc-dot.cbc-idle{background:transparent;box-shadow:inset 0 0 0 1px var(--dsw-alias-border-l2,rgba(128,128,128,.45))}",
|
|
153
|
+
// ---- 区块头「已保存 ✓」短提示 ----
|
|
154
|
+
// 常驻占位(visibility 切换而非条件渲染):flash 出现/消失瞬间不改变
|
|
155
|
+
// 同行 checkbox 的水平位置,避免 1.8s 内同坐标二次点击误触展开 button。
|
|
156
|
+
".cbc-saveflash{margin-left:auto;font-size:12px;color:var(--dsw-alias-state-success-primary,#2a9d4a);padding:0 8px;white-space:nowrap;visibility:hidden}",
|
|
157
|
+
".cbc-saveflash.cbc-on{visibility:visible}",
|
|
158
|
+
// sr-only(live region 专用):流外 1px clip,文本进出 DOM 不参与布局,
|
|
159
|
+
// 可见行的宽度恒定契约不受影响。
|
|
160
|
+
".cbc-srlive{position:absolute;width:1px;height:1px;margin:-1px;overflow:hidden;clip:rect(0 0 0 0);clip-path:inset(50%);white-space:nowrap}",
|
|
161
|
+
// ---- 分区通用 ----
|
|
162
|
+
".cbc-divider{border:0;border-top:1px solid var(--dsw-alias-border-l2,rgba(128,128,128,.18));margin:4px 0}",
|
|
163
|
+
".cbc-row{display:flex;align-items:center;gap:10px;margin-bottom:8px}",
|
|
164
|
+
".cbc-row-label{flex:0 0 132px;font-size:13px;font-weight:500;color:var(--dsw-alias-label-primary,inherit)}",
|
|
165
|
+
".cbc-row-control{flex:1;display:flex;align-items:center;gap:8px;min-width:0;flex-wrap:wrap}",
|
|
166
|
+
".cbc-check{width:16px;height:16px;flex:0 0 auto;accent-color:var(--dsw-alias-state-business-primary,#1a66ff)}",
|
|
167
|
+
// 命中面 ≥24×24(WCAG 2.2 SC 2.5.8):原生 checkbox 命中就是 16×16,Chrome
|
|
168
|
+
// 对 appearance:checkbox 不吃 input 自身的 padding(命中仍 16×16 且框挪
|
|
169
|
+
// 7px,tmp-geom-probe.js padOnInput 证伪);label 包裹 + 负 margin 让 label
|
|
170
|
+
// 在 flex 布局里仍按 16px 占位——实测命中 31×30、开关 x 与行高不变
|
|
171
|
+
// (padOnLabel 变体)。label 对落在控件自身的 click 不再转发,只对 label
|
|
172
|
+
// 空白处派发一次合成 click ⇒ 不引入双 change(#27 的双 change 是受控回弹
|
|
173
|
+
// 时序问题,与 label 无关;[C3]「恰 1 次 POST」断言在跑)。
|
|
174
|
+
".cbc-checkhit{display:inline-flex;align-items:center;padding:4px;margin:-4px;flex:0 0 auto}",
|
|
175
|
+
".cbc-hint{font-size:12px;color:var(--dsw-alias-label-tertiary,gray);margin:4px 0 6px;line-height:1.5}",
|
|
176
|
+
".cbc-status{font-size:12px;color:var(--dsw-alias-label-tertiary,gray);line-height:1.5}",
|
|
177
|
+
".cbc-error{font-size:12px;color:var(--dsw-alias-state-error-primary,#d33);display:flex;align-items:center;justify-content:space-between;gap:8px;border:1px solid rgba(211,51,51,.35);border-radius:8px;padding:6px 10px;margin:6px 0}",
|
|
178
|
+
".cbc-error-close{all:unset;cursor:pointer;font-size:13px;padding:0 4px;color:inherit;display:inline-flex}",
|
|
179
|
+
".cbc-warn{font-size:12px;color:var(--dsw-alias-state-warn-primary,#b80);line-height:1.5;margin:4px 0}",
|
|
180
|
+
".cbc-muted{color:var(--dsw-alias-label-tertiary,gray);font-size:12px}",
|
|
181
|
+
".cbc-group-title{font-size:12px;font-weight:600;color:var(--dsw-alias-label-tertiary,gray);margin:10px 0 6px}",
|
|
182
|
+
// 模型列表内的子标题(当前可用/未启用/选择器内 n/m):比一级分组标题
|
|
183
|
+
// 低一层,不参与「凭据/模型/工具/网关/高级」的一级序列契约。
|
|
184
|
+
".cbc-subtitle{font-size:12px;font-weight:600;color:var(--dsw-alias-label-secondary,inherit);margin:8px 0 4px}",
|
|
185
|
+
// 模型组操作条(三家同构):同步按钮 + 状态文字 + 筛选框同一行。
|
|
186
|
+
".cbc-syncbar{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin:4px 0 8px}",
|
|
187
|
+
".cbc-link{color:var(--dsw-alias-state-business-primary,#1a66ff);text-decoration:none}",
|
|
188
|
+
".cbc-link:hover{text-decoration:underline}",
|
|
189
|
+
// ---- 列表行 / 徽标 ----
|
|
190
|
+
".cbc-listrow{display:flex;align-items:center;flex-wrap:wrap;row-gap:2px;gap:6px;padding:6px 8px;border-radius:8px;border:1px solid var(--dsw-alias-border-l2,rgba(128,128,128,.25));margin-bottom:6px;font-size:13px;background:var(--dsw-alias-bg-layer-1,transparent)}",
|
|
191
|
+
".cbc-cell-name{font-weight:600;min-width:72px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--dsw-alias-label-primary,inherit)}",
|
|
192
|
+
".cbc-cell-masked{color:var(--dsw-alias-label-tertiary,gray);flex:1;font-family:var(--ds-font-family-code,monospace);font-size:12px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
|
|
193
|
+
".cbc-badge{font-size:11px;color:var(--dsw-alias-state-success-primary,#2a9d4a);border:1px solid rgba(42,157,74,.35);border-radius:4px;padding:1px 4px;white-space:nowrap;flex:0 0 auto}",
|
|
194
|
+
// P2-4:已知不可路由(恒 11102)条目置灰 + 警示色徽标。
|
|
195
|
+
".cbc-listrow.cbc-unroutable{opacity:.55}",
|
|
196
|
+
".cbc-badge-dead{color:var(--dsw-alias-state-warning-primary,#b27d00);border-color:rgba(178,125,0,.4)}",
|
|
197
|
+
".cbc-effort{font-size:11px;color:var(--dsw-alias-label-tertiary,gray);font-family:var(--ds-font-family-code,monospace);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:100%;flex:0 0 auto}",
|
|
198
|
+
".cbc-effort-sel{font-size:11px;padding:2px 4px;max-width:110px;flex:0 0 auto}",
|
|
199
|
+
".cbc-model-name{font-weight:600;flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--dsw-alias-label-primary,inherit)}",
|
|
200
|
+
".cbc-tname{font-weight:600;flex:0 1 auto;min-width:90px;max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--dsw-alias-label-primary,inherit)}",
|
|
201
|
+
".cbc-model-ctx{font-size:11px;font-family:var(--ds-font-family-code,monospace);color:var(--dsw-alias-label-tertiary,gray);white-space:nowrap;flex:0 0 auto}",
|
|
202
|
+
".cbc-radio{display:flex;align-items:center;gap:6px;font-size:13px;cursor:pointer;color:var(--dsw-alias-label-primary,inherit)}",
|
|
203
|
+
".cbc-radio input{accent-color:var(--dsw-alias-state-business-primary,#1a66ff)}",
|
|
204
|
+
".cbc-mode{display:inline-flex;align-items:center;border:1px solid var(--dsw-alias-border-l2,rgba(128,128,128,.3));border-radius:10px;padding:2px;gap:2px}",
|
|
205
|
+
".cbc-mode .cbc-radio{padding:5px 14px;border-radius:8px}",
|
|
206
|
+
// ---- 额度 hero / 统计卡 ----
|
|
207
|
+
".cbc-hero{border:1px solid var(--dsw-alias-border-l2,rgba(128,128,128,.25));border-radius:10px;padding:12px 14px;margin:6px 0 10px;background:var(--dsw-alias-bg-layer-1,transparent)}",
|
|
208
|
+
".cbc-hero-top{display:flex;justify-content:space-between;align-items:baseline;gap:8px}",
|
|
209
|
+
".cbc-hero-label{font-size:12px;color:var(--dsw-alias-label-tertiary,gray)}",
|
|
210
|
+
".cbc-hero-num{font-size:24px;font-weight:700;color:var(--dsw-alias-label-primary,inherit);font-family:var(--ds-font-family-number,var(--ds-font-family-code,inherit))}",
|
|
211
|
+
".cbc-hero-unit{font-size:12px;font-weight:400;color:var(--dsw-alias-label-tertiary,gray);margin-left:4px}",
|
|
212
|
+
".cbc-bar{height:6px;border-radius:3px;background:var(--dsw-alias-bg-layer-3,#eee);margin:8px 0 6px;overflow:hidden}",
|
|
213
|
+
".cbc-bar-fill{height:100%;border-radius:3px;background:var(--dsw-alias-state-business-primary,#1a66ff)}",
|
|
214
|
+
".cbc-hero-sub{font-size:12px;color:var(--dsw-alias-label-tertiary,gray);line-height:1.5}",
|
|
215
|
+
".cbc-stats{display:flex;gap:8px;margin:8px 0}",
|
|
216
|
+
".cbc-stat{flex:1;border:1px solid var(--dsw-alias-border-l2,rgba(128,128,128,.25));border-radius:10px;padding:10px 12px;min-width:0}",
|
|
217
|
+
".cbc-statlabel{font-size:12px;color:var(--dsw-alias-label-tertiary,gray)}",
|
|
218
|
+
".cbc-statnum{font-size:17px;font-weight:600;color:var(--dsw-alias-label-primary,inherit);margin:3px 0 1px}",
|
|
219
|
+
".cbc-minibar{display:inline-block;width:56px;height:4px;border-radius:2px;background:var(--dsw-alias-bg-layer-3,#eee);overflow:hidden;vertical-align:middle;margin-right:6px;flex:0 0 auto}",
|
|
220
|
+
".cbc-minibar-fill{display:block;height:100%;background:var(--dsw-alias-state-business-primary,#1a66ff)}",
|
|
221
|
+
".cbc-toggle{all:unset;display:inline-flex;align-items:center;gap:4px;font-size:12px;cursor:pointer;color:var(--dsw-alias-state-business-primary,#1a66ff);white-space:nowrap}",
|
|
222
|
+
".cbc-toggle:hover{text-decoration:underline}",
|
|
223
|
+
// ---- 控件 ----
|
|
224
|
+
".cbc-w90{width:90px}.cbc-w110{width:110px}.cbc-w140{width:140px}.cbc-w200{width:200px}.cbc-w220{width:220px}.cbc-w260{width:260px}.cbc-wfull{width:100%}",
|
|
225
|
+
".cbc-danger{color:var(--dsw-alias-state-error-primary,#d33)!important}",
|
|
226
|
+
".cbc-select{font:inherit;font-size:13px;height:32px;padding:0 8px;border-radius:8px;border:1px solid var(--dsw-alias-border-l2,rgba(128,128,128,.4));background:var(--dsw-alias-bg-layer-1,transparent);color:var(--dsw-alias-label-primary,inherit);box-sizing:border-box}",
|
|
227
|
+
".cbc-input{font:inherit;font-size:13px;height:32px;padding:0 10px;border-radius:8px;border:1px solid var(--dsw-alias-border-l2,rgba(128,128,128,.4));background:var(--dsw-alias-bg-layer-1,transparent);color:var(--dsw-alias-label-primary,inherit);box-sizing:border-box}",
|
|
228
|
+
".cbc-input:focus-visible{border-color:var(--dsw-alias-state-business-primary,#1a66ff);outline:none}",
|
|
229
|
+
".cbc-btn{all:unset;display:inline-flex;align-items:center;gap:4px;font-size:12px;cursor:pointer;padding:4px 10px;border-radius:14px;white-space:nowrap;color:var(--dsw-alias-label-primary,inherit);box-sizing:border-box}",
|
|
230
|
+
".cbc-btn-outline{border:1px solid var(--dsw-alias-border-l2,rgba(128,128,128,.4))}",
|
|
231
|
+
".cbc-btn-primary{background:var(--dsw-alias-button-primary-fill,#1a66ff);color:var(--dsw-alias-label-primary-foreground,#fff)}",
|
|
232
|
+
// ghost 变名实归:9 处 ghost 按钮(含两处破坏性「删除」与区块头「重置」)此前
|
|
233
|
+
// 无任何规则 ⇒ 零 hover 零焦点环,读起来就是普通文本(区块头紧邻开关时会被
|
|
234
|
+
// 当成开关的标签)。hover 给底色、focus-visible 给环,与 .cbc-headact 同口径。
|
|
235
|
+
".cbc-btn-ghost:hover{background:var(--dsw-alias-bg-layer-1,rgba(128,128,128,.12))}",
|
|
236
|
+
".cbc-btn-ghost:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary,#1a66ff);outline-offset:1px}",
|
|
237
|
+
".cbc-btn:disabled{cursor:not-allowed;opacity:.4}",
|
|
238
|
+
".cbc-scrollbox{max-height:300px;overflow-y:auto}",
|
|
239
|
+
".cbc-addrow{display:flex;align-items:center;gap:8px;margin:6px 0;flex-wrap:wrap}",
|
|
240
|
+
// ---- 幽灵输入(模型行上限):常态无边框、hover/focus 显边框、去数字步进器 ----
|
|
241
|
+
// 宿主 Input 原语把 className 落在 wrapper span(边框也在 wrapper),
|
|
242
|
+
// 焦点在内层 input ⇒ 用 :focus-within;原生兜底路径 className 在 input 自身。
|
|
243
|
+
".cbc-ghost{border-color:transparent!important;background:var(--dsw-alias-bg-layer-1,transparent)!important}",
|
|
244
|
+
".cbc-ghost:hover{border-color:var(--dsw-alias-border-l2,rgba(128,128,128,.4))!important}",
|
|
245
|
+
".cbc-ghost:focus,.cbc-ghost:focus-visible,.cbc-ghost:focus-within{border-color:var(--dsw-alias-state-business-primary,#1a66ff)!important;background:var(--dsw-alias-bg-layer-1,transparent)!important}",
|
|
246
|
+
".cbc-ghost::-webkit-outer-spin-button,.cbc-ghost::-webkit-inner-spin-button,.cbc-ghost input::-webkit-outer-spin-button,.cbc-ghost input::-webkit-inner-spin-button{-webkit-appearance:none;margin:0}",
|
|
247
|
+
".cbc-ghost,.cbc-ghost input{-moz-appearance:textfield;appearance:textfield}",
|
|
248
|
+
".cbc-updated{font-size:12px;color:var(--dsw-alias-label-tertiary,gray)}",
|
|
249
|
+
// ---- 折叠说明(HelpNote):长背景说明默认收起,信息一次点击可达 ----
|
|
250
|
+
".cbc-help{margin:6px 0 2px}",
|
|
251
|
+
".cbc-help summary{cursor:pointer;font-size:12px;color:var(--dsw-alias-label-tertiary,gray);list-style:none;display:inline-flex;align-items:center;gap:4px;user-select:none}",
|
|
252
|
+
".cbc-help summary::-webkit-details-marker{display:none}",
|
|
253
|
+
".cbc-help summary:after{content:\"▸\";font-size:10px}",
|
|
254
|
+
".cbc-help[open] summary:after{content:\"▾\"}",
|
|
255
|
+
".cbc-help summary:hover{color:var(--dsw-alias-label-primary,inherit)}",
|
|
256
|
+
".cbc-help summary:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary,#1a66ff);outline-offset:1px;border-radius:4px}",
|
|
257
|
+
".cbc-helpbody{font-size:12px;color:var(--dsw-alias-label-tertiary,gray);line-height:1.6;margin:4px 0 2px}",
|
|
258
|
+
".cbc-helpbody .cbc-hint{margin:2px 0}",
|
|
259
|
+
// ---- 高级工程项折叠(details.cbc-adv):与使用说明(cbc-help)同款纪律,
|
|
260
|
+
// 开合状态由 <details> 自带,零 JS。域名/baseURL/client_id 收进这里。 ----
|
|
261
|
+
".cbc-adv{margin:10px 0 2px}",
|
|
262
|
+
".cbc-adv>summary{cursor:pointer;font-size:12px;font-weight:600;color:var(--dsw-alias-label-tertiary,gray);list-style:none;display:inline-flex;align-items:center;gap:4px;user-select:none}",
|
|
263
|
+
".cbc-adv>summary::-webkit-details-marker{display:none}",
|
|
264
|
+
".cbc-adv>summary:after{content:\"▸\";font-size:10px}",
|
|
265
|
+
".cbc-adv[open]>summary:after{content:\"▾\"}",
|
|
266
|
+
".cbc-adv>summary:hover{color:var(--dsw-alias-label-primary,inherit)}",
|
|
267
|
+
".cbc-adv>summary:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary,#1a66ff);outline-offset:1px;border-radius:4px}",
|
|
268
|
+
".cbc-advbody{margin-top:6px}",
|
|
269
|
+
// ---- 通道手风琴(0.10.0 交互模型)----
|
|
270
|
+
".cbc-acc{border:1px solid var(--dsw-alias-border-l2,rgba(128,128,128,.25));border-radius:10px;margin-bottom:8px;background:var(--dsw-alias-bg-layer-1,transparent)}",
|
|
271
|
+
".cbc-acc.cbc-open{background:var(--dsw-alias-bg-layer-2,#fff)}",
|
|
272
|
+
".cbc-acc-head{display:flex;align-items:center;gap:8px;padding:10px 12px}",
|
|
273
|
+
".cbc-acc-toggle{all:unset;flex:1 1 auto;display:flex;align-items:center;gap:8px;cursor:pointer;min-width:0}",
|
|
274
|
+
".cbc-acc-toggle:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary,#1a66ff);outline-offset:-2px;border-radius:8px}",
|
|
275
|
+
".cbc-acc-title{font-size:14px;font-weight:600;color:var(--dsw-alias-label-primary,inherit);flex:0 0 auto}",
|
|
276
|
+
".cbc-acc-status{font-size:12px;color:var(--dsw-alias-label-tertiary,gray);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0;flex:1 1 auto}",
|
|
277
|
+
// 状态行 warn/err 段自带色(点仍取最差):整行底色是 tertiary 灰,需要处理
|
|
278
|
+
// 的段落跳出来;token 与 .cbc-dot 同名同值,深色主题自动跟随。
|
|
279
|
+
".cbc-acc-status .cbc-seg-warn{color:var(--dsw-alias-state-warn-primary,#b80)}",
|
|
280
|
+
".cbc-acc-status .cbc-seg-err{color:var(--dsw-alias-state-error-primary,#d33)}",
|
|
281
|
+
".cbc-acc-body{border-top:1px solid var(--dsw-alias-border-l2,rgba(128,128,128,.2));padding:8px 12px 10px}",
|
|
282
|
+
".cbc-acc-body[hidden]{display:none}",
|
|
283
|
+
".cbc-acc-ctl{flex:0 0 auto;display:inline-flex;align-items:center;gap:6px;font-size:12px;color:var(--dsw-alias-label-tertiary,gray)}",
|
|
284
|
+
// P2-3 区块头就地动作(重试监听/复制诊断):仅 warn/err 态渲染,正常态零占位。
|
|
285
|
+
".cbc-acc-acts{flex:0 0 auto;display:inline-flex;align-items:center;gap:4px}",
|
|
286
|
+
".cbc-headact{all:unset;cursor:pointer;font-size:11px;line-height:1.4;padding:2px 7px;border-radius:5px;border:1px solid var(--dsw-alias-border-l2,rgba(128,128,128,.35));color:var(--dsw-alias-label-secondary,gray);white-space:nowrap}",
|
|
287
|
+
".cbc-headact:hover{background:var(--dsw-alias-bg-layer-2,rgba(128,128,128,.12))}",
|
|
288
|
+
".cbc-headact:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary,#1a66ff);outline-offset:1px}",
|
|
289
|
+
".cbc-headact[disabled]{opacity:.5;cursor:default}",
|
|
290
|
+
".cbc-headconfirm{font-size:11px;line-height:1.4;color:var(--dsw-alias-state-warn-primary,#b80);white-space:nowrap;padding:0 2px}",
|
|
291
|
+
".cbc-chevronwrap{flex:0 0 auto;display:inline-flex}",
|
|
292
|
+
].join("\n");
|
|
293
|
+
|
|
294
|
+
function ensureStyles() {
|
|
295
|
+
if (typeof document === "undefined") return;
|
|
296
|
+
if (document.querySelector('style[data-plugin-css="dsh-tap-card"]')) return;
|
|
297
|
+
var el = document.createElement("style");
|
|
298
|
+
// Host protocol: data-plugin / data-plugin-css let the module loader
|
|
299
|
+
// attribute (and hot-clean) injected styles per plugin.
|
|
300
|
+
el.setAttribute("data-plugin", "dsh-tap");
|
|
301
|
+
el.setAttribute("data-plugin-css", "dsh-tap-card");
|
|
302
|
+
el.textContent = CSS_TEXT;
|
|
303
|
+
document.head.appendChild(el);
|
|
304
|
+
}
|
|
305
|
+
ensureStyles();
|
|
306
|
+
|
|
307
|
+
// ------------------------------------------------------------------
|
|
308
|
+
// Control adapters: primitives when available, native fallback when not.
|
|
309
|
+
// ------------------------------------------------------------------
|
|
310
|
+
function CbcButton(props) {
|
|
311
|
+
if (UIButton) {
|
|
312
|
+
return createElement(UIButton, {
|
|
313
|
+
variant: props.variant || "outline",
|
|
314
|
+
size: "sm",
|
|
315
|
+
icon: props.icon || undefined,
|
|
316
|
+
className: props.danger ? "cbc-danger" : undefined,
|
|
317
|
+
type: "button",
|
|
318
|
+
title: props.title,
|
|
319
|
+
disabled: props.disabled,
|
|
320
|
+
onClick: props.onClick,
|
|
321
|
+
}, props.children);
|
|
322
|
+
}
|
|
323
|
+
return createElement("button", {
|
|
324
|
+
type: "button",
|
|
325
|
+
className: "cbc-btn cbc-btn-" + (props.variant || "outline") + (props.danger ? " cbc-danger" : ""),
|
|
326
|
+
title: props.title,
|
|
327
|
+
disabled: props.disabled,
|
|
328
|
+
onClick: props.onClick,
|
|
329
|
+
}, props.icon || null, props.children);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function CbcInput(props) {
|
|
333
|
+
var w = props.widthClass || "cbc-wfull";
|
|
334
|
+
var shared = {
|
|
335
|
+
type: props.type || "text",
|
|
336
|
+
value: props.value,
|
|
337
|
+
placeholder: props.placeholder,
|
|
338
|
+
autoComplete: props.autoComplete,
|
|
339
|
+
"aria-label": props.ariaLabel,
|
|
340
|
+
onInput: props.onInput,
|
|
341
|
+
onBlur: props.onBlur,
|
|
342
|
+
onKeyDown: props.onKeyDown,
|
|
343
|
+
};
|
|
344
|
+
if (UIInput) return createElement(UIInput, Object.assign({ className: w }, shared));
|
|
345
|
+
return createElement("input", Object.assign({ className: "cbc-input " + w }, shared));
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// 折叠说明:长背景说明(>60 字)默认收进 details——每标签少一堵小字墙,
|
|
349
|
+
// 信息一次点击可达;短的就地点提示(模型组/OAuth 覆盖范围/空态)保持直显。
|
|
350
|
+
function HelpNote(props) {
|
|
351
|
+
return createElement("details", { className: "cbc-help" },
|
|
352
|
+
createElement("summary", null, props.label || "使用说明"),
|
|
353
|
+
createElement("div", { className: "cbc-helpbody" }, props.children));
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// 分区级错误隔离:任一标签渲染抛错只塌该标签(fallback + 重试按钮),
|
|
357
|
+
// 不拖垮整卡——React 对未捕获的渲染错误会卸载整棵树(健壮性)。
|
|
358
|
+
var PanelBoundary = (function () {
|
|
359
|
+
class PB extends react.Component {
|
|
360
|
+
constructor(props) {
|
|
361
|
+
super(props);
|
|
362
|
+
this.state = { err: null, nonce: 0 };
|
|
363
|
+
}
|
|
364
|
+
render() {
|
|
365
|
+
if (this.state.err) {
|
|
366
|
+
var self = this;
|
|
367
|
+
return createElement("div", { className: "cbc-error" },
|
|
368
|
+
createElement("span", null, "此分区渲染失败:" + (this.state.err && this.state.err.message ? this.state.err.message : String(this.state.err))),
|
|
369
|
+
createElement("button", {
|
|
370
|
+
type: "button", className: "cbc-error-close", title: "重试渲染该分区",
|
|
371
|
+
onClick: function () { self.setState(function (s) { return { err: null, nonce: s.nonce + 1 }; }); },
|
|
372
|
+
}, "重试"));
|
|
373
|
+
}
|
|
374
|
+
// nonce 进 key:重试时换 key 强制重挂载子树。
|
|
375
|
+
return createElement(Fragment, { key: this.state.nonce }, this.props.children);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
PB.getDerivedStateFromError = function (err) { return { err: err }; };
|
|
379
|
+
// fallback 只显示 message;堆栈进 console,否则排查无线索(踩坑 #7)。
|
|
380
|
+
PB.prototype.componentDidCatch = function (err, info) {
|
|
381
|
+
console.error("[dsh-tap] 分区渲染失败", err, info && info.componentStack);
|
|
382
|
+
};
|
|
383
|
+
return PB;
|
|
384
|
+
})();
|
|
385
|
+
|
|
386
|
+
// 状态点:ok 绿 / warn 黄 / err 红 / off 灰(token 驱动,深色主题自动跟随)。
|
|
387
|
+
function Dot(props) {
|
|
388
|
+
return createElement("span", { className: "cbc-dot" + (props.tone ? " cbc-" + props.tone : "") });
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function fmtTime(ms) {
|
|
392
|
+
if (!ms) return "";
|
|
393
|
+
var d = new Date(ms);
|
|
394
|
+
var p = function (n) { return n < 10 ? "0" + n : "" + n; };
|
|
395
|
+
return (d.getMonth() + 1) + "-" + d.getDate() + " " + p(d.getHours()) + ":" + p(d.getMinutes());
|
|
396
|
+
}
|
|
397
|
+
function fmtCredit(n) {
|
|
398
|
+
if (n == null) return "-";
|
|
399
|
+
return (Math.round(n * 100) / 100).toFixed(2);
|
|
400
|
+
}
|
|
401
|
+
function fmtHit(hit, miss) {
|
|
402
|
+
var total = (hit || 0) + (miss || 0);
|
|
403
|
+
if (!total) return "-";
|
|
404
|
+
return Math.round((hit / total) * 100) + "%";
|
|
405
|
+
}
|
|
406
|
+
// 千分位:791237 → 791,237(用量 token 读数可读性)。
|
|
407
|
+
function fmtNum(n) {
|
|
408
|
+
if (n == null || n === "") return "-";
|
|
409
|
+
return String(n).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
|
410
|
+
}
|
|
411
|
+
function fmtClock(ms) {
|
|
412
|
+
var d = new Date(ms);
|
|
413
|
+
var p = function (n) { return n < 10 ? "0" + n : "" + n; };
|
|
414
|
+
return p(d.getHours()) + ":" + p(d.getMinutes()) + ":" + p(d.getSeconds());
|
|
415
|
+
}
|
|
416
|
+
function pct(remain, size) {
|
|
417
|
+
if (!size || size <= 0) return null;
|
|
418
|
+
return Math.max(0, Math.min(100, Math.round((remain / size) * 100)));
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// ------------------------------------------------------------------
|
|
422
|
+
// Blocks(通道手风琴)。顺序 = 主通道 → 两家订阅通道 → 跨通道通用;
|
|
423
|
+
// 归类原则「谁提供归谁」,证据见 docs/goals/settings-card-ux-redesign.md §3。
|
|
424
|
+
// ------------------------------------------------------------------
|
|
425
|
+
var BLOCK_DEFS = [
|
|
426
|
+
{ id: "codebuddy", title: "CodeBuddy" },
|
|
427
|
+
{ id: "trae", title: "TraeWork CN" },
|
|
428
|
+
{ id: "qoder", title: "Qoder CN" },
|
|
429
|
+
{ id: "general", title: "通用" },
|
|
430
|
+
];
|
|
431
|
+
// 区块 → 分区序列(渲染顺序即数组顺序)。
|
|
432
|
+
var BLOCK_SECTIONS = {
|
|
433
|
+
codebuddy: ["login", "models", "tools", "bridge"],
|
|
434
|
+
trae: ["trae"],
|
|
435
|
+
qoder: ["qoder"],
|
|
436
|
+
general: ["usage", "providers", "hostrecon"],
|
|
437
|
+
};
|
|
438
|
+
var SECTION_RENDERERS = {};
|
|
439
|
+
|
|
440
|
+
// 保存后网关「未监听」的退避补拉节奏(见 CodeBuddyCard 里的 settleGateways)。
|
|
441
|
+
var GATEWAY_SETTLE_DELAYS = [1000, 2000, 4000];
|
|
442
|
+
|
|
443
|
+
// ------------------------------------------------------------------
|
|
444
|
+
// Root card.
|
|
445
|
+
// ------------------------------------------------------------------
|
|
446
|
+
function CodeBuddyCard(props) {
|
|
447
|
+
props = props || {};
|
|
448
|
+
// embedded = dsh 0.1.6 Plugin Manager 的 page 视图:页面自带标题与
|
|
449
|
+
// 返回 crumb,卡片常开且不再画自己的折叠头部。
|
|
450
|
+
var embedded = props.embedded === true;
|
|
451
|
+
var cardOpenState = useState(embedded);
|
|
452
|
+
var cardOpen = cardOpenState[0];
|
|
453
|
+
var setCardOpen = cardOpenState[1];
|
|
454
|
+
var dataState = useState(null);
|
|
455
|
+
var data = dataState[0];
|
|
456
|
+
var setData = dataState[1];
|
|
457
|
+
var errState = useState("");
|
|
458
|
+
var err = errState[0];
|
|
459
|
+
var setErr = errState[1];
|
|
460
|
+
// 手风琴:多开互不联动;展开才挂载、收起不卸载(hidden 保留 DOM,
|
|
461
|
+
// 草稿/滚动/已拉目录跨展开与保存保留)。
|
|
462
|
+
var openBlocksState = useState({});
|
|
463
|
+
var openBlocks = openBlocksState[0];
|
|
464
|
+
var setOpenBlocks = openBlocksState[1];
|
|
465
|
+
var mountedBlocksState = useState({});
|
|
466
|
+
var mountedBlocks = mountedBlocksState[0];
|
|
467
|
+
var setMountedBlocks = mountedBlocksState[1];
|
|
468
|
+
// 「已保存 ✓」落在触发该次保存的区块头右侧,1.8s 自愈。
|
|
469
|
+
var savedState = useState(null); // { block, at }
|
|
470
|
+
var saved = savedState[0];
|
|
471
|
+
var setSaved = savedState[1];
|
|
472
|
+
// P2-3 区块头就地动作状态:retryBusy = 重试中的通道 id;copied = 刚复制过
|
|
473
|
+
// 诊断的通道 id(1.8s 自愈,复用「已保存 ✓」的节奏)。
|
|
474
|
+
var retryState = useState(null);
|
|
475
|
+
var retryBusy = retryState[0];
|
|
476
|
+
var setRetryBusy = retryState[1];
|
|
477
|
+
var copiedState = useState(null);
|
|
478
|
+
var copied = copiedState[0];
|
|
479
|
+
var setCopied = copiedState[1];
|
|
480
|
+
var copiedTimerRef = useRef(null);
|
|
481
|
+
useEffect(function () { return function () { if (copiedTimerRef.current) clearTimeout(copiedTimerRef.current); }; }, []);
|
|
482
|
+
// P3-7 头部开关误触缓解(设计文档 §8 风险表首选方案):Trae/Qoder 区块头
|
|
483
|
+
// 启用开关在**收起态**第一次点击只进确认态(沿用 askConfirm 模式,4s 不确认
|
|
484
|
+
// 自动复位;受控 checkbox 不落 save 会自己弹回原态),展开态直切不确认。
|
|
485
|
+
// 误触代价不对称——收起态即可改路由(1 POST),确认只挡收起态这一面。
|
|
486
|
+
var swConfirmState = useState(null); // 确认中的 fieldKey,或 null
|
|
487
|
+
var swConfirm = swConfirmState[0];
|
|
488
|
+
var setSwConfirm = swConfirmState[1];
|
|
489
|
+
var swConfirmTimerRef = useRef(null);
|
|
490
|
+
useEffect(function () { return function () { if (swConfirmTimerRef.current) clearTimeout(swConfirmTimerRef.current); }; }, []);
|
|
491
|
+
|
|
492
|
+
var toggleBlock = function (id) {
|
|
493
|
+
setOpenBlocks(function (prev) {
|
|
494
|
+
var next = Object.assign({}, prev);
|
|
495
|
+
if (next[id]) delete next[id]; else next[id] = true;
|
|
496
|
+
return next;
|
|
497
|
+
});
|
|
498
|
+
setMountedBlocks(function (prev) {
|
|
499
|
+
if (prev[id]) return prev;
|
|
500
|
+
var next = Object.assign({}, prev);
|
|
501
|
+
next[id] = true;
|
|
502
|
+
return next;
|
|
503
|
+
});
|
|
504
|
+
};
|
|
505
|
+
|
|
506
|
+
// 请求代次(终审 Important 2 / 踩坑 #45 的另一半):save 的 .then 里必跟一次
|
|
507
|
+
// load(),而「四区块可同时展开 + 每分区各持一个 saveIn(block)」让 1 秒内并发
|
|
508
|
+
// 保存成为正常路径。两次 POST 落在同一个 GET 往返窗口(实测 ~1.0–1.3s)时,
|
|
509
|
+
// 先发后回的旧响应会覆掉新响应、UI 停在旧值且主视图无轮询自救 ⇒ 每次 load
|
|
510
|
+
// 领一个代次号,回包时代次不是最新就整包丢弃(含失败分支——陈旧请求的报错也
|
|
511
|
+
// 不该盖住新态)。作废包**不写** dataRef ⇒ ref 里永远是「最新被采纳的那份」。
|
|
512
|
+
// 补拉链的**两个判定点都读 dataRef、都不读回调参数**:启动点(save 的 .then)
|
|
513
|
+
// 与续跑点(settleGateways 的退避回调)同形——load() 被更新的请求抢先时返回的
|
|
514
|
+
// 是 null,任何一处拿 null 去判 pending 都会让链条在那一刻静默停摆(踩坑 #45)。
|
|
515
|
+
var loadGenRef = useRef(0);
|
|
516
|
+
var dataRef = useRef(null);
|
|
517
|
+
// P3-6 冷启动引导:首次被采纳的视图里三通道均无凭据 ⇒ 默认展开 CodeBuddy
|
|
518
|
+
// 区块一次(useRef 守卫——同一挂载内只引导一次,用户手动收起后不被后续
|
|
519
|
+
// load 反复撬开;不引入持久化状态,任一通道有凭据即保持默认全收)。
|
|
520
|
+
var coldStartRef = useRef(false);
|
|
521
|
+
var load = function () {
|
|
522
|
+
var my = ++loadGenRef.current;
|
|
523
|
+
return fetchWithTimeout(ROUTE, { headers: { accept: "application/json" } }, 20000)
|
|
524
|
+
.then(function (r) {
|
|
525
|
+
// Tolerate a non-JSON error page instead of dying in the
|
|
526
|
+
// parser and misreporting it as a network failure.
|
|
527
|
+
return r.text().then(function (t) {
|
|
528
|
+
var d = null;
|
|
529
|
+
try { d = JSON.parse(t); } catch (e) { /* not JSON */ }
|
|
530
|
+
if (d == null) throw new Error("设置服务返回了非 JSON(HTTP " + r.status + ")");
|
|
531
|
+
return d;
|
|
532
|
+
});
|
|
533
|
+
})
|
|
534
|
+
.then(function (d) {
|
|
535
|
+
if (my !== loadGenRef.current) return null; // 有更新的请求在飞/已回 ⇒ 本包作废
|
|
536
|
+
dataRef.current = d;
|
|
537
|
+
setData(d); setErr("");
|
|
538
|
+
if (!coldStartRef.current) {
|
|
539
|
+
coldStartRef.current = true;
|
|
540
|
+
var v = d.value || {};
|
|
541
|
+
var cbHasCred = v.authMode === "oauth" ? !!((d.oauth || {}).signedIn) : !!v.activeApiKey;
|
|
542
|
+
var traeHasCred = !!(((d.trae || {}).oauth || {}).signedIn);
|
|
543
|
+
var qoderHasCred = !!(((d.qoder || {}).oauth || {}).signedIn);
|
|
544
|
+
if (!cbHasCred && !traeHasCred && !qoderHasCred) {
|
|
545
|
+
setOpenBlocks(function (prev) { var n = Object.assign({}, prev); n.codebuddy = true; return n; });
|
|
546
|
+
setMountedBlocks(function (prev) { var n = Object.assign({}, prev); n.codebuddy = true; return n; });
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
return d;
|
|
550
|
+
})
|
|
551
|
+
.catch(function (e) {
|
|
552
|
+
if (my !== loadGenRef.current) return null;
|
|
553
|
+
setErr(e && e.message ? e.message : "设置服务不可达"); return null;
|
|
554
|
+
});
|
|
555
|
+
};
|
|
556
|
+
// Mount 即拉一次(折叠态头部芯片要有真值),每次展开(false→true)
|
|
557
|
+
// 再刷新——收起不拉:[cardOpen] 依赖在 true→false 时同样 fire。
|
|
558
|
+
var prevOpenRef = useRef(null);
|
|
559
|
+
useEffect(function () {
|
|
560
|
+
if (prevOpenRef.current === null || (cardOpen && prevOpenRef.current === false)) load();
|
|
561
|
+
prevOpenRef.current = cardOpen;
|
|
562
|
+
return undefined;
|
|
563
|
+
}, [cardOpen]);
|
|
564
|
+
|
|
565
|
+
// 保存后的网关补拉:三个网关的 running 都由 'listening' 事件异步翻转
|
|
566
|
+
//(index.js syncBridge/syncTraeBridge/syncQoderBridge),紧跟 POST 的 GET
|
|
567
|
+
// 很容易采样到 pre-listening 窗口;主视图没有轮询,这枚假「未监听」会一直
|
|
568
|
+
// 挂到用户收起再展开。有限次退避补拉即自愈;真是 EADDRINUSE 之类的失败,
|
|
569
|
+
// 用尽次数后停手,warn 如实留下。
|
|
570
|
+
var settleRef = useRef(null);
|
|
571
|
+
useEffect(function () {
|
|
572
|
+
return function () { if (settleRef.current) clearTimeout(settleRef.current); };
|
|
573
|
+
}, []);
|
|
574
|
+
var gatewayPending = function (d) {
|
|
575
|
+
if (!d) return false;
|
|
576
|
+
var c = buildChips(d);
|
|
577
|
+
return [c.bridge, c.trae, c.qoder].some(function (x) {
|
|
578
|
+
return !!x && x.tone !== "ok" && x.tone !== "off" && /未监听/.test(x.text);
|
|
579
|
+
});
|
|
580
|
+
};
|
|
581
|
+
var settleGateways = function (attempt) {
|
|
582
|
+
if (settleRef.current) clearTimeout(settleRef.current);
|
|
583
|
+
if (attempt >= GATEWAY_SETTLE_DELAYS.length) return;
|
|
584
|
+
settleRef.current = setTimeout(function () {
|
|
585
|
+
settleRef.current = null;
|
|
586
|
+
// 读 dataRef 而非回调参数:本包若被更新的请求作废,load() 返回 null,
|
|
587
|
+
// 拿 null 去判 pending 会让退避链就地停摆(踩坑 #45 的自愈保护被悄悄削
|
|
588
|
+
// 弱)。dataRef 只被采纳分支写 ⇒ 读到的必是最新采纳态,链照旧走完。
|
|
589
|
+
load().then(function () { if (gatewayPending(dataRef.current)) settleGateways(attempt + 1); });
|
|
590
|
+
}, GATEWAY_SETTLE_DELAYS[attempt]);
|
|
591
|
+
};
|
|
592
|
+
|
|
593
|
+
var post = function (body) {
|
|
594
|
+
return fetchWithTimeout(ROUTE, {
|
|
595
|
+
method: "POST",
|
|
596
|
+
headers: { "content-type": "application/json" },
|
|
597
|
+
body: JSON.stringify(body),
|
|
598
|
+
}, 30000).then(function (r) {
|
|
599
|
+
return r.text().then(function (t) {
|
|
600
|
+
var d = null;
|
|
601
|
+
try { d = JSON.parse(t); } catch (e) { /* not JSON */ }
|
|
602
|
+
return { ok: r.ok, status: r.status, d: d || {} };
|
|
603
|
+
});
|
|
604
|
+
});
|
|
605
|
+
};
|
|
606
|
+
var save = function (patch, blockId) {
|
|
607
|
+
post({ patch: patch }).then(function (res) {
|
|
608
|
+
if (!res.ok) { setErr(res.d && res.d.error ? res.d.error : "保存失败(HTTP " + res.status + ")"); return; }
|
|
609
|
+
setSaved({ block: blockId || null, at: Date.now() });
|
|
610
|
+
// 启动点与续跑点(settleGateways 内那处)同形:判 pending 读 dataRef 而非
|
|
611
|
+
// 回调参数,否则本次 GET 被更新的一发抢先作废时拿到 null ⇒ 退避链一次都
|
|
612
|
+
// 没启动(踩坑 #45 的自愈保护被静默削掉)。
|
|
613
|
+
load().then(function () { if (gatewayPending(dataRef.current)) settleGateways(0); });
|
|
614
|
+
}).catch(function (e) { setErr(e && e.message ? e.message : "保存失败(网络)"); });
|
|
615
|
+
};
|
|
616
|
+
// 分区拿到的是绑定了所属区块的 save —— 「已保存 ✓」才知道浮在哪个区块头。
|
|
617
|
+
var saveIn = function (blockId) {
|
|
618
|
+
return function (patch) { save(patch, blockId); };
|
|
619
|
+
};
|
|
620
|
+
|
|
621
|
+
// P2-3 状态行可操作化:warn/err 通道的区块头尾部出两个就地动作。
|
|
622
|
+
// 重试监听 = 服务端 action 重跑该通道 sync*(enabled 限定、不写配置);
|
|
623
|
+
// running 由 'listening' 异步翻转 ⇒ 补拉链启动点与 save 同形(踩坑 #45:
|
|
624
|
+
// 判 pending 读 dataRef,不读回调参数)。
|
|
625
|
+
var retryGateway = function (channel) {
|
|
626
|
+
if (retryBusy) return;
|
|
627
|
+
setRetryBusy(channel);
|
|
628
|
+
post({ action: "gateway-retry", channel: channel }).then(function (res) {
|
|
629
|
+
setRetryBusy(null);
|
|
630
|
+
if (!res.ok) { setErr((res.d && res.d.error) || ("重试失败(HTTP " + res.status + ")")); return; }
|
|
631
|
+
load().then(function () { if (gatewayPending(dataRef.current)) settleGateways(0); });
|
|
632
|
+
}).catch(function (e) {
|
|
633
|
+
setRetryBusy(null);
|
|
634
|
+
setErr("重试失败:" + (e && e.message ? e.message : String(e)));
|
|
635
|
+
});
|
|
636
|
+
};
|
|
637
|
+
// 复制诊断 = 纯客户端聚合(数据全在 dataRef 的 GET 视图里:running/端口/
|
|
638
|
+
// lastError/最近同步时间——该视图本就脱敏,无密钥无令牌,踩坑 #26 纪律不破)。
|
|
639
|
+
var copyDiagnostics = function (channel) {
|
|
640
|
+
var d = dataRef.current || {};
|
|
641
|
+
var value = d.value || {};
|
|
642
|
+
var br, port, sync;
|
|
643
|
+
if (channel === "trae") { br = (d.trae || {}).bridge || {}; port = value.traeBridgePort; sync = ((d.trae || {}).models || {}).sync; }
|
|
644
|
+
else if (channel === "qoder") { br = (d.qoder || {}).bridge || {}; port = value.qoderBridgePort; sync = ((d.qoder || {}).models || {}).sync; }
|
|
645
|
+
else { br = d.bridge || {}; port = value.bridgePort; sync = (d.models || {}).sync; }
|
|
646
|
+
var drifts = (((d.host || {}).drift) || []).filter(function (x) { return x && x.channel === channel; });
|
|
647
|
+
var lines = [
|
|
648
|
+
"[dsh-tap] " + channel + " 诊断 · " + new Date().toLocaleString(),
|
|
649
|
+
"网关: " + (br.running ? ("运行中 127.0.0.1:" + br.port) : ("未监听 :" + (br.port || port || "?")) + (br.lastError === "EADDRINUSE" ? "(另一实例代管)" : "")),
|
|
650
|
+
"lastError: " + (br.lastError || "(无)") + (br.lastError === "EADDRINUSE" ? "——若占用者是另一个 dsh 实例,其桥仍会代管本实例流量,功能等价" : ""),
|
|
651
|
+
"模型目录: " + (sync
|
|
652
|
+
? (sync.count != null ? (sync.count + " 个 · 同步于 " + fmtTime(sync.at)) : "未同步") + (sync.error ? "|最近同步失败: " + sync.error : "")
|
|
653
|
+
: "未同步"),
|
|
654
|
+
"宿主漂移: " + (drifts.length ? drifts.map(function (x) { return "期望 " + x.expected + " / 实际 " + (x.actual == null ? "路由缺席" : x.actual); }).join(";") : "无"),
|
|
655
|
+
];
|
|
656
|
+
var fail = function () { setErr("复制失败:浏览器拒绝了剪贴板写入"); };
|
|
657
|
+
if (!navigator.clipboard || !navigator.clipboard.writeText) { fail(); return; }
|
|
658
|
+
navigator.clipboard.writeText(lines.join("\n")).then(function () {
|
|
659
|
+
setCopied(channel);
|
|
660
|
+
if (copiedTimerRef.current) clearTimeout(copiedTimerRef.current);
|
|
661
|
+
copiedTimerRef.current = setTimeout(function () { setCopied(null); }, 1800);
|
|
662
|
+
}, fail);
|
|
663
|
+
};
|
|
664
|
+
|
|
665
|
+
// “已保存 ✓”短提示:1.8s 后自动消失(保存成功无其他反馈曾是盲区)。
|
|
666
|
+
useEffect(function () {
|
|
667
|
+
if (!saved) return undefined;
|
|
668
|
+
var t = setTimeout(function () { setSaved(null); }, 1800);
|
|
669
|
+
return function () { clearTimeout(t); };
|
|
670
|
+
}, [saved]);
|
|
671
|
+
|
|
672
|
+
// 通用区块头的「额度 / 服务商数」不在 GET 视图里(额度来自 action:'usage'、
|
|
673
|
+
// 服务商数来自 provider-list)⇒ ①卡片挂载时各取样一次 ②「通用」区块**由展开
|
|
674
|
+
// 转为收起**那一刻再重采一次(终审 Important 1)。
|
|
675
|
+
// 为什么必须有 ②:头部状态行是本轮卖点「首屏即总览 / 区块头 = 状态单一真源」
|
|
676
|
+
// 的载体,而它正下方的展开区每 10s 轮询——只在挂载时采一次会让头部停在"打开
|
|
677
|
+
// 页面那一刻"的快照,用户加/删一个服务商或聊了一小时之后,头部与同屏正文自相
|
|
678
|
+
// 矛盾。为什么不做周期轮询:头部四行常驻,轮询等于每次开卡都多打一份上游额度
|
|
679
|
+
// 只读(成本考虑,spec §3)⇒ 只在一个交互边界上补采,请求量最多 +1 次/收起。
|
|
680
|
+
// credential-scan 不上移——它扫本机文件,仍只在通用区块首次挂载时做。
|
|
681
|
+
var genSampleState = useState(null); // { text }
|
|
682
|
+
var genSample = genSampleState[0];
|
|
683
|
+
var setGenSample = genSampleState[1];
|
|
684
|
+
// 取样代次:同一刻只认最后一次(展开边界与保存补拉可能撞车)。
|
|
685
|
+
var genSampleGenRef = useRef(0);
|
|
686
|
+
var sampleGeneralHead = function () {
|
|
687
|
+
var my = ++genSampleGenRef.current;
|
|
688
|
+
Promise.all([
|
|
689
|
+
post({ action: "usage" }).catch(function () { return null; }),
|
|
690
|
+
post({ action: "provider-list" }).catch(function () { return null; }),
|
|
691
|
+
]).then(function (res) {
|
|
692
|
+
if (my !== genSampleGenRef.current) return;
|
|
693
|
+
setGenSample({ text: generalSummary(res[0], res[1]) });
|
|
694
|
+
}).catch(function (e) {
|
|
695
|
+
// fire-and-forget 必须落地(踩坑 #33):genSample 保持 null ⇒ 头部
|
|
696
|
+
// 回落「额度与用量 · 服务商」无数字文案,绝不用假值填坑。
|
|
697
|
+
// 两个输入各自 .catch(() => null),网络失败到不了这里 ⇒ 能进这个分支的
|
|
698
|
+
// 只有 generalSummary / setGenSample 自己的 bug;静默置 null 会让头部
|
|
699
|
+
// 永久停在占位文案且零线索(踩坑 #7:错误提示要带原因)。只 warn 不弹
|
|
700
|
+
// 横幅——头部取样是装饰性信息,横幅反而打扰。
|
|
701
|
+
console.warn("dsh-tap: 通用区块头取样失败", e);
|
|
702
|
+
if (my !== genSampleGenRef.current) return;
|
|
703
|
+
setGenSample(null);
|
|
704
|
+
});
|
|
705
|
+
};
|
|
706
|
+
// deps 只挂 general 的开/关:prev 为 null = 挂载首采;true→false = 收起边界重采。
|
|
707
|
+
// 展开(false→true)不采——正文自己会拉,且 [F4] 的"展开后 ≥2"来自轮询不是这里。
|
|
708
|
+
var prevGenOpenRef = useRef(null);
|
|
709
|
+
useEffect(function () {
|
|
710
|
+
var was = prevGenOpenRef.current;
|
|
711
|
+
var isOpen = !!openBlocks.general;
|
|
712
|
+
prevGenOpenRef.current = isOpen;
|
|
713
|
+
if (was === null || (was === true && isOpen === false)) sampleGeneralHead();
|
|
714
|
+
return undefined;
|
|
715
|
+
}, [!!openBlocks.general]);
|
|
716
|
+
|
|
717
|
+
var chevron = IconChevron
|
|
718
|
+
? createElement("span", { className: "cbc-chevron" + (cardOpen ? " cbc-open" : "") }, createElement(IconChevron, { size: 14 }))
|
|
719
|
+
: createElement("span", { className: "cbc-chevron" + (cardOpen ? " cbc-open" : "") }, "▾");
|
|
720
|
+
|
|
721
|
+
// 折叠态(仅旧槽 settings.plugin.item 会走到):不再常显三芯片——
|
|
722
|
+
// 状态由区块头承载;只在确有待处理项时浮一枚计数芯片。
|
|
723
|
+
var headChips = null;
|
|
724
|
+
if (!cardOpen && data) {
|
|
725
|
+
var n = attentionCount(data);
|
|
726
|
+
if (n > 0) {
|
|
727
|
+
headChips = createElement("span", { className: "cbc-headchips" },
|
|
728
|
+
createElement("span", { className: "cbc-chip" },
|
|
729
|
+
createElement(Dot, { tone: "warn" }), n + " 项需处理"));
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
var header = createElement(
|
|
734
|
+
"button",
|
|
735
|
+
{ type: "button", className: "cbc-header", "aria-expanded": cardOpen ? "true" : "false", onClick: function () { setCardOpen(!cardOpen); } },
|
|
736
|
+
createElement("span", null,
|
|
737
|
+
createElement("div", { className: "cbc-name" }, "dsh-tap"),
|
|
738
|
+
createElement("div", { className: "cbc-desc" }, "CodeBuddy / Trae / Qoder CN 通道接入:凭据、模型、额度、工具与流式桥。")),
|
|
739
|
+
headChips,
|
|
740
|
+
chevron,
|
|
741
|
+
);
|
|
742
|
+
var cardClass = "cbc-card" + ((cardOpen || embedded) ? " cbc-open" : "");
|
|
743
|
+
var cardTag = embedded ? "div" : "li";
|
|
744
|
+
if (!cardOpen && !embedded) return createElement(cardTag, { className: cardClass }, header);
|
|
745
|
+
if (!data) {
|
|
746
|
+
return createElement(cardTag, { className: cardClass }, embedded ? null : header,
|
|
747
|
+
createElement("div", { className: "cbc-body" }, createElement("p", { className: "cbc-status" }, err || "正在读取设置…")));
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
var errorBanner = err
|
|
751
|
+
? createElement("p", { className: "cbc-error", role: "alert" },
|
|
752
|
+
createElement("span", null, err),
|
|
753
|
+
createElement("button", { type: "button", className: "cbc-error-close", title: "关闭", onClick: function () { setErr(""); } }, "✕"))
|
|
754
|
+
: null;
|
|
755
|
+
|
|
756
|
+
// 分区:展开才挂载 + 收起隐藏不卸载(hidden 保留组件状态与 DOM)。
|
|
757
|
+
var sectionProps = {
|
|
758
|
+
login: { value: data.value || {}, oauth: data.oauth || {}, save: saveIn("codebuddy"), post: post, reload: load, setErr: setErr, overridden: overriddenFor(data) },
|
|
759
|
+
models: { post: post, setErr: setErr, reload: load, modelsInfo: data.models || {}, value: data.value || {}, save: saveIn("codebuddy") },
|
|
760
|
+
tools: { value: data.value || {}, save: saveIn("codebuddy"), overridden: overriddenFor(data) },
|
|
761
|
+
bridge: { value: data.value || {}, save: saveIn("codebuddy"), overridden: overriddenFor(data), bridgeView: data.bridge || {} },
|
|
762
|
+
trae: { value: data.value || {}, save: saveIn("trae"), post: post, reload: load, setErr: setErr, overridden: overriddenFor(data), trae: data.trae || {}, active: !!openBlocks.trae },
|
|
763
|
+
qoder: { value: data.value || {}, save: saveIn("qoder"), post: post, reload: load, setErr: setErr, overridden: overriddenFor(data), qoder: data.qoder || {}, active: !!openBlocks.qoder },
|
|
764
|
+
usage: { post: post, value: data.value || {}, save: saveIn("general"), overridden: overriddenFor(data), active: !!openBlocks.general },
|
|
765
|
+
providers: { post: post, setErr: setErr },
|
|
766
|
+
hostrecon: { host: data.host || null },
|
|
767
|
+
};
|
|
768
|
+
var status = blockStatus(data, genSample ? genSample.text : null);
|
|
769
|
+
var blocks = BLOCK_DEFS.map(function (def) {
|
|
770
|
+
var isOpen = !!openBlocks[def.id];
|
|
771
|
+
var isMounted = !!mountedBlocks[def.id];
|
|
772
|
+
// 通道级启用开关:Trae/Qoder 的唯一落点(收起态也能一眼看状态并直接启停)。
|
|
773
|
+
// 头部是 div + 两个独立交互元素,不构成嵌套交互。
|
|
774
|
+
var channelSwitch = function (blockId, fieldKey, blockTitle) {
|
|
775
|
+
var curOn = (data.value || {})[fieldKey] === true;
|
|
776
|
+
var confirming = swConfirm === fieldKey;
|
|
777
|
+
// 子节点顺序 = [确认提示?, 重置?, 开关]:ctl 右边缘被常驻的 flash +
|
|
778
|
+
// chevron 钉死,开关放最后才能吃到固定 x 坐标。反序时"重置"在位与否
|
|
779
|
+
// 会让开关行间错开 ≈50px,而第一次点击插入的确认提示(≈90px)会把
|
|
780
|
+
// 开关当场推走——收起态的两次点击(P3-7)就落在非交互文本上,确认永远
|
|
781
|
+
// 完不成(偏右一点还正好命中「重置」)。元素级 click 的测试看不见这个。
|
|
782
|
+
return createElement("span", null,
|
|
783
|
+
confirming
|
|
784
|
+
? createElement("span", {
|
|
785
|
+
className: "cbc-headconfirm", "data-field": fieldKey + "-confirm", role: "status",
|
|
786
|
+
}, "再点一次确认" + (curOn ? "停用" : "启用"))
|
|
787
|
+
: null,
|
|
788
|
+
createElement(ResetButton, { fieldKey: fieldKey, overridden: overriddenFor(data)(fieldKey), save: saveIn(blockId) }),
|
|
789
|
+
// label 包裹扩命中面(.cbc-checkhit,SC 2.5.8);title/aria-label
|
|
790
|
+
// 仍在 input 上,可访问名与工具提示行为不变。
|
|
791
|
+
createElement("label", { className: "cbc-checkhit" },
|
|
792
|
+
createElement("input", {
|
|
793
|
+
type: "checkbox", className: "cbc-check", "data-field": fieldKey,
|
|
794
|
+
checked: curOn,
|
|
795
|
+
title: confirming
|
|
796
|
+
? ("再点一次确认" + (curOn ? "停用" : "启用") + "(4 秒内有效)")
|
|
797
|
+
: (curOn ? "停用该通道(其模型整体移出选择器)" : "启用该通道"),
|
|
798
|
+
// 可访问名(键盘/AT):无可见文案,aria-label 动态报出「启用/停用 <通道> 通道」。
|
|
799
|
+
"aria-label": confirming
|
|
800
|
+
? ("确认" + (curOn ? "停用 " : "启用 ") + blockTitle + " 通道(再点一次)")
|
|
801
|
+
: ((curOn ? "停用 " : "启用 ") + blockTitle + " 通道"),
|
|
802
|
+
onChange: function (e) {
|
|
803
|
+
// 收起态先确认(P3-7):不 save,受控框自动弹回原态;4s 窗内
|
|
804
|
+
// 第二次点击才落盘。展开态直切——用户正看着分区内容,误触面不同。
|
|
805
|
+
if (!openBlocks[blockId] && swConfirm !== fieldKey) {
|
|
806
|
+
if (swConfirmTimerRef.current) clearTimeout(swConfirmTimerRef.current);
|
|
807
|
+
setSwConfirm(fieldKey);
|
|
808
|
+
swConfirmTimerRef.current = setTimeout(function () {
|
|
809
|
+
setSwConfirm(null); swConfirmTimerRef.current = null;
|
|
810
|
+
}, 4000);
|
|
811
|
+
return;
|
|
812
|
+
}
|
|
813
|
+
if (swConfirmTimerRef.current) { clearTimeout(swConfirmTimerRef.current); swConfirmTimerRef.current = null; }
|
|
814
|
+
setSwConfirm(null);
|
|
815
|
+
var p = {};
|
|
816
|
+
p[fieldKey] = e.target.checked;
|
|
817
|
+
save(p, blockId);
|
|
818
|
+
},
|
|
819
|
+
})),
|
|
820
|
+
);
|
|
821
|
+
};
|
|
822
|
+
// P2-3:仅 warn/err 态在区块头状态行尾部出就地动作(重试监听/复制诊断);
|
|
823
|
+
// 正常态零动作零噪音(spec P2-3)。通用区块无网关,不参与。
|
|
824
|
+
var acts = null;
|
|
825
|
+
if ((def.id === "codebuddy" || def.id === "trae" || def.id === "qoder")
|
|
826
|
+
&& status[def.id] && (status[def.id].tone === "warn" || status[def.id].tone === "err")) {
|
|
827
|
+
acts = [
|
|
828
|
+
createElement("button", {
|
|
829
|
+
key: "r", type: "button", className: "cbc-headact", "data-act": "retry",
|
|
830
|
+
disabled: retryBusy === def.id,
|
|
831
|
+
title: "重跑该通道的本地网关监听(不改配置;端口被占等失败会如实回到状态行)",
|
|
832
|
+
onClick: function () { retryGateway(def.id); },
|
|
833
|
+
}, retryBusy === def.id ? "重试中…" : "重试监听"),
|
|
834
|
+
createElement("button", {
|
|
835
|
+
key: "d", type: "button", className: "cbc-headact", "data-act": "diag",
|
|
836
|
+
title: "复制该通道的 running/端口/lastError/最近同步时间到剪贴板",
|
|
837
|
+
onClick: function () { copyDiagnostics(def.id); },
|
|
838
|
+
}, copied === def.id ? "已复制 ✓" : "复制诊断"),
|
|
839
|
+
];
|
|
840
|
+
}
|
|
841
|
+
return createElement("div", {
|
|
842
|
+
key: def.id, className: "cbc-acc" + (isOpen ? " cbc-open" : ""), "data-block": def.id,
|
|
843
|
+
},
|
|
844
|
+
createElement(BlockHead, {
|
|
845
|
+
id: def.id, title: def.title, status: status[def.id],
|
|
846
|
+
open: isOpen, mounted: isMounted,
|
|
847
|
+
onToggle: function () { toggleBlock(def.id); },
|
|
848
|
+
flash: !!(saved && saved.block === def.id),
|
|
849
|
+
actions: acts,
|
|
850
|
+
control: def.id === "trae" ? channelSwitch("trae", "traeEnabled", def.title)
|
|
851
|
+
: def.id === "qoder" ? channelSwitch("qoder", "qoderEnabled", def.title)
|
|
852
|
+
: null,
|
|
853
|
+
}),
|
|
854
|
+
isMounted ? createElement("div", {
|
|
855
|
+
className: "cbc-acc-body cbc-block", "data-block": def.id,
|
|
856
|
+
id: "cbc-block-" + def.id,
|
|
857
|
+
hidden: isOpen ? undefined : true,
|
|
858
|
+
}, BLOCK_SECTIONS[def.id].map(function (sid) {
|
|
859
|
+
// PanelBoundary 粒度 = 每分区:一处塌落不影响同区块其它分区。
|
|
860
|
+
return createElement(PanelBoundary, { key: sid },
|
|
861
|
+
createElement(SECTION_RENDERERS[sid], sectionProps[sid]));
|
|
862
|
+
})) : null);
|
|
863
|
+
});
|
|
864
|
+
|
|
865
|
+
return createElement(cardTag, { className: cardClass }, embedded ? null : header,
|
|
866
|
+
createElement("div", { className: "cbc-body" },
|
|
867
|
+
errorBanner,
|
|
868
|
+
blocks,
|
|
869
|
+
createElement("p", { className: "cbc-status" }, "修改即保存(写入 ~/.dsh/codebuddy-plugin.json),立即生效;标\u201C重置\u201D的字段可一键恢复默认值。")));
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
function overriddenFor(data) {
|
|
873
|
+
var user = data.user || {};
|
|
874
|
+
return function (k) { return Object.prototype.hasOwnProperty.call(user, k); };
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
// 状态芯片:全部由 GET 视图推导,不硬编码任何当前值。
|
|
878
|
+
function buildChips(data) {
|
|
879
|
+
var value = data.value || {};
|
|
880
|
+
var oauth = data.oauth || {};
|
|
881
|
+
var bridge = data.bridge || {};
|
|
882
|
+
var modelsInfo = data.models || {};
|
|
883
|
+
var trae = data.trae || {};
|
|
884
|
+
var qoauth = (data.qoder || {}).oauth || {};
|
|
885
|
+
// P1-1 宿主实况对账:漂移通道就地 warn(状态行尾部追加「宿主漂移」标记)。
|
|
886
|
+
var hostView = data.host || {};
|
|
887
|
+
var driftAt = {};
|
|
888
|
+
(hostView.drift || []).forEach(function (d) { if (d && d.channel) driftAt[d.channel] = d; });
|
|
889
|
+
// web 钉选漂移(searchProvider/fetchProvider 未指向 codebuddy)同样落在
|
|
890
|
+
// codebuddy 头——搜索/抓取工具归该区块;它不在 drift[](通道形状)里,直读 webPin。
|
|
891
|
+
var codebuddyDrift = !!driftAt.codebuddy || !!(hostView.webPin && hostView.webPin.ok === false);
|
|
892
|
+
var login;
|
|
893
|
+
if (value.authMode === "oauth") {
|
|
894
|
+
login = oauth.needsRelogin
|
|
895
|
+
? { tone: "err", text: "OAuth 需重新登录" }
|
|
896
|
+
: oauth.signedIn
|
|
897
|
+
? { tone: "ok", text: "OAuth 已登录" }
|
|
898
|
+
: (oauth.pending ? { tone: "warn", text: "OAuth 登录中…" } : { tone: "err", text: "OAuth 未登录" });
|
|
899
|
+
} else {
|
|
900
|
+
login = value.activeApiKey
|
|
901
|
+
? { tone: "ok", text: "API Key · " + value.activeApiKey }
|
|
902
|
+
: { tone: "err", text: "API Key 未配置" };
|
|
903
|
+
}
|
|
904
|
+
var effCount = modelsInfo.effectiveCount;
|
|
905
|
+
var models = { tone: effCount > 0 ? "ok" : "warn", text: "模型 " + (effCount != null ? effCount : "?") + " 个" };
|
|
906
|
+
var bridgeChip;
|
|
907
|
+
if (value.bridgeEnabled === false) bridgeChip = { tone: "err", text: "桥已禁用" };
|
|
908
|
+
else if (bridge.running) bridgeChip = { tone: "ok", text: "桥 :" + bridge.port };
|
|
909
|
+
else bridgeChip = { tone: "warn", text: ("桥未监听 :" + (bridge.port || value.bridgePort || "?")) + (bridge.lastError === "EADDRINUSE" ? "·另一实例代管" : "") };
|
|
910
|
+
var traeChip = {
|
|
911
|
+
// 启用但网关未监听 = 需要处理(warn),与 Qoder 芯片同口径。
|
|
912
|
+
// 品牌前缀不进 text——Trae/Qoder 已各自成块,标题承载品牌。
|
|
913
|
+
tone: value.traeEnabled === true
|
|
914
|
+
? (((data.trae || {}).bridge || {}).running === true ? "ok" : "warn")
|
|
915
|
+
: "off",
|
|
916
|
+
text: value.traeEnabled === true
|
|
917
|
+
? ((((data.trae || {}).bridge || {}).running === true) ? "运行中" : ("网关未监听 :" + (value.traeBridgePort || "?")) + ((((data.trae || {}).bridge || {}).lastError === "EADDRINUSE") ? "·另一实例代管" : ""))
|
|
918
|
+
: "未启用",
|
|
919
|
+
};
|
|
920
|
+
var qoderChip = {
|
|
921
|
+
tone: value.qoderEnabled === true
|
|
922
|
+
? (((data.qoder || {}).bridge || {}).running === true ? "ok" : "warn")
|
|
923
|
+
: (qoauth.signedIn ? (qoauth.needsRelogin ? "err" : "ok") : "off"),
|
|
924
|
+
text: value.qoderEnabled === true
|
|
925
|
+
? ((((data.qoder || {}).bridge || {}).running === true) ? "运行中" : ("网关未监听 :" + (value.qoderBridgePort || "?")) + ((((data.qoder || {}).bridge || {}).lastError === "EADDRINUSE") ? "·另一实例代管" : ""))
|
|
926
|
+
: (qoauth.signedIn ? (qoauth.needsRelogin ? "需重登" : "已登录·未启用") : (qoauth.pending ? "登录中" : "未启用")),
|
|
927
|
+
};
|
|
928
|
+
// 漂移标记:只升不降(已是 err 不再降成 warn);文案追加而非替换,原有因保留可见。
|
|
929
|
+
if (driftAt.trae) {
|
|
930
|
+
traeChip = { tone: TONE_RANK[traeChip.tone] >= TONE_RANK.warn ? traeChip.tone : "warn", text: traeChip.text + " · 宿主漂移" };
|
|
931
|
+
}
|
|
932
|
+
if (driftAt.qoder) {
|
|
933
|
+
qoderChip = { tone: TONE_RANK[qoderChip.tone] >= TONE_RANK.warn ? qoderChip.tone : "warn", text: qoderChip.text + " · 宿主漂移" };
|
|
934
|
+
}
|
|
935
|
+
return {
|
|
936
|
+
login: login,
|
|
937
|
+
models: models,
|
|
938
|
+
bridge: bridgeChip,
|
|
939
|
+
// codebuddy 的漂移单独成芯片(它的行是 login/models/bridge 三枚拼接)。
|
|
940
|
+
hostDrift: codebuddyDrift ? { tone: "warn", text: "宿主漂移" } : null,
|
|
941
|
+
search: {
|
|
942
|
+
tone: value.searchEnabled === true ? "ok" : "off",
|
|
943
|
+
text: "搜索 " + (value.searchEnabled === true ? "已启用" : "已禁用"),
|
|
944
|
+
},
|
|
945
|
+
image: {
|
|
946
|
+
tone: value.imageGenEnabled === true ? "ok" : "off",
|
|
947
|
+
text: "生图 " + (value.imageGenEnabled === true ? "已启用" : "已禁用"),
|
|
948
|
+
},
|
|
949
|
+
trae: traeChip,
|
|
950
|
+
qoder: qoderChip,
|
|
951
|
+
};
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
// 区块头状态行:判定口径沿用 buildChips(唯一真源)。每个区块取自己那几枚
|
|
955
|
+
// 芯片拼一行,tone = 其中最差的一枚——warn/err 因此就地出现在所属区块头,
|
|
956
|
+
// 不再需要独立的注意条(0.10.0 状态收敛)。
|
|
957
|
+
var TONE_RANK = { off: 0, ok: 1, warn: 2, err: 3 };
|
|
958
|
+
function worstTone(chips) {
|
|
959
|
+
var t = "off";
|
|
960
|
+
chips.forEach(function (c) {
|
|
961
|
+
if (c && (TONE_RANK[c.tone] || 0) > TONE_RANK[t]) t = c.tone;
|
|
962
|
+
});
|
|
963
|
+
return t;
|
|
964
|
+
}
|
|
965
|
+
// 状态行分段渲染(族2):warn/err 段自带色(token 同 .cbc-dot 口径),点仍取
|
|
966
|
+
// 最差(worstTone 不变);ok/off/idle 段保持整行灰底色——绿色文字铺开会把
|
|
967
|
+
// 「一切正常」渲染成绿海噪音,且点的语义已覆盖 ok。分隔符用裸文本节点 " · ",
|
|
968
|
+
// textContent 与 chip.join(" · ") 逐字全等——[B2] 预言机等值断言依赖这一点。
|
|
969
|
+
function statusSegs(status) {
|
|
970
|
+
var segs = status.segs || [{ tone: status.tone, text: status.text }];
|
|
971
|
+
var kids = [];
|
|
972
|
+
segs.forEach(function (s, i) {
|
|
973
|
+
if (i > 0) kids.push(" · ");
|
|
974
|
+
kids.push(createElement("span", {
|
|
975
|
+
key: i,
|
|
976
|
+
className: s.tone === "warn" ? "cbc-seg-warn" : s.tone === "err" ? "cbc-seg-err" : undefined,
|
|
977
|
+
}, s.text));
|
|
978
|
+
});
|
|
979
|
+
return kids;
|
|
980
|
+
}
|
|
981
|
+
function blockStatus(data, generalText) {
|
|
982
|
+
var c = buildChips(data);
|
|
983
|
+
var cb = [c.login, c.models, c.bridge];
|
|
984
|
+
if (c.hostDrift) cb.push(c.hostDrift);
|
|
985
|
+
var join = function (list) {
|
|
986
|
+
return list.map(function (x) { return x.text; }).join(" · ");
|
|
987
|
+
};
|
|
988
|
+
return {
|
|
989
|
+
codebuddy: { tone: worstTone(cb), text: join(cb), segs: cb },
|
|
990
|
+
trae: { tone: c.trae.tone, text: c.trae.text, segs: [c.trae] },
|
|
991
|
+
qoder: { tone: c.qoder.tone, text: c.qoder.text, segs: [c.qoder] },
|
|
992
|
+
// idle 而非 off:通用区块没有「通道启用」这回事,实心灰点会被读成未启用。
|
|
993
|
+
general: { tone: "idle", text: generalText || "额度与用量 · 服务商" },
|
|
994
|
+
};
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
// 通用区块头文案:额度(OAuth = 真实周期余量;api-key = 标注估算,不编数字)
|
|
998
|
+
// + 服务商数。取样失败时回落到无数字文案,绝不显示假值。
|
|
999
|
+
function generalSummary(usageRes, providersRes) {
|
|
1000
|
+
var parts = [];
|
|
1001
|
+
var q = usageRes && usageRes.ok ? usageRes.d && usageRes.d.quota : null;
|
|
1002
|
+
var u = usageRes && usageRes.ok ? usageRes.d && usageRes.d.usage : null;
|
|
1003
|
+
if (q && q.numericQuota && q.resource && q.resource.cycleRemain != null) {
|
|
1004
|
+
parts.push("额度 " + fmtCredit(q.resource.cycleRemain) + " credit");
|
|
1005
|
+
} else if (q && q.numericQuota && q.resourceError) {
|
|
1006
|
+
// 已声明数值额度(OAuth)但读取失败(resourceError 是字符串:"code <n>" /
|
|
1007
|
+
// "http <n>" / "network";分区自己渲染「数值额度读取失败:…」)⇒ 不得落进
|
|
1008
|
+
// 下面的「估算(累计 …)」分支:那是本插件计量口径的累计消耗,摆在「额度」
|
|
1009
|
+
// 位上等于编一个余额(spec §3:取不到就不给数字)。
|
|
1010
|
+
parts.push("额度 —");
|
|
1011
|
+
} else if (u && typeof u.totalCredit === "number") {
|
|
1012
|
+
parts.push("额度 估算(累计 " + fmtCredit(u.totalCredit) + ")");
|
|
1013
|
+
} else {
|
|
1014
|
+
parts.push("额度 —");
|
|
1015
|
+
}
|
|
1016
|
+
var list = providersRes && providersRes.ok && providersRes.d ? providersRes.d.providers : null;
|
|
1017
|
+
parts.push("服务商 " + (list ? list.length : "—"));
|
|
1018
|
+
return parts.join(" · ");
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
// 待处理项计数(warn/err):旧槽折叠态的按需芯片用;展开态由区块头就地承载。
|
|
1022
|
+
function attentionCount(data) {
|
|
1023
|
+
var c = buildChips(data);
|
|
1024
|
+
return [c.login, c.models, c.bridge, c.hostDrift, c.search, c.image, c.trae, c.qoder]
|
|
1025
|
+
.filter(function (x) { return x && (x.tone === "warn" || x.tone === "err"); }).length;
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
// 区块头:div 里若干独立交互元素(展开 button + 可选动作 + 可选开关),避免
|
|
1029
|
+
// 嵌套交互。Task 3 起 control 槽放 Trae/Qoder 的启用开关;P2-3 起 actions 槽
|
|
1030
|
+
// 放 warn/err 态的就地动作(重试监听/复制诊断),视觉落在状态行尾部。
|
|
1031
|
+
function BlockHead(props) {
|
|
1032
|
+
var chevron = IconChevron
|
|
1033
|
+
? createElement("span", { className: "cbc-chevron" + (props.open ? " cbc-open" : "") }, createElement(IconChevron, { size: 14 }))
|
|
1034
|
+
: createElement("span", { className: "cbc-chevron" + (props.open ? " cbc-open" : "") }, "▾");
|
|
1035
|
+
var kids = [
|
|
1036
|
+
createElement("button", {
|
|
1037
|
+
key: "t", type: "button", className: "cbc-acc-toggle", "data-block": props.id,
|
|
1038
|
+
"aria-expanded": props.open ? "true" : "false",
|
|
1039
|
+
// 未挂载时没有对应 body,不留空引用(同原 tablist 纪律)。
|
|
1040
|
+
"aria-controls": props.mounted ? "cbc-block-" + props.id : undefined,
|
|
1041
|
+
onClick: props.onToggle,
|
|
1042
|
+
},
|
|
1043
|
+
createElement(Dot, { tone: props.status.tone }),
|
|
1044
|
+
createElement("span", { className: "cbc-acc-title" }, props.title),
|
|
1045
|
+
// 状态行单行省略(窄窗口/漂移追加时必被截),而尾部恰是原因段
|
|
1046
|
+
// (「· 宿主漂移」「网关未监听 :3902」)⇒ title 兜住完整文本。
|
|
1047
|
+
// 内容走 statusSegs:warn/err 段自带色,文本逐字不变。
|
|
1048
|
+
createElement("span", { className: "cbc-acc-status", title: props.status.text }, statusSegs(props.status))),
|
|
1049
|
+
];
|
|
1050
|
+
// 动作槽紧随展开 button:状态文本在 button 内吃满弹性宽度,动作视觉
|
|
1051
|
+
// 上正好落在状态行尾部;独立元素(不是 button 的子节点),点击不触发展开。
|
|
1052
|
+
if (props.actions) kids.push(createElement("span", { key: "a", className: "cbc-acc-acts" }, props.actions));
|
|
1053
|
+
if (props.control) kids.push(createElement("span", { key: "c", className: "cbc-acc-ctl" }, props.control));
|
|
1054
|
+
// flash 常驻占位(visibility 切换):出现/消失不挤压同行 checkbox 的水平位置。
|
|
1055
|
+
kids.push(createElement("span", {
|
|
1056
|
+
key: "f", className: "cbc-saveflash" + (props.flash ? " cbc-on" : ""),
|
|
1057
|
+
"aria-hidden": props.flash ? undefined : "true",
|
|
1058
|
+
}, "已保存 ✓"));
|
|
1059
|
+
// live region 收尾:可见占位是宽度恒定契约的载体(P3-7 依赖,文本从不进出
|
|
1060
|
+
// DOM ⇒ live region 永不播报),播报职责移到流外 sr-only role=status——
|
|
1061
|
+
// 文本 "" ⇄ "已保存 ✓" 进出 DOM 才会被播报;position:absolute 不参与
|
|
1062
|
+
// flex 布局,宽度契约原样保住(不加任何会改变行宽的东西)。
|
|
1063
|
+
kids.push(createElement("span", {
|
|
1064
|
+
key: "fl", className: "cbc-srlive", role: "status",
|
|
1065
|
+
}, props.flash ? "已保存 ✓" : ""));
|
|
1066
|
+
kids.push(createElement("span", { key: "v", className: "cbc-chevronwrap" }, chevron));
|
|
1067
|
+
return createElement("div", { className: "cbc-acc-head" }, kids);
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
// --- 登录:模式选择 + 多 API Key + OAuth(一切功能的前提,排第一) --------
|
|
1071
|
+
function LoginSection(props) {
|
|
1072
|
+
var value = props.value;
|
|
1073
|
+
var oauth = props.oauth;
|
|
1074
|
+
var save = props.save;
|
|
1075
|
+
var post = props.post;
|
|
1076
|
+
var reload = props.reload;
|
|
1077
|
+
var setErr = props.setErr;
|
|
1078
|
+
|
|
1079
|
+
var addNameState = useState("");
|
|
1080
|
+
var addName = addNameState[0];
|
|
1081
|
+
var setAddName = addNameState[1];
|
|
1082
|
+
var addKeyState = useState("");
|
|
1083
|
+
var addKey = addKeyState[0];
|
|
1084
|
+
var setAddKey = addKeyState[1];
|
|
1085
|
+
var oauthBusyState = useState(false);
|
|
1086
|
+
var oauthBusy = oauthBusyState[0];
|
|
1087
|
+
var setOauthBusy = oauthBusyState[1];
|
|
1088
|
+
// 破坏性操作二次确认(同 ProvidersSection 模式):第一次点击只进确认
|
|
1089
|
+
// 态(按钮变"确认删除/确认退出"),4 秒不确认自动复位。确认态标识 =
|
|
1090
|
+
// Key 名,或 "__logout__" 哨兵(OAuth 视图与 Key 列表互斥不会同屏)。
|
|
1091
|
+
var confirmActState = useState(null);
|
|
1092
|
+
var confirmAct = confirmActState[0];
|
|
1093
|
+
var setConfirmAct = confirmActState[1];
|
|
1094
|
+
var confirmTimerRef = useRef(null);
|
|
1095
|
+
var askConfirm = function (id) {
|
|
1096
|
+
if (confirmTimerRef.current) clearTimeout(confirmTimerRef.current);
|
|
1097
|
+
setConfirmAct(id);
|
|
1098
|
+
confirmTimerRef.current = setTimeout(function () { setConfirmAct(null); }, 4000);
|
|
1099
|
+
};
|
|
1100
|
+
var clearConfirm = function () {
|
|
1101
|
+
if (confirmTimerRef.current) { clearTimeout(confirmTimerRef.current); confirmTimerRef.current = null; }
|
|
1102
|
+
setConfirmAct(null);
|
|
1103
|
+
};
|
|
1104
|
+
|
|
1105
|
+
// P2-5「测一下」:目录面 GET /v3/config 主动验证当前凭据(零额度消耗,
|
|
1106
|
+
// UA 口径同目录拉取);结果就地显示,不进全局横幅。
|
|
1107
|
+
var cbTestState = useState(null); // null | {busy:true} | {ok:bool, text:string}
|
|
1108
|
+
var cbTest = cbTestState[0];
|
|
1109
|
+
var setCbTest = cbTestState[1];
|
|
1110
|
+
var testCredential = function () {
|
|
1111
|
+
setCbTest({ busy: true });
|
|
1112
|
+
post({ action: "credential-test", channel: "codebuddy" }).then(function (res) {
|
|
1113
|
+
if (res.ok && res.d.available) setCbTest({ ok: true, text: "✓ 可用(" + (res.d.detail || "验证通过") + ")" });
|
|
1114
|
+
else setCbTest({ ok: false, text: "✗ " + ((res.d && res.d.error) || ("HTTP " + res.status)) });
|
|
1115
|
+
}).catch(function (e) { setCbTest({ ok: false, text: "✗ " + (e && e.message ? e.message : "网络错误") }); });
|
|
1116
|
+
};
|
|
1117
|
+
|
|
1118
|
+
// While a login handshake is pending, poll its status.
|
|
1119
|
+
useEffect(function () {
|
|
1120
|
+
if (!oauth.pending) return undefined;
|
|
1121
|
+
var timer = setInterval(function () {
|
|
1122
|
+
post({ action: "oauth-status" }).then(function (res) {
|
|
1123
|
+
if (res.ok && res.d.oauth && !res.d.oauth.pending) reload();
|
|
1124
|
+
}).catch(function () {});
|
|
1125
|
+
}, 3000);
|
|
1126
|
+
return function () { clearInterval(timer); };
|
|
1127
|
+
}, [oauth.pending]);
|
|
1128
|
+
|
|
1129
|
+
var modeRow = createElement("div", { className: "cbc-row" },
|
|
1130
|
+
createElement("div", { className: "cbc-row-label" }, "登录方式"),
|
|
1131
|
+
createElement("div", { className: "cbc-row-control" },
|
|
1132
|
+
createElement("span", { className: "cbc-mode" },
|
|
1133
|
+
createElement("label", { className: "cbc-radio" },
|
|
1134
|
+
createElement("input", { type: "radio", checked: value.authMode !== "oauth", onChange: function () { save({ authMode: "api-key" }); } }),
|
|
1135
|
+
"API Key"),
|
|
1136
|
+
createElement("label", { className: "cbc-radio" },
|
|
1137
|
+
createElement("input", { type: "radio", checked: value.authMode === "oauth", onChange: function () { save({ authMode: "oauth" }); } }),
|
|
1138
|
+
"OAuth 登录"))));
|
|
1139
|
+
|
|
1140
|
+
var body;
|
|
1141
|
+
if (value.authMode === "oauth") {
|
|
1142
|
+
var parts = [];
|
|
1143
|
+
if (oauth.signedIn && !oauth.needsRelogin) {
|
|
1144
|
+
var acct = oauth.account || {};
|
|
1145
|
+
parts.push(createElement("div", { key: "acct", className: "cbc-listrow" },
|
|
1146
|
+
createElement(Dot, { tone: "ok" }),
|
|
1147
|
+
createElement("span", { className: "cbc-cell-name" }, acct.nickname || acct.uid || "未知账号"),
|
|
1148
|
+
createElement("span", { className: "cbc-badge" }, "已登录"),
|
|
1149
|
+
acct.enterpriseName ? createElement("span", { className: "cbc-muted" }, acct.enterpriseName) : null,
|
|
1150
|
+
createElement("span", { className: "cbc-effort" },
|
|
1151
|
+
(acct.type ? "套餐 " + acct.type : ""),
|
|
1152
|
+
oauth.accessTokenExpiresAt ? (acct.type ? " · 令牌至 " : "令牌至 ") + fmtTime(oauth.accessTokenExpiresAt) : "")));
|
|
1153
|
+
parts.push(createElement("p", { key: "scope", className: "cbc-hint" },
|
|
1154
|
+
"此登录覆盖模型对话(主聊天经流式桥统一取凭据)、网络搜索与抓取、生图与额度读数。"));
|
|
1155
|
+
parts.push(createElement("div", { key: "out", className: "cbc-addrow" },
|
|
1156
|
+
confirmAct === "__logout__"
|
|
1157
|
+
? createElement(CbcButton, { variant: "outline", danger: true, onClick: function () {
|
|
1158
|
+
clearConfirm();
|
|
1159
|
+
post({ action: "oauth-logout" }).then(function (res) {
|
|
1160
|
+
if (res.ok) reload();
|
|
1161
|
+
else setErr(res.d && res.d.error ? res.d.error : "退出失败(HTTP " + res.status + ")");
|
|
1162
|
+
}).catch(function (e) { setErr(e && e.message ? e.message : "退出失败(网络)"); });
|
|
1163
|
+
} }, "确认退出")
|
|
1164
|
+
: createElement(CbcButton, { variant: "outline", danger: true, onClick: function () { askConfirm("__logout__"); } }, "退出登录")));
|
|
1165
|
+
} else if (oauth.pending) {
|
|
1166
|
+
parts.push(createElement("p", { key: "pend", className: "cbc-status" }, "等待浏览器完成登录…(3 秒轮询,完成后自动刷新)"));
|
|
1167
|
+
parts.push(createElement("div", { key: "url", className: "cbc-addrow" },
|
|
1168
|
+
createElement(CbcButton, { variant: "outline", onClick: function () { window.open(oauth.authUrl, "_blank"); } }, "重新打开登录页")));
|
|
1169
|
+
} else {
|
|
1170
|
+
// 令牌在但 refresh 已不可用/已实败:signedIn 的字面状态不再误导,
|
|
1171
|
+
// 明说"需重新登录"(此时聊天会因凭据不可用全 503)。
|
|
1172
|
+
if (oauth.needsRelogin) {
|
|
1173
|
+
parts.push(createElement("p", { key: "relogin", className: "cbc-warn" },
|
|
1174
|
+
"登录已失效(刷新令牌过期或被拒绝),当前对话会报凭据不可用——请重新登录。"));
|
|
1175
|
+
}
|
|
1176
|
+
parts.push(createElement("div", { key: "start", className: "cbc-addrow" },
|
|
1177
|
+
createElement(CbcButton, { variant: "primary", disabled: oauthBusy, onClick: function () {
|
|
1178
|
+
startOAuthFlow(post, "oauth-start", setOauthBusy, setErr, reload);
|
|
1179
|
+
} }, oauthBusy ? "发起中…" : (oauth.needsRelogin ? "重新登录 CodeBuddy 账号" : "登录 CodeBuddy 账号"))));
|
|
1180
|
+
}
|
|
1181
|
+
if (oauth.error) parts.push(createElement("p", { key: "err", className: "cbc-error" }, oauth.error));
|
|
1182
|
+
body = parts;
|
|
1183
|
+
} else {
|
|
1184
|
+
// 使用中置顶,其余按名称字典序(仅展示层排序,不动存储顺序)。
|
|
1185
|
+
var sortedKeys = (value.apiKeys || []).slice().sort(function (a, b) {
|
|
1186
|
+
var wa = value.activeApiKey === a.name ? 0 : 1;
|
|
1187
|
+
var wb = value.activeApiKey === b.name ? 0 : 1;
|
|
1188
|
+
return wa - wb || String(a.name).localeCompare(String(b.name));
|
|
1189
|
+
});
|
|
1190
|
+
var rows = sortedKeys.map(function (k) {
|
|
1191
|
+
var isActive = value.activeApiKey === k.name;
|
|
1192
|
+
return createElement("div", { key: k.name, className: "cbc-listrow" },
|
|
1193
|
+
createElement("input", { type: "radio", className: "cbc-check", checked: isActive, onChange: function () { save({ activeApiKey: k.name }); }, title: "设为当前使用" }),
|
|
1194
|
+
createElement("span", { className: "cbc-cell-name", title: k.name }, k.name),
|
|
1195
|
+
createElement("span", { className: "cbc-cell-masked" }, k.masked),
|
|
1196
|
+
isActive ? createElement("span", { className: "cbc-badge" }, "使用中") : null,
|
|
1197
|
+
confirmAct === k.name
|
|
1198
|
+
? createElement(CbcButton, { variant: "outline", danger: true, onClick: function () {
|
|
1199
|
+
clearConfirm();
|
|
1200
|
+
post({ patch: { apiKeysRemove: k.name } }).then(function (res) {
|
|
1201
|
+
if (res.ok) reload();
|
|
1202
|
+
else setErr(res.d && res.d.error ? res.d.error : "删除失败(HTTP " + res.status + ")");
|
|
1203
|
+
}).catch(function (e) { setErr(e && e.message ? e.message : "删除失败(网络)"); });
|
|
1204
|
+
} }, "确认删除")
|
|
1205
|
+
: createElement(CbcButton, { variant: "ghost", danger: true, onClick: function () { askConfirm(k.name); } }, "删除"));
|
|
1206
|
+
});
|
|
1207
|
+
body = rows.concat([
|
|
1208
|
+
createElement("div", { key: "add", className: "cbc-addrow" },
|
|
1209
|
+
createElement(CbcInput, { widthClass: "cbc-w140", placeholder: "名称(如 工作)", value: addName, onInput: function (e) { setAddName(e.target.value); } }),
|
|
1210
|
+
createElement(CbcInput, { widthClass: "cbc-w260", placeholder: "ck_…", type: "password", autoComplete: "off", value: addKey, onInput: function (e) { setAddKey(e.target.value); } }),
|
|
1211
|
+
createElement(CbcButton, { variant: "outline", onClick: function () {
|
|
1212
|
+
var name = addName.trim();
|
|
1213
|
+
var key = addKey.trim();
|
|
1214
|
+
if (!name || !key) { setErr("名称和 Key 都不能为空"); return; }
|
|
1215
|
+
post({ patch: { apiKeysAdd: { name: name, key: key } } }).then(function (res) {
|
|
1216
|
+
if (res.ok) { setAddName(""); setAddKey(""); reload(); }
|
|
1217
|
+
else setErr(res.d && res.d.error ? res.d.error : "添加失败");
|
|
1218
|
+
}).catch(function (e) { setErr(e && e.message ? e.message : "添加失败(网络)"); });
|
|
1219
|
+
} }, "添加")),
|
|
1220
|
+
createElement("div", { key: "env", className: "cbc-row" },
|
|
1221
|
+
createElement("div", { className: "cbc-row-label" }, "环境变量引用"),
|
|
1222
|
+
createElement("div", { className: "cbc-row-control" },
|
|
1223
|
+
createElement(TextField, { fieldKey: "apiKeyEnv", value: value.apiKeyEnv, save: save, widthClass: "cbc-w220", overridden: props.overridden("apiKeyEnv") }))),
|
|
1224
|
+
createElement("div", { key: "cooldown", className: "cbc-row" },
|
|
1225
|
+
createElement("div", { className: "cbc-row-label" }, "失败冷却 (ms)"),
|
|
1226
|
+
createElement("div", { className: "cbc-row-control" },
|
|
1227
|
+
createElement(NumberField, { fieldKey: "keyCooldownMs", value: value.keyCooldownMs, save: save, overridden: props.overridden("keyCooldownMs") }))),
|
|
1228
|
+
createElement(HelpNote, { key: "hint" }, "多把 Key 时逐请求轮询;遇 401/403/429/5xx 或网络错误自动换下一把,失败 Key 冷却指定毫秒后自动回到轮换。单选仅决定模型目录拉取用的 Key;一个都不选时回落到环境变量引用(进程环境或 ~/.dsh/.credentials.yaml 中的同名条目)。"),
|
|
1229
|
+
]);
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
return createElement("div", { className: "cbc-section" },
|
|
1233
|
+
createElement("p", { className: "cbc-group-title" }, "凭据"),
|
|
1234
|
+
modeRow, body,
|
|
1235
|
+
createElement("div", { className: "cbc-row" },
|
|
1236
|
+
createElement("div", { className: "cbc-row-label" }, "凭据验证"),
|
|
1237
|
+
createElement("div", { className: "cbc-row-control" },
|
|
1238
|
+
createElement(CbcButton, { variant: "ghost", disabled: !!(cbTest && cbTest.busy), onClick: testCredential },
|
|
1239
|
+
cbTest && cbTest.busy ? "验证中…" : "测一下"),
|
|
1240
|
+
cbTest && !cbTest.busy
|
|
1241
|
+
? createElement("span", { className: cbTest.ok ? "cbc-status" : "cbc-warn", "data-test": "codebuddy" }, cbTest.text)
|
|
1242
|
+
: null,
|
|
1243
|
+
createElement("span", { className: "cbc-muted" }, "目录面 GET /v3/config,零额度消耗"))));
|
|
1244
|
+
}
|
|
1245
|
+
SECTION_RENDERERS.login = LoginSection;
|
|
1246
|
+
|
|
1247
|
+
// --- 额度与用量:桥计量的消耗 + 账户侧额度(登录之后) ----------------------
|
|
1248
|
+
// 消耗量来自桥对每请求 usage.credit 的计量(精确,本插件路径);剩余额度在
|
|
1249
|
+
// OAuth 模式显示真实数值(/billing/meter/get-user-resource,R-Q7),
|
|
1250
|
+
// api-key 模式该 API 为 OAuth 专享(401)→ 手填总额度的估算档(标注"估算")。
|
|
1251
|
+
// 轮询只在分区可见(active)时跑;资源包按名称聚合,明细可展开。
|
|
1252
|
+
function UsageSection(props) {
|
|
1253
|
+
var post = props.post;
|
|
1254
|
+
var value = props.value || {};
|
|
1255
|
+
var active = props.active === true; // 严格随「通用」区块展开:未展开/缺省一律不轮询(sectionProps.usage.active = !!openBlocks.general)
|
|
1256
|
+
var dataState = useState(null); // {usage, bridge, quota}
|
|
1257
|
+
var usageData = dataState[0];
|
|
1258
|
+
var setUsageData = dataState[1];
|
|
1259
|
+
var failState = useState("");
|
|
1260
|
+
var fail = failState[0];
|
|
1261
|
+
var setFail = failState[1];
|
|
1262
|
+
var expandPacksState = useState(false);
|
|
1263
|
+
var expandPacks = expandPacksState[0];
|
|
1264
|
+
var setExpandPacks = expandPacksState[1];
|
|
1265
|
+
var updState = useState(0);
|
|
1266
|
+
var lastUpd = updState[0];
|
|
1267
|
+
var setLastUpd = updState[1];
|
|
1268
|
+
|
|
1269
|
+
// 实时 = 通用区块展开期间 10s 轮询;收起即停(隐藏不卸载,但不可见的轮询是浪费)。
|
|
1270
|
+
// pull 提到组件级并经 ref 持有:手动刷新按钮共用同一入口,
|
|
1271
|
+
// interval 永远调最新闭包(props.post 每渲染换新)。
|
|
1272
|
+
var pullRef = useRef(null);
|
|
1273
|
+
pullRef.current = function () {
|
|
1274
|
+
post({ action: "usage" }).then(function (res) {
|
|
1275
|
+
if (res.ok) { setUsageData(res.d); setFail(""); setLastUpd(Date.now()); }
|
|
1276
|
+
else setFail(res.d && res.d.error ? res.d.error : "用量读取失败");
|
|
1277
|
+
}).catch(function (e) { setFail(e && e.message ? e.message : "用量读取失败(网络)"); });
|
|
1278
|
+
};
|
|
1279
|
+
useEffect(function () {
|
|
1280
|
+
if (!active) return undefined;
|
|
1281
|
+
pullRef.current();
|
|
1282
|
+
var timer = setInterval(function () { pullRef.current(); }, 10000);
|
|
1283
|
+
return function () { clearInterval(timer); };
|
|
1284
|
+
}, [active]);
|
|
1285
|
+
|
|
1286
|
+
var KIND_LABEL = { chat: "对话", title: "标题", compaction: "压缩", image: "生图", search: "搜索", fetch: "抓取" };
|
|
1287
|
+
var usage = usageData && usageData.usage || null;
|
|
1288
|
+
var bridge = usageData && usageData.bridge || null;
|
|
1289
|
+
var quota = usageData && usageData.quota || null;
|
|
1290
|
+
|
|
1291
|
+
var rows = [];
|
|
1292
|
+
|
|
1293
|
+
// 更新时间戳 + 手动刷新(原先只有隐形 10s 轮询,数据新旧无从判断)。
|
|
1294
|
+
rows.push(createElement("div", { key: "upd", className: "cbc-row" },
|
|
1295
|
+
createElement("div", { className: "cbc-row-label" }, "实时用量"),
|
|
1296
|
+
createElement("div", { className: "cbc-row-control" },
|
|
1297
|
+
createElement(CbcButton, { variant: "outline", icon: IconRefresh ? createElement(IconRefresh, { size: 14 }) : undefined, onClick: function () { pullRef.current(); } }, "刷新"),
|
|
1298
|
+
createElement("span", { className: "cbc-updated" }, lastUpd ? "更新于 " + fmtClock(lastUpd) + " · 通用区块展开时每 10 秒自动刷新" : "正在读取…"))));
|
|
1299
|
+
|
|
1300
|
+
// 账户与剩余额度(账户级,与 WorkBuddy 共用)。
|
|
1301
|
+
if (quota && quota.error) {
|
|
1302
|
+
rows.push(createElement("p", { key: "qerr", className: "cbc-status" }, "账户信息:" + quota.error));
|
|
1303
|
+
} else if (quota) {
|
|
1304
|
+
var acct = quota.account;
|
|
1305
|
+
if (acct) {
|
|
1306
|
+
rows.push(createElement("p", { key: "acct", className: "cbc-status" },
|
|
1307
|
+
createElement("span", { className: "cbc-chip", style: { marginRight: 6 } }, createElement(Dot, { tone: "ok" }), "账户"),
|
|
1308
|
+
acct.nickname || "-",
|
|
1309
|
+
acct.enterpriseName ? "(" + acct.enterpriseName + ")" : "",
|
|
1310
|
+
" · 套餐:" + (acct.type || "?")));
|
|
1311
|
+
}
|
|
1312
|
+
// 真实数值额度(OAuth 模式):hero 大数字 + 周期进度条。
|
|
1313
|
+
if (quota.numericQuota && quota.resource) {
|
|
1314
|
+
var res = quota.resource;
|
|
1315
|
+
var pPct = pct(res.cycleRemain, res.cycleSize);
|
|
1316
|
+
rows.push(createElement("div", { key: "hero", className: "cbc-hero" },
|
|
1317
|
+
createElement("div", { className: "cbc-hero-top" },
|
|
1318
|
+
createElement("span", { className: "cbc-hero-label" }, "剩余额度(当前周期)"),
|
|
1319
|
+
createElement("span", null,
|
|
1320
|
+
createElement("span", { className: "cbc-hero-num" }, fmtCredit(res.cycleRemain)),
|
|
1321
|
+
createElement("span", { className: "cbc-hero-unit" }, " credit"))),
|
|
1322
|
+
createElement("div", { className: "cbc-bar" },
|
|
1323
|
+
createElement("div", { className: "cbc-bar-fill", style: { width: (pPct == null ? 0 : pPct) + "%" } })),
|
|
1324
|
+
createElement("div", { className: "cbc-hero-sub" },
|
|
1325
|
+
"总量口径 " + fmtCredit(res.totalRemain) + " credit" +
|
|
1326
|
+
(res.cycleSize ? " · 周期余量 " + pPct + "%(已用 " + fmtCredit(res.cycleUsed) + " / " + fmtCredit(res.cycleSize) + ")" : ""))));
|
|
1327
|
+
var packs = res.packs || [];
|
|
1328
|
+
if (packs.length) {
|
|
1329
|
+
var agg = aggregatePacks(packs);
|
|
1330
|
+
var showAgg = agg.length < packs.length || agg.length > 4;
|
|
1331
|
+
var packRows = (showAgg && !expandPacks ? agg : packs).map(function (p, i) {
|
|
1332
|
+
var bar = pct(p.cycleRemain != null ? p.cycleRemain : p.remain, p.cycleSize != null && p.cycleSize > 0 ? p.cycleSize : p.size);
|
|
1333
|
+
var count = showAgg && !expandPacks && p.count > 1 ? " ×" + p.count : "";
|
|
1334
|
+
return createElement("div", { key: "pk" + i, className: "cbc-listrow" },
|
|
1335
|
+
createElement("span", { className: "cbc-cell-name", title: p.name }, (p.name || "?") + count),
|
|
1336
|
+
createElement("span", { className: "cbc-minibar" }, createElement("span", { className: "cbc-minibar-fill", style: { width: (bar == null ? 0 : bar) + "%" } })),
|
|
1337
|
+
createElement("span", { className: "cbc-effort" },
|
|
1338
|
+
"余 " + fmtCredit(p.cycleRemain != null ? p.cycleRemain : p.remain) +
|
|
1339
|
+
((p.cycleSize != null && p.cycleSize > 0) ? " / " + fmtCredit(p.cycleSize) : "")),
|
|
1340
|
+
createElement("span", { className: "cbc-cell-masked" },
|
|
1341
|
+
p.cycleEnd ? "周期至 " + String(p.cycleEnd).slice(0, 10) : ""));
|
|
1342
|
+
});
|
|
1343
|
+
rows.push(createElement("div", { key: "pkh", className: "cbc-subtitle" },
|
|
1344
|
+
"资源包(" + packs.length + ")",
|
|
1345
|
+
showAgg ? createElement("button", { type: "button", className: "cbc-toggle", style: { marginLeft: 8, fontWeight: 400 }, onClick: function () { setExpandPacks(!expandPacks); } },
|
|
1346
|
+
expandPacks ? "收起明细" : "展开明细") : null));
|
|
1347
|
+
rows.push.apply(rows, packRows);
|
|
1348
|
+
}
|
|
1349
|
+
} else if (quota.numericQuota && quota.resourceError) {
|
|
1350
|
+
rows.push(createElement("p", { key: "reserr", className: "cbc-warn" }, "数值额度读取失败:" + quota.resourceError));
|
|
1351
|
+
}
|
|
1352
|
+
if (quota.dosage && quota.dosage.text) {
|
|
1353
|
+
rows.push(createElement("p", { key: "dosage", className: "cbc-warn" }, "额度告警:" + quota.dosage.text));
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
// api-key 模式:数值额度 API 是 OAuth 专享(实测 401)→ 手填总额度估算档。
|
|
1357
|
+
if (quota && !quota.numericQuota) {
|
|
1358
|
+
var manual = typeof value.quotaTotalManual === "number" ? value.quotaTotalManual : 0;
|
|
1359
|
+
rows.push(createElement("div", { key: "est", className: "cbc-row" },
|
|
1360
|
+
createElement("span", { className: "cbc-row-label" }, "总额度(手填)"),
|
|
1361
|
+
createElement("div", { className: "cbc-row-control" },
|
|
1362
|
+
createElement(NumberField, { fieldKey: "quotaTotalManual", value: value.quotaTotalManual, save: props.save, overridden: props.overridden("quotaTotalManual") }),
|
|
1363
|
+
usage && manual > 0
|
|
1364
|
+
? createElement("span", { className: "cbc-status" },
|
|
1365
|
+
"估算剩余:" + fmtCredit(manual - usage.totalCredit) + " credit(估算)")
|
|
1366
|
+
: null)));
|
|
1367
|
+
rows.push(createElement(HelpNote, { key: "qlink" },
|
|
1368
|
+
createElement("p", { className: "cbc-hint" },
|
|
1369
|
+
"api-key 模式网关不开放数值额度 API(OAuth 专享,实测 401);上方为「手填总额 − 本插件计量累计」的估算值,准确余额见 ",
|
|
1370
|
+
createElement("a", { className: "cbc-link", href: "https://www.codebuddy.cn/profile/plan", target: "_blank", rel: "noreferrer" }, "codebuddy.cn 套餐页"),
|
|
1371
|
+
" 或切到 OAuth 登录。额度为账户级,与 WorkBuddy 共用。")));
|
|
1372
|
+
} else if (quota && quota.numericQuota) {
|
|
1373
|
+
rows.push(createElement(HelpNote, { key: "qlink" },
|
|
1374
|
+
createElement("p", { className: "cbc-hint" },
|
|
1375
|
+
"数值来自 /billing/meter/get-user-resource(账户级,与 WorkBuddy 共用),每分钟缓存;准确口径以 ",
|
|
1376
|
+
createElement("a", { className: "cbc-link", href: "https://www.codebuddy.cn/profile/plan", target: "_blank", rel: "noreferrer" }, "codebuddy.cn 套餐页"),
|
|
1377
|
+
" 为准。")));
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
// 消耗统计(本插件路径实测):今日 / 累计两张统计卡。
|
|
1381
|
+
if (usage) {
|
|
1382
|
+
rows.push(createElement("div", { key: "totals", className: "cbc-stats" },
|
|
1383
|
+
createElement("div", { className: "cbc-stat" },
|
|
1384
|
+
createElement("div", { className: "cbc-statlabel" }, "今日消耗"),
|
|
1385
|
+
createElement("div", { className: "cbc-statnum" }, fmtCredit(usage.today.credit) + " credit"),
|
|
1386
|
+
createElement("div", { className: "cbc-muted" }, fmtNum(usage.today.requests) + " 请求")),
|
|
1387
|
+
createElement("div", { className: "cbc-stat" },
|
|
1388
|
+
createElement("div", { className: "cbc-statlabel" }, "累计消耗"),
|
|
1389
|
+
createElement("div", { className: "cbc-statnum" }, fmtCredit(usage.totalCredit) + " credit"),
|
|
1390
|
+
createElement("div", { className: "cbc-muted" }, fmtNum(usage.totalRequests) + " 请求 · 自 " + fmtTime(usage.since)))));
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
// 桥状态(EADDRINUSE 等失败在这里可见,不再静默崩溃)。
|
|
1394
|
+
if (bridge) {
|
|
1395
|
+
var bridgeText = !bridge.enabled
|
|
1396
|
+
? "已禁用(主聊天将直连失败,请保持启用)"
|
|
1397
|
+
: bridge.running
|
|
1398
|
+
? "运行中(127.0.0.1:" + bridge.port + ")"
|
|
1399
|
+
: "未监听(:" + bridge.port + " " + (bridge.lastError || "启动中") + ")" + (bridge.lastError === "EADDRINUSE" ? "——若占用者是另一个 dsh 实例,其桥仍会代管本实例流量" : "");
|
|
1400
|
+
rows.push(createElement("p", { key: "bridge", className: "cbc-status" },
|
|
1401
|
+
createElement("span", { className: "cbc-chip", style: { marginRight: 6 } },
|
|
1402
|
+
createElement(Dot, { tone: !bridge.enabled ? "err" : (bridge.running ? "ok" : "warn") }), "流式桥"),
|
|
1403
|
+
bridgeText));
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
// 最近轮次(>45s 间隔聚类的近似口径):credit + token 拆解。
|
|
1407
|
+
if (usage && usage.turns && usage.turns.length) {
|
|
1408
|
+
rows.push(createElement("p", { key: "th", className: "cbc-subtitle" }, "最近轮次(按间隔聚类,近似)"));
|
|
1409
|
+
usage.turns.forEach(function (t, i) {
|
|
1410
|
+
var kinds = t.kinds.map(function (k) { return KIND_LABEL[k] || k; }).join("+");
|
|
1411
|
+
rows.push(createElement("div", { key: "t" + i, className: "cbc-listrow" },
|
|
1412
|
+
createElement("span", { className: "cbc-model-ctx" }, fmtTime(t.start)),
|
|
1413
|
+
createElement("span", { className: "cbc-cell-name" }, fmtCredit(t.credit)),
|
|
1414
|
+
createElement("span", { className: "cbc-effort", title: "缓存命中 " + (t.hit || 0) + " / 未命中 " + (t.miss || 0) + "(命中率 = 命中 / 总和)" }, "入 " + fmtNum(t.prompt) + " · 命中 " + fmtHit(t.hit, t.miss) + " · 出 " + fmtNum(t.completion || 0)),
|
|
1415
|
+
createElement("span", { className: "cbc-effort" }, fmtNum(t.requests) + " 请求"),
|
|
1416
|
+
createElement("span", { className: "cbc-cell-masked" }, kinds + (t.models.length ? " · " + t.models.join("/") : ""))));
|
|
1417
|
+
});
|
|
1418
|
+
} else if (usage) {
|
|
1419
|
+
rows.push(createElement("p", { key: "empty", className: "cbc-hint" }, "还没有经过桥的计费请求。消耗数据来自流式桥与搜索/抓取/生图路径的网关 usage 自报。"));
|
|
1420
|
+
}
|
|
1421
|
+
if (fail) rows.push(createElement("p", { key: "fail", className: "cbc-error" }, fail));
|
|
1422
|
+
if (!usageData && !fail) rows.push(createElement("p", { key: "loading", className: "cbc-status" }, "正在读取用量…"));
|
|
1423
|
+
|
|
1424
|
+
return createElement("div", { className: "cbc-section" },
|
|
1425
|
+
createElement("p", { className: "cbc-group-title" }, "额度与用量"),
|
|
1426
|
+
rows);
|
|
1427
|
+
}
|
|
1428
|
+
SECTION_RENDERERS.usage = UsageSection;
|
|
1429
|
+
|
|
1430
|
+
// 资源包按名称聚合:×N、余量/总量求和、最早周期结束日。
|
|
1431
|
+
function aggregatePacks(packs) {
|
|
1432
|
+
var byName = {};
|
|
1433
|
+
var order = [];
|
|
1434
|
+
packs.forEach(function (p) {
|
|
1435
|
+
var key = p.name || "?";
|
|
1436
|
+
if (!byName[key]) {
|
|
1437
|
+
byName[key] = { name: key, count: 0, remain: 0, size: 0, cycleRemain: 0, cycleSize: 0, cycleEnd: null };
|
|
1438
|
+
order.push(key);
|
|
1439
|
+
}
|
|
1440
|
+
var a = byName[key];
|
|
1441
|
+
a.count += 1;
|
|
1442
|
+
a.remain += p.remain || 0;
|
|
1443
|
+
a.size += p.size || 0;
|
|
1444
|
+
a.cycleRemain += p.cycleRemain || 0;
|
|
1445
|
+
a.cycleSize += p.cycleSize || 0;
|
|
1446
|
+
if (p.cycleEnd && (!a.cycleEnd || String(p.cycleEnd) < String(a.cycleEnd))) a.cycleEnd = p.cycleEnd;
|
|
1447
|
+
});
|
|
1448
|
+
var r2 = function (n) { return Math.round(n * 100) / 100; };
|
|
1449
|
+
return order.map(function (k) {
|
|
1450
|
+
var a = byName[k];
|
|
1451
|
+
return { name: a.name, count: a.count, remain: r2(a.remain), size: r2(a.size), cycleRemain: r2(a.cycleRemain), cycleSize: r2(a.cycleSize), cycleEnd: a.cycleEnd };
|
|
1452
|
+
});
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
// 三家模型列表共用的筛选谓词:按 id / name 子串匹配(大小写不敏感)。
|
|
1456
|
+
function matchesModelFilter(item, filterText) {
|
|
1457
|
+
var needle = String(filterText == null ? "" : filterText).trim().toLowerCase();
|
|
1458
|
+
if (!needle) return true;
|
|
1459
|
+
return String(item.id == null ? "" : item.id).toLowerCase().indexOf(needle) >= 0
|
|
1460
|
+
|| (item.name && String(item.name).toLowerCase().indexOf(needle) >= 0);
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
// --- 模型:网关目录同步 + 逐模型启停(同步到对话选择器) ------------------
|
|
1464
|
+
// G4:清单默认跟 /v3/config 走(启动自动同步 + 这里的手动刷新),
|
|
1465
|
+
// 静态清单只做离线兜底;勾选语义 = 在不在对话选择器里(effectiveIds)。
|
|
1466
|
+
function ModelsSection(props) {
|
|
1467
|
+
var post = props.post;
|
|
1468
|
+
var setErr = props.setErr;
|
|
1469
|
+
var reload = props.reload;
|
|
1470
|
+
var modelsInfo = props.modelsInfo || {};
|
|
1471
|
+
// G8:逐模型思考强度(effortByModel 存文件层;桥出站按档位表注入
|
|
1472
|
+
// reasoning_effort)。静态行有档位表才出 select;off 不出现在选项里
|
|
1473
|
+
// (其线值是 null = 省略参数,与"默认"等价)。
|
|
1474
|
+
var effortMap = (props.value && props.value.effortByModel) || {};
|
|
1475
|
+
var saveEffort = function (id, level) {
|
|
1476
|
+
if (!props.save) return;
|
|
1477
|
+
var next = Object.assign({}, effortMap);
|
|
1478
|
+
if (level) next[id] = level; else delete next[id];
|
|
1479
|
+
props.save({ effortByModel: next });
|
|
1480
|
+
};
|
|
1481
|
+
var dataState = useState(null); // {catalog, staticIds, state, effectiveIds, ...}
|
|
1482
|
+
var data = dataState[0];
|
|
1483
|
+
var setData = dataState[1];
|
|
1484
|
+
var syncBusyState = useState(false);
|
|
1485
|
+
var syncBusy = syncBusyState[0];
|
|
1486
|
+
var setSyncBusy = syncBusyState[1];
|
|
1487
|
+
var filterState = useState("");
|
|
1488
|
+
var filter = filterState[0];
|
|
1489
|
+
var setFilter = filterState[1];
|
|
1490
|
+
|
|
1491
|
+
var fetchList = function () {
|
|
1492
|
+
// 清空同值去重表——重新拉取意味着勾选态可能已被外部改动,
|
|
1493
|
+
// 旧表会把「与上次发送同值」的真实切换吞掉(踩坑 #27 的配套
|
|
1494
|
+
// 纪律:去重挡的是同一渲染周期内的双 change,不是跨拉取的状态漂移)。
|
|
1495
|
+
lastSentRef.current = {};
|
|
1496
|
+
post({ action: "model-list" }).then(function (res) {
|
|
1497
|
+
if (res.ok) setData(res.d);
|
|
1498
|
+
else setErr(res.d && res.d.error ? res.d.error : "获取失败");
|
|
1499
|
+
}).catch(function (e) { setErr(e && e.message ? e.message : "获取失败(网络)"); });
|
|
1500
|
+
};
|
|
1501
|
+
// 首次挂载即拉取(标签页懒挂载保证此时才发生),不用先找按钮。
|
|
1502
|
+
useEffect(function () { fetchList(); }, []);
|
|
1503
|
+
|
|
1504
|
+
// G4 手动刷新:重拉 /v3/config 并重铺 settings.yaml 镜像(选择器即时刷新)。
|
|
1505
|
+
// 「同步目录」= 一次点按做两件事:拉网关目录并铺镜像(model-sync)、
|
|
1506
|
+
// 刷新管理列表(model-list)。原先是两个按钮,语义重叠、用户要判断点哪个。
|
|
1507
|
+
// 「同步目录」按钮的忙碌态用独立的 syncBusy。
|
|
1508
|
+
var syncNow = function () {
|
|
1509
|
+
setSyncBusy(true);
|
|
1510
|
+
post({ action: "model-sync" }).then(function (res) {
|
|
1511
|
+
if (!res.ok) { setSyncBusy(false); setErr(res.d && res.d.error ? res.d.error : "同步失败"); return; }
|
|
1512
|
+
if (res.d.sync && res.d.sync.ok === false) setErr("目录同步失败(已" + (res.d.sync.kept ? "保留上次清单" : "回落静态清单") + "):" + res.d.sync.error);
|
|
1513
|
+
reload(); // modelsInfo.sync / effectiveCount 刷新
|
|
1514
|
+
// 目录同步失败也要刷新管理列表——两侧原因分别可见,不互相掩盖。
|
|
1515
|
+
return post({ action: "model-list" }).then(function (r2) {
|
|
1516
|
+
setSyncBusy(false);
|
|
1517
|
+
if (r2.ok) setData(r2.d);
|
|
1518
|
+
else setErr(r2.d && r2.d.error ? r2.d.error : "获取失败");
|
|
1519
|
+
});
|
|
1520
|
+
}).catch(function (e) { setSyncBusy(false); setErr(e && e.message ? e.message : "同步失败(网络)"); });
|
|
1521
|
+
};
|
|
1522
|
+
|
|
1523
|
+
// 受控 checkbox 可能一次交互双 change——用“同值去重”挡(useRef 跨渲染
|
|
1524
|
+
// 存活,踩坑 #27):重复事件携带与上次已发送相同的目标态,真实切换
|
|
1525
|
+
// 必然反值。不用时间窗——勾选往返(POST+reload)可以快过任何时间窗。
|
|
1526
|
+
var lastSentRef = useRef({});
|
|
1527
|
+
var toggleModel = function (m, enabled) {
|
|
1528
|
+
if (lastSentRef.current[m.id] === enabled) return;
|
|
1529
|
+
lastSentRef.current[m.id] = enabled;
|
|
1530
|
+
var staticIds = (data && data.staticIds) || [];
|
|
1531
|
+
var isStatic = staticIds.indexOf(m.id) >= 0;
|
|
1532
|
+
var payload = { id: m.id, enabled: enabled };
|
|
1533
|
+
if (enabled && !isStatic) {
|
|
1534
|
+
payload.profile = {
|
|
1535
|
+
id: m.id,
|
|
1536
|
+
name: m.name || m.id,
|
|
1537
|
+
contextWindow: m.maxInputTokens != null ? m.maxInputTokens : 262144,
|
|
1538
|
+
maxTokens: m.maxOutputTokens != null ? m.maxOutputTokens : 32768,
|
|
1539
|
+
};
|
|
1540
|
+
if (m.images) payload.profile.input = ["text", "image"];
|
|
1541
|
+
}
|
|
1542
|
+
post({ patch: { modelSetEnabled: payload } }).then(function (res) {
|
|
1543
|
+
// 失败回滚同值去重表——否则该模型同向操作被永久吞掉(踩坑 #27 去重的配套纪律)。
|
|
1544
|
+
if (!res.ok) { delete lastSentRef.current[m.id]; setErr(res.d && res.d.error ? res.d.error : "操作失败"); return; }
|
|
1545
|
+
if (res.d.models) {
|
|
1546
|
+
// settingsView ships disabled/extraIds as arrays; turn
|
|
1547
|
+
// them into lookup maps and keep the fetched catalog.
|
|
1548
|
+
var dis = {}; (res.d.models.disabled || []).forEach(function (id) { dis[id] = true; });
|
|
1549
|
+
var ext = {}; (res.d.models.extraIds || []).forEach(function (id) { ext[id] = true; });
|
|
1550
|
+
setData(function (prev) {
|
|
1551
|
+
// G4:勾选状态跟随服务端选择器真值 effectiveIds
|
|
1552
|
+
// (动态目录模型默认在选择器里,不在 extra 里)。
|
|
1553
|
+
// unroutable 必须随拷贝保留——漏掉则勾选后「不可路由」
|
|
1554
|
+
// 徽标与「可路由 M 个」计数静默失真直到下次 model-list。
|
|
1555
|
+
return prev ? { catalog: prev.catalog, staticIds: prev.staticIds, efforts: prev.efforts, staticEfforts: prev.staticEfforts, profiles: prev.profiles, ceilings: prev.ceilings, unroutable: prev.unroutable, effectiveIds: res.d.models.effectiveIds || prev.effectiveIds, state: { disabled: dis, extra: ext, overrides: res.d.models.overrides || {} } } : prev;
|
|
1556
|
+
});
|
|
1557
|
+
}
|
|
1558
|
+
reload(); // 状态条/徽标的可用模型数随 effectiveCount 刷新
|
|
1559
|
+
}).catch(function (e) { delete lastSentRef.current[m.id]; setErr("操作失败:" + (e && e.message ? e.message : String(e))); });
|
|
1560
|
+
};
|
|
1561
|
+
|
|
1562
|
+
// G5:上限保存后本地合并——overrides 取服务端真值,profiles 依
|
|
1563
|
+
// ceilings 重算(输入框显示值随之刷新;清空覆盖即回基值)。
|
|
1564
|
+
var onModelsSaved = function (models) {
|
|
1565
|
+
setData(function (prev) {
|
|
1566
|
+
if (!prev) return prev;
|
|
1567
|
+
var ov = models.overrides || {};
|
|
1568
|
+
var profiles = {};
|
|
1569
|
+
Object.keys(prev.ceilings || {}).forEach(function (id) {
|
|
1570
|
+
var c = prev.ceilings[id] || {};
|
|
1571
|
+
var o = ov[id] || {};
|
|
1572
|
+
profiles[id] = {
|
|
1573
|
+
contextWindow: o.contextWindow != null ? o.contextWindow : c.contextWindow,
|
|
1574
|
+
maxTokens: o.maxTokens != null ? o.maxTokens : c.maxTokens,
|
|
1575
|
+
};
|
|
1576
|
+
});
|
|
1577
|
+
var st = prev.state || {};
|
|
1578
|
+
return Object.assign({}, prev, {
|
|
1579
|
+
effectiveIds: models.effectiveIds || prev.effectiveIds,
|
|
1580
|
+
profiles: profiles,
|
|
1581
|
+
state: Object.assign({}, st, { overrides: ov }),
|
|
1582
|
+
});
|
|
1583
|
+
});
|
|
1584
|
+
};
|
|
1585
|
+
|
|
1586
|
+
var body = null;
|
|
1587
|
+
if (data && data.catalog) {
|
|
1588
|
+
var staticIds = data.staticIds || [];
|
|
1589
|
+
// G4:勾选语义唯一权威 = effectiveIds(服务端算好的选择器真实内容)。
|
|
1590
|
+
// 动态目录启用后目录模型默认在选择器里(不是 extra),不能再靠
|
|
1591
|
+
// disabled/extra 反推。
|
|
1592
|
+
var effectiveIds = data.effectiveIds || [];
|
|
1593
|
+
// P2-4:已知不可路由(目录在列但 /v2 恒 11102)条目表 id→原因,服务端事实源。
|
|
1594
|
+
var unroutable = data.unroutable || {};
|
|
1595
|
+
var needle = filter.trim().toLowerCase();
|
|
1596
|
+
var seen = {};
|
|
1597
|
+
var enabledRows = [];
|
|
1598
|
+
var inactiveRows = [];
|
|
1599
|
+
var makeRow = function (m, fromCatalog) {
|
|
1600
|
+
if (seen[m.id]) return;
|
|
1601
|
+
seen[m.id] = true;
|
|
1602
|
+
if (!matchesModelFilter(m, filter)) return;
|
|
1603
|
+
var isStatic = staticIds.indexOf(m.id) >= 0;
|
|
1604
|
+
var enabled = effectiveIds.indexOf(m.id) >= 0;
|
|
1605
|
+
// P2-4:已知不可路由条目置灰 + 原因徽标;勾选框只在「未启用」时禁用
|
|
1606
|
+
// (不能把已知死模型加进选择器),已启用的保留可勾掉(出清通道)。
|
|
1607
|
+
var deadReason = unroutable[m.id] || null;
|
|
1608
|
+
// 思考档位:档位表来自服务端 `efforts`(静态 reasoningEfforts ∪
|
|
1609
|
+
// 目录声明的 supportedEfforts,2026-09-22 起目录也发能力清单)。
|
|
1610
|
+
// G8:有档位表就出 select 直接设档(存 effortByModel,桥出站注入
|
|
1611
|
+
// reasoning_effort);"关"只在它是真开关(线值非空)时出现,
|
|
1612
|
+
// 否则 off ≡ 省略参数 ≡ 默认态,不出选项。
|
|
1613
|
+
var effortText = null;
|
|
1614
|
+
var effortSel = null;
|
|
1615
|
+
var tiers = ((data.efforts || {})[m.id] || []).slice();
|
|
1616
|
+
if (tiers.length) {
|
|
1617
|
+
effortSel = createElement("select", {
|
|
1618
|
+
className: "cbc-select cbc-effort-sel",
|
|
1619
|
+
title: "思考强度:桥出站对该模型注入 reasoning_effort(默认 = 不注入)",
|
|
1620
|
+
value: effortMap[m.id] || "",
|
|
1621
|
+
onChange: function (e) { saveEffort(m.id, e.target.value); },
|
|
1622
|
+
},
|
|
1623
|
+
createElement("option", { value: "" }, "思考:默认"),
|
|
1624
|
+
tiers.map(function (t) {
|
|
1625
|
+
return createElement("option", { key: t, value: t }, "思考:" + (t === "off" ? "关" : t));
|
|
1626
|
+
}));
|
|
1627
|
+
} else if (typeof m.reasoningEffort === "string" && m.reasoningEffort) {
|
|
1628
|
+
effortText = "思考:" + m.reasoningEffort;
|
|
1629
|
+
}
|
|
1630
|
+
// G5:有效值 = 覆盖 ?? 目录/静态基值;ceiling 用于本地快检与 title。
|
|
1631
|
+
var prof = (data.profiles || {})[m.id] || {};
|
|
1632
|
+
var ceil = (data.ceilings || {})[m.id] || {};
|
|
1633
|
+
var ov = (((data.state || {}).overrides) || {})[m.id] || {};
|
|
1634
|
+
var ctxVal = prof.contextWindow != null ? prof.contextWindow : (m.maxInputTokens != null ? m.maxInputTokens : null);
|
|
1635
|
+
var outVal = prof.maxTokens != null ? prof.maxTokens : (m.maxOutputTokens != null ? m.maxOutputTokens : null);
|
|
1636
|
+
var ctxCeil = ceil.contextWindow != null ? ceil.contextWindow : (m.maxInputTokens != null ? m.maxInputTokens : null);
|
|
1637
|
+
var outCeil = ceil.maxTokens != null ? ceil.maxTokens : (m.maxOutputTokens != null ? m.maxOutputTokens : null);
|
|
1638
|
+
var row = createElement("div", { key: m.id, className: "cbc-listrow" + (deadReason ? " cbc-unroutable" : "") },
|
|
1639
|
+
createElement("input", {
|
|
1640
|
+
type: "checkbox", className: "cbc-check", checked: enabled,
|
|
1641
|
+
disabled: deadReason && !enabled ? true : undefined,
|
|
1642
|
+
title: deadReason
|
|
1643
|
+
? deadReason + (enabled ? "——建议勾掉移出选择器" : "——不可加入选择器")
|
|
1644
|
+
: enabled ? "从对话选择器移除" : "加入对话选择器",
|
|
1645
|
+
onChange: function (e) { toggleModel(m, e.target.checked); },
|
|
1646
|
+
}),
|
|
1647
|
+
createElement("span", { className: "cbc-model-name", title: m.id }, m.id),
|
|
1648
|
+
createElement("span", { className: "cbc-model-ctx" },
|
|
1649
|
+
"ctx ",
|
|
1650
|
+
createElement(LimitInput, { id: m.id, field: "contextWindow", value: ctxVal, ceiling: ctxCeil, overridden: ov.contextWindow != null, post: post, setErr: setErr, onSaved: onModelsSaved }),
|
|
1651
|
+
" / ",
|
|
1652
|
+
createElement(LimitInput, { id: m.id, field: "maxTokens", value: outVal, ceiling: outCeil, overridden: ov.maxTokens != null, post: post, setErr: setErr, onSaved: onModelsSaved }),
|
|
1653
|
+
fromCatalog ? null : createElement("span", { className: "cbc-muted" }, " 旧 id")),
|
|
1654
|
+
isStatic ? createElement("span", { className: "cbc-badge" }, "插件") : createElement("span", { className: "cbc-muted" }, "目录"),
|
|
1655
|
+
deadReason ? createElement("span", { className: "cbc-badge cbc-badge-dead", title: deadReason }, "不可路由") : null,
|
|
1656
|
+
m.cli ? createElement("span", { className: "cbc-badge" }, "CLI") : null,
|
|
1657
|
+
m.images ? createElement("span", { className: "cbc-badge" }, "图") : null,
|
|
1658
|
+
effortSel
|
|
1659
|
+
? effortSel
|
|
1660
|
+
: effortText
|
|
1661
|
+
? createElement("span", { className: "cbc-effort", title: "目录标注的思考强度" }, effortText)
|
|
1662
|
+
: (m.reasoning ? createElement("span", { className: "cbc-badge" }, "思考") : null));
|
|
1663
|
+
(enabled ? enabledRows : inactiveRows).push(row);
|
|
1664
|
+
};
|
|
1665
|
+
(data.catalog.models || []).forEach(function (m) { makeRow(m, true); });
|
|
1666
|
+
// 静态清单里目录没有的(旧 id):目录对象无 ctx 数据
|
|
1667
|
+
staticIds.forEach(function (id) {
|
|
1668
|
+
if (seen[id]) return;
|
|
1669
|
+
makeRow({ id: id, name: id }, false);
|
|
1670
|
+
});
|
|
1671
|
+
body = createElement("div", { style: { marginTop: 8 } },
|
|
1672
|
+
createElement("div", { className: "cbc-scrollbox", style: { maxHeight: 400 } },
|
|
1673
|
+
createElement("p", { className: "cbc-subtitle" }, needle
|
|
1674
|
+
? "筛选命中 " + (enabledRows.length + inactiveRows.length) + "(可用 " + enabledRows.length + " / 未启用 " + inactiveRows.length + ")"
|
|
1675
|
+
: "当前可用(" + enabledRows.length + ")"),
|
|
1676
|
+
enabledRows.length ? enabledRows : createElement("p", { className: "cbc-muted" }, "无匹配模型"),
|
|
1677
|
+
inactiveRows.length
|
|
1678
|
+
? createElement("p", { className: "cbc-subtitle" }, "未启用(" + inactiveRows.length + ")")
|
|
1679
|
+
: null,
|
|
1680
|
+
inactiveRows),
|
|
1681
|
+
createElement("p", { className: "cbc-status" },
|
|
1682
|
+
// P2-4 口径透明:「目录 N · 可路由 M」(M = N − 已知恒 11102 在列条目)。
|
|
1683
|
+
"目录 " + (data.catalog.models || []).length + " 个 · 可路由 "
|
|
1684
|
+
+ (data.catalog.models || []).filter(function (m) { return !unroutable[m.id]; }).length + " 个"
|
|
1685
|
+
+ "(按当前登录凭据获取);勾选即同步到对话模型选择器,下次请求生效。"));
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1688
|
+
var syncInfo = modelsInfo.sync;
|
|
1689
|
+
// P2-4:「目录 N · 可路由 M」(routable 由服务端按已知恒 11102 表算出;
|
|
1690
|
+
// 老服务端无该字段时不显示 M,不编造)。
|
|
1691
|
+
var syncText = syncInfo
|
|
1692
|
+
? "上次同步 " + fmtTime(syncInfo.at) + " · 网关目录 " + syncInfo.count + " 个"
|
|
1693
|
+
+ (syncInfo.routable != null ? " · 可路由 " + syncInfo.routable + " 个" : "")
|
|
1694
|
+
+ "(选择器跟随网关)"
|
|
1695
|
+
: "静态清单兜底(启动与手动同步均未取到网关目录)";
|
|
1696
|
+
return createElement("div", { className: "cbc-section" },
|
|
1697
|
+
createElement("p", { className: "cbc-group-title" }, "模型"),
|
|
1698
|
+
// 操作条(三家同构):同步按钮 + 状态文字 + 筛选框同一行,
|
|
1699
|
+
// 不再各占一个带标签的行。
|
|
1700
|
+
createElement(SyncBar, { busy: syncBusy, onSync: syncNow, info: syncText, filter: filter, onFilter: setFilter }),
|
|
1701
|
+
body,
|
|
1702
|
+
createElement(HelpNote, null, "启动时自动同步网关目录(/v3/config)并入对话选择器,此处可手动再同步;勾选控制每个模型是否出现在选择器,ctx/输出上限可直接改(清空恢复目录默认),均写入宿主配置层(dsh 0.1.7+ 落 profile 的 cordis.patch.yml,更早版本落 ~/.dsh/settings.yaml 的 llm-pi-ai 覆盖层),下次请求生效。"));
|
|
1703
|
+
}
|
|
1704
|
+
SECTION_RENDERERS.models = ModelsSection;
|
|
1705
|
+
|
|
1706
|
+
// --- 工具:网络搜索与抓取 + 图像生成(都是 dsh 工具缝的注册开关) ---------
|
|
1707
|
+
function ToolsSection(props) {
|
|
1708
|
+
var value = props.value;
|
|
1709
|
+
var save = props.save;
|
|
1710
|
+
var overridden = props.overridden;
|
|
1711
|
+
return createElement("div", { className: "cbc-section" },
|
|
1712
|
+
createElement("p", { className: "cbc-group-title" }, "工具"),
|
|
1713
|
+
createElement("p", { className: "cbc-hint" }, "网络搜索与抓取"),
|
|
1714
|
+
createElement("div", { className: "cbc-row" },
|
|
1715
|
+
createElement("div", { className: "cbc-row-label" }, "搜索与抓取"),
|
|
1716
|
+
createElement("div", { className: "cbc-row-control" },
|
|
1717
|
+
createElement("input", { type: "checkbox", className: "cbc-check", checked: value.searchEnabled === true, onChange: function (e) { save({ searchEnabled: e.target.checked }); } }),
|
|
1718
|
+
createElement("span", { className: "cbc-muted" }, value.searchEnabled === true ? "web_search / web_fetch 走 CodeBuddy" : "已禁用(web_search 将报 provider 未注册)"),
|
|
1719
|
+
createElement(ResetButton, { fieldKey: "searchEnabled", overridden: overridden("searchEnabled"), save: save }))),
|
|
1720
|
+
createElement("div", { className: "cbc-row" },
|
|
1721
|
+
createElement("div", { className: "cbc-row-label" }, "搜索默认条数"),
|
|
1722
|
+
createElement("div", { className: "cbc-row-control" },
|
|
1723
|
+
createElement(NumberField, { fieldKey: "searchMaxResults", value: value.searchMaxResults, save: save, overridden: overridden("searchMaxResults") }))),
|
|
1724
|
+
createElement("div", { className: "cbc-row" },
|
|
1725
|
+
createElement("div", { className: "cbc-row-label" }, "抓取正文上限"),
|
|
1726
|
+
createElement("div", { className: "cbc-row-control" },
|
|
1727
|
+
createElement(NumberField, { fieldKey: "fetchBodyCap", value: value.fetchBodyCap, save: save, overridden: overridden("fetchBodyCap") }))),
|
|
1728
|
+
createElement("p", { className: "cbc-hint" }, "禁用即注销 dsh 原生 web_search/web_fetch 的 codebuddy 后端;搜索条数 1–20。"),
|
|
1729
|
+
createElement("hr", { className: "cbc-divider" }),
|
|
1730
|
+
createElement("p", { className: "cbc-hint" }, "图像生成"),
|
|
1731
|
+
createElement("div", { className: "cbc-row" },
|
|
1732
|
+
createElement("div", { className: "cbc-row-label" }, "生图工具"),
|
|
1733
|
+
createElement("div", { className: "cbc-row-control" },
|
|
1734
|
+
createElement("input", { type: "checkbox", className: "cbc-check", checked: value.imageGenEnabled === true, onChange: function (e) { save({ imageGenEnabled: e.target.checked }); } }),
|
|
1735
|
+
createElement("span", { className: "cbc-muted" }, value.imageGenEnabled === true ? "image_generate 工具已注册到 agent" : "已禁用(agent 不再看到 image_generate)"),
|
|
1736
|
+
createElement(ResetButton, { fieldKey: "imageGenEnabled", overridden: overridden("imageGenEnabled"), save: save }))),
|
|
1737
|
+
createElement("div", { className: "cbc-row" },
|
|
1738
|
+
createElement("div", { className: "cbc-row-label" }, "生图模型"),
|
|
1739
|
+
createElement("div", { className: "cbc-row-control" },
|
|
1740
|
+
createElement(TextField, { fieldKey: "imageGenModel", value: value.imageGenModel, save: save, widthClass: "cbc-w260", overridden: overridden("imageGenModel") }))),
|
|
1741
|
+
createElement(HelpNote, null, "经 dsh 工具缝注册 image_generate(/v2/images/generations,约 20s/张,目录标注 x5 credits);图片保存到会话工作区 generated-images/(无工作区信息时落 ~/.dsh/generated-images/)。"));
|
|
1742
|
+
}
|
|
1743
|
+
SECTION_RENDERERS.tools = ToolsSection;
|
|
1744
|
+
|
|
1745
|
+
// --- 服务商(G6:key 型 OpenAI 兼容上游注册表) ------------------------------
|
|
1746
|
+
// preset(ark/百炼/DeepSeek/智谱/Moonshot/OpenRouter/qwen)或自定义:
|
|
1747
|
+
// 添加 = 实测验 key(有目录走 GET /models,无目录/公开目录走 chat 探针)→ 写
|
|
1748
|
+
// settings.yaml provider 块 + .credentials.yaml(<ID>_API_KEY,0600),
|
|
1749
|
+
// 模型进选择器免重启;删除连凭据一起清。key 只回脱敏值。
|
|
1750
|
+
function ProvidersSection(props) {
|
|
1751
|
+
var post = props.post;
|
|
1752
|
+
var setErr = props.setErr;
|
|
1753
|
+
var dataState = useState(null); // {providers, presets}
|
|
1754
|
+
var data = dataState[0];
|
|
1755
|
+
var setData = dataState[1];
|
|
1756
|
+
// 单 flight 行级进度:'add' 或 provider id——谁忙谁的按钮变进度文案。
|
|
1757
|
+
var busyState = useState(null);
|
|
1758
|
+
var busyId = busyState[0];
|
|
1759
|
+
var setBusyId = busyState[1];
|
|
1760
|
+
// 操作成功的行内反馈(失败走全局横幅,踩坑 #7)。
|
|
1761
|
+
var noticeState = useState("");
|
|
1762
|
+
var notice = noticeState[0];
|
|
1763
|
+
var setNotice = noticeState[1];
|
|
1764
|
+
// 删除连同凭据一起清、不可逆——第一次点击只进确认态(4 秒不确认自动复位)。
|
|
1765
|
+
var confirmDelState = useState(null);
|
|
1766
|
+
var confirmDelId = confirmDelState[0];
|
|
1767
|
+
var setConfirmDelId = confirmDelState[1];
|
|
1768
|
+
var confirmTimerRef = useRef(null);
|
|
1769
|
+
var presetState = useState("ark");
|
|
1770
|
+
var presetSel = presetState[0];
|
|
1771
|
+
var setPresetSel = presetState[1];
|
|
1772
|
+
var addIdState = useState("");
|
|
1773
|
+
var addId = addIdState[0];
|
|
1774
|
+
var setAddId = addIdState[1];
|
|
1775
|
+
var addBaseState = useState("");
|
|
1776
|
+
var addBase = addBaseState[0];
|
|
1777
|
+
var setAddBase = addBaseState[1];
|
|
1778
|
+
var addKeyState = useState("");
|
|
1779
|
+
var addKey = addKeyState[0];
|
|
1780
|
+
var setAddKey = addKeyState[1];
|
|
1781
|
+
|
|
1782
|
+
var fetchList = function () {
|
|
1783
|
+
post({ action: "provider-list" }).then(function (res) {
|
|
1784
|
+
if (res.ok) setData(res.d);
|
|
1785
|
+
else setErr(res.d && res.d.error ? res.d.error : "获取失败");
|
|
1786
|
+
}).catch(function (e) { setErr(e && e.message ? e.message : "获取失败(网络)"); });
|
|
1787
|
+
};
|
|
1788
|
+
useEffect(function () { fetchList(); }, []);
|
|
1789
|
+
|
|
1790
|
+
// G7 本机凭据:只读扫描(自动一次)+ 用户点按钮才导入。
|
|
1791
|
+
var findingsState = useState(null); // null=未扫到/未扫;[]=无命中
|
|
1792
|
+
var findings = findingsState[0];
|
|
1793
|
+
var setFindings = findingsState[1];
|
|
1794
|
+
var importBusyState = useState(null); // 正在导入的 source
|
|
1795
|
+
var importBusy = importBusyState[0];
|
|
1796
|
+
var setImportBusy = importBusyState[1];
|
|
1797
|
+
var scanLocal = function () {
|
|
1798
|
+
post({ action: "credential-scan" }).then(function (res) {
|
|
1799
|
+
if (res.ok) setFindings(res.d.findings || []);
|
|
1800
|
+
}).catch(function () { /* 扫描失败不影响分区其余功能 */ });
|
|
1801
|
+
};
|
|
1802
|
+
useEffect(function () { scanLocal(); }, []);
|
|
1803
|
+
var importSource = function (source) {
|
|
1804
|
+
setImportBusy(source);
|
|
1805
|
+
post({ action: "credential-import", source: source }).then(function (res) {
|
|
1806
|
+
setImportBusy(null);
|
|
1807
|
+
if (!res.ok) { setErr(res.d && res.d.error ? res.d.error : "导入失败"); return; }
|
|
1808
|
+
mergeProviders(res.d.providers || []);
|
|
1809
|
+
if (res.d.findings) setFindings(res.d.findings);
|
|
1810
|
+
}).catch(function (e) { setImportBusy(null); setErr("导入失败:" + (e && e.message ? e.message : String(e))); });
|
|
1811
|
+
};
|
|
1812
|
+
|
|
1813
|
+
var mergeProviders = function (list) {
|
|
1814
|
+
setData(function (prev) { return prev ? { presets: prev.presets, providers: list } : prev; });
|
|
1815
|
+
};
|
|
1816
|
+
|
|
1817
|
+
var addUpstream = function () {
|
|
1818
|
+
if (!addKey.trim()) { setErr("添加失败:需要 API Key"); return; }
|
|
1819
|
+
var body = { action: "provider-add", apiKey: addKey.trim() };
|
|
1820
|
+
if (presetSel === "custom") {
|
|
1821
|
+
if (!addId.trim() || !addBase.trim()) { setErr("添加失败:自定义上游需要 id 与 baseURL"); return; }
|
|
1822
|
+
body.id = addId.trim();
|
|
1823
|
+
body.baseURL = addBase.trim();
|
|
1824
|
+
} else {
|
|
1825
|
+
body.preset = presetSel;
|
|
1826
|
+
}
|
|
1827
|
+
var newId = body.preset || body.id;
|
|
1828
|
+
setBusyId("add");
|
|
1829
|
+
post(body).then(function (res) {
|
|
1830
|
+
setBusyId(null);
|
|
1831
|
+
if (!res.ok) { setErr(res.d && res.d.error ? res.d.error : "添加失败"); return; }
|
|
1832
|
+
var list = res.d.providers || [];
|
|
1833
|
+
mergeProviders(list);
|
|
1834
|
+
setAddKey(""); setAddId(""); setAddBase("");
|
|
1835
|
+
var added = list.find(function (p) { return p.id === newId; });
|
|
1836
|
+
setNotice(added ? ("已添加 " + added.displayName + "(" + added.modelCount + " 个模型)") : ("已添加 " + newId));
|
|
1837
|
+
}).catch(function (e) { setBusyId(null); setErr("添加失败:" + (e && e.message ? e.message : String(e))); });
|
|
1838
|
+
};
|
|
1839
|
+
|
|
1840
|
+
var askRemove = function (id) {
|
|
1841
|
+
if (confirmTimerRef.current) clearTimeout(confirmTimerRef.current);
|
|
1842
|
+
setConfirmDelId(id);
|
|
1843
|
+
confirmTimerRef.current = setTimeout(function () { setConfirmDelId(null); }, 4000);
|
|
1844
|
+
};
|
|
1845
|
+
|
|
1846
|
+
var removeUpstream = function (id) {
|
|
1847
|
+
if (confirmTimerRef.current) { clearTimeout(confirmTimerRef.current); confirmTimerRef.current = null; }
|
|
1848
|
+
setConfirmDelId(null);
|
|
1849
|
+
setBusyId(id);
|
|
1850
|
+
post({ action: "provider-remove", id: id }).then(function (res) {
|
|
1851
|
+
setBusyId(null);
|
|
1852
|
+
if (!res.ok) { setErr(res.d && res.d.error ? res.d.error : "删除失败"); return; }
|
|
1853
|
+
mergeProviders(res.d.providers || []);
|
|
1854
|
+
setNotice("已删除 " + id + "(连同凭据)");
|
|
1855
|
+
}).catch(function (e) { setBusyId(null); setErr("删除失败:" + (e && e.message ? e.message : String(e))); });
|
|
1856
|
+
};
|
|
1857
|
+
|
|
1858
|
+
var refreshUpstream = function (id) {
|
|
1859
|
+
setBusyId(id);
|
|
1860
|
+
post({ action: "provider-refresh", id: id }).then(function (res) {
|
|
1861
|
+
setBusyId(null);
|
|
1862
|
+
if (!res.ok) { setErr(res.d && res.d.error ? res.d.error : "刷新失败"); return; }
|
|
1863
|
+
var list = res.d.providers || [];
|
|
1864
|
+
mergeProviders(list);
|
|
1865
|
+
var p = list.find(function (x) { return x.id === id; });
|
|
1866
|
+
setNotice(p ? ("已刷新 " + id + ":" + p.modelCount + " 个模型") : ("已刷新 " + id));
|
|
1867
|
+
}).catch(function (e) { setBusyId(null); setErr("刷新失败:" + (e && e.message ? e.message : String(e))); });
|
|
1868
|
+
};
|
|
1869
|
+
|
|
1870
|
+
// P2-5「测一下」:只读验证(/models 或聊天探针),不写 provider 块;
|
|
1871
|
+
// 结果就地落在行内(testResults[id]),不进全局横幅。
|
|
1872
|
+
var testResultsState = useState({});
|
|
1873
|
+
var testResults = testResultsState[0];
|
|
1874
|
+
var setTestResults = testResultsState[1];
|
|
1875
|
+
var testUpstream = function (id) {
|
|
1876
|
+
setBusyId(id);
|
|
1877
|
+
setTestResults(function (prev) { var n = Object.assign({}, prev); delete n[id]; return n; });
|
|
1878
|
+
post({ action: "provider-test", id: id }).then(function (res) {
|
|
1879
|
+
setBusyId(null);
|
|
1880
|
+
var cell = res.ok && res.d.available
|
|
1881
|
+
? { ok: true, text: "✓ 可用(" + (res.d.detail || "验证通过") + ")" }
|
|
1882
|
+
: { ok: false, text: "✗ " + ((res.d && res.d.error) || ("HTTP " + res.status)) };
|
|
1883
|
+
setTestResults(function (prev) { var n = Object.assign({}, prev); n[id] = cell; return n; });
|
|
1884
|
+
}).catch(function (e) {
|
|
1885
|
+
setBusyId(null);
|
|
1886
|
+
var cell = { ok: false, text: "✗ " + (e && e.message ? e.message : "网络错误") };
|
|
1887
|
+
setTestResults(function (prev) { var n = Object.assign({}, prev); n[id] = cell; return n; });
|
|
1888
|
+
});
|
|
1889
|
+
};
|
|
1890
|
+
|
|
1891
|
+
var providers = (data && data.providers) || [];
|
|
1892
|
+
var presets = (data && data.presets) || [];
|
|
1893
|
+
var isCustom = presetSel === "custom";
|
|
1894
|
+
// 本机凭据行只渲染可导入项:不可导入命中(登录态在 keyring、方言未接入等)
|
|
1895
|
+
// 没有任何可执行操作,罗列即噪音。扫描器/路由保留,将来新增可导入源自动出现。
|
|
1896
|
+
var importableFindings = findings ? findings.filter(function (f) { return f.importable; }) : findings;
|
|
1897
|
+
|
|
1898
|
+
return createElement("div", { className: "cbc-section" },
|
|
1899
|
+
createElement("p", { className: "cbc-group-title" }, "服务商"),
|
|
1900
|
+
(importableFindings && importableFindings.length)
|
|
1901
|
+
? createElement("div", { className: "cbc-row" },
|
|
1902
|
+
createElement("div", { className: "cbc-row-label" }, "本机凭据"),
|
|
1903
|
+
createElement("div", { className: "cbc-row-control" },
|
|
1904
|
+
importableFindings.map(function (f) {
|
|
1905
|
+
var already = f.detail && f.detail.import && providers.some(function (p) { return p.id === f.detail.import.id; });
|
|
1906
|
+
var kids = [f.label + "(" + (f.kind === "apikey" ? "API Key" : f.kind === "oauth" ? "OAuth" : "未知") + ")"];
|
|
1907
|
+
if (!already) kids.push(createElement(CbcButton, { key: "go", variant: "ghost", disabled: importBusy !== null, onClick: function () { importSource(f.source); } }, importBusy === f.source ? "导入中…" : "一键导入"));
|
|
1908
|
+
if (already) kids.push(createElement("span", { key: "done", className: "cbc-badge" }, "已导入"));
|
|
1909
|
+
if (!already && f.detail && f.detail.expiredHint) kids.push(createElement("span", { key: "exp", className: "cbc-muted" }, "令牌疑似过期,导入时实测"));
|
|
1910
|
+
return createElement("span", { key: f.source, title: f.path, style: { display: "inline-flex", alignItems: "center", gap: 6 } }, kids);
|
|
1911
|
+
})))
|
|
1912
|
+
: null,
|
|
1913
|
+
providers.map(function (p) {
|
|
1914
|
+
var tr = testResults[p.id];
|
|
1915
|
+
return createElement("div", { key: p.id, className: "cbc-listrow" },
|
|
1916
|
+
createElement("span", { className: "cbc-cell-name", title: p.baseURL }, p.displayName),
|
|
1917
|
+
createElement("span", { className: "cbc-muted" }, p.id + " · " + p.modelCount + " 模型"),
|
|
1918
|
+
createElement("span", { className: "cbc-muted", title: "凭据引用 " + p.keyRef }, p.maskedKey || "无凭据"),
|
|
1919
|
+
createElement(CbcButton, { variant: "ghost", disabled: busyId !== null, onClick: function () { testUpstream(p.id); } },
|
|
1920
|
+
busyId === p.id ? "验证中…" : "测一下"),
|
|
1921
|
+
createElement(CbcButton, { variant: "ghost", disabled: busyId !== null, onClick: function () { refreshUpstream(p.id); } }, busyId === p.id ? "刷新中…" : "刷新模型"),
|
|
1922
|
+
confirmDelId === p.id
|
|
1923
|
+
? createElement(CbcButton, { variant: "outline", danger: true, disabled: busyId !== null, onClick: function () { removeUpstream(p.id); } }, busyId === p.id ? "删除中…" : "确认删除")
|
|
1924
|
+
: createElement(CbcButton, { variant: "ghost", danger: true, disabled: busyId !== null, onClick: function () { askRemove(p.id); } }, "删除"),
|
|
1925
|
+
tr ? createElement("span", { className: tr.ok ? "cbc-status" : "cbc-warn", "data-test": p.id }, tr.text) : null);
|
|
1926
|
+
}),
|
|
1927
|
+
createElement("div", { className: "cbc-row" },
|
|
1928
|
+
createElement("div", { className: "cbc-row-label" }, "添加上游"),
|
|
1929
|
+
createElement("div", { className: "cbc-row-control" },
|
|
1930
|
+
createElement("select", {
|
|
1931
|
+
className: "cbc-select cbc-w140",
|
|
1932
|
+
value: presetSel,
|
|
1933
|
+
onChange: function (e) { setPresetSel(e.target.value); },
|
|
1934
|
+
},
|
|
1935
|
+
presets.map(function (p) { return createElement("option", { key: p.id, value: p.id }, p.displayName); }),
|
|
1936
|
+
createElement("option", { value: "custom" }, "自定义 OpenAI 兼容")),
|
|
1937
|
+
isCustom ? createElement(CbcInput, { widthClass: "cbc-w110", placeholder: "id(小写字母/数字/-)", value: addId, onInput: function (e) { setAddId(e.target.value); } }) : null,
|
|
1938
|
+
isCustom ? createElement(CbcInput, { widthClass: "cbc-w200", placeholder: "baseURL(如 https://…/v1)", value: addBase, onInput: function (e) { setAddBase(e.target.value); } }) : null,
|
|
1939
|
+
createElement(CbcInput, { widthClass: "cbc-w200", placeholder: "API Key", type: "password", autoComplete: "off", value: addKey, onInput: function (e) { setAddKey(e.target.value); } }),
|
|
1940
|
+
createElement(CbcButton, { variant: "outline", disabled: busyId !== null, onClick: addUpstream }, busyId === "add" ? "验证中…" : "测试并添加"))),
|
|
1941
|
+
notice ? createElement("p", { className: "cbc-status" }, notice) : null,
|
|
1942
|
+
!providers.length ? createElement("p", { className: "cbc-muted" }, "还没有注册上游;从下拉选预设或自定义添加。") : null,
|
|
1943
|
+
createElement(HelpNote, null, "key 型 OpenAI 兼容上游:添加时实测验证 key(有目录的拉 GET /models,无目录/公开目录的走聊天探针)并把模型写进选择器(免重启);key 落 ~/.dsh/.credentials.yaml(<ID>_API_KEY,0600),只回脱敏值;删除连同凭据一起清。"));
|
|
1944
|
+
}
|
|
1945
|
+
SECTION_RENDERERS.providers = ProvidersSection;
|
|
1946
|
+
|
|
1947
|
+
// --- 宿主实况(P1-1,只读对账):期望 = 插件镜像纪律,实际 = 宿主配置层
|
|
1948
|
+
// 合成结果(hostConfig 读出)。漂移 = 上游改模块名 patch 条目被整条跳过
|
|
1949
|
+
// (踩坑 #43/#49,退出码仍 0)这类静默失效在卡上的唯一可见出口。
|
|
1950
|
+
// 数据全部来自 GET 视图的 host 字段,本分区不发任何请求。
|
|
1951
|
+
function HostReconSection(props) {
|
|
1952
|
+
var host = props.host;
|
|
1953
|
+
if (!host) {
|
|
1954
|
+
return createElement("div", { className: "cbc-section" },
|
|
1955
|
+
createElement("p", { className: "cbc-group-title" }, "宿主实况"),
|
|
1956
|
+
createElement("p", { className: "cbc-muted" }, "对账数据不可用(旧版视图无 host 字段)。"));
|
|
1957
|
+
}
|
|
1958
|
+
var chanRow = function (label, ch) {
|
|
1959
|
+
if (!ch) return null;
|
|
1960
|
+
var drifted = host.drift && host.drift.some(function (d) { return d && d.channel === label.key; });
|
|
1961
|
+
var text;
|
|
1962
|
+
if (ch.expected == null && ch.actual == null) text = "无镜像(符合预期)";
|
|
1963
|
+
else text = "期望 " + (ch.expected == null ? "无镜像" : ch.expected + " 模型") +
|
|
1964
|
+
" · 实际 " + (ch.actual == null ? "无镜像" : ch.actual + " 模型") +
|
|
1965
|
+
(ch.inferred ? "(推断自静态基线)" : "");
|
|
1966
|
+
return createElement("div", { className: "cbc-row", key: label.key, "data-recon": label.key },
|
|
1967
|
+
createElement("div", { className: "cbc-row-label" }, label.name),
|
|
1968
|
+
createElement("div", { className: "cbc-row-control" },
|
|
1969
|
+
createElement("span", { className: drifted ? "cbc-warn" : "cbc-status", style: { display: "inline-flex", alignItems: "center", gap: 6 } },
|
|
1970
|
+
createElement(Dot, { tone: drifted ? "warn" : "ok" }), text)));
|
|
1971
|
+
};
|
|
1972
|
+
var missing = host.missingEntries;
|
|
1973
|
+
var entriesText = missing == null
|
|
1974
|
+
? "—(当前宿主无 describe 可查)"
|
|
1975
|
+
: missing.length === 0
|
|
1976
|
+
? "在位"
|
|
1977
|
+
: "缺失:" + missing.join("、");
|
|
1978
|
+
var webPin = host.webPin || null;
|
|
1979
|
+
var webPinText = webPin == null
|
|
1980
|
+
? null
|
|
1981
|
+
: webPin.ok
|
|
1982
|
+
? "web 钉选在位(search/fetch → codebuddy)"
|
|
1983
|
+
: "web 钉选漂移:searchProvider=" + (webPin.searchProvider || "(无)") + " · fetchProvider=" + (webPin.fetchProvider || "(无)");
|
|
1984
|
+
return createElement("div", { className: "cbc-section" },
|
|
1985
|
+
createElement("p", { className: "cbc-group-title" }, "宿主实况"),
|
|
1986
|
+
createElement("div", { className: "cbc-row" },
|
|
1987
|
+
createElement("div", { className: "cbc-row-label" }, "配置层"),
|
|
1988
|
+
createElement("div", { className: "cbc-row-control" },
|
|
1989
|
+
createElement("span", { className: "cbc-status" },
|
|
1990
|
+
host.mode === "forms" ? "forms(profile patch)" : "legacy(settings.yaml)",
|
|
1991
|
+
host.servicePresent ? "" : " · settings 服务不在位"))),
|
|
1992
|
+
chanRow({ key: "codebuddy", name: "CodeBuddy 模型镜像" }, host.channels && host.channels.codebuddy),
|
|
1993
|
+
chanRow({ key: "trae", name: "Trae 路由镜像" }, host.channels && host.channels.trae),
|
|
1994
|
+
chanRow({ key: "qoder", name: "Qoder 路由镜像" }, host.channels && host.channels.qoder),
|
|
1995
|
+
createElement("div", { className: "cbc-row", "data-recon": "entries" },
|
|
1996
|
+
createElement("div", { className: "cbc-row-label" }, "patch 条目"),
|
|
1997
|
+
createElement("div", { className: "cbc-row-control" },
|
|
1998
|
+
createElement("span", { className: (missing && missing.length) ? "cbc-warn" : "cbc-status", style: { display: "inline-flex", alignItems: "center", gap: 6 } },
|
|
1999
|
+
missing == null ? null : createElement(Dot, { tone: missing.length ? "warn" : "ok" }), entriesText))),
|
|
2000
|
+
webPinText == null ? null : createElement("div", { className: "cbc-row", "data-recon": "webpin" },
|
|
2001
|
+
createElement("div", { className: "cbc-row-label" }, "web 钉选"),
|
|
2002
|
+
createElement("div", { className: "cbc-row-control" },
|
|
2003
|
+
createElement("span", { className: webPin.ok ? "cbc-status" : "cbc-warn", style: { display: "inline-flex", alignItems: "center", gap: 6 } },
|
|
2004
|
+
createElement(Dot, { tone: webPin.ok ? "ok" : "warn" }), webPinText))),
|
|
2005
|
+
createElement(HelpNote, null, "只读对账:左侧是插件应铺的镜像(模型数/路由存在性),右侧是宿主配置层实际加载值;不一致即漂移(区块头出现「宿主漂移」warn)。patch 条目行的口径 = settings 命名空间(llm-pi-ai、agent-default-model;web 钉选不是 settings 条目,单独一行直查 web 服务的实际钉选值)。离线权威映射(entry id → 模块名)用 dsh --dump-config 的 bundle 清单段核对——上游改模块名会让 profile patch 条目被整条跳过且退出码仍为 0(踩坑 #49)。"));
|
|
2006
|
+
}
|
|
2007
|
+
SECTION_RENDERERS.hostrecon = HostReconSection;
|
|
2008
|
+
|
|
2009
|
+
// --- Trae/Qoder(+模型区)同构行组件 --------------------------------------
|
|
2010
|
+
// 模块级定义:组件身份随渲染位置变化会丢焦点(与下方字段控件同纪律)。
|
|
2011
|
+
// 三者只做形状收敛,DOM 树与抽取前逐字节同形——套件按 .cbc-* 结构断言,
|
|
2012
|
+
// 形状即契约。
|
|
2013
|
+
|
|
2014
|
+
// 模型组操作条(三家同构):同步按钮 + 状态文字 + 过滤框同一行。
|
|
2015
|
+
// info 由调用方拼好(CodeBuddy 是「上次同步…」长文案,Trae/Qoder 追加
|
|
2016
|
+
// 「(通道未启用)」),组件不掺和口径。
|
|
2017
|
+
function SyncBar(props) {
|
|
2018
|
+
return createElement("div", { className: "cbc-syncbar" },
|
|
2019
|
+
createElement(CbcButton, {
|
|
2020
|
+
variant: "outline",
|
|
2021
|
+
icon: IconRefresh ? createElement(IconRefresh, { size: 14 }) : undefined,
|
|
2022
|
+
disabled: props.busy,
|
|
2023
|
+
onClick: props.onSync,
|
|
2024
|
+
}, props.busy ? "同步中…" : "同步目录"),
|
|
2025
|
+
createElement("span", { className: "cbc-muted" }, props.info),
|
|
2026
|
+
createElement(CbcInput, {
|
|
2027
|
+
widthClass: "cbc-w220", placeholder: "输入 id 关键字过滤模型…",
|
|
2028
|
+
value: props.filter, onInput: function (e) { props.onFilter(e.target.value); },
|
|
2029
|
+
}));
|
|
2030
|
+
}
|
|
2031
|
+
|
|
2032
|
+
// 通道模型行外壳(Trae/Qoder 同构):checkbox + id + 显示名 + ctx 徽标。
|
|
2033
|
+
// Qoder 的思考强度/上下文 select 走 extra(Fragment 不产生 DOM 包装)。
|
|
2034
|
+
function SubModelRow(props) {
|
|
2035
|
+
return createElement("div", { className: "cbc-listrow" },
|
|
2036
|
+
createElement("input", {
|
|
2037
|
+
type: "checkbox", className: "cbc-check", checked: props.enabled,
|
|
2038
|
+
title: props.enabled ? props.titleOff : props.titleOn,
|
|
2039
|
+
onChange: props.onToggle,
|
|
2040
|
+
}),
|
|
2041
|
+
createElement("span", { className: "cbc-tname", title: props.id }, props.id),
|
|
2042
|
+
createElement("span", { className: "cbc-muted" }, props.name && props.name !== props.id ? props.name : ""),
|
|
2043
|
+
createElement("span", { className: "cbc-model-ctx" },
|
|
2044
|
+
props.ctx != null ? "ctx " + props.ctx : "",
|
|
2045
|
+
props.image ? " · 图" : ""),
|
|
2046
|
+
props.extra || null);
|
|
2047
|
+
}
|
|
2048
|
+
|
|
2049
|
+
// 通道登录行外壳(Trae/Qoder 同构):标签 + 主按钮 + Dot 状态文本。
|
|
2050
|
+
// Qoder 的重开登录页/测一下/测一下结果走 extra。
|
|
2051
|
+
function ChannelLoginRow(props) {
|
|
2052
|
+
return createElement("div", { className: "cbc-row" },
|
|
2053
|
+
createElement("div", { className: "cbc-row-label" }, props.label),
|
|
2054
|
+
createElement("div", { className: "cbc-row-control" },
|
|
2055
|
+
props.button,
|
|
2056
|
+
createElement("span", {
|
|
2057
|
+
className: props.status.tone === "warn" ? "cbc-warn" : "cbc-status",
|
|
2058
|
+
style: { display: "inline-flex", alignItems: "center", gap: 6 },
|
|
2059
|
+
},
|
|
2060
|
+
createElement(Dot, { tone: props.status.tone }), props.status.text),
|
|
2061
|
+
props.extra || null));
|
|
2062
|
+
}
|
|
2063
|
+
|
|
2064
|
+
// --- TraeWork CN(订阅额度通道,v0.8.x)--------------------------------
|
|
2065
|
+
// 翻译网关 + 自持设备密钥 OAuth + 本机 state.vscdb 目录;目录同步后可逐
|
|
2066
|
+
// 模型启停(traeModelSetEnabled,全部禁用 = 整块路由从选择器移除)。
|
|
2067
|
+
function TraeSection(props) {
|
|
2068
|
+
var value = props.value;
|
|
2069
|
+
var save = props.save;
|
|
2070
|
+
var post = props.post;
|
|
2071
|
+
var reload = props.reload;
|
|
2072
|
+
var setErr = props.setErr;
|
|
2073
|
+
var overridden = props.overridden;
|
|
2074
|
+
var trae = props.trae || {};
|
|
2075
|
+
var toauth = trae.oauth || {};
|
|
2076
|
+
var tmodels = trae.models || {};
|
|
2077
|
+
|
|
2078
|
+
var busyState = useState(false);
|
|
2079
|
+
var busy = busyState[0];
|
|
2080
|
+
var setBusy = busyState[1];
|
|
2081
|
+
// 退出登录二次确认(同 ProvidersSection 模式):第一次点击进确认态
|
|
2082
|
+
//(按钮变"确认退出"),4 秒不确认自动复位。
|
|
2083
|
+
var tLogoutConfirmState = useState(false);
|
|
2084
|
+
var tLogoutConfirm = tLogoutConfirmState[0];
|
|
2085
|
+
var setTLogoutConfirm = tLogoutConfirmState[1];
|
|
2086
|
+
var tLogoutTimerRef = useRef(null);
|
|
2087
|
+
var askTLogout = function () {
|
|
2088
|
+
if (tLogoutTimerRef.current) clearTimeout(tLogoutTimerRef.current);
|
|
2089
|
+
setTLogoutConfirm(true);
|
|
2090
|
+
tLogoutTimerRef.current = setTimeout(function () { setTLogoutConfirm(false); }, 4000);
|
|
2091
|
+
};
|
|
2092
|
+
var doTLogout = function () {
|
|
2093
|
+
if (tLogoutTimerRef.current) { clearTimeout(tLogoutTimerRef.current); tLogoutTimerRef.current = null; }
|
|
2094
|
+
setTLogoutConfirm(false);
|
|
2095
|
+
post({ action: "trae-oauth-logout" }).then(function (res) {
|
|
2096
|
+
if (res.ok) reload();
|
|
2097
|
+
else setErr(res.d && res.d.error ? res.d.error : "退出失败(HTTP " + res.status + ")");
|
|
2098
|
+
}).catch(function (e) { setErr(e && e.message ? e.message : "退出失败(网络)"); });
|
|
2099
|
+
};
|
|
2100
|
+
var listState = useState(null); // {profiles, disabled:{id:true}} 来自 trae-model-list
|
|
2101
|
+
var tlist = listState[0];
|
|
2102
|
+
var setTlist = listState[1];
|
|
2103
|
+
// 模型筛选(与 CodeBuddy 模型组同款):只过滤渲染,不发请求。
|
|
2104
|
+
var tfilterState = useState("");
|
|
2105
|
+
var tfilter = tfilterState[0];
|
|
2106
|
+
var setTfilter = tfilterState[1];
|
|
2107
|
+
|
|
2108
|
+
var fetchTlist = function () {
|
|
2109
|
+
// 清空同值去重表——重新拉取意味着勾选态可能已被外部改动(同 CodeBuddy 模型行)。
|
|
2110
|
+
tlastSentRef.current = {};
|
|
2111
|
+
post({ action: "trae-model-list" }).then(function (res) {
|
|
2112
|
+
if (!res.ok) return;
|
|
2113
|
+
var disabled = {};
|
|
2114
|
+
(res.d.disabled || []).forEach(function (id) { disabled[id] = true; });
|
|
2115
|
+
setTlist({ profiles: (res.d.view && res.d.view.profiles) || [], disabled: disabled });
|
|
2116
|
+
}).catch(function (e) {
|
|
2117
|
+
// 拉取失败必须带原因(踩坑 #7,终审 Minor):原先的空 catch 是全文件唯一
|
|
2118
|
+
// 真吞错——"请求失败"与"目录本来是空的"在 UI 上不可区分。走本分区已有的
|
|
2119
|
+
// 错误出口(setErr = 卡片顶部横幅)。注意 `!res.ok`(目录未同步等**已知**
|
|
2120
|
+
// 空态)在上面正常 return、分组不显示,不进这里 ⇒ 两种态仍可辨。
|
|
2121
|
+
setErr("Trae 模型目录读取失败:" + (e && e.message ? e.message : "(网络)"));
|
|
2122
|
+
});
|
|
2123
|
+
};
|
|
2124
|
+
useEffect(function () { fetchTlist(); }, []);
|
|
2125
|
+
// 开启通道或目录同步完成(sync.at 变化)后重拉启停清单——挂载时拉到的
|
|
2126
|
+
// 空结果(目录尚未同步)必须被替换,否则用户开启后看不到启停组。
|
|
2127
|
+
var traeSyncAt = tmodels.sync && tmodels.sync.at;
|
|
2128
|
+
var traeOn = value.traeEnabled === true;
|
|
2129
|
+
useEffect(function () { fetchTlist(); }, [traeSyncAt, traeOn]);
|
|
2130
|
+
|
|
2131
|
+
var startLogin = function () { startOAuthFlow(post, "trae-oauth-start", setBusy, setErr, reload); };
|
|
2132
|
+
// P1-2 余额行(双额度池,只读):展开/收起两个边界各采一次 + 登录态翻转
|
|
2133
|
+
// 时重采,不周期轮询;服务端 60s memoize 控成本。取不到一律「—(原因)」,
|
|
2134
|
+
// 绝不编造数值;未登录不发请求。
|
|
2135
|
+
var active = props.active === true;
|
|
2136
|
+
var quotaState = useState(null);
|
|
2137
|
+
var quota = quotaState[0];
|
|
2138
|
+
var setQuota = quotaState[1];
|
|
2139
|
+
var quotaGenRef = useRef(0);
|
|
2140
|
+
var fetchQuota = function () {
|
|
2141
|
+
if (!toauth.signedIn) { setQuota({ error: "未登录" }); return; }
|
|
2142
|
+
var my = ++quotaGenRef.current;
|
|
2143
|
+
post({ action: "trae-quota" }).then(function (res) {
|
|
2144
|
+
if (my !== quotaGenRef.current) return;
|
|
2145
|
+
setQuota(res.ok && res.d && res.d.quota ? res.d.quota : { error: (res.d && res.d.error) || ("HTTP " + res.status) });
|
|
2146
|
+
}).catch(function (e) {
|
|
2147
|
+
if (my !== quotaGenRef.current) return;
|
|
2148
|
+
setQuota({ error: e && e.message ? e.message : "(网络)" });
|
|
2149
|
+
});
|
|
2150
|
+
};
|
|
2151
|
+
useEffect(function () { fetchQuota(); }, [active, toauth.signedIn]);
|
|
2152
|
+
var balanceText = !quota ? "读取中…"
|
|
2153
|
+
: quota.error ? "—(" + quota.error + ")"
|
|
2154
|
+
: quota.pools
|
|
2155
|
+
? "IDE 池 余 " + fmtCredit(quota.pools.ide.remain) + " / " + fmtCredit(quota.pools.ide.limit) +
|
|
2156
|
+
" · work 池 余 " + fmtCredit(quota.pools.work.remain) + " / " + fmtCredit(quota.pools.work.limit)
|
|
2157
|
+
: "—";
|
|
2158
|
+
var syncModels = function () {
|
|
2159
|
+
setBusy(true);
|
|
2160
|
+
post({ action: "trae-model-sync" }).then(function (res) {
|
|
2161
|
+
setBusy(false);
|
|
2162
|
+
// 同步失败路由也回 HTTP 200({ok:false,sync:{error}})——必须看 res.d.ok
|
|
2163
|
+
// (同 Qoder 行口径),且要 reload:失败原因经 GET 视图进模型区文案。
|
|
2164
|
+
if (!res.ok || (res.d && res.d.ok === false)) {
|
|
2165
|
+
setErr(res.d && res.d.sync && res.d.sync.error ? res.d.sync.error : "目录同步失败");
|
|
2166
|
+
reload();
|
|
2167
|
+
return;
|
|
2168
|
+
}
|
|
2169
|
+
fetchTlist();
|
|
2170
|
+
reload();
|
|
2171
|
+
}).catch(function (e) { setBusy(false); setErr(e && e.message ? e.message : "目录同步失败(网络)"); });
|
|
2172
|
+
};
|
|
2173
|
+
|
|
2174
|
+
// Trae 模型启停:受控 checkbox 双 change 同值去重(同 CodeBuddy 模型行)。
|
|
2175
|
+
var tlastSentRef = useRef({});
|
|
2176
|
+
var toggleTmodel = function (p, enabled) {
|
|
2177
|
+
if (tlastSentRef.current[p.id] === enabled) return;
|
|
2178
|
+
tlastSentRef.current[p.id] = enabled;
|
|
2179
|
+
post({ patch: { traeModelSetEnabled: { id: p.id, enabled: enabled } } }).then(function (res) {
|
|
2180
|
+
// 失败回滚同值去重表(同 CodeBuddy 模型行)。
|
|
2181
|
+
if (!res.ok) { delete tlastSentRef.current[p.id]; setErr(res.d && res.d.error ? res.d.error : "操作失败"); return; }
|
|
2182
|
+
var disabled = {};
|
|
2183
|
+
(((res.d.trae || {}).models || {}).disabled || []).forEach(function (id) { disabled[id] = true; });
|
|
2184
|
+
setTlist(function (prev) { return prev ? { profiles: prev.profiles, disabled: disabled } : prev; });
|
|
2185
|
+
reload(); // 徽标/状态随镜像刷新
|
|
2186
|
+
}).catch(function (e) { delete tlastSentRef.current[p.id]; setErr("操作失败:" + (e && e.message ? e.message : String(e))); });
|
|
2187
|
+
};
|
|
2188
|
+
|
|
2189
|
+
// 逐模型思考强度(G3,同 Qoder prefs 纪律):后端完整替换语义,发全量
|
|
2190
|
+
// 期望态;useRef 同值去重 + 失败销账(踩坑 #27/#32)。成功后以响应回带
|
|
2191
|
+
// 的 trae.modelPrefs 为权威刷新本地态。
|
|
2192
|
+
var tPrefsState = useState(null);
|
|
2193
|
+
var tPrefs = tPrefsState[0];
|
|
2194
|
+
var setTPrefs = tPrefsState[1];
|
|
2195
|
+
var tprefLastSentRef = useRef({});
|
|
2196
|
+
var saveTPref = function (id, prefs) {
|
|
2197
|
+
var rec = {};
|
|
2198
|
+
if (prefs.effort) rec.effort = prefs.effort;
|
|
2199
|
+
var json = JSON.stringify(rec);
|
|
2200
|
+
if (tprefLastSentRef.current[id] === json) return;
|
|
2201
|
+
tprefLastSentRef.current[id] = json;
|
|
2202
|
+
post({ patch: { traeModelSetPrefs: { id: id, prefs: rec } } }).then(function (res) {
|
|
2203
|
+
if (!res.ok) {
|
|
2204
|
+
delete tprefLastSentRef.current[id];
|
|
2205
|
+
setErr(res.d && res.d.error ? res.d.error : "操作失败");
|
|
2206
|
+
reload(); // 服务端未变,回同步本地(select 弹回旧值)
|
|
2207
|
+
return;
|
|
2208
|
+
}
|
|
2209
|
+
setTPrefs((((res.d.trae || {}).models || {}).modelPrefs) || {});
|
|
2210
|
+
reload();
|
|
2211
|
+
}).catch(function (e) {
|
|
2212
|
+
delete tprefLastSentRef.current[id];
|
|
2213
|
+
setErr("操作失败:" + (e && e.message ? e.message : String(e)));
|
|
2214
|
+
});
|
|
2215
|
+
};
|
|
2216
|
+
|
|
2217
|
+
var account = toauth.account;
|
|
2218
|
+
var loginStatus = toauth.pending
|
|
2219
|
+
? { tone: "warn", text: "登录进行中:浏览器完成授权后会自动回调本机(10 分钟内有效)。" }
|
|
2220
|
+
: toauth.signedIn
|
|
2221
|
+
? { tone: "ok", text: "已登录" + (account && (account.nickname || account.uid) ? ":" + (account.nickname || account.uid) : "") +
|
|
2222
|
+
(toauth.accessTokenExpiresAt ? "(令牌至 " + fmtTime(toauth.accessTokenExpiresAt) + ")" : "") + (toauth.error ? "|" + toauth.error : "") }
|
|
2223
|
+
: { tone: "off", text: "未登录" + (toauth.error ? "|" + toauth.error : "") };
|
|
2224
|
+
|
|
2225
|
+
// 同步失败时 error 随视图回来(providers/trae syncView)——模型区直接显示
|
|
2226
|
+
// 真实原因,不再只给泛化「未同步」(goal: trae-work-cn-repair G2)。
|
|
2227
|
+
var syncInfo = tmodels.sync
|
|
2228
|
+
? (tmodels.sync.error
|
|
2229
|
+
? (tmodels.sync.count != null
|
|
2230
|
+
? "已同步 " + tmodels.sync.count + " 个模型(" + fmtTime(tmodels.sync.at) + ")|最近同步失败:" + tmodels.sync.error
|
|
2231
|
+
: "同步失败:" + tmodels.sync.error)
|
|
2232
|
+
: "已同步 " + tmodels.sync.count + " 个模型(候选 " + tmodels.sync.candidate + "," + fmtTime(tmodels.sync.at) + ")")
|
|
2233
|
+
: "未同步(启用后自动从本机 TRAE SOLO CN 缓存同步)";
|
|
2234
|
+
|
|
2235
|
+
// 网关状态行(:3902 失败原因的唯一 UI 出口,踩坑 #7):口径照
|
|
2236
|
+
// CodeBuddy 流式桥行——未监听时把 lastError(如 EADDRINUSE)带出来。
|
|
2237
|
+
var tbridge = trae.bridge || {};
|
|
2238
|
+
var bridgeStatus = value.traeEnabled === true
|
|
2239
|
+
? (tbridge.running
|
|
2240
|
+
? { tone: "ok", text: "运行中(127.0.0.1:" + (tbridge.port != null ? tbridge.port : value.traeBridgePort) + ")" }
|
|
2241
|
+
: { tone: "warn", text: "未监听" + (tbridge.lastError ? ":" + tbridge.lastError : "") + (tbridge.lastError === "EADDRINUSE" ? "——若占用者是另一个 dsh 实例,其桥仍会代管本实例流量" : "(启动中或端口被占)") })
|
|
2242
|
+
: { tone: "off", text: "通道未启用" };
|
|
2243
|
+
|
|
2244
|
+
// 模型启停组:通道开启且目录已拉到才显示。
|
|
2245
|
+
var tmodelRows = null;
|
|
2246
|
+
if (value.traeEnabled === true && tlist && tlist.profiles.length) {
|
|
2247
|
+
var enabledCount = tlist.profiles.filter(function (p) { return !tlist.disabled[p.id]; }).length;
|
|
2248
|
+
var tshown = tlist.profiles.filter(function (p) { return matchesModelFilter(p, tfilter); });
|
|
2249
|
+
tmodelRows = createElement("div", { key: "tmodels", style: { marginTop: 4 } },
|
|
2250
|
+
createElement("div", { className: "cbc-subtitle" }, "Trae 模型(选择器内 " + enabledCount + " / " + tlist.profiles.length + ")"),
|
|
2251
|
+
createElement("div", { className: "cbc-scrollbox", style: { maxHeight: 220 } },
|
|
2252
|
+
tshown.length ? tshown.map(function (p) {
|
|
2253
|
+
// 思考强度(G3):选项 = 服务端 `trae.models.efforts` 的目录声明
|
|
2254
|
+
// 档位(无声明 → 不出控件,踩坑 #42)。读侧已按声明过滤
|
|
2255
|
+
//(readTraeModelPrefs),故不出现"目录未声明"存量项(与
|
|
2256
|
+
// Qoder 行的 R3 兼容分支不同——Trae 无历史固定表包袱)。
|
|
2257
|
+
var prefsRec = (tPrefs || tmodels.modelPrefs || {})[p.id] || {};
|
|
2258
|
+
var tiers = (tmodels.efforts || {})[p.id] || [];
|
|
2259
|
+
var storedEffort = prefsRec.effort || "";
|
|
2260
|
+
var effortSel = tiers.length ? createElement("select", {
|
|
2261
|
+
className: "cbc-select cbc-effort-sel",
|
|
2262
|
+
title: "思考强度:remote 传输出站对该模型注入 reasoning_effort(默认 = 不注入;档位来自 Trae 目录声明)",
|
|
2263
|
+
value: storedEffort,
|
|
2264
|
+
onChange: function (e) { saveTPref(p.id, { effort: e.target.value }); },
|
|
2265
|
+
},
|
|
2266
|
+
createElement("option", { value: "" }, "思考:默认"),
|
|
2267
|
+
tiers.map(function (t) {
|
|
2268
|
+
return createElement("option", { key: t, value: t }, "思考:" + t);
|
|
2269
|
+
})) : null;
|
|
2270
|
+
return createElement(SubModelRow, {
|
|
2271
|
+
key: p.id, id: p.id, name: p.name, enabled: !tlist.disabled[p.id],
|
|
2272
|
+
titleOn: "加入 Trae 路由", titleOff: "从 Trae 路由移除",
|
|
2273
|
+
ctx: p.contextWindow, image: !!(p.input && p.input.indexOf("image") >= 0),
|
|
2274
|
+
onToggle: function (e) { toggleTmodel(p, e.target.checked); },
|
|
2275
|
+
extra: effortSel,
|
|
2276
|
+
});
|
|
2277
|
+
}) : createElement("p", { className: "cbc-muted" }, "无匹配模型")),
|
|
2278
|
+
createElement(HelpNote, null, "勾选控制每个 Trae 模型是否进对话选择器(写入宿主配置层的 providers.trae 块——dsh 0.1.7+ 落 profile patch,更早落 ~/.dsh/settings.yaml;免重启);行内 select 逐模型调思考强度(档位来自 Trae 目录的逐模型声明,与输入框模型菜单里的「推理等级」同一真源;remote 传输出站注入 reasoning_effort);目录没声明档位的模型不出思考强度控件;全部取消 = 整个 Trae 路由从选择器移除。"));
|
|
2279
|
+
}
|
|
2280
|
+
|
|
2281
|
+
return createElement("div", { className: "cbc-section" },
|
|
2282
|
+
// 「启用通道」开关已上移到区块头(唯一落点):网关运行态由区块头状态行承载。
|
|
2283
|
+
createElement("p", { className: "cbc-group-title" }, "凭据"),
|
|
2284
|
+
createElement(ChannelLoginRow, {
|
|
2285
|
+
label: "Trae 登录", status: loginStatus,
|
|
2286
|
+
button: toauth.signedIn
|
|
2287
|
+
? (tLogoutConfirm
|
|
2288
|
+
? createElement(CbcButton, { variant: "outline", danger: true, onClick: doTLogout }, "确认退出")
|
|
2289
|
+
: createElement(CbcButton, { variant: "outline", danger: true, onClick: askTLogout }, "退出登录"))
|
|
2290
|
+
: createElement(CbcButton, { variant: "primary", disabled: busy, onClick: startLogin }, busy ? "启动中…" : "登录(浏览器授权)"),
|
|
2291
|
+
}),
|
|
2292
|
+
createElement("div", { className: "cbc-row" },
|
|
2293
|
+
createElement("div", { className: "cbc-row-label" }, "余额"),
|
|
2294
|
+
createElement("div", { className: "cbc-row-control" },
|
|
2295
|
+
createElement("span", { className: "cbc-status cbc-balance" }, balanceText))),
|
|
2296
|
+
createElement("p", { className: "cbc-group-title" }, "模型"),
|
|
2297
|
+
// 操作条(三家同构):通道未启用时也要在——按钮不生效的原因写在
|
|
2298
|
+
// 状态文字里,比整组消失更好排查;只有下面的启停列表受启用条件约束。
|
|
2299
|
+
createElement(SyncBar, { busy: busy, onSync: syncModels, info: syncInfo + (traeOn ? "" : "(通道未启用)"), filter: tfilter, onFilter: setTfilter }),
|
|
2300
|
+
tmodelRows,
|
|
2301
|
+
createElement("p", { className: "cbc-group-title" }, "网关"),
|
|
2302
|
+
createElement("div", { className: "cbc-row" },
|
|
2303
|
+
createElement("div", { className: "cbc-row-label" }, "网关状态"),
|
|
2304
|
+
createElement("div", { className: "cbc-row-control" },
|
|
2305
|
+
createElement("span", { className: bridgeStatus.tone === "warn" ? "cbc-warn" : "cbc-status", style: { display: "inline-flex", alignItems: "center", gap: 6 } },
|
|
2306
|
+
createElement(Dot, { tone: bridgeStatus.tone }), bridgeStatus.text))),
|
|
2307
|
+
createElement("div", { className: "cbc-row" },
|
|
2308
|
+
createElement("div", { className: "cbc-row-label" }, "聊天传输"),
|
|
2309
|
+
createElement("div", { className: "cbc-row-control" },
|
|
2310
|
+
createElement("select", {
|
|
2311
|
+
className: "cbc-select cbc-w220",
|
|
2312
|
+
value: value.traeChatTransport || "inline",
|
|
2313
|
+
onChange: function (e) { save({ traeChatTransport: e.target.value }); },
|
|
2314
|
+
},
|
|
2315
|
+
createElement("option", { value: "agent" }, "agent(默认:solo_work_lite,支持工具+并行调用,模型恒 glm-5.2,耗 IDE 额度)"),
|
|
2316
|
+
createElement("option", { value: "inline" }, "inline(账户默认模型,支持工具,耗 IDE 额度;2026-08 起服务端 3003 故障中)"),
|
|
2317
|
+
createElement("option", { value: "remote" }, "remote(模型切换真实生效,不支持工具,耗 work 额度)")),
|
|
2318
|
+
createElement(ResetButton, { fieldKey: "traeChatTransport", overridden: overridden("traeChatTransport"), save: save }))),
|
|
2319
|
+
createElement("div", { className: "cbc-row" },
|
|
2320
|
+
createElement("div", { className: "cbc-row-label" }, "网关端口"),
|
|
2321
|
+
createElement("div", { className: "cbc-row-control" },
|
|
2322
|
+
createElement(NumberField, { fieldKey: "traeBridgePort", value: value.traeBridgePort, save: save, overridden: overridden("traeBridgePort") }),
|
|
2323
|
+
createElement("span", { className: "cbc-muted" }, "须与 cordis.patch.yml 的 trae baseURL 端口一致(默认 3902)"))),
|
|
2324
|
+
createElement("div", { className: "cbc-row" },
|
|
2325
|
+
createElement("div", { className: "cbc-row-label" }, "首字节超时 (ms)"),
|
|
2326
|
+
createElement("div", { className: "cbc-row-control" },
|
|
2327
|
+
createElement(NumberField, { fieldKey: "upstreamFirstByteTimeoutMs", value: value.upstreamFirstByteTimeoutMs, save: save, overridden: overridden("upstreamFirstByteTimeoutMs") }),
|
|
2328
|
+
createElement("span", { className: "cbc-muted" }, "inline 传输护栏:响应头超时即快速失败(SSE 长流不受限)"))),
|
|
2329
|
+
createElement("details", { className: "cbc-adv" },
|
|
2330
|
+
// summary 的 title 复述旧折叠按钮携带的范围提示(可见文本仍是「高级」)。
|
|
2331
|
+
createElement("summary", { title: "认证 / 聊天 / 登录域" }, "高级"),
|
|
2332
|
+
createElement("div", { className: "cbc-advbody" },
|
|
2333
|
+
createElement("div", { className: "cbc-row" },
|
|
2334
|
+
createElement("div", { className: "cbc-row-label" }, "连接域名"),
|
|
2335
|
+
createElement("div", { className: "cbc-row-control" },
|
|
2336
|
+
createElement(TextField, { fieldKey: "traeAuthBaseURL", value: value.traeAuthBaseURL, save: save, widthClass: "cbc-w220", overridden: overridden("traeAuthBaseURL") }),
|
|
2337
|
+
createElement(TextField, { fieldKey: "traeChatBaseURL", value: value.traeChatBaseURL, save: save, widthClass: "cbc-w220", overridden: overridden("traeChatBaseURL") }),
|
|
2338
|
+
createElement(TextField, { fieldKey: "traeLoginHost", value: value.traeLoginHost, save: save, widthClass: "cbc-w220", overridden: overridden("traeLoginHost") }))))),
|
|
2339
|
+
createElement(HelpNote, null, "走 TraeWork CN 订阅额度:本插件用自持设备密钥完成 OAuth(凭据只存本机 ~/.dsh/trae-plugin-auth.json),经本地翻译网关把 OpenAI 请求转成 Trae 云端协议。模型清单来自本机 TRAE SOLO CN 的缓存数据库(只读、自动脱敏)。"));
|
|
2340
|
+
}
|
|
2341
|
+
SECTION_RENDERERS.trae = TraeSection;
|
|
2342
|
+
|
|
2343
|
+
// --- Qoder CN:设备流 OAuth + 翻译网关 + 网关目录模型启停/逐模型调节 ----
|
|
2344
|
+
// 行内两个 select(思考强度/上下文长度)绑后端 qoderModelSetPrefs 契约:
|
|
2345
|
+
// prefs 发全量期望态(完整替换),成功后用响应回带的 qoder 区刷新。
|
|
2346
|
+
// 思考档位**不用固定表**:选项来自服务端 `qoder.models.efforts`(= 目录
|
|
2347
|
+
// thinking_config 的逐模型声明,与宿主「推理等级」同一真源)——各上游的
|
|
2348
|
+
// 拼写不同(Qoder 有 xhigh、CodeBuddy 有 max、Trae 是 light/high),固定表
|
|
2349
|
+
// 必然对不上;目录没声明命名档位就不出该 select(踩坑 #42:不摆假档位)。
|
|
2350
|
+
function QoderSection(props) {
|
|
2351
|
+
var value = props.value;
|
|
2352
|
+
var save = props.save;
|
|
2353
|
+
var post = props.post;
|
|
2354
|
+
var reload = props.reload;
|
|
2355
|
+
var setErr = props.setErr;
|
|
2356
|
+
var overridden = props.overridden;
|
|
2357
|
+
var qoder = props.qoder || {};
|
|
2358
|
+
var qoauth = qoder.oauth || {};
|
|
2359
|
+
var qmodels = qoder.models || {};
|
|
2360
|
+
|
|
2361
|
+
var busyState = useState(false);
|
|
2362
|
+
var busy = busyState[0];
|
|
2363
|
+
var setBusy = busyState[1];
|
|
2364
|
+
// 退出登录二次确认(同 Trae 区模式):第一次点击进确认态,4 秒复位。
|
|
2365
|
+
var qLogoutConfirmState = useState(false);
|
|
2366
|
+
var qLogoutConfirm = qLogoutConfirmState[0];
|
|
2367
|
+
var setQLogoutConfirm = qLogoutConfirmState[1];
|
|
2368
|
+
var qLogoutTimerRef = useRef(null);
|
|
2369
|
+
var askQLogout = function () {
|
|
2370
|
+
if (qLogoutTimerRef.current) clearTimeout(qLogoutTimerRef.current);
|
|
2371
|
+
setQLogoutConfirm(true);
|
|
2372
|
+
qLogoutTimerRef.current = setTimeout(function () { setQLogoutConfirm(false); }, 4000);
|
|
2373
|
+
};
|
|
2374
|
+
var doQLogout = function () {
|
|
2375
|
+
if (qLogoutTimerRef.current) { clearTimeout(qLogoutTimerRef.current); qLogoutTimerRef.current = null; }
|
|
2376
|
+
setQLogoutConfirm(false);
|
|
2377
|
+
post({ action: "qoder-oauth-logout" }).then(function (res) {
|
|
2378
|
+
if (res.ok) reload();
|
|
2379
|
+
else setErr(res.d && res.d.error ? res.d.error : "退出失败(HTTP " + res.status + ")");
|
|
2380
|
+
}).catch(function (e) { setErr(e && e.message ? e.message : "退出失败(网络)"); });
|
|
2381
|
+
};
|
|
2382
|
+
var listState = useState(null); // {profiles, disabled:{id:true}} 来自 qoder-model-list
|
|
2383
|
+
var qlist = listState[0];
|
|
2384
|
+
var setQlist = listState[1];
|
|
2385
|
+
// 模型筛选(与 CodeBuddy/Trae 模型组同款):只过滤渲染,不发请求。
|
|
2386
|
+
var qfilterState = useState("");
|
|
2387
|
+
var qfilter = qfilterState[0];
|
|
2388
|
+
var setQfilter = qfilterState[1];
|
|
2389
|
+
// 逐模型 prefs 本地态:跟随 GET(props.qoder 经 reload 刷新),POST 成功
|
|
2390
|
+
// 后用响应回带的 qoder 区覆写(与启停同款"响应权威"纪律)。
|
|
2391
|
+
var qPrefsState = useState(null); // { [id]: { effort?, contextVariant? } }
|
|
2392
|
+
var qPrefs = qPrefsState[0];
|
|
2393
|
+
var setQPrefs = qPrefsState[1];
|
|
2394
|
+
useEffect(function () { setQPrefs(qmodels.modelPrefs || {}); }, [qmodels.modelPrefs]);
|
|
2395
|
+
|
|
2396
|
+
// P2-5「测一下」:签名目录 GET(catalog 面 /algo,非聊天面)验证设备流
|
|
2397
|
+
// 凭据;结果就地显示,不进全局横幅。
|
|
2398
|
+
var qTestState = useState(null); // null | {busy:true} | {ok:bool, text:string}
|
|
2399
|
+
var qTest = qTestState[0];
|
|
2400
|
+
var setQTest = qTestState[1];
|
|
2401
|
+
var testCredential = function () {
|
|
2402
|
+
setQTest({ busy: true });
|
|
2403
|
+
post({ action: "credential-test", channel: "qoder" }).then(function (res) {
|
|
2404
|
+
if (res.ok && res.d.available) setQTest({ ok: true, text: "✓ 可用(" + (res.d.detail || "验证通过") + ")" });
|
|
2405
|
+
else setQTest({ ok: false, text: "✗ " + ((res.d && res.d.error) || ("HTTP " + res.status)) });
|
|
2406
|
+
}).catch(function (e) { setQTest({ ok: false, text: "✗ " + (e && e.message ? e.message : "网络错误") }); });
|
|
2407
|
+
};
|
|
2408
|
+
|
|
2409
|
+
// 无本地回环回调(授权在服务端完成、本机只轮询):pending 期间 3 秒
|
|
2410
|
+
// 轮询状态,收敛后 reload(同 CodeBuddy 登录区的 pending 轮询)。
|
|
2411
|
+
useEffect(function () {
|
|
2412
|
+
if (!qoauth.pending) return undefined;
|
|
2413
|
+
var timer = setInterval(function () {
|
|
2414
|
+
post({ action: "qoder-oauth-status" }).then(function (res) {
|
|
2415
|
+
if (res.ok && res.d.qoder && !res.d.qoder.pending) reload();
|
|
2416
|
+
}).catch(function () {});
|
|
2417
|
+
}, 3000);
|
|
2418
|
+
return function () { clearInterval(timer); };
|
|
2419
|
+
}, [qoauth.pending]);
|
|
2420
|
+
|
|
2421
|
+
var fetchQlist = function () {
|
|
2422
|
+
// 清空同值去重表——重新拉取意味着勾选态可能已被外部改动(同 CodeBuddy 模型行)。
|
|
2423
|
+
qlastSentRef.current = {};
|
|
2424
|
+
post({ action: "qoder-model-list" }).then(function (res) {
|
|
2425
|
+
if (!res.ok) return;
|
|
2426
|
+
var disabled = {};
|
|
2427
|
+
(res.d.disabled || []).forEach(function (id) { disabled[id] = true; });
|
|
2428
|
+
setQlist({ profiles: (res.d.view && res.d.view.profiles) || [], disabled: disabled });
|
|
2429
|
+
}).catch(function (e) {
|
|
2430
|
+
// 同 Trae 区纪律:拉取失败带原因(踩坑 #7),`!res.ok` 的已知空态不进来。
|
|
2431
|
+
setErr("Qoder 模型目录读取失败:" + (e && e.message ? e.message : "(网络)"));
|
|
2432
|
+
});
|
|
2433
|
+
};
|
|
2434
|
+
useEffect(function () { fetchQlist(); }, []);
|
|
2435
|
+
// 开启通道或目录同步完成(sync.at 变化)后重拉启停清单(同 Trae 区纪律)。
|
|
2436
|
+
var qoderSyncAt = qmodels.sync && qmodels.sync.at;
|
|
2437
|
+
var qoderOn = value.qoderEnabled === true;
|
|
2438
|
+
useEffect(function () { fetchQlist(); }, [qoderSyncAt, qoderOn]);
|
|
2439
|
+
|
|
2440
|
+
var startLogin = function () { startOAuthFlow(post, "qoder-oauth-start", setBusy, setErr, reload); };
|
|
2441
|
+
// P1-2 余额行(账户配额,只读):展开/收起边界各采一次 + 登录态翻转折采,
|
|
2442
|
+
// 不周期轮询;服务端 60s memoize 控成本。取不到一律「—(原因)」,绝不编造。
|
|
2443
|
+
var active = props.active === true;
|
|
2444
|
+
var quotaState = useState(null);
|
|
2445
|
+
var quota = quotaState[0];
|
|
2446
|
+
var setQuota = quotaState[1];
|
|
2447
|
+
var quotaGenRef = useRef(0);
|
|
2448
|
+
var fetchQuota = function () {
|
|
2449
|
+
if (!qoauth.signedIn) { setQuota({ error: "未登录" }); return; }
|
|
2450
|
+
var my = ++quotaGenRef.current;
|
|
2451
|
+
post({ action: "qoder-quota" }).then(function (res) {
|
|
2452
|
+
if (my !== quotaGenRef.current) return;
|
|
2453
|
+
setQuota(res.ok && res.d && res.d.quota ? res.d.quota : { error: (res.d && res.d.error) || ("HTTP " + res.status) });
|
|
2454
|
+
}).catch(function (e) {
|
|
2455
|
+
if (my !== quotaGenRef.current) return;
|
|
2456
|
+
setQuota({ error: e && e.message ? e.message : "(网络)" });
|
|
2457
|
+
});
|
|
2458
|
+
};
|
|
2459
|
+
useEffect(function () { fetchQuota(); }, [active, qoauth.signedIn]);
|
|
2460
|
+
var qfmtPool = function (q) { return "余 " + fmtCredit(q.remaining) + " / " + fmtCredit(q.total) + (q.unit ? " " + q.unit : ""); };
|
|
2461
|
+
var balanceText = !quota ? "读取中…"
|
|
2462
|
+
: quota.error ? "—(" + quota.error + ")"
|
|
2463
|
+
: quota.addOnQuota && quota.addOnQuota.total != null
|
|
2464
|
+
? "附加额度 " + qfmtPool(quota.addOnQuota) +
|
|
2465
|
+
(quota.userQuota && quota.userQuota.total > 0 ? " · 订阅 " + qfmtPool(quota.userQuota) : "")
|
|
2466
|
+
: "—";
|
|
2467
|
+
var syncModels = function () {
|
|
2468
|
+
setBusy(true);
|
|
2469
|
+
post({ action: "qoder-model-sync" }).then(function (res) {
|
|
2470
|
+
setBusy(false);
|
|
2471
|
+
if (!res.ok || res.d.ok === false) { setErr(res.d && (res.d.error || (res.d.sync && res.d.sync.error)) ? (res.d.error || res.d.sync.error) : "目录同步失败"); return; }
|
|
2472
|
+
fetchQlist();
|
|
2473
|
+
reload();
|
|
2474
|
+
}).catch(function (e) { setBusy(false); setErr(e && e.message ? e.message : "目录同步失败(网络)"); });
|
|
2475
|
+
};
|
|
2476
|
+
|
|
2477
|
+
// Qoder 模型启停:受控 checkbox 双 change 同值去重(踩坑 #27/#32 纪律)。
|
|
2478
|
+
var qlastSentRef = useRef({});
|
|
2479
|
+
var toggleQmodel = function (p, enabled) {
|
|
2480
|
+
if (qlastSentRef.current[p.id] === enabled) return;
|
|
2481
|
+
qlastSentRef.current[p.id] = enabled;
|
|
2482
|
+
post({ patch: { qoderModelSetEnabled: { id: p.id, enabled: enabled } } }).then(function (res) {
|
|
2483
|
+
if (!res.ok) { delete qlastSentRef.current[p.id]; setErr(res.d && res.d.error ? res.d.error : "操作失败"); return; }
|
|
2484
|
+
var disabled = {};
|
|
2485
|
+
(((res.d.qoder || {}).models || {}).disabled || []).forEach(function (id) { disabled[id] = true; });
|
|
2486
|
+
setQlist(function (prev) { return prev ? { profiles: prev.profiles, disabled: disabled } : prev; });
|
|
2487
|
+
reload();
|
|
2488
|
+
}).catch(function (e) { delete qlastSentRef.current[p.id]; setErr("操作失败:" + (e && e.message ? e.message : String(e))); });
|
|
2489
|
+
};
|
|
2490
|
+
|
|
2491
|
+
// 逐模型 prefs(思考强度/上下文长度):后端完整替换语义,这里发全量
|
|
2492
|
+
// 期望态;useRef 同值去重 + 失败销账(踩坑 #27/#32 纪律)。空 rec = 删
|
|
2493
|
+
// 记录回默认。成功后以响应回带的 qoder.modelPrefs 为权威刷新本地态。
|
|
2494
|
+
var qprefLastSentRef = useRef({});
|
|
2495
|
+
var saveQpref = function (id, prefs) {
|
|
2496
|
+
var rec = {};
|
|
2497
|
+
if (prefs.effort) rec.effort = prefs.effort;
|
|
2498
|
+
if (prefs.contextVariant) rec.contextVariant = prefs.contextVariant;
|
|
2499
|
+
var json = JSON.stringify(rec);
|
|
2500
|
+
if (qprefLastSentRef.current[id] === json) return;
|
|
2501
|
+
qprefLastSentRef.current[id] = json;
|
|
2502
|
+
post({ patch: { qoderModelSetPrefs: { id: id, prefs: rec } } }).then(function (res) {
|
|
2503
|
+
if (!res.ok) {
|
|
2504
|
+
delete qprefLastSentRef.current[id];
|
|
2505
|
+
setErr(res.d && res.d.error ? res.d.error : "操作失败");
|
|
2506
|
+
reload(); // 服务端未变,回同步本地(select 弹回旧值)
|
|
2507
|
+
return;
|
|
2508
|
+
}
|
|
2509
|
+
setQPrefs((((res.d.qoder || {}).models || {}).modelPrefs) || {});
|
|
2510
|
+
reload();
|
|
2511
|
+
}).catch(function (e) {
|
|
2512
|
+
delete qprefLastSentRef.current[id];
|
|
2513
|
+
setErr("操作失败:" + (e && e.message ? e.message : String(e)));
|
|
2514
|
+
});
|
|
2515
|
+
};
|
|
2516
|
+
|
|
2517
|
+
var account = qoauth.account;
|
|
2518
|
+
var loginStatus = qoauth.pending
|
|
2519
|
+
? { tone: "warn", text: "登录进行中:浏览器完成授权后自动收敛(5 分钟内有效)。" }
|
|
2520
|
+
: qoauth.signedIn
|
|
2521
|
+
? {
|
|
2522
|
+
tone: qoauth.needsRelogin ? "err" : "ok",
|
|
2523
|
+
text: (qoauth.needsRelogin ? "需重新登录" : "已登录") +
|
|
2524
|
+
(account && (account.nickname || account.uid) ? ":" + (account.nickname || account.uid) : "") +
|
|
2525
|
+
(qoauth.accessTokenExpiresAt ? "(令牌至 " + fmtTime(qoauth.accessTokenExpiresAt) + ")" : "") +
|
|
2526
|
+
(qoauth.error ? "|" + qoauth.error : ""),
|
|
2527
|
+
}
|
|
2528
|
+
: { tone: "off", text: "未登录" + (qoauth.error ? "|" + qoauth.error : "") };
|
|
2529
|
+
|
|
2530
|
+
var syncInfo = qmodels.sync
|
|
2531
|
+
? "已同步 " + qmodels.sync.count + " 个模型(" + fmtTime(qmodels.sync.at) + ")"
|
|
2532
|
+
: "未同步(需已登录;启用后自动从网关同步)";
|
|
2533
|
+
|
|
2534
|
+
// 网关状态行(:3903 失败原因的唯一 UI 出口,踩坑 #7):口径同 Trae 区。
|
|
2535
|
+
var qbridge = qoder.bridge || {};
|
|
2536
|
+
var bridgeStatus = value.qoderEnabled === true
|
|
2537
|
+
? (qbridge.running
|
|
2538
|
+
? { tone: "ok", text: "运行中(127.0.0.1:" + (qbridge.port != null ? qbridge.port : value.qoderBridgePort) + ")" }
|
|
2539
|
+
: { tone: "warn", text: "未监听" + (qbridge.lastError ? ":" + qbridge.lastError : "") + (qbridge.lastError === "EADDRINUSE" ? "——若占用者是另一个 dsh 实例,其桥仍会代管本实例流量" : "(启动中或端口被占)") })
|
|
2540
|
+
: { tone: "off", text: "通道未启用" };
|
|
2541
|
+
|
|
2542
|
+
// 模型启停组:通道开启且目录已拉到才显示。
|
|
2543
|
+
var qmodelRows = null;
|
|
2544
|
+
if (value.qoderEnabled === true && qlist && qlist.profiles.length) {
|
|
2545
|
+
var enabledCount = qlist.profiles.filter(function (p) { return !qlist.disabled[p.id]; }).length;
|
|
2546
|
+
var qshown = qlist.profiles.filter(function (p) { return matchesModelFilter(p, qfilter); });
|
|
2547
|
+
qmodelRows = createElement("div", { key: "qmodels", style: { marginTop: 4 } },
|
|
2548
|
+
createElement("div", { className: "cbc-subtitle" }, "Qoder 模型(选择器内 " + enabledCount + " / " + qlist.profiles.length + ")"),
|
|
2549
|
+
createElement("div", { className: "cbc-scrollbox", style: { maxHeight: 220 } },
|
|
2550
|
+
qshown.length ? qshown.map(function (p) {
|
|
2551
|
+
var enabled = !qlist.disabled[p.id];
|
|
2552
|
+
var prefsRec = (qPrefs || qmodels.modelPrefs || {})[p.id] || {};
|
|
2553
|
+
var variants = (qmodels.variants || {})[p.id] || [];
|
|
2554
|
+
// 思考强度:选项 = 服务端给的**目录声明档位**(无声明 → 不出控件)。
|
|
2555
|
+
// 存量值若不在声明里(旧版本写下的拼写,如 qmodel_38max 的 max),
|
|
2556
|
+
// 额外保留一项并标注「目录未声明」——值不静默丢,用户可就地改成声明档位。
|
|
2557
|
+
var declaredTiers = (qmodels.efforts || {})[p.id] || [];
|
|
2558
|
+
var tiers = declaredTiers.slice();
|
|
2559
|
+
var storedEffort = prefsRec.effort || "";
|
|
2560
|
+
if (storedEffort && tiers.indexOf(storedEffort) < 0) tiers.push(storedEffort);
|
|
2561
|
+
var effortSel = tiers.length ? createElement("select", {
|
|
2562
|
+
className: "cbc-select cbc-effort-sel",
|
|
2563
|
+
title: "思考强度:网关出站对该模型注入 reasoning_effort(默认 = 不注入;档位来自 Qoder 目录声明)",
|
|
2564
|
+
value: storedEffort,
|
|
2565
|
+
onChange: function (e) { saveQpref(p.id, { effort: e.target.value, contextVariant: prefsRec.contextVariant }); },
|
|
2566
|
+
},
|
|
2567
|
+
createElement("option", { value: "" }, "思考:默认"),
|
|
2568
|
+
tiers.map(function (t) {
|
|
2569
|
+
var label = "思考:" + (t === "off" ? "关" : t);
|
|
2570
|
+
if (declaredTiers.indexOf(t) < 0) label += "(目录未声明)";
|
|
2571
|
+
return createElement("option", { key: t, value: t }, label);
|
|
2572
|
+
})) : null;
|
|
2573
|
+
// 上下文长度:无变体模型不出控件;默认档 = isDefault 项(无则第一项)。
|
|
2574
|
+
var ctxSel = null;
|
|
2575
|
+
var effWindow = p.contextWindow;
|
|
2576
|
+
if (variants.length) {
|
|
2577
|
+
var defVar = variants.filter(function (v) { return v.isDefault; })[0] || variants[0];
|
|
2578
|
+
var curVar = variants.filter(function (v) { return v.name === prefsRec.contextVariant; })[0];
|
|
2579
|
+
if (curVar) effWindow = curVar.tokenCount;
|
|
2580
|
+
ctxSel = createElement("select", {
|
|
2581
|
+
className: "cbc-select cbc-effort-sel",
|
|
2582
|
+
title: "上下文长度:选中变体镜像为该模型的 contextWindow(默认 = 目录默认档)",
|
|
2583
|
+
value: (curVar && curVar.name) || "",
|
|
2584
|
+
onChange: function (e) { saveQpref(p.id, { effort: prefsRec.effort, contextVariant: e.target.value }); },
|
|
2585
|
+
},
|
|
2586
|
+
createElement("option", { value: "" }, "上下文:" + defVar.name),
|
|
2587
|
+
variants.map(function (v) { return createElement("option", { key: v.name, value: v.name }, "上下文:" + v.name); }));
|
|
2588
|
+
}
|
|
2589
|
+
return createElement(SubModelRow, {
|
|
2590
|
+
key: p.id, id: p.id, name: p.name, enabled: enabled,
|
|
2591
|
+
titleOn: "加入 Qoder 路由", titleOff: "从 Qoder 路由移除",
|
|
2592
|
+
ctx: effWindow, image: !!(p.input && p.input.indexOf("image") >= 0),
|
|
2593
|
+
onToggle: function (e) { toggleQmodel(p, e.target.checked); },
|
|
2594
|
+
extra: createElement(Fragment, null, effortSel, ctxSel),
|
|
2595
|
+
});
|
|
2596
|
+
}) : createElement("p", { className: "cbc-muted" }, "无匹配模型")),
|
|
2597
|
+
createElement(HelpNote, null, "勾选控制每个 Qoder 模型是否进对话选择器;行内 select 逐模型调思考强度(档位来自 Qoder 目录的逐模型声明,与输入框模型菜单里的「推理等级」同一真源)与上下文长度(写文件层 prefs 并镜像进 settings.yaml,均免重启);目录没声明档位的模型不出思考强度控件;全部取消 = 整个 Qoder 路由从选择器移除。"));
|
|
2598
|
+
}
|
|
2599
|
+
|
|
2600
|
+
return createElement("div", { className: "cbc-section" },
|
|
2601
|
+
// 「启用通道」开关已上移到区块头(唯一落点):网关运行态由区块头状态行承载。
|
|
2602
|
+
createElement("p", { className: "cbc-group-title" }, "凭据"),
|
|
2603
|
+
createElement(ChannelLoginRow, {
|
|
2604
|
+
label: "Qoder 登录", status: loginStatus,
|
|
2605
|
+
button: qoauth.signedIn && !qoauth.needsRelogin
|
|
2606
|
+
? (qLogoutConfirm
|
|
2607
|
+
? createElement(CbcButton, { variant: "outline", danger: true, onClick: doQLogout }, "确认退出")
|
|
2608
|
+
: createElement(CbcButton, { variant: "outline", danger: true, onClick: askQLogout }, "退出登录"))
|
|
2609
|
+
: createElement(CbcButton, { variant: "primary", disabled: busy, onClick: startLogin },
|
|
2610
|
+
busy ? "启动中…" : (qoauth.needsRelogin ? "重新登录(浏览器授权)" : "登录(浏览器授权)")),
|
|
2611
|
+
extra: createElement(Fragment, null,
|
|
2612
|
+
qoauth.pending && qoauth.authUrl
|
|
2613
|
+
? createElement(CbcButton, { variant: "outline", onClick: function () { window.open(qoauth.authUrl, "_blank"); } }, "重新打开登录页")
|
|
2614
|
+
: null,
|
|
2615
|
+
createElement(CbcButton, { variant: "ghost", disabled: !!(qTest && qTest.busy), onClick: testCredential },
|
|
2616
|
+
qTest && qTest.busy ? "验证中…" : "测一下"),
|
|
2617
|
+
qTest && !qTest.busy
|
|
2618
|
+
? createElement("span", { className: qTest.ok ? "cbc-status" : "cbc-warn", "data-test": "qoder" }, qTest.text)
|
|
2619
|
+
: null),
|
|
2620
|
+
}),
|
|
2621
|
+
createElement("div", { className: "cbc-row" },
|
|
2622
|
+
createElement("div", { className: "cbc-row-label" }, "余额"),
|
|
2623
|
+
createElement("div", { className: "cbc-row-control" },
|
|
2624
|
+
createElement("span", { className: "cbc-status cbc-balance" }, balanceText))),
|
|
2625
|
+
createElement("p", { className: "cbc-group-title" }, "模型"),
|
|
2626
|
+
// 操作条(三家同构):通道未启用时也渲染,理由同 Trae 区。
|
|
2627
|
+
createElement(SyncBar, { busy: busy, onSync: syncModels, info: syncInfo + (qoderOn ? "" : "(通道未启用)"), filter: qfilter, onFilter: setQfilter }),
|
|
2628
|
+
qmodelRows,
|
|
2629
|
+
createElement("p", { className: "cbc-group-title" }, "网关"),
|
|
2630
|
+
createElement("div", { className: "cbc-row" },
|
|
2631
|
+
createElement("div", { className: "cbc-row-label" }, "网关状态"),
|
|
2632
|
+
createElement("div", { className: "cbc-row-control" },
|
|
2633
|
+
createElement("span", { className: bridgeStatus.tone === "warn" ? "cbc-warn" : "cbc-status", style: { display: "inline-flex", alignItems: "center", gap: 6 } },
|
|
2634
|
+
createElement(Dot, { tone: bridgeStatus.tone }), bridgeStatus.text))),
|
|
2635
|
+
createElement("div", { className: "cbc-row" },
|
|
2636
|
+
createElement("div", { className: "cbc-row-label" }, "网关端口"),
|
|
2637
|
+
createElement("div", { className: "cbc-row-control" },
|
|
2638
|
+
createElement(NumberField, { fieldKey: "qoderBridgePort", value: value.qoderBridgePort, save: save, overridden: overridden("qoderBridgePort") }),
|
|
2639
|
+
createElement("span", { className: "cbc-muted" }, "settings.yaml 镜像里的 qoder baseURL 端口(默认 3903),改端口重铺镜像热生效"))),
|
|
2640
|
+
createElement("details", { className: "cbc-adv" },
|
|
2641
|
+
// summary 的 title 复述旧折叠按钮携带的范围提示(可见文本仍是「高级」)。
|
|
2642
|
+
createElement("summary", { title: "登录域 / OpenAPI / infer / client_id" }, "高级"),
|
|
2643
|
+
createElement("div", { className: "cbc-advbody" },
|
|
2644
|
+
createElement("div", { className: "cbc-row" },
|
|
2645
|
+
createElement("div", { className: "cbc-row-label" }, "连接域名"),
|
|
2646
|
+
createElement("div", { className: "cbc-row-control" },
|
|
2647
|
+
createElement(TextField, { fieldKey: "qoderLoginHost", value: value.qoderLoginHost, save: save, widthClass: "cbc-w220", overridden: overridden("qoderLoginHost") }),
|
|
2648
|
+
createElement(TextField, { fieldKey: "qoderOpenapiBaseURL", value: value.qoderOpenapiBaseURL, save: save, widthClass: "cbc-w220", overridden: overridden("qoderOpenapiBaseURL") }),
|
|
2649
|
+
createElement(TextField, { fieldKey: "qoderInferBaseURL", value: value.qoderInferBaseURL, save: save, widthClass: "cbc-w220", overridden: overridden("qoderInferBaseURL") }),
|
|
2650
|
+
createElement(TextField, { fieldKey: "qoderClientId", value: value.qoderClientId, save: save, widthClass: "cbc-w220", overridden: overridden("qoderClientId") }))))),
|
|
2651
|
+
createElement(HelpNote, null, "Qoder CN(qoder.cn)设备流 OAuth + COSY 签名翻译网关:对话经本地 :3903 网关转成官方 infer 协议(凭据只存 ~/.dsh/qoder-plugin-auth.json,不回传浏览器)。额度跟账号走(按 usage.credits 计量,额度与用量页可见)。"));
|
|
2652
|
+
}
|
|
2653
|
+
SECTION_RENDERERS.qoder = QoderSection;
|
|
2654
|
+
|
|
2655
|
+
// --- 网关与高级:流式桥(主聊天链路)+ 网关地址(高级折叠) -----------------
|
|
2656
|
+
function BridgeAdvancedSection(props) {
|
|
2657
|
+
var value = props.value;
|
|
2658
|
+
var save = props.save;
|
|
2659
|
+
var overridden = props.overridden;
|
|
2660
|
+
var bridgeView = props.bridgeView || {};
|
|
2661
|
+
return createElement("div", { className: "cbc-section" },
|
|
2662
|
+
createElement("p", { className: "cbc-group-title" }, "网关"),
|
|
2663
|
+
createElement("div", { className: "cbc-row" },
|
|
2664
|
+
createElement("div", { className: "cbc-row-label" }, "启用流式桥"),
|
|
2665
|
+
createElement("div", { className: "cbc-row-control" },
|
|
2666
|
+
createElement("input", { type: "checkbox", className: "cbc-check", checked: value.bridgeEnabled === true, onChange: function (e) { save({ bridgeEnabled: e.target.checked }); } }),
|
|
2667
|
+
createElement("span", { className: "cbc-muted" },
|
|
2668
|
+
value.bridgeEnabled === true
|
|
2669
|
+
? (bridgeView.running
|
|
2670
|
+
? "运行中(127.0.0.1:" + bridgeView.port + ")——主聊天与工具请求均经由此桥"
|
|
2671
|
+
: "已启用,当前未监听" + (bridgeView.lastError ? ":" + bridgeView.lastError : "") + "(启动中或端口被占)")
|
|
2672
|
+
: "已禁用——主聊天将中断(模型请求经由此桥)"),
|
|
2673
|
+
createElement(ResetButton, { fieldKey: "bridgeEnabled", overridden: overridden("bridgeEnabled"), save: save }))),
|
|
2674
|
+
createElement("div", { className: "cbc-row" },
|
|
2675
|
+
createElement("div", { className: "cbc-row-label" }, "端口"),
|
|
2676
|
+
createElement("div", { className: "cbc-row-control" },
|
|
2677
|
+
createElement(NumberField, { fieldKey: "bridgePort", value: value.bridgePort, save: save, overridden: overridden("bridgePort") }),
|
|
2678
|
+
createElement("span", { className: "cbc-muted" }, "须与 cordis.patch.yml 的 baseURL 端口一致(默认 3901),否则主聊天断"))),
|
|
2679
|
+
createElement("div", { className: "cbc-row" },
|
|
2680
|
+
createElement("div", { className: "cbc-row-label" }, "会话归因注入"),
|
|
2681
|
+
createElement("div", { className: "cbc-row-control" },
|
|
2682
|
+
createElement("input", { type: "checkbox", className: "cbc-check", checked: value.sessionHeadersEnabled === true, onChange: function (e) { save({ sessionHeadersEnabled: e.target.checked }); } }),
|
|
2683
|
+
createElement("span", { className: "cbc-muted" }, "按入站会话 id 注入网关会话头(已设置的逐头保留)"),
|
|
2684
|
+
createElement(ResetButton, { fieldKey: "sessionHeadersEnabled", overridden: overridden("sessionHeadersEnabled"), save: save }))),
|
|
2685
|
+
createElement("div", { className: "cbc-row" },
|
|
2686
|
+
createElement("div", { className: "cbc-row-label" }, "会话头格式"),
|
|
2687
|
+
createElement("div", { className: "cbc-row-control" },
|
|
2688
|
+
createElement("select", {
|
|
2689
|
+
className: "cbc-select cbc-w200",
|
|
2690
|
+
value: value.sessionHeaderFormat || "openai",
|
|
2691
|
+
onChange: function (e) { save({ sessionHeaderFormat: e.target.value }); },
|
|
2692
|
+
},
|
|
2693
|
+
createElement("option", { value: "openai" }, "openai(session_id 等)"),
|
|
2694
|
+
createElement("option", { value: "openrouter" }, "openrouter(x-session-id)")),
|
|
2695
|
+
createElement(ResetButton, { fieldKey: "sessionHeaderFormat", overridden: overridden("sessionHeaderFormat"), save: save }))),
|
|
2696
|
+
createElement("div", { className: "cbc-row" },
|
|
2697
|
+
createElement("div", { className: "cbc-row-label" }, "每会话并发上限"),
|
|
2698
|
+
createElement("div", { className: "cbc-row-control" },
|
|
2699
|
+
createElement(NumberField, { fieldKey: "maxConcurrentPerSession", value: value.maxConcurrentPerSession, save: save, overridden: overridden("maxConcurrentPerSession") }))),
|
|
2700
|
+
createElement(HelpNote, null, "桥统一解析凭据(凭据组选 OAuth 或 Key),主聊天也经由此桥。同一会话超过上限的请求排队(FIFO);无会话 id 的请求不限流。非流式入站聚合成标准 JSON,chat/completions 之外的请求直接透传。"),
|
|
2701
|
+
createElement("details", { className: "cbc-adv" },
|
|
2702
|
+
createElement("summary", null, "高级"),
|
|
2703
|
+
createElement("div", { className: "cbc-advbody" },
|
|
2704
|
+
createElement("div", { className: "cbc-row" },
|
|
2705
|
+
createElement("div", { className: "cbc-row-label" }, "网关地址"),
|
|
2706
|
+
createElement("div", { className: "cbc-row-control" },
|
|
2707
|
+
createElement(TextField, { fieldKey: "baseURL", value: value.baseURL, save: save, overridden: overridden("baseURL") }))),
|
|
2708
|
+
createElement("p", { className: "cbc-hint" }, "CodeBuddy 网关地址,一般无需修改;必须是 http/https 绝对地址。"))));
|
|
2709
|
+
}
|
|
2710
|
+
SECTION_RENDERERS.bridge = BridgeAdvancedSection;
|
|
2711
|
+
|
|
2712
|
+
// --- 字段控件(必须在模块级定义——组件身份随父重渲染变化会丢焦点) ---------
|
|
2713
|
+
|
|
2714
|
+
// “重置”:字段在文件层被覆盖时出现,点一下写 null 删覆盖、回默认值。
|
|
2715
|
+
function ResetButton(props) {
|
|
2716
|
+
if (!props.overridden) return null;
|
|
2717
|
+
return createElement(CbcButton, {
|
|
2718
|
+
variant: "ghost", title: "恢复默认值",
|
|
2719
|
+
onClick: function () { var p = {}; p[props.fieldKey] = null; props.save(p); },
|
|
2720
|
+
}, "重置");
|
|
2721
|
+
}
|
|
2722
|
+
|
|
2723
|
+
function TextField(props) {
|
|
2724
|
+
var draftState = useState(String(props.value == null ? "" : props.value));
|
|
2725
|
+
var draft = draftState[0];
|
|
2726
|
+
var setDraft = draftState[1];
|
|
2727
|
+
// 服务端回值变化(保存成功/被规范化/重置)时同步草稿,不困住旧值。
|
|
2728
|
+
useEffect(function () { setDraft(String(props.value == null ? "" : props.value)); }, [props.value]);
|
|
2729
|
+
var commit = function () {
|
|
2730
|
+
if (props.numeric) {
|
|
2731
|
+
var n = Number(draft);
|
|
2732
|
+
if (!Number.isFinite(n) || draft.trim() === "" || n === props.value) return;
|
|
2733
|
+
var np = {};
|
|
2734
|
+
np[props.fieldKey] = n;
|
|
2735
|
+
props.save(np);
|
|
2736
|
+
return;
|
|
2737
|
+
}
|
|
2738
|
+
if (draft === String(props.value == null ? "" : props.value)) return;
|
|
2739
|
+
var patch = {};
|
|
2740
|
+
patch[props.fieldKey] = draft;
|
|
2741
|
+
props.save(patch);
|
|
2742
|
+
};
|
|
2743
|
+
// Enter 只 blur:提交统一走 onBlur。曾经 Enter 先 commit 再 blur,
|
|
2744
|
+
// blur 又触发一次 commit —— 一次保存产生两趟 POST+GET。
|
|
2745
|
+
return createElement(Fragment, null,
|
|
2746
|
+
createElement(CbcInput, {
|
|
2747
|
+
type: props.numeric ? "number" : undefined,
|
|
2748
|
+
widthClass: props.numeric ? "cbc-w110" : props.widthClass,
|
|
2749
|
+
value: draft,
|
|
2750
|
+
onInput: function (e) { setDraft(e.target.value); },
|
|
2751
|
+
onBlur: commit,
|
|
2752
|
+
onKeyDown: function (e) { if (e.key === "Enter") { e.preventDefault(); e.target.blur(); } },
|
|
2753
|
+
}),
|
|
2754
|
+
createElement(ResetButton, { fieldKey: props.fieldKey, overridden: props.overridden, save: props.save }));
|
|
2755
|
+
}
|
|
2756
|
+
|
|
2757
|
+
// 数值字段 = TextField 的 numeric 变体(提交前 Number() 解析+有限性
|
|
2758
|
+
// 校验,宽度钉死 cbc-w110)——原先是 TextField 的逐行拷贝,双份维护。
|
|
2759
|
+
function NumberField(props) {
|
|
2760
|
+
return createElement(TextField, Object.assign({}, props, { numeric: true }));
|
|
2761
|
+
}
|
|
2762
|
+
|
|
2763
|
+
// OAuth 启动流程(CodeBuddy 与 Trae 两处共用):window.open 必须在点击
|
|
2764
|
+
// 处理器内同步发起,异步(fetch 后)的调用会被弹窗拦截器吃掉——先开
|
|
2765
|
+
// 空白页,拿到 authUrl 再导航。
|
|
2766
|
+
function startOAuthFlow(post, action, setBusy, setErr, reload) {
|
|
2767
|
+
var win = window.open("", "_blank");
|
|
2768
|
+
setBusy(true);
|
|
2769
|
+
post({ action: action }).then(function (res) {
|
|
2770
|
+
setBusy(false);
|
|
2771
|
+
if (res.ok && res.d.authUrl) {
|
|
2772
|
+
if (win) { win.location.href = res.d.authUrl; }
|
|
2773
|
+
else setErr("浏览器拦截了登录弹窗——请允许本站弹窗后重试");
|
|
2774
|
+
reload();
|
|
2775
|
+
} else {
|
|
2776
|
+
if (win) win.close();
|
|
2777
|
+
setErr(res.d && res.d.error ? res.d.error : "发起登录失败");
|
|
2778
|
+
}
|
|
2779
|
+
}).catch(function (e) { setBusy(false); if (win) win.close(); setErr(e && e.message ? e.message : "发起登录失败(网络)"); });
|
|
2780
|
+
}
|
|
2781
|
+
|
|
2782
|
+
// G5:模型行内上限输入框(contextWindow/maxTokens 覆盖值)。
|
|
2783
|
+
// 显示有效值(覆盖 ?? 基值),Enter/blur 提交;清空 = 清除覆盖回基值。
|
|
2784
|
+
// 正整数/上限本地快检,服务端仍是权威校验(报错走全局横幅并回退草稿)。
|
|
2785
|
+
function LimitInput(props) {
|
|
2786
|
+
var cur = props.value == null ? "" : String(props.value);
|
|
2787
|
+
var draftState = useState(cur);
|
|
2788
|
+
var draft = draftState[0];
|
|
2789
|
+
var setDraft = draftState[1];
|
|
2790
|
+
useEffect(function () { setDraft(props.value == null ? "" : String(props.value)); }, [props.value]);
|
|
2791
|
+
var commit = function () {
|
|
2792
|
+
var raw = draft.trim();
|
|
2793
|
+
if (raw === cur) return;
|
|
2794
|
+
if (raw !== "" && (!/^\d+$/.test(raw) || Number(raw) <= 0)) {
|
|
2795
|
+
props.setErr("上限必须是正整数(清空 = 恢复目录默认)");
|
|
2796
|
+
setDraft(cur);
|
|
2797
|
+
return;
|
|
2798
|
+
}
|
|
2799
|
+
if (raw !== "" && props.ceiling != null && Number(raw) > props.ceiling) {
|
|
2800
|
+
props.setErr("超出该模型实际上限 " + props.ceiling);
|
|
2801
|
+
setDraft(cur);
|
|
2802
|
+
return;
|
|
2803
|
+
}
|
|
2804
|
+
var payload = { id: props.id };
|
|
2805
|
+
payload[props.field] = raw === "" ? null : Number(raw);
|
|
2806
|
+
props.post({ patch: { modelSetLimits: payload } }).then(function (res) {
|
|
2807
|
+
if (!res.ok) { props.setErr(res.d && res.d.error ? res.d.error : "保存失败"); setDraft(cur); return; }
|
|
2808
|
+
if (res.d && res.d.models) props.onSaved(res.d.models);
|
|
2809
|
+
}).catch(function (e) {
|
|
2810
|
+
props.setErr("保存失败:" + (e && e.message ? e.message : String(e)));
|
|
2811
|
+
setDraft(cur);
|
|
2812
|
+
});
|
|
2813
|
+
};
|
|
2814
|
+
return createElement("span", {
|
|
2815
|
+
title: props.overridden
|
|
2816
|
+
? "覆盖中(目录基值 " + (props.ceiling != null ? props.ceiling : "?") + "),清空恢复默认"
|
|
2817
|
+
: (props.ceiling != null ? "可改,上限 " + props.ceiling : "可改"),
|
|
2818
|
+
}, createElement(CbcInput, {
|
|
2819
|
+
type: "number",
|
|
2820
|
+
widthClass: "cbc-w90 cbc-ghost",
|
|
2821
|
+
ariaLabel: props.field === "contextWindow" ? "上下文长度上限(清空恢复目录默认)" : "输出长度上限(清空恢复目录默认)",
|
|
2822
|
+
value: draft,
|
|
2823
|
+
onInput: function (e) { setDraft(e.target.value); },
|
|
2824
|
+
onBlur: commit,
|
|
2825
|
+
onKeyDown: function (e) { if (e.key === "Enter") { e.preventDefault(); e.target.blur(); } },
|
|
2826
|
+
}));
|
|
2827
|
+
}
|
|
2828
|
+
|
|
2829
|
+
var inject = ["slots"];
|
|
2830
|
+
|
|
2831
|
+
// dsh 0.1.6 plugins.item 槽契约:owner props {view}——summary = 标题下的
|
|
2832
|
+
// 一行简介;page = 完整表单(页面自带标题/图标/返回 crumb,卡片常开、
|
|
2833
|
+
// 不再画自己的折叠头部)。
|
|
2834
|
+
function PluginManagerEntry(props) {
|
|
2835
|
+
if (props && props.view === "summary") {
|
|
2836
|
+
return createElement("span", null, "CodeBuddy / Trae / Qoder CN 通道:凭据、模型、额度与流式桥。");
|
|
2837
|
+
}
|
|
2838
|
+
return createElement(CodeBuddyCard, { embedded: true });
|
|
2839
|
+
}
|
|
2840
|
+
|
|
2841
|
+
function apply(ctx) {
|
|
2842
|
+
ctx.inject(["slots"], function (sctx) {
|
|
2843
|
+
// dsh 0.1.6 拆除了 settings.plugin.item 槽(卡片迁入 Plugin Manager
|
|
2844
|
+
// 的 plugins.item,boot 即声明,inject 立即回调);旧槽名在新宿主上
|
|
2845
|
+
// 永不声明,而 slots.inject 对未声明槽是静默等待——0.1.5→0.1.6 升级
|
|
2846
|
+
// "卡片消失且零报错"正是这个机制。两槽都注册:哪个声明了走哪个。
|
|
2847
|
+
if (typeof sctx.slots.inject === "function") {
|
|
2848
|
+
sctx.slots.inject("plugins.item", function () {
|
|
2849
|
+
return sctx.slots.register(
|
|
2850
|
+
{
|
|
2851
|
+
name: "plugins.item",
|
|
2852
|
+
id: "dsh-tap",
|
|
2853
|
+
order: 60,
|
|
2854
|
+
label: function () { return "dsh-tap"; },
|
|
2855
|
+
inject: function () { return {}; },
|
|
2856
|
+
},
|
|
2857
|
+
PluginManagerEntry,
|
|
2858
|
+
);
|
|
2859
|
+
});
|
|
2860
|
+
sctx.slots.inject("settings.plugin.item", function () {
|
|
2861
|
+
return sctx.slots.register(
|
|
2862
|
+
{
|
|
2863
|
+
name: "settings.plugin.item",
|
|
2864
|
+
// keyed 槽:设置页按 Host 服务的 settings 命名空间逐个
|
|
2865
|
+
// 派发,配对本 key(host 半注册同名命名空间)。
|
|
2866
|
+
key: "dsh-tap",
|
|
2867
|
+
inject: function () { return {}; },
|
|
2868
|
+
},
|
|
2869
|
+
CodeBuddyCard,
|
|
2870
|
+
);
|
|
2871
|
+
});
|
|
2872
|
+
} else {
|
|
2873
|
+
sctx.slots.register(
|
|
2874
|
+
{
|
|
2875
|
+
name: "settings.plugin.item",
|
|
2876
|
+
key: "dsh-tap",
|
|
2877
|
+
id: "codebuddy",
|
|
2878
|
+
order: 60,
|
|
2879
|
+
label: function () { return "dsh-tap"; },
|
|
2880
|
+
inject: function () { return {}; },
|
|
2881
|
+
},
|
|
2882
|
+
CodeBuddyCard,
|
|
2883
|
+
);
|
|
2884
|
+
}
|
|
2885
|
+
});
|
|
2886
|
+
}
|
|
2887
|
+
|
|
2888
|
+
exports.ROUTE = ROUTE;
|
|
2889
|
+
exports.apply = apply;
|
|
2890
|
+
exports.inject = inject;
|
|
2891
|
+
return module.exports;
|
|
2892
|
+
},
|
|
2893
|
+
});
|