dsh-llm-codebuddy-power 1.0.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/LICENSE +22 -0
- package/README.md +66 -0
- package/README.zh.md +66 -0
- package/cordis.patch.yml +32 -0
- package/lib/cli-login.js +2099 -0
- package/lib/client.js +2814 -0
- package/lib/index.js +3957 -0
- package/lib/types/adapter.d.ts +58 -0
- package/lib/types/atomic-file.d.ts +43 -0
- package/lib/types/auth-service.d.ts +195 -0
- package/lib/types/cli/login.d.ts +23 -0
- package/lib/types/client/AccountSwitcher.d.ts +53 -0
- package/lib/types/client/CodeBuddySection.d.ts +62 -0
- package/lib/types/client/ComposerControls.d.ts +29 -0
- package/lib/types/client/DrawToolCard.d.ts +24 -0
- package/lib/types/client/ModelPanel.d.ts +77 -0
- package/lib/types/client/TurnCreditPill.d.ts +33 -0
- package/lib/types/client/UsageIndicator.d.ts +21 -0
- package/lib/types/client/icons.d.ts +24 -0
- package/lib/types/client/index.d.ts +34 -0
- package/lib/types/client/locales.d.ts +90 -0
- package/lib/types/client/poll.d.ts +19 -0
- package/lib/types/client/store.d.ts +59 -0
- package/lib/types/client/wire.d.ts +147 -0
- package/lib/types/codebuddy.d.ts +187 -0
- package/lib/types/constants.d.ts +67 -0
- package/lib/types/credit-store.d.ts +25 -0
- package/lib/types/image-tool.d.ts +34 -0
- package/lib/types/index.d.ts +79 -0
- package/lib/types/login.d.ts +43 -0
- package/lib/types/model-enrich.d.ts +125 -0
- package/lib/types/prefs.d.ts +43 -0
- package/lib/types/rpc-route.d.ts +25 -0
- package/lib/types/selection.d.ts +35 -0
- package/lib/types/serialize.d.ts +37 -0
- package/lib/types/session-store.d.ts +63 -0
- package/lib/types/session.d.ts +415 -0
- package/lib/types/settings.d.ts +18 -0
- package/lib/types/sse.d.ts +22 -0
- package/lib/types/storage.d.ts +118 -0
- package/lib/types/translate.d.ts +61 -0
- package/lib/types/types.d.ts +345 -0
- package/lib/types/usage.d.ts +109 -0
- package/package.json +119 -0
package/lib/client.js
ADDED
|
@@ -0,0 +1,2814 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({
|
|
2
|
+
id: "dsh-llm-codebuddy-power",
|
|
3
|
+
factory: (require) => {
|
|
4
|
+
var module = { exports: {} };
|
|
5
|
+
var exports = module.exports;
|
|
6
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
|
+
let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
8
|
+
let _deepseek_ai_dsh_client_store = require("@deepseek-ai/dsh-client-store");
|
|
9
|
+
let react = require("react");
|
|
10
|
+
let react_jsx_runtime = require("react/jsx-runtime");
|
|
11
|
+
let react_dom = require("react-dom");
|
|
12
|
+
//#region src/prefs.ts
|
|
13
|
+
/**
|
|
14
|
+
* CodeBuddy 界面偏好的命名空间契约,Host 与 Client 两面共享。
|
|
15
|
+
*
|
|
16
|
+
* 这些偏好是**插件自己的设置命名空间**里的一段:`Config` 提供部署级 base,
|
|
17
|
+
* 用户在 Web 设置页的写入落在 harness 的用户设置文档里,因此换浏览器、重启
|
|
18
|
+
* harness 后仍然生效 —— 其中「极简模式画图工具」还会被宿主读取来决定
|
|
19
|
+
* `codebuddy-draw` 对模型是否可见,存在浏览器里会让同一次部署因浏览器而异。
|
|
20
|
+
*
|
|
21
|
+
* 本文件必须保持零依赖:两面都会编译它,而 Client 面若因此引入 schemastery
|
|
22
|
+
* 就会把它打进浏览器产物(校验 schema 是宿主面的事,见 `./settings.ts`)。
|
|
23
|
+
*
|
|
24
|
+
* @module dsh-llm-codebuddy-power/prefs
|
|
25
|
+
*/
|
|
26
|
+
/** 本插件拥有的设置命名空间,与插件名一致。 */
|
|
27
|
+
const CODEBUDDY_SETTINGS_NAMESPACE = "llm-codebuddy-power";
|
|
28
|
+
/** 未覆盖任何一层时使用的偏好默认值。 */
|
|
29
|
+
const DEFAULT_CODEBUDDY_SETTINGS = {
|
|
30
|
+
showUsage: true,
|
|
31
|
+
dangerPct: 90,
|
|
32
|
+
drawToolInMinimal: false
|
|
33
|
+
};
|
|
34
|
+
//#endregion
|
|
35
|
+
//#region \0dsh-css:D:\data\code\deepseek-harness\packages\my\dsh-llm-codebuddy-power\src\client\CodeBuddySection.module.css.mjs
|
|
36
|
+
const css$4 = ".-h11cq_section{flex-direction:column;gap:12px;max-width:720px;padding:8px 0;display:flex}.-h11cq_title{color:var(--dsw-alias-label-primary);margin:0;font-size:18px;font-weight:600}.-h11cq_desc{color:var(--dsw-alias-label-secondary);margin:0;font-size:14px;line-height:22px}.-h11cq_muted{color:var(--dsw-alias-label-secondary);margin:0;font-size:14px}.-h11cq_error{color:var(--dsw-alias-state-error-primary);margin:0;font-size:14px}.-h11cq_accountHeader{border-bottom:1px solid var(--dsw-alias-border-l2);flex-wrap:wrap;justify-content:flex-start;align-items:center;gap:8px;padding-bottom:8px;display:flex}.-h11cq_splitButton{align-items:center;display:inline-flex}.-h11cq_splitMain{box-shadow:inset -1px 0 0 var(--dsw-alias-border-l3);border-right-width:0;border-top-right-radius:0;border-bottom-right-radius:0}.-h11cq_splitActionAnchor{display:inline-flex}.-h11cq_splitAction{border-top-left-radius:0;border-bottom-left-radius:0;padding:0 10px}.-h11cq_splitMain:focus-visible,.-h11cq_splitAction:focus-visible{outline-offset:-2px}.-h11cq_badges{flex-wrap:wrap;gap:8px;display:flex}.-h11cq_badge{box-sizing:border-box;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);border-radius:16px;align-items:center;gap:6px;max-width:100%;min-height:32px;padding:0 4px 0 0;display:flex}.-h11cq_badge[data-default]{border-color:var(--dsw-alias-brand-primary);background:var(--dsw-alias-interactive-bg-hover-accent)}.-h11cq_badgeInfo{align-self:stretch;align-items:center;min-width:0;padding-left:12px;display:flex}.-h11cq_badgeDragging{opacity:.4}.-h11cq_badgeDropBefore,.-h11cq_badgeDropAfter{position:relative}.-h11cq_badgeDropBefore:before,.-h11cq_badgeDropAfter:after{content:\"\";z-index:1;background:var(--dsw-alias-brand-primary);pointer-events:none;border-radius:1px;width:2px;position:absolute;top:0;bottom:0}.-h11cq_badgeDropBefore:before{left:-5px}.-h11cq_badgeDropAfter:after{right:-5px}.-h11cq_badgeInfoText{color:var(--dsw-alias-label-primary);text-overflow:ellipsis;white-space:nowrap;max-width:180px;font-size:13px;line-height:20px;overflow:hidden}.-h11cq_badge:not(:has(.-h11cq_badgeDefaultTag)):not(:has(.-h11cq_badgeCheck)) .-h11cq_badgeInfo{padding-right:8px}.-h11cq_badgeDefaultTag{background:var(--dsw-alias-interactive-bg-hover-accent);color:var(--dsw-alias-brand-primary);cursor:pointer;border:0;border-radius:999px;flex:none;padding:0 6px;font-size:12px;line-height:20px}.-h11cq_badgeDefaultTag:hover{background:var(--dsw-alias-interactive-bg-hover)}.-h11cq_badgeDefaultTag:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}.-h11cq_badgeActionAnchor{flex:none;display:inline-flex}.-h11cq_badgeAction{width:24px;height:24px;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:0;border-radius:12px;justify-content:center;align-items:center;padding:0;display:inline-flex}.-h11cq_badgeAction:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover-danger);color:var(--dsw-alias-state-error-primary)}.-h11cq_badgeAction:disabled{cursor:default;opacity:.4}.-h11cq_badgeAction:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}.-h11cq_badgeCheck{border:1px solid var(--dsw-alias-border-l3);width:20px;height:20px;color:var(--dsw-alias-label-primary-foreground);cursor:pointer;background:0 0;border-radius:6px;flex:none;justify-content:center;align-items:center;padding:0;display:inline-flex}.-h11cq_badgeCheckOn{border-color:var(--dsw-alias-brand-primary);background:var(--dsw-alias-brand-primary)}.-h11cq_badgeCheck:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px}.-h11cq_divider{background:var(--dsw-alias-border-l2);height:1px}.-h11cq_prefRow{border-bottom:1px solid var(--dsw-alias-border-l2);align-items:center;gap:8px;padding:8px 0;display:flex}.-h11cq_prefRowText{flex-direction:column;flex:1;gap:4px;min-width:0;padding-right:48px;display:flex}.-h11cq_prefTitle{color:var(--dsw-alias-label-primary);font-size:14px;font-weight:400;line-height:22px}.-h11cq_prefDesc{color:var(--dsw-alias-label-secondary);font-size:12px;font-weight:400;line-height:18px}.-h11cq_switch{box-sizing:border-box;background:var(--dsw-alias-border-l3);cursor:pointer;border:0;border-radius:10px;flex:none;width:36px;height:20px;padding:2px;position:relative}.-h11cq_switchOn{background:var(--dsw-alias-brand-primary)}.-h11cq_switch:disabled{cursor:default;opacity:.5}.-h11cq_switch:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px}.-h11cq_thumb{corner-shape:round;background:var(--dsw-alias-label-primary-foreground);border-radius:50%;width:16px;height:16px;transition:transform .12s;display:block}.-h11cq_switchOn .-h11cq_thumb{transform:translate(16px)}.-h11cq_prefInput{width:120px}";
|
|
37
|
+
const tagId$4 = "dsh-llm-codebuddy-power/CodeBuddySection.module.css";
|
|
38
|
+
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$4) + "]") === null) {
|
|
39
|
+
const tag = document.createElement("style");
|
|
40
|
+
tag.dataset.plugin = "dsh-llm-codebuddy-power";
|
|
41
|
+
tag.dataset.pluginCss = tagId$4;
|
|
42
|
+
tag.textContent = css$4;
|
|
43
|
+
document.head.appendChild(tag);
|
|
44
|
+
}
|
|
45
|
+
var CodeBuddySection_module_css_default = {
|
|
46
|
+
"accountHeader": "-h11cq_accountHeader",
|
|
47
|
+
"badge": "-h11cq_badge",
|
|
48
|
+
"badgeAction": "-h11cq_badgeAction",
|
|
49
|
+
"badgeActionAnchor": "-h11cq_badgeActionAnchor",
|
|
50
|
+
"badgeCheck": "-h11cq_badgeCheck",
|
|
51
|
+
"badgeCheckOn": "-h11cq_badgeCheckOn",
|
|
52
|
+
"badgeDefaultTag": "-h11cq_badgeDefaultTag",
|
|
53
|
+
"badgeDragging": "-h11cq_badgeDragging",
|
|
54
|
+
"badgeDropAfter": "-h11cq_badgeDropAfter",
|
|
55
|
+
"badgeDropBefore": "-h11cq_badgeDropBefore",
|
|
56
|
+
"badgeInfo": "-h11cq_badgeInfo",
|
|
57
|
+
"badgeInfoText": "-h11cq_badgeInfoText",
|
|
58
|
+
"badges": "-h11cq_badges",
|
|
59
|
+
"desc": "-h11cq_desc",
|
|
60
|
+
"divider": "-h11cq_divider",
|
|
61
|
+
"error": "-h11cq_error",
|
|
62
|
+
"muted": "-h11cq_muted",
|
|
63
|
+
"prefDesc": "-h11cq_prefDesc",
|
|
64
|
+
"prefInput": "-h11cq_prefInput",
|
|
65
|
+
"prefRow": "-h11cq_prefRow",
|
|
66
|
+
"prefRowText": "-h11cq_prefRowText",
|
|
67
|
+
"prefTitle": "-h11cq_prefTitle",
|
|
68
|
+
"section": "-h11cq_section",
|
|
69
|
+
"splitAction": "-h11cq_splitAction",
|
|
70
|
+
"splitActionAnchor": "-h11cq_splitActionAnchor",
|
|
71
|
+
"splitButton": "-h11cq_splitButton",
|
|
72
|
+
"splitMain": "-h11cq_splitMain",
|
|
73
|
+
"switch": "-h11cq_switch",
|
|
74
|
+
"switchOn": "-h11cq_switchOn",
|
|
75
|
+
"thumb": "-h11cq_thumb",
|
|
76
|
+
"title": "-h11cq_title"
|
|
77
|
+
};
|
|
78
|
+
//#endregion
|
|
79
|
+
//#region src/client/CodeBuddySection.tsx
|
|
80
|
+
/**
|
|
81
|
+
* CodeBuddy 设置分区:多账号登录页。
|
|
82
|
+
*
|
|
83
|
+
* 展示全部已登录账号(每个账号一个徽章),在此发起浏览器登录以**追加**账号、
|
|
84
|
+
* 退出单个或选中的多个账号、指定或取消默认账号;下方是两种登录状态下都渲染的
|
|
85
|
+
* 界面偏好行。退出是破坏性操作(会删除磁盘上的凭据),因此单个与批量都先弹确认框。
|
|
86
|
+
* 弹窗可见性由 shell 拥有,故此处不需要关闭控件。
|
|
87
|
+
*/
|
|
88
|
+
/** 客户端轮询已发起登录的间隔(毫秒)。 */
|
|
89
|
+
const POLL_INTERVAL_MS = 1500;
|
|
90
|
+
/** 客户端放弃前持续轮询的时长(毫秒)。 */
|
|
91
|
+
const POLL_DEADLINE_MS = 600 * 1e3;
|
|
92
|
+
/** 复制成功后图标保持对勾的时长(毫秒)。 */
|
|
93
|
+
const COPIED_FEEDBACK_MS = 1e3;
|
|
94
|
+
/**
|
|
95
|
+
* 解码 CodeBuddy 的 `departmentFullName`,它是 base64 编码的 UTF-8。
|
|
96
|
+
* 不是合法 base64 时回退为原始值。
|
|
97
|
+
*/
|
|
98
|
+
function decodeDepartment(raw) {
|
|
99
|
+
try {
|
|
100
|
+
const decoded = atob(raw);
|
|
101
|
+
return new TextDecoder().decode(Uint8Array.from(decoded, (c) => c.charCodeAt(0)));
|
|
102
|
+
} catch {
|
|
103
|
+
return raw;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* 徽章悬停提示的正文:逐行列出该账号披露的字段,缺失的不占行。
|
|
108
|
+
*
|
|
109
|
+
* 顺序固定为昵称 → 登录到期 → 两个 id → 企业信息。
|
|
110
|
+
*
|
|
111
|
+
* 登录到期按"有时才显示"处理:这是跨进程的 wire 数据,而两个编译面可以处于不同版本
|
|
112
|
+
* (客户端插件经 HMR 单独重载时,宿主仍在跑旧的 `lib/index.js`),旧宿主不会带这个字段。
|
|
113
|
+
* 不判断就会渲染出 `Invalid Date`,那比缺这一行更糟。
|
|
114
|
+
*/
|
|
115
|
+
function accountDetail(account, t) {
|
|
116
|
+
const rows = [`${t("nickname")}: ${account.nickname}`];
|
|
117
|
+
if (account.loginExpiresAt !== void 0 && Number.isFinite(account.loginExpiresAt)) rows.push(`${t("loginExpires")}: ${new Date(account.loginExpiresAt).toLocaleString(void 0, {
|
|
118
|
+
dateStyle: "medium",
|
|
119
|
+
timeStyle: "short"
|
|
120
|
+
})}`);
|
|
121
|
+
rows.push(`${t("uid")}: ${account.uid}`);
|
|
122
|
+
if (account.uin !== void 0) rows.push(`${t("uin")}: ${account.uin}`);
|
|
123
|
+
if (account.enterpriseName !== void 0) rows.push(`${t("enterprise")}: ${account.enterpriseName}`);
|
|
124
|
+
if (account.enterpriseId !== void 0) rows.push(`${t("enterpriseId")}: ${account.enterpriseId}`);
|
|
125
|
+
if (account.enterpriseUserName !== void 0) rows.push(`${t("enterpriseUser")}: ${account.enterpriseUserName}`);
|
|
126
|
+
if (account.departmentFullName !== void 0) rows.push(`${t("department")}: ${decodeDepartment(account.departmentFullName)}`);
|
|
127
|
+
return rows.join("\n");
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* 指针落在徽章的哪一侧。徽章是横向排列的胶囊,因此按左右分半,而不是按上下。
|
|
131
|
+
*/
|
|
132
|
+
function badgeHalf(e) {
|
|
133
|
+
const rect = e.currentTarget.getBoundingClientRect();
|
|
134
|
+
return e.clientX < rect.left + rect.width / 2 ? "before" : "after";
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* 一个账号徽章:整块信息区(昵称及其内边距)悬停看明细,默认账号另带一个「默认」标记。
|
|
138
|
+
*
|
|
139
|
+
* 提示挂在整块信息区而不是只挂在昵称文字上:昵称常常很短,能触发提示的只有那几个
|
|
140
|
+
* 字;把锚点扩到这块区域的整个高度与左侧留白,徽章上除功能按钮外的地方都能唤起它。
|
|
141
|
+
* 不用"Tooltip 包住整个徽章、按钮上禁用"的写法 —— `Tooltip` 的 `disabled` 会直接
|
|
142
|
+
* 清空它的悬停/聚焦状态,从昵称移到按钮再移回来时提示不会再出现。
|
|
143
|
+
*
|
|
144
|
+
* 徽章本身不承担任何管理动作:退出与默认账号的选择都在顶部操作区,徽章只回答
|
|
145
|
+
* "这是哪个账号、它是不是默认的"。唯一可点的例外是那个「默认」标记 —— 它标出默认
|
|
146
|
+
* 账号,自己也就是取消默认的入口。
|
|
147
|
+
*/
|
|
148
|
+
function AccountBadge(props) {
|
|
149
|
+
const { account, isDefault, multiSelect, checked, t, onClearDefault, onToggleChecked, drag } = props;
|
|
150
|
+
const dragging = drag?.dragging ? ` ${CodeBuddySection_module_css_default.badgeDragging}` : "";
|
|
151
|
+
const marker = drag?.marker === "before" ? ` ${CodeBuddySection_module_css_default.badgeDropBefore}` : drag?.marker === "after" ? ` ${CodeBuddySection_module_css_default.badgeDropAfter}` : "";
|
|
152
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
153
|
+
className: `${CodeBuddySection_module_css_default.badge}${dragging}${marker}`,
|
|
154
|
+
"data-default": isDefault ? "" : void 0,
|
|
155
|
+
draggable: drag !== void 0,
|
|
156
|
+
onDragStart: drag === void 0 ? void 0 : (e) => {
|
|
157
|
+
e.dataTransfer.effectAllowed = "move";
|
|
158
|
+
e.dataTransfer.setData("text/plain", account.uid);
|
|
159
|
+
drag.start();
|
|
160
|
+
},
|
|
161
|
+
onDragEnd: drag?.end,
|
|
162
|
+
onDragOver: drag === void 0 || !drag.active ? void 0 : (e) => {
|
|
163
|
+
e.preventDefault();
|
|
164
|
+
e.dataTransfer.dropEffect = "move";
|
|
165
|
+
drag.hover(badgeHalf(e));
|
|
166
|
+
},
|
|
167
|
+
onDrop: drag === void 0 || !drag.active ? void 0 : (e) => {
|
|
168
|
+
e.preventDefault();
|
|
169
|
+
drag.drop(badgeHalf(e));
|
|
170
|
+
},
|
|
171
|
+
children: [
|
|
172
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
|
|
173
|
+
label: accountDetail(account, t),
|
|
174
|
+
side: "top",
|
|
175
|
+
delayMs: 300,
|
|
176
|
+
maxWidth: 320,
|
|
177
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
178
|
+
className: CodeBuddySection_module_css_default.badgeInfo,
|
|
179
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
180
|
+
className: CodeBuddySection_module_css_default.badgeInfoText,
|
|
181
|
+
children: account.nickname
|
|
182
|
+
})
|
|
183
|
+
})
|
|
184
|
+
}),
|
|
185
|
+
isDefault && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
|
|
186
|
+
label: t("clearDefault"),
|
|
187
|
+
side: "top",
|
|
188
|
+
delayMs: 300,
|
|
189
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
190
|
+
type: "button",
|
|
191
|
+
className: CodeBuddySection_module_css_default.badgeDefaultTag,
|
|
192
|
+
"aria-pressed": true,
|
|
193
|
+
"aria-label": `${t("clearDefault")}: ${account.nickname}`,
|
|
194
|
+
onClick: onClearDefault,
|
|
195
|
+
children: t("defaultTag")
|
|
196
|
+
})
|
|
197
|
+
}),
|
|
198
|
+
multiSelect && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
199
|
+
type: "button",
|
|
200
|
+
role: "checkbox",
|
|
201
|
+
"aria-checked": checked,
|
|
202
|
+
"aria-label": `${t("selectAccount")}: ${account.nickname}`,
|
|
203
|
+
className: `${CodeBuddySection_module_css_default.badgeCheck} ${checked ? CodeBuddySection_module_css_default.badgeCheckOn : ""}`,
|
|
204
|
+
onClick: onToggleChecked,
|
|
205
|
+
children: checked && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCheckOutline16, { size: 14 })
|
|
206
|
+
})
|
|
207
|
+
]
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* CodeBuddy 设置分区。
|
|
212
|
+
* @param props - 组合后的 slot props。
|
|
213
|
+
* @returns 分区的元素树。
|
|
214
|
+
*/
|
|
215
|
+
function CodeBuddySection(props) {
|
|
216
|
+
const { t, useStore, usePrefs, refreshAccounts, startLogin, copyLoginLink, cancelLogin, pollLogin, logoutAccounts, reorderAccounts, setDefault, setPref, actions } = props;
|
|
217
|
+
const phase = useStore((s) => s.phase);
|
|
218
|
+
const error = useStore((s) => s.error);
|
|
219
|
+
const accounts = useStore((s) => s.accounts);
|
|
220
|
+
const defaultUid = useStore((s) => s.defaultUid);
|
|
221
|
+
const loginState = useStore((s) => s.loginState);
|
|
222
|
+
const prefs = usePrefs((value) => value);
|
|
223
|
+
const [dangerText, setDangerText] = (0, react.useState)(() => String(prefs.dangerPct));
|
|
224
|
+
/** 多选模式;开启后每个徽章的右侧控件变成勾选框。 */
|
|
225
|
+
const [multiSelect, setMultiSelect] = (0, react.useState)(false);
|
|
226
|
+
/** 多选模式下已勾选的账号 uid。 */
|
|
227
|
+
const [checked, setChecked] = (0, react.useState)([]);
|
|
228
|
+
/** 待确认的退出目标;null 表示没有待确认的退出。 */
|
|
229
|
+
const [pendingLogout, setPendingLogout] = (0, react.useState)(null);
|
|
230
|
+
const halted = (0, react.useRef)(false);
|
|
231
|
+
/** 卸载后为 true;登录轮询就此停止,而不是触碰状态。 */
|
|
232
|
+
const isHalted = () => halted.current;
|
|
233
|
+
(0, react.useEffect)(() => {
|
|
234
|
+
refreshAccounts();
|
|
235
|
+
}, [refreshAccounts]);
|
|
236
|
+
(0, react.useEffect)(() => {
|
|
237
|
+
if (loginState === null) return;
|
|
238
|
+
halted.current = false;
|
|
239
|
+
const { state, startedAt } = loginState;
|
|
240
|
+
let timer;
|
|
241
|
+
const tick = async () => {
|
|
242
|
+
if (isHalted()) return;
|
|
243
|
+
let done = false;
|
|
244
|
+
try {
|
|
245
|
+
done = await pollLogin(state);
|
|
246
|
+
} catch {
|
|
247
|
+
done = false;
|
|
248
|
+
}
|
|
249
|
+
if (isHalted()) return;
|
|
250
|
+
if (done) return;
|
|
251
|
+
if (Date.now() - startedAt >= POLL_DEADLINE_MS) {
|
|
252
|
+
actions.setLoginState(null);
|
|
253
|
+
actions.fail(t("timeout"));
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
timer = window.setTimeout(tick, POLL_INTERVAL_MS);
|
|
257
|
+
};
|
|
258
|
+
tick();
|
|
259
|
+
return () => {
|
|
260
|
+
halted.current = true;
|
|
261
|
+
if (timer !== void 0) window.clearTimeout(timer);
|
|
262
|
+
};
|
|
263
|
+
}, [
|
|
264
|
+
loginState,
|
|
265
|
+
pollLogin,
|
|
266
|
+
actions,
|
|
267
|
+
t
|
|
268
|
+
]);
|
|
269
|
+
const onStartLogin = (0, react.useCallback)(() => {
|
|
270
|
+
startLogin();
|
|
271
|
+
}, [startLogin]);
|
|
272
|
+
/**
|
|
273
|
+
* 放弃这次登录:先中止宿主的轮询,再清掉本地的握手状态。
|
|
274
|
+
*
|
|
275
|
+
* 顺序不能反 —— 先清状态的话,正在飞的那次 `pollLogin` 仍会按 `done: false` 排下一次
|
|
276
|
+
* 轮询;而先中止,状态一清,effect 的 `loginState === null` 分支就不再重建轮询链。
|
|
277
|
+
*/
|
|
278
|
+
const onCancelLogin = (0, react.useCallback)(() => {
|
|
279
|
+
if (loginState === null) return;
|
|
280
|
+
cancelLogin(loginState.state).finally(() => {
|
|
281
|
+
actions.setLoginState(null);
|
|
282
|
+
});
|
|
283
|
+
}, [
|
|
284
|
+
loginState,
|
|
285
|
+
cancelLogin,
|
|
286
|
+
actions
|
|
287
|
+
]);
|
|
288
|
+
const [copied, setCopied] = (0, react.useState)(false);
|
|
289
|
+
const copiedTimer = (0, react.useRef)(void 0);
|
|
290
|
+
(0, react.useEffect)(() => () => {
|
|
291
|
+
if (copiedTimer.current !== void 0) window.clearTimeout(copiedTimer.current);
|
|
292
|
+
}, []);
|
|
293
|
+
const onCopyLoginLink = (0, react.useCallback)(() => {
|
|
294
|
+
if (copied) return;
|
|
295
|
+
copyLoginLink().then((ok) => {
|
|
296
|
+
if (!ok) return;
|
|
297
|
+
setCopied(true);
|
|
298
|
+
copiedTimer.current = window.setTimeout(() => {
|
|
299
|
+
copiedTimer.current = void 0;
|
|
300
|
+
setCopied(false);
|
|
301
|
+
}, COPIED_FEEDBACK_MS);
|
|
302
|
+
});
|
|
303
|
+
}, [copied, copyLoginLink]);
|
|
304
|
+
const [defaultMenuOpen, setDefaultMenuOpen] = (0, react.useState)(false);
|
|
305
|
+
const onClearDefault = (0, react.useCallback)(() => {
|
|
306
|
+
setDefault(null);
|
|
307
|
+
}, [setDefault]);
|
|
308
|
+
const onToggleChecked = (0, react.useCallback)((uid) => {
|
|
309
|
+
setChecked((current) => current.includes(uid) ? current.filter((item) => item !== uid) : [...current, uid]);
|
|
310
|
+
}, []);
|
|
311
|
+
const onConfirmLogout = (0, react.useCallback)(() => {
|
|
312
|
+
const uids = pendingLogout ?? [];
|
|
313
|
+
setPendingLogout(null);
|
|
314
|
+
setChecked([]);
|
|
315
|
+
logoutAccounts(uids);
|
|
316
|
+
}, [logoutAccounts, pendingLogout]);
|
|
317
|
+
const [dragUid, setDragUid] = (0, react.useState)(null);
|
|
318
|
+
const [dropAt, setDropAt] = (0, react.useState)(null);
|
|
319
|
+
const onBadgeDrop = (0, react.useCallback)((target) => {
|
|
320
|
+
const moving = dragUid;
|
|
321
|
+
setDragUid(null);
|
|
322
|
+
setDropAt(null);
|
|
323
|
+
if (moving === null) return;
|
|
324
|
+
const uids = accounts.map((account) => account.uid);
|
|
325
|
+
const from = uids.indexOf(moving);
|
|
326
|
+
const targetIndex = uids.indexOf(target.uid);
|
|
327
|
+
if (from < 0 || targetIndex < 0) return;
|
|
328
|
+
const to = targetIndex + (target.half === "after" ? 1 : 0);
|
|
329
|
+
if (to === from || to === from + 1) return;
|
|
330
|
+
const next = [...uids];
|
|
331
|
+
next.splice(from, 1);
|
|
332
|
+
next.splice(from < to ? to - 1 : to, 0, moving);
|
|
333
|
+
reorderAccounts(next);
|
|
334
|
+
}, [
|
|
335
|
+
accounts,
|
|
336
|
+
dragUid,
|
|
337
|
+
reorderAccounts
|
|
338
|
+
]);
|
|
339
|
+
const onDragEnded = (0, react.useCallback)(() => {
|
|
340
|
+
setDragUid(null);
|
|
341
|
+
setDropAt(null);
|
|
342
|
+
}, []);
|
|
343
|
+
if (phase === "loading" && accounts.length === 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
344
|
+
className: CodeBuddySection_module_css_default.section,
|
|
345
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
346
|
+
className: CodeBuddySection_module_css_default.muted,
|
|
347
|
+
children: t("loading")
|
|
348
|
+
})
|
|
349
|
+
});
|
|
350
|
+
const pendingCount = pendingLogout?.length ?? 0;
|
|
351
|
+
const defaultItems = accounts.map((account) => ({
|
|
352
|
+
id: account.uid,
|
|
353
|
+
label: account.nickname
|
|
354
|
+
}));
|
|
355
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
356
|
+
className: CodeBuddySection_module_css_default.section,
|
|
357
|
+
children: [
|
|
358
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
|
|
359
|
+
className: CodeBuddySection_module_css_default.title,
|
|
360
|
+
children: t("nav")
|
|
361
|
+
}),
|
|
362
|
+
accounts.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
363
|
+
className: CodeBuddySection_module_css_default.desc,
|
|
364
|
+
children: t("intro")
|
|
365
|
+
}),
|
|
366
|
+
error !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
367
|
+
className: CodeBuddySection_module_css_default.error,
|
|
368
|
+
children: error
|
|
369
|
+
}),
|
|
370
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
371
|
+
className: CodeBuddySection_module_css_default.accountHeader,
|
|
372
|
+
children: [
|
|
373
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
374
|
+
className: CodeBuddySection_module_css_default.splitButton,
|
|
375
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
376
|
+
variant: "outline",
|
|
377
|
+
size: "md",
|
|
378
|
+
className: CodeBuddySection_module_css_default.splitMain,
|
|
379
|
+
disabled: loginState !== null,
|
|
380
|
+
onClick: onStartLogin,
|
|
381
|
+
children: loginState !== null ? t("signingIn") : t("signIn")
|
|
382
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
|
|
383
|
+
label: loginState === null ? copied ? t("copied") : t("copyLoginLink") : t("cancelLogin"),
|
|
384
|
+
side: "top",
|
|
385
|
+
delayMs: 300,
|
|
386
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
387
|
+
className: CodeBuddySection_module_css_default.splitActionAnchor,
|
|
388
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
389
|
+
variant: "outline",
|
|
390
|
+
size: "md",
|
|
391
|
+
className: CodeBuddySection_module_css_default.splitAction,
|
|
392
|
+
"aria-label": loginState === null ? copied ? t("copied") : t("copyLoginLink") : t("cancelLogin"),
|
|
393
|
+
icon: loginState === null ? copied ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCheckOutline16, { size: 14 }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCopyOutline16, { size: 14 }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCloseOutline16, { size: 14 }),
|
|
394
|
+
onClick: loginState === null ? onCopyLoginLink : onCancelLogin
|
|
395
|
+
})
|
|
396
|
+
})
|
|
397
|
+
})]
|
|
398
|
+
}),
|
|
399
|
+
accounts.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Menu, {
|
|
400
|
+
open: defaultMenuOpen,
|
|
401
|
+
portal: true,
|
|
402
|
+
align: "start",
|
|
403
|
+
selectedId: defaultUid ?? void 0,
|
|
404
|
+
items: defaultItems,
|
|
405
|
+
onClose: () => {
|
|
406
|
+
setDefaultMenuOpen(false);
|
|
407
|
+
},
|
|
408
|
+
onSelect: (uid) => {
|
|
409
|
+
setDefaultMenuOpen(false);
|
|
410
|
+
if (uid === defaultUid) return;
|
|
411
|
+
setDefault(uid);
|
|
412
|
+
},
|
|
413
|
+
anchor: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
414
|
+
variant: "outline",
|
|
415
|
+
size: "md",
|
|
416
|
+
"aria-haspopup": "menu",
|
|
417
|
+
"aria-expanded": defaultMenuOpen,
|
|
418
|
+
onClick: () => {
|
|
419
|
+
setDefaultMenuOpen((current) => !current);
|
|
420
|
+
},
|
|
421
|
+
children: t("chooseDefault")
|
|
422
|
+
})
|
|
423
|
+
}),
|
|
424
|
+
accounts.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
425
|
+
variant: "outline",
|
|
426
|
+
size: "md",
|
|
427
|
+
"aria-pressed": multiSelect,
|
|
428
|
+
onClick: () => {
|
|
429
|
+
setMultiSelect((current) => !current);
|
|
430
|
+
setChecked([]);
|
|
431
|
+
},
|
|
432
|
+
children: multiSelect ? t("multiSelectDone") : t("multiSelect")
|
|
433
|
+
}),
|
|
434
|
+
multiSelect && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
|
|
435
|
+
label: t("logoutSelectedTip"),
|
|
436
|
+
side: "top",
|
|
437
|
+
delayMs: 300,
|
|
438
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
439
|
+
className: CodeBuddySection_module_css_default.badgeActionAnchor,
|
|
440
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
441
|
+
type: "button",
|
|
442
|
+
className: CodeBuddySection_module_css_default.badgeAction,
|
|
443
|
+
"aria-label": t("logoutSelectedTip"),
|
|
444
|
+
disabled: checked.length === 0,
|
|
445
|
+
onClick: () => {
|
|
446
|
+
setPendingLogout([...checked]);
|
|
447
|
+
},
|
|
448
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconTrashOutline16, { size: 14 })
|
|
449
|
+
})
|
|
450
|
+
})
|
|
451
|
+
})
|
|
452
|
+
]
|
|
453
|
+
}),
|
|
454
|
+
accounts.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
455
|
+
className: CodeBuddySection_module_css_default.muted,
|
|
456
|
+
children: loginState !== null ? t("waiting") : t("notSignedIn")
|
|
457
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
458
|
+
className: CodeBuddySection_module_css_default.badges,
|
|
459
|
+
children: accounts.map((account) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AccountBadge, {
|
|
460
|
+
account,
|
|
461
|
+
isDefault: account.uid === defaultUid,
|
|
462
|
+
multiSelect,
|
|
463
|
+
checked: checked.includes(account.uid),
|
|
464
|
+
t,
|
|
465
|
+
onClearDefault,
|
|
466
|
+
onToggleChecked: () => {
|
|
467
|
+
onToggleChecked(account.uid);
|
|
468
|
+
},
|
|
469
|
+
drag: multiSelect ? void 0 : {
|
|
470
|
+
start: () => {
|
|
471
|
+
setDragUid(account.uid);
|
|
472
|
+
},
|
|
473
|
+
dragging: dragUid === account.uid,
|
|
474
|
+
active: dragUid !== null && dragUid !== account.uid,
|
|
475
|
+
marker: dropAt?.uid === account.uid ? dropAt.half : null,
|
|
476
|
+
hover: (half) => {
|
|
477
|
+
setDropAt({
|
|
478
|
+
uid: account.uid,
|
|
479
|
+
half
|
|
480
|
+
});
|
|
481
|
+
},
|
|
482
|
+
drop: (half) => {
|
|
483
|
+
onBadgeDrop({
|
|
484
|
+
uid: account.uid,
|
|
485
|
+
half
|
|
486
|
+
});
|
|
487
|
+
},
|
|
488
|
+
end: onDragEnded
|
|
489
|
+
}
|
|
490
|
+
}, account.uid))
|
|
491
|
+
}),
|
|
492
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: CodeBuddySection_module_css_default.divider }),
|
|
493
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(UsagePrefRows, {
|
|
494
|
+
t,
|
|
495
|
+
prefs,
|
|
496
|
+
dangerText,
|
|
497
|
+
setDangerText,
|
|
498
|
+
setPref
|
|
499
|
+
}),
|
|
500
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Modal, {
|
|
501
|
+
open: pendingLogout !== null,
|
|
502
|
+
onClose: () => {
|
|
503
|
+
setPendingLogout(null);
|
|
504
|
+
},
|
|
505
|
+
title: pendingCount > 1 ? t("logoutSelectedTitle") : t("logoutTitle"),
|
|
506
|
+
closeLabel: t("cancel"),
|
|
507
|
+
description: pendingCount > 1 ? t("logoutSelectedDesc", { count: pendingCount }) : t("logoutDesc", { name: accounts.find((account) => account.uid === pendingLogout?.[0])?.nickname ?? "" }),
|
|
508
|
+
footer: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
509
|
+
variant: "outline",
|
|
510
|
+
onClick: () => {
|
|
511
|
+
setPendingLogout(null);
|
|
512
|
+
},
|
|
513
|
+
children: t("cancel")
|
|
514
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
515
|
+
variant: "primary",
|
|
516
|
+
onClick: onConfirmLogout,
|
|
517
|
+
children: t("logoutConfirm")
|
|
518
|
+
})] })
|
|
519
|
+
})
|
|
520
|
+
]
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
/**
|
|
524
|
+
* 三行界面偏好,单独抽出以便与账号区解耦 —— 它们与登录状态无关。两个布尔行是
|
|
525
|
+
* 开关按钮(role="switch")而非下拉框:开关一次点击即可翻转,与文件浏览器的
|
|
526
|
+
* 显示隐藏开关一致。
|
|
527
|
+
*/
|
|
528
|
+
function UsagePrefRows(props) {
|
|
529
|
+
const { t, prefs, dangerText, setDangerText, setPref } = props;
|
|
530
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
531
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
532
|
+
className: CodeBuddySection_module_css_default.prefRow,
|
|
533
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
534
|
+
className: CodeBuddySection_module_css_default.prefRowText,
|
|
535
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
536
|
+
className: CodeBuddySection_module_css_default.prefTitle,
|
|
537
|
+
children: t("showUsage")
|
|
538
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
539
|
+
className: CodeBuddySection_module_css_default.prefDesc,
|
|
540
|
+
children: t("showUsageDesc")
|
|
541
|
+
})]
|
|
542
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
543
|
+
type: "button",
|
|
544
|
+
role: "switch",
|
|
545
|
+
"aria-checked": prefs.showUsage,
|
|
546
|
+
"aria-label": t("showUsage"),
|
|
547
|
+
className: `${CodeBuddySection_module_css_default.switch} ${prefs.showUsage ? CodeBuddySection_module_css_default.switchOn : ""}`,
|
|
548
|
+
onClick: () => {
|
|
549
|
+
setPref("showUsage", !prefs.showUsage);
|
|
550
|
+
},
|
|
551
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: CodeBuddySection_module_css_default.thumb })
|
|
552
|
+
})]
|
|
553
|
+
}),
|
|
554
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
555
|
+
className: CodeBuddySection_module_css_default.prefRow,
|
|
556
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
557
|
+
className: CodeBuddySection_module_css_default.prefRowText,
|
|
558
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
559
|
+
className: CodeBuddySection_module_css_default.prefTitle,
|
|
560
|
+
children: t("drawToolMinimal")
|
|
561
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
562
|
+
className: CodeBuddySection_module_css_default.prefDesc,
|
|
563
|
+
children: t("drawToolMinimalDesc")
|
|
564
|
+
})]
|
|
565
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
566
|
+
type: "button",
|
|
567
|
+
role: "switch",
|
|
568
|
+
"aria-checked": prefs.drawToolInMinimal,
|
|
569
|
+
"aria-label": t("drawToolMinimal"),
|
|
570
|
+
className: `${CodeBuddySection_module_css_default.switch} ${prefs.drawToolInMinimal ? CodeBuddySection_module_css_default.switchOn : ""}`,
|
|
571
|
+
onClick: () => {
|
|
572
|
+
setPref("drawToolInMinimal", !prefs.drawToolInMinimal);
|
|
573
|
+
},
|
|
574
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: CodeBuddySection_module_css_default.thumb })
|
|
575
|
+
})]
|
|
576
|
+
}),
|
|
577
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
578
|
+
className: CodeBuddySection_module_css_default.prefRow,
|
|
579
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
580
|
+
className: CodeBuddySection_module_css_default.prefRowText,
|
|
581
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
582
|
+
className: CodeBuddySection_module_css_default.prefTitle,
|
|
583
|
+
children: t("dangerPct")
|
|
584
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
585
|
+
className: CodeBuddySection_module_css_default.prefDesc,
|
|
586
|
+
children: t("dangerPctDesc")
|
|
587
|
+
})]
|
|
588
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Input, {
|
|
589
|
+
type: "number",
|
|
590
|
+
inputMode: "numeric",
|
|
591
|
+
min: 1,
|
|
592
|
+
max: 100,
|
|
593
|
+
step: 1,
|
|
594
|
+
className: CodeBuddySection_module_css_default.prefInput ?? "",
|
|
595
|
+
value: dangerText,
|
|
596
|
+
onChange: (e) => {
|
|
597
|
+
setDangerText(e.currentTarget.value);
|
|
598
|
+
},
|
|
599
|
+
onBlur: () => {
|
|
600
|
+
const parsed = Number(dangerText);
|
|
601
|
+
const rounded = Number.isFinite(parsed) ? Math.round(parsed) : prefs.dangerPct;
|
|
602
|
+
const next = rounded >= 1 && rounded <= 100 ? rounded : prefs.dangerPct;
|
|
603
|
+
setDangerText(String(next));
|
|
604
|
+
setPref("dangerPct", next);
|
|
605
|
+
}
|
|
606
|
+
})]
|
|
607
|
+
})
|
|
608
|
+
] });
|
|
609
|
+
}
|
|
610
|
+
//#endregion
|
|
611
|
+
//#region \0dsh-css:D:\data\code\deepseek-harness\packages\my\dsh-llm-codebuddy-power\src\client\AccountSwitcher.module.css.mjs
|
|
612
|
+
const css$3 = "._7gEqNa_root{flex:none;align-items:center;gap:4px;display:inline-flex}._7gEqNa_ringAnchor{align-items:center;display:inline-flex}._7gEqNa_trigger{box-sizing:border-box;min-width:calc(28px + var(--dsh-content-font-delta,0px));height:calc(28px + var(--dsh-content-font-delta,0px));max-width:96px;color:var(--dsw-alias-label-secondary);white-space:nowrap;text-overflow:ellipsis;cursor:pointer;background:0 0;border:0;border-radius:6px;justify-content:center;align-items:center;padding:0 6px;font-size:12px;line-height:1;display:inline-flex;overflow:hidden}._7gEqNa_trigger:hover{background:var(--dsw-alias-interactive-bg-hover)}._7gEqNa_trigger:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}._7gEqNa_menuDefault{background:var(--dsw-alias-interactive-bg-hover-accent);color:var(--dsw-alias-label-secondary);border-radius:8px;margin-left:6px;padding:0 6px;font-size:11px;line-height:16px}";
|
|
613
|
+
const tagId$3 = "dsh-llm-codebuddy-power/AccountSwitcher.module.css";
|
|
614
|
+
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$3) + "]") === null) {
|
|
615
|
+
const tag = document.createElement("style");
|
|
616
|
+
tag.dataset.plugin = "dsh-llm-codebuddy-power";
|
|
617
|
+
tag.dataset.pluginCss = tagId$3;
|
|
618
|
+
tag.textContent = css$3;
|
|
619
|
+
document.head.appendChild(tag);
|
|
620
|
+
}
|
|
621
|
+
var AccountSwitcher_module_css_default = {
|
|
622
|
+
"menuDefault": "_7gEqNa_menuDefault",
|
|
623
|
+
"ringAnchor": "_7gEqNa_ringAnchor",
|
|
624
|
+
"root": "_7gEqNa_root",
|
|
625
|
+
"trigger": "_7gEqNa_trigger"
|
|
626
|
+
};
|
|
627
|
+
//#endregion
|
|
628
|
+
//#region src/client/AccountSwitcher.tsx
|
|
629
|
+
/**
|
|
630
|
+
* 对话框输入区里的 CodeBuddy 账号控件:一个额度圆环加一个账号切换按钮。
|
|
631
|
+
*
|
|
632
|
+
* 它注册在 `conversation.input.right`,与 CodeBuddy 模型按钮同处一个插槽元素、
|
|
633
|
+
* 排在其左侧。两者都按**当前会话**取数:圆环显示该会话此刻生效账号的用量
|
|
634
|
+
* (主额度与赠送额度合并成一个圆环,百分比按两者之和算),按钮显示该账号昵称的
|
|
635
|
+
* 缩写,点开在账号之间切换 —— 每个会话各选各的,不影响别的会话。
|
|
636
|
+
*
|
|
637
|
+
* **不挂 store**:本插槽是会话作用域,而设置页是 root 作用域,一个 store handle
|
|
638
|
+
* 只能绑一个作用域(挂两处会在加载时抛 "one handle, one scope")。账号集合因此在
|
|
639
|
+
* 这里由注入面按轮询间隔自取。
|
|
640
|
+
*
|
|
641
|
+
* 偏好不是自取的:它经注入面的 `hooks` 隔舱与设置页共享同一个活来源(用户设置
|
|
642
|
+
* 文档里的命名空间),因此「显示额度余量」开关翻动的那一帧圆环就跟着出现或消失,
|
|
643
|
+
* 不经过轮询。
|
|
644
|
+
*
|
|
645
|
+
* @module dsh-llm-codebuddy-power/client/AccountSwitcher
|
|
646
|
+
*/
|
|
647
|
+
/**
|
|
648
|
+
* 账号集合与该会话生效账号的轮询间隔(毫秒)。宿主在一分钟内返回缓存的计量快照,
|
|
649
|
+
* 之后才会重新查询 CodeBuddy,因此较快的轮询响应及时,又不会频繁冲击计量 API。
|
|
650
|
+
*/
|
|
651
|
+
const REFRESH_MS = 5e3;
|
|
652
|
+
/** 圆环几何:外径、描边宽度,以及环内百分比的字号。 */
|
|
653
|
+
const RING_SIZE = 28;
|
|
654
|
+
const RING_STROKE = 2.5;
|
|
655
|
+
const RING_FONT_SIZE = 8;
|
|
656
|
+
/** 昵称缩写保留的头尾两个字之后仍允许直接显示的最长长度。 */
|
|
657
|
+
const ABBREVIATE_ABOVE = 2;
|
|
658
|
+
/**
|
|
659
|
+
* 昵称缩写:第一个字与最后一个字之间用 `~` 连接。
|
|
660
|
+
*
|
|
661
|
+
* 按码位切而不是按 UTF-16 单元切,否则 emoji 或增补平面字符会被截成半个。
|
|
662
|
+
* 一到两个字的昵称已经是全名,再缩写就只剩连接符。
|
|
663
|
+
* @param name - 账号昵称。
|
|
664
|
+
* @returns 按钮上显示的缩写。
|
|
665
|
+
*/
|
|
666
|
+
function abbreviate(name) {
|
|
667
|
+
const chars = Array.from(name);
|
|
668
|
+
if (chars.length <= ABBREVIATE_ABOVE) return name;
|
|
669
|
+
return `${chars[0]}~${chars[chars.length - 1]}`;
|
|
670
|
+
}
|
|
671
|
+
/** 将用量数值格式化为最多两位小数,不使用千位分隔符。 */
|
|
672
|
+
function formatAmount(value) {
|
|
673
|
+
return (Math.round(value * 100) / 100).toLocaleString(void 0, {
|
|
674
|
+
maximumFractionDigits: 2,
|
|
675
|
+
useGrouping: false
|
|
676
|
+
});
|
|
677
|
+
}
|
|
678
|
+
/**
|
|
679
|
+
* 构建圆环提示:合并后的已用/总量与百分比,其后是主池数值与它的重置时刻、赠送池,
|
|
680
|
+
* 最后是最近到期的赠送窗口明细。
|
|
681
|
+
*
|
|
682
|
+
* 重置时刻紧跟在主池之后,而不是排在赠送行之后:它描述的是主窗口,隔着赠送行会
|
|
683
|
+
* 被读成赠送的重置时间。
|
|
684
|
+
*/
|
|
685
|
+
function ringTooltip(usage, total, pct, t) {
|
|
686
|
+
const lines = [t("nav"), `${t("usageUsed")}: ${formatAmount(total.used)} / ${formatAmount(total.limit)} (${pct.toFixed(2)}%)`];
|
|
687
|
+
if (usage.main !== void 0) lines.push(`${t("usageMain")}: ${formatAmount(usage.main.used)} / ${formatAmount(usage.main.limit)}`);
|
|
688
|
+
if (usage.primaryResetsAt !== void 0) lines.push(`${t("usageResets")}: ${usage.primaryResetsAt}`);
|
|
689
|
+
if (usage.gift !== void 0) lines.push(`${t("usageGift")}: ${formatAmount(usage.gift.used)} / ${formatAmount(usage.gift.limit)}`);
|
|
690
|
+
for (const window of usage.gift?.soonest ?? []) {
|
|
691
|
+
const figures = `${formatAmount(window.used ?? 0)} / ${formatAmount(window.limit ?? 0)}`;
|
|
692
|
+
const expiry = window.resetsAt === void 0 ? void 0 : window.resetsAt.replace(/:\d{2}$/, "");
|
|
693
|
+
lines.push(expiry === void 0 ? `${window.name}: ${figures}` : `${expiry}: ${figures}`);
|
|
694
|
+
}
|
|
695
|
+
return lines.join("\n");
|
|
696
|
+
}
|
|
697
|
+
/**
|
|
698
|
+
* 会话账号控件:额度圆环(可选)+ 账号切换按钮。
|
|
699
|
+
* @param props - 带注入 face 的插槽 props。
|
|
700
|
+
* @returns 控件元素;一个账号都没有时为 null。
|
|
701
|
+
*/
|
|
702
|
+
function AccountSwitcher(props) {
|
|
703
|
+
const { t, usePrefs, fetchAccounts, fetchSessionAccount, activate, fetchUsage } = props;
|
|
704
|
+
const [accounts, setAccounts] = (0, react.useState)([]);
|
|
705
|
+
const [defaultUid, setDefaultUid] = (0, react.useState)(null);
|
|
706
|
+
const prefs = usePrefs((value) => value);
|
|
707
|
+
/** 该会话此刻生效的账号 uid;未读到时为 null。 */
|
|
708
|
+
const [activeUid, setActiveUid] = (0, react.useState)(null);
|
|
709
|
+
const [usage, setUsage] = (0, react.useState)(null);
|
|
710
|
+
const [menuOpen, setMenuOpen] = (0, react.useState)(false);
|
|
711
|
+
/**
|
|
712
|
+
* 轮询一次:账号集合与该会话生效的账号一起读。
|
|
713
|
+
*
|
|
714
|
+
* 两者放在同一个 effect 里,是因为它们都要"设置页一改这里就跟着变"这一条时间线;
|
|
715
|
+
* 分成两个 effect 只会让同一个 tick 里发出更多次读取。**两者与偏好开关无关**:
|
|
716
|
+
* 关闭圆环只隐藏圆环,账号按钮仍在。
|
|
717
|
+
*
|
|
718
|
+
* 用量搭同一次轮询,但只在圆环要显示时才读 —— 关掉圆环后没必要继续问计量面。
|
|
719
|
+
* `prefs.showUsage` 因此在依赖里:开关打开的瞬间重跑本 effect,用量立刻取回,
|
|
720
|
+
* 圆环不必等下一个 tick。
|
|
721
|
+
*/
|
|
722
|
+
(0, react.useEffect)(() => {
|
|
723
|
+
let stopped = false;
|
|
724
|
+
const read = () => {
|
|
725
|
+
fetchAccounts().then((value) => {
|
|
726
|
+
if (stopped || value === null) return;
|
|
727
|
+
setAccounts(value.accounts);
|
|
728
|
+
setDefaultUid(value.defaultUid);
|
|
729
|
+
});
|
|
730
|
+
fetchSessionAccount().then((uid) => {
|
|
731
|
+
if (!stopped) setActiveUid(uid);
|
|
732
|
+
});
|
|
733
|
+
if (!prefs.showUsage) return;
|
|
734
|
+
fetchUsage().then((value) => {
|
|
735
|
+
if (!stopped) setUsage(value);
|
|
736
|
+
});
|
|
737
|
+
};
|
|
738
|
+
if (!prefs.showUsage) setUsage(null);
|
|
739
|
+
read();
|
|
740
|
+
const timer = window.setInterval(read, REFRESH_MS);
|
|
741
|
+
return () => {
|
|
742
|
+
stopped = true;
|
|
743
|
+
window.clearInterval(timer);
|
|
744
|
+
};
|
|
745
|
+
}, [
|
|
746
|
+
fetchAccounts,
|
|
747
|
+
fetchSessionAccount,
|
|
748
|
+
fetchUsage,
|
|
749
|
+
prefs.showUsage
|
|
750
|
+
]);
|
|
751
|
+
if (accounts.length === 0) return null;
|
|
752
|
+
const active = accounts.find((account) => account.uid === activeUid) ?? accounts.find((account) => account.uid === defaultUid) ?? accounts[0];
|
|
753
|
+
if (active === void 0) return null;
|
|
754
|
+
const label = `${t("switchAccount")}: ${active.nickname}`;
|
|
755
|
+
const limit = (usage?.main?.limit ?? 0) + (usage?.gift?.limit ?? 0);
|
|
756
|
+
const used = (usage?.main?.used ?? 0) + (usage?.gift?.used ?? 0);
|
|
757
|
+
const pct = limit > 0 ? Math.min(Math.max(used / limit * 100, 0), 100) : void 0;
|
|
758
|
+
const tip = usage !== null && pct !== void 0 ? ringTooltip(usage, {
|
|
759
|
+
used,
|
|
760
|
+
limit
|
|
761
|
+
}, pct, t) : null;
|
|
762
|
+
const items = accounts.map((account) => ({
|
|
763
|
+
id: account.uid,
|
|
764
|
+
label: account.uid === defaultUid ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [account.nickname, /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
765
|
+
className: AccountSwitcher_module_css_default.menuDefault,
|
|
766
|
+
children: t("defaultTag")
|
|
767
|
+
})] }) : account.nickname
|
|
768
|
+
}));
|
|
769
|
+
const size = RING_SIZE;
|
|
770
|
+
const stroke = RING_STROKE;
|
|
771
|
+
const r = (size - stroke) / 2;
|
|
772
|
+
const circumference = 2 * Math.PI * r;
|
|
773
|
+
const dash = pct === void 0 ? 0 : pct / 100 * circumference;
|
|
774
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
775
|
+
className: AccountSwitcher_module_css_default.root,
|
|
776
|
+
children: [prefs.showUsage && tip !== null && pct !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
|
|
777
|
+
label: tip,
|
|
778
|
+
side: "top",
|
|
779
|
+
delayMs: 300,
|
|
780
|
+
maxWidth: 320,
|
|
781
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
782
|
+
className: AccountSwitcher_module_css_default.ringAnchor,
|
|
783
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
|
|
784
|
+
width: size,
|
|
785
|
+
height: size,
|
|
786
|
+
viewBox: `0 0 ${size} ${size}`,
|
|
787
|
+
"aria-hidden": "true",
|
|
788
|
+
children: [
|
|
789
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
|
|
790
|
+
cx: size / 2,
|
|
791
|
+
cy: size / 2,
|
|
792
|
+
r,
|
|
793
|
+
fill: "none",
|
|
794
|
+
stroke: "var(--dsw-alias-border-l2)",
|
|
795
|
+
strokeWidth: stroke
|
|
796
|
+
}),
|
|
797
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
|
|
798
|
+
cx: size / 2,
|
|
799
|
+
cy: size / 2,
|
|
800
|
+
r,
|
|
801
|
+
fill: "none",
|
|
802
|
+
stroke: pct >= prefs.dangerPct ? "var(--dsw-alias-state-error-primary)" : "var(--dsw-alias-brand-primary)",
|
|
803
|
+
strokeWidth: stroke,
|
|
804
|
+
strokeLinecap: "round",
|
|
805
|
+
strokeDasharray: `${dash} ${circumference}`,
|
|
806
|
+
transform: `rotate(-90 ${size / 2} ${size / 2})`
|
|
807
|
+
}),
|
|
808
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("text", {
|
|
809
|
+
x: size / 2,
|
|
810
|
+
y: size / 2,
|
|
811
|
+
textAnchor: "middle",
|
|
812
|
+
dominantBaseline: "central",
|
|
813
|
+
fill: "var(--dsw-alias-label-primary)",
|
|
814
|
+
fontSize: RING_FONT_SIZE,
|
|
815
|
+
fontWeight: 600,
|
|
816
|
+
children: pct.toFixed(2)
|
|
817
|
+
})
|
|
818
|
+
]
|
|
819
|
+
})
|
|
820
|
+
})
|
|
821
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Menu, {
|
|
822
|
+
open: menuOpen,
|
|
823
|
+
portal: true,
|
|
824
|
+
side: "top",
|
|
825
|
+
align: "end",
|
|
826
|
+
dense: true,
|
|
827
|
+
selectedId: active.uid,
|
|
828
|
+
items,
|
|
829
|
+
onClose: () => {
|
|
830
|
+
setMenuOpen(false);
|
|
831
|
+
},
|
|
832
|
+
onSelect: (uid) => {
|
|
833
|
+
setMenuOpen(false);
|
|
834
|
+
if (uid === active.uid) return;
|
|
835
|
+
activate(uid).then((accepted) => {
|
|
836
|
+
if (!accepted) {
|
|
837
|
+
fetchAccounts().then((value) => {
|
|
838
|
+
if (value === null) return;
|
|
839
|
+
setAccounts(value.accounts);
|
|
840
|
+
setDefaultUid(value.defaultUid);
|
|
841
|
+
});
|
|
842
|
+
return;
|
|
843
|
+
}
|
|
844
|
+
setActiveUid(uid);
|
|
845
|
+
fetchUsage().then((value) => {
|
|
846
|
+
setUsage(value);
|
|
847
|
+
});
|
|
848
|
+
});
|
|
849
|
+
},
|
|
850
|
+
anchor: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
|
|
851
|
+
label,
|
|
852
|
+
side: "top",
|
|
853
|
+
delayMs: 300,
|
|
854
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
855
|
+
type: "button",
|
|
856
|
+
className: AccountSwitcher_module_css_default.trigger,
|
|
857
|
+
"aria-label": label,
|
|
858
|
+
"aria-haspopup": "menu",
|
|
859
|
+
"aria-expanded": menuOpen,
|
|
860
|
+
onClick: () => {
|
|
861
|
+
setMenuOpen((current) => !current);
|
|
862
|
+
},
|
|
863
|
+
children: abbreviate(active.nickname)
|
|
864
|
+
})
|
|
865
|
+
})
|
|
866
|
+
})]
|
|
867
|
+
});
|
|
868
|
+
}
|
|
869
|
+
//#endregion
|
|
870
|
+
//#region src/client/icons.tsx
|
|
871
|
+
/**
|
|
872
|
+
* CodeBuddy 徽标图形。
|
|
873
|
+
* @param props - 尺寸与 class。
|
|
874
|
+
* @returns 行内 SVG。
|
|
875
|
+
*/
|
|
876
|
+
function CodeBuddyLogo({ size = 16, className }) {
|
|
877
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
|
|
878
|
+
width: size,
|
|
879
|
+
height: size,
|
|
880
|
+
viewBox: "0 0 40 40",
|
|
881
|
+
fill: "none",
|
|
882
|
+
className,
|
|
883
|
+
"aria-hidden": "true",
|
|
884
|
+
children: [
|
|
885
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
|
|
886
|
+
x: "-0.00146484",
|
|
887
|
+
width: "40",
|
|
888
|
+
height: "40",
|
|
889
|
+
rx: "8.63158",
|
|
890
|
+
fill: "#6C4DFF"
|
|
891
|
+
}),
|
|
892
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
|
|
893
|
+
d: "M30.5918 3.12856C30.984 2.77679 31.0078 2.7632 31.2955 2.74593C31.7615 2.71193 32.1882 2.93586 32.9147 3.59728C34.6119 5.13959 36.9755 8.30995 38.4449 11.0177L39.0125 12.0691L39.8143 12.4677C40.5885 12.8589 41.8587 13.6611 42.389 14.0913C42.6286 14.2894 42.6626 14.2934 42.912 14.1964C44.0375 13.7583 45.6494 14.3393 47.0714 15.7033C48.3516 16.9303 49.5781 19.0269 50.0478 20.7767C50.1164 21.0582 50.2074 21.6636 50.2405 22.1144C50.3477 23.6973 49.84 24.9617 48.8624 25.5341C48.6628 25.6493 48.6492 25.6807 48.6548 26.1783C48.6998 28.5492 48.0606 30.9165 46.7768 33.2244C45.3276 35.8156 42.7467 38.496 39.2544 41.0214C37.3789 42.3862 32.9421 44.9717 30.9361 45.8792C26.1304 48.0428 22.278 48.8718 18.9316 48.4618C16.9356 48.22 14.6761 47.4417 13.3392 46.5373C12.9873 46.294 12.9318 46.2791 12.6629 46.3561C11.2318 46.7671 9.35752 45.9219 7.76528 44.1544C7.13027 43.448 6.10508 41.7136 5.77273 40.7853C5.00409 38.6128 5.15721 36.6516 6.18105 35.4808C6.44522 35.1797 6.4538 35.1667 6.39603 34.6598C6.30065 33.8298 6.25703 32.6017 6.30061 31.809L6.33535 31.0683L5.22371 29.1019C3.50212 26.0386 2.40857 23.4663 1.98661 21.501C1.76389 20.4233 1.77734 19.9446 2.05091 19.5908C2.21741 19.3773 2.76347 19.1568 3.42155 19.0352C5.07869 18.7442 8.69327 19.0065 12.7142 19.7165L13.1316 19.789L14.0497 18.977C15.5733 17.6274 16.5858 16.8705 18.4518 15.707C20.3967 14.4901 22.5922 13.4895 25.064 12.6968L25.8564 12.4423L26.2926 11.2974C27.8535 7.17701 29.452 4.13917 30.5918 3.12856ZM17.5169 24.2439C15.7528 25.2625 14.8705 25.7716 14.2223 26.3423C11.5975 28.6536 10.6172 32.3151 11.7346 35.6292C12.0106 36.4475 12.5193 37.3301 13.5378 39.0941C14.5563 40.8582 15.0662 41.7401 15.637 42.3882C17.9483 45.0128 21.6091 45.9938 24.923 44.8764C25.7414 44.6004 26.6233 44.0909 28.3875 43.0724L38.5362 37.213C40.3004 36.1945 41.1826 35.6854 41.8308 35.1147C44.4555 32.8034 45.4363 29.1426 44.319 25.8286C44.043 25.0103 43.5343 24.1277 42.5158 22.3637C41.4974 20.5997 40.9873 19.7177 40.4166 19.0696C38.1053 16.4448 34.4441 15.4631 31.1301 16.5806C30.3118 16.8565 29.4297 17.3661 27.6656 18.3846L17.5169 24.2439Z",
|
|
894
|
+
fill: "white"
|
|
895
|
+
}),
|
|
896
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
|
|
897
|
+
x: "18.4944",
|
|
898
|
+
y: "31.334",
|
|
899
|
+
width: "4.00904",
|
|
900
|
+
height: "8.32646",
|
|
901
|
+
rx: "2.00452",
|
|
902
|
+
transform: "rotate(-30 18.4944 31.334)",
|
|
903
|
+
fill: "white"
|
|
904
|
+
}),
|
|
905
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
|
|
906
|
+
x: "29.311",
|
|
907
|
+
y: "25.0898",
|
|
908
|
+
width: "4.00904",
|
|
909
|
+
height: "8.32646",
|
|
910
|
+
rx: "2.00452",
|
|
911
|
+
transform: "rotate(-30 29.311 25.0898)",
|
|
912
|
+
fill: "white"
|
|
913
|
+
})
|
|
914
|
+
]
|
|
915
|
+
});
|
|
916
|
+
}
|
|
917
|
+
//#endregion
|
|
918
|
+
//#region \0dsh-css:D:\data\code\deepseek-harness\packages\my\dsh-llm-codebuddy-power\src\client\ModelPanel.module.css.mjs
|
|
919
|
+
const css$2 = ".pAq4ya_root{flex:none;display:inline-flex;position:relative}.pAq4ya_trigger{min-width:calc(28px + var(--dsh-content-font-delta,0px));height:calc(28px + var(--dsh-content-font-delta,0px));color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:0;border-radius:6px;justify-content:center;align-items:center;gap:4px;padding:6px;display:inline-flex}.pAq4ya_rate{font-variant-numeric:tabular-nums;color:var(--dsw-alias-label-secondary);font-size:12px}.pAq4ya_trigger:hover{background:var(--dsw-alias-interactive-bg-hover)}.pAq4ya_trigger[data-inactive] svg{opacity:.36}.pAq4ya_trigger[data-inactive]:hover svg{opacity:.6}.pAq4ya_panel{z-index:1100;box-sizing:border-box;visibility:hidden;background:var(--dsw-specific-menu);--dsw-elevation-stroke-color:var(--dsw-alias-border-l1);width:min(360px,100vw - 24px);max-height:min(560px,70vh);box-shadow:var(--dsw-elevation-prominent);color:var(--dsw-alias-label-primary);border:0;border-radius:20px;flex-direction:column;gap:8px;padding:12px;font-size:13px;line-height:20px;display:flex;position:fixed}.pAq4ya_panel[data-positioned]{visibility:visible}.pAq4ya_header{border-bottom:.5px solid var(--dsw-alias-border-l2);align-items:baseline;gap:8px;padding-bottom:6px;display:flex}.pAq4ya_headerTitle{color:var(--dsw-alias-label-primary);font-weight:600}.pAq4ya_error{color:var(--dsw-alias-state-error-primary);font-size:12px}.pAq4ya_list{flex-direction:column;gap:2px;min-height:0;display:flex;overflow-y:auto}.pAq4ya_row{border-radius:8px;flex-direction:column;gap:2px;padding:6px 8px;display:flex}.pAq4ya_row:hover,.pAq4ya_rowSelected{background:var(--dsw-alias-interactive-bg-hover)}.pAq4ya_rowMain{text-align:left;width:100%;color:var(--dsw-alias-label-primary);cursor:pointer;font:inherit;background:0 0;border:0;flex-wrap:wrap;align-items:flex-start;gap:6px;padding:0;display:flex}.pAq4ya_rowName{white-space:nowrap;flex:none;font-weight:500}.pAq4ya_tag{border:.5px solid;border-radius:4px;flex:none;padding:0 5px;font-size:11px;line-height:16px}.pAq4ya_tagTier{color:var(--dsw-alias-label-secondary);border-color:var(--dsw-alias-border-l3)}.pAq4ya_rowTail{flex:none;align-items:baseline;gap:4px;margin-left:auto;display:inline-flex}.pAq4ya_credits{color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums;flex:none;font-size:12px}.pAq4ya_creditsStruck{color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums;flex:none;font-size:12px;text-decoration:line-through}.pAq4ya_discounted{color:var(--dsw-alias-brand-primary);font-variant-numeric:tabular-nums;flex:none;font-size:12px}.pAq4ya_check{color:var(--dsw-alias-label-primary);flex:0 0 16px;place-items:center;display:grid}.pAq4ya_empty{text-align:center;color:var(--dsw-alias-label-tertiary);padding:16px 0}.pAq4ya_tip{z-index:1099;box-sizing:border-box;overscroll-behavior:contain;background:var(--dsw-specific-menu);--dsw-elevation-stroke-color:var(--dsw-alias-border-l1);max-width:calc(100vw - 16px);max-height:min(360px,70vh);box-shadow:var(--dsw-elevation-prominent);color:var(--dsw-alias-label-primary);border:0;border-radius:14px;flex-direction:column;gap:6px;padding:10px 12px;font-size:12px;line-height:18px;display:flex;position:fixed;overflow-y:auto}.pAq4ya_tipName{color:var(--dsw-alias-label-primary);font-size:13px;font-weight:600}.pAq4ya_tipDesc{color:var(--dsw-alias-label-secondary)}.pAq4ya_tipFeatures,.pAq4ya_tipSpend,.pAq4ya_tipPromo{align-items:baseline;gap:8px;display:flex}.pAq4ya_tipLabel{color:var(--dsw-alias-label-tertiary);flex:none}.pAq4ya_tipVal{text-align:right;margin-left:auto}.pAq4ya_tipCreditsStruck{color:var(--dsw-alias-label-tertiary);text-decoration:line-through}.pAq4ya_tipDiscounted{color:var(--dsw-alias-brand-primary);margin-left:4px}.pAq4ya_tipTier{color:var(--dsw-alias-label-secondary)}.pAq4ya_tipDivider{background:var(--dsw-alias-border-l2);height:.5px}.pAq4ya_tipCaps{color:var(--dsw-alias-label-secondary);align-items:center;gap:8px;display:flex}.pAq4ya_tipInput{border:.5px solid var(--dsw-alias-border-l3);background:var(--dsw-alias-bg-layer-1);min-width:0;height:24px;color:var(--dsw-alias-label-primary);font:inherit;border-radius:5px;flex:1;padding:0 6px;font-size:12px}.pAq4ya_tipInput:focus-visible{outline:1.5px solid var(--dsw-alias-brand-primary);outline-offset:1px}.pAq4ya_tipEffort{border:.5px solid var(--dsw-alias-border-l3);background:var(--dsw-alias-bg-layer-1);max-width:100%;height:24px;color:var(--dsw-alias-label-primary);cursor:pointer;font:inherit;border-radius:5px;align-items:center;gap:4px;padding:0 6px 0 8px;font-size:12px;display:inline-flex}.pAq4ya_tipEffort:hover{background:var(--dsw-alias-interactive-bg-hover)}.pAq4ya_tipEffortMenu{min-width:0;margin-left:auto}.pAq4ya_tipEffortValue{white-space:nowrap;text-overflow:ellipsis;overflow:hidden}.pAq4ya_tipEffortChevron{color:var(--dsw-alias-label-tertiary);flex:none}.pAq4ya_tipValue{min-width:0;color:var(--dsw-alias-label-primary);font:inherit;flex:1;font-size:12px;line-height:20px}.pAq4ya_tipActions{justify-content:flex-end;gap:8px;display:flex}.pAq4ya_tipSave{background:var(--dsw-alias-button-primary-fill);height:28px;color:var(--dsw-alias-label-primary-foreground);cursor:pointer;font:inherit;border:0;border-radius:14px;padding:0 12px;font-size:12px;line-height:18px}.pAq4ya_tipSave:disabled{opacity:.4;cursor:not-allowed}.pAq4ya_tipSave:hover:not(:disabled){background:var(--dsw-alias-button-primary-hover)}.pAq4ya_tipAction{border:.5px solid var(--dsw-alias-border-l3);height:28px;color:var(--dsw-alias-label-primary);cursor:pointer;font:inherit;background:0 0;border-radius:14px;padding:0 12px;font-size:12px;line-height:18px}.pAq4ya_tipAction:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}.pAq4ya_tipLink{color:var(--dsw-alias-link);cursor:pointer;font:inherit;background:0 0;border:0;align-self:flex-start;padding:0;font-size:12px;line-height:18px}.pAq4ya_tipLink:hover{text-decoration:underline}.pAq4ya_tipLink:focus-visible{outline:1.5px solid var(--dsw-alias-brand-primary);outline-offset:2px;border-radius:3px}.pAq4ya_tipLinkChevron{margin-left:2px}@media (hover:none){.pAq4ya_row{padding:10px 8px}.pAq4ya_tipAction,.pAq4ya_tipSave{border-radius:18px;height:36px;padding:0 16px}.pAq4ya_tipLink{align-items:center;min-height:28px;display:inline-flex}.pAq4ya_tipInput{height:32px;font-size:14px}.pAq4ya_tipCaps{gap:10px}}";
|
|
920
|
+
const tagId$2 = "dsh-llm-codebuddy-power/ModelPanel.module.css";
|
|
921
|
+
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$2) + "]") === null) {
|
|
922
|
+
const tag = document.createElement("style");
|
|
923
|
+
tag.dataset.plugin = "dsh-llm-codebuddy-power";
|
|
924
|
+
tag.dataset.pluginCss = tagId$2;
|
|
925
|
+
tag.textContent = css$2;
|
|
926
|
+
document.head.appendChild(tag);
|
|
927
|
+
}
|
|
928
|
+
var ModelPanel_module_css_default = {
|
|
929
|
+
"check": "pAq4ya_check",
|
|
930
|
+
"credits": "pAq4ya_credits",
|
|
931
|
+
"creditsStruck": "pAq4ya_creditsStruck",
|
|
932
|
+
"discounted": "pAq4ya_discounted",
|
|
933
|
+
"empty": "pAq4ya_empty",
|
|
934
|
+
"error": "pAq4ya_error",
|
|
935
|
+
"header": "pAq4ya_header",
|
|
936
|
+
"headerTitle": "pAq4ya_headerTitle",
|
|
937
|
+
"list": "pAq4ya_list",
|
|
938
|
+
"panel": "pAq4ya_panel",
|
|
939
|
+
"rate": "pAq4ya_rate",
|
|
940
|
+
"root": "pAq4ya_root",
|
|
941
|
+
"row": "pAq4ya_row",
|
|
942
|
+
"rowMain": "pAq4ya_rowMain",
|
|
943
|
+
"rowName": "pAq4ya_rowName",
|
|
944
|
+
"rowSelected": "pAq4ya_rowSelected",
|
|
945
|
+
"rowTail": "pAq4ya_rowTail",
|
|
946
|
+
"tag": "pAq4ya_tag",
|
|
947
|
+
"tagTier": "pAq4ya_tagTier",
|
|
948
|
+
"tip": "pAq4ya_tip",
|
|
949
|
+
"tipAction": "pAq4ya_tipAction",
|
|
950
|
+
"tipActions": "pAq4ya_tipActions",
|
|
951
|
+
"tipCaps": "pAq4ya_tipCaps",
|
|
952
|
+
"tipCreditsStruck": "pAq4ya_tipCreditsStruck",
|
|
953
|
+
"tipDesc": "pAq4ya_tipDesc",
|
|
954
|
+
"tipDiscounted": "pAq4ya_tipDiscounted",
|
|
955
|
+
"tipDivider": "pAq4ya_tipDivider",
|
|
956
|
+
"tipEffort": "pAq4ya_tipEffort",
|
|
957
|
+
"tipEffortChevron": "pAq4ya_tipEffortChevron",
|
|
958
|
+
"tipEffortMenu": "pAq4ya_tipEffortMenu",
|
|
959
|
+
"tipEffortValue": "pAq4ya_tipEffortValue",
|
|
960
|
+
"tipFeatures": "pAq4ya_tipFeatures",
|
|
961
|
+
"tipInput": "pAq4ya_tipInput",
|
|
962
|
+
"tipLabel": "pAq4ya_tipLabel",
|
|
963
|
+
"tipLink": "pAq4ya_tipLink",
|
|
964
|
+
"tipLinkChevron": "pAq4ya_tipLinkChevron",
|
|
965
|
+
"tipName": "pAq4ya_tipName",
|
|
966
|
+
"tipPromo": "pAq4ya_tipPromo",
|
|
967
|
+
"tipSave": "pAq4ya_tipSave",
|
|
968
|
+
"tipSpend": "pAq4ya_tipSpend",
|
|
969
|
+
"tipTier": "pAq4ya_tipTier",
|
|
970
|
+
"tipVal": "pAq4ya_tipVal",
|
|
971
|
+
"tipValue": "pAq4ya_tipValue",
|
|
972
|
+
"trigger": "pAq4ya_trigger"
|
|
973
|
+
};
|
|
974
|
+
//#endregion
|
|
975
|
+
//#region src/client/ModelPanel.tsx
|
|
976
|
+
/**
|
|
977
|
+
* CodeBuddy 模型能力面板:官方模型选择器(conversation.input.right)旁边的
|
|
978
|
+
* 一个按钮,点击后打开仅包含 CodeBuddy 模型的列表,其中的模型带有促销/档位
|
|
979
|
+
* 信息。悬停某一行会显示提示框,内含模型信息,以及就地覆盖上下文窗口和最大
|
|
980
|
+
* 输出 token 数的输入项;两者都通过宿主覆盖存储按模型持久化,并传给
|
|
981
|
+
* resolveModel,由 dsh 依据它们管理上下文与输出上限。
|
|
982
|
+
*
|
|
983
|
+
* 与上游项目不同,本面板不会遮蔽官方的 conversation.input.model 座位——
|
|
984
|
+
* 选择仍然流经共享的 ModelDirectory,因此官方座位、/model 弹窗和本面板保持
|
|
985
|
+
* 同步。
|
|
986
|
+
*
|
|
987
|
+
* @module dsh-llm-codebuddy-power/client/ModelPanel
|
|
988
|
+
*/
|
|
989
|
+
/** "去升级" 操作打开的定价页。 */
|
|
990
|
+
const PRICING_URL = "https://www.codebuddy.cn/pricing/";
|
|
991
|
+
/** 提示框几何参数:宽度上限,以及用于翻转决策的估算高度。 */
|
|
992
|
+
const TIP_WIDTH = 280;
|
|
993
|
+
const TIP_EST_HEIGHT = 260;
|
|
994
|
+
/** 浮层边缘与视口边缘之间保留的间距。 */
|
|
995
|
+
const TIP_MARGIN = 8;
|
|
996
|
+
/**
|
|
997
|
+
* 触摸之后的一段时间窗口,在此期间合成的兼容性鼠标事件(`pointerenter`、
|
|
998
|
+
* `click`)会被忽略。触摸浏览器会在点按之后重放这些事件,没有这层防护时,
|
|
999
|
+
* 浮层会切回它的悬停来源,而点按产生的 click 还会选中该次点按本只想查看的
|
|
1000
|
+
* 那一行。
|
|
1001
|
+
*/
|
|
1002
|
+
const TOUCH_GHOST_MS = 700;
|
|
1003
|
+
/** 触发按钮与位于其上方的固定面板之间的间距。 */
|
|
1004
|
+
const PANEL_GAP = 8;
|
|
1005
|
+
/**
|
|
1006
|
+
* 思考等级下拉里「默认」项的哨兵 id。
|
|
1007
|
+
*
|
|
1008
|
+
* `Menu` 以 id 标记选中项,而"不指定等级"没有 id;用一个不可能与真实等级重名的
|
|
1009
|
+
* 取值占位,在回调里再翻译回 `undefined`。取值带前缀正是为了不会与真实 id 相撞。
|
|
1010
|
+
*/
|
|
1011
|
+
const EFFORT_DEFAULT_ID = "dsh-default-effort";
|
|
1012
|
+
/**
|
|
1013
|
+
* 目录数据的轮询间隔(毫秒)。促销时段以 `HH:MM` 表达,按分钟刷新则滞后不超过一个
|
|
1014
|
+
* 时段单位。目录本身由宿主按自己的缓存期持有(官方 IDE 的 8 分钟配置缓存,不改动),
|
|
1015
|
+
* 因此这些读取落在内存快照上,不触网。
|
|
1016
|
+
*/
|
|
1017
|
+
const RATE_REFRESH_MS = 6e4;
|
|
1018
|
+
/**
|
|
1019
|
+
* 触摸移动距离小于该像素值时才计为一次点按。列表行位于面板自身的滚动容器
|
|
1020
|
+
* 内,因此手指移动超过该距离就是在滚动列表,不得打开浮层。
|
|
1021
|
+
*/
|
|
1022
|
+
const TOUCH_TAP_SLOP = 10;
|
|
1023
|
+
/**
|
|
1024
|
+
* 判断一个矩形是否与视口有任何重叠区域。这是滚动可以关闭浮层的唯一理由:
|
|
1025
|
+
* 其余情况下滚动只重新定位,因为触发按钮通常完全不动(活动会话中输入区座位是
|
|
1026
|
+
* `position: sticky`),而关闭用户正在查看的浮层并不是无关窗格中的滚动所表达
|
|
1027
|
+
* 的意思。
|
|
1028
|
+
* @param rect - 待检测的已测量盒子。
|
|
1029
|
+
* @returns 盒子的至少一部分在屏幕上时为 true。
|
|
1030
|
+
*/
|
|
1031
|
+
function intersectsViewport(rect) {
|
|
1032
|
+
return rect.bottom > 0 && rect.top < window.innerHeight && rect.right > 0 && rect.left < window.innerWidth;
|
|
1033
|
+
}
|
|
1034
|
+
/**
|
|
1035
|
+
* 判断一个矩形是否与裁剪祖先的盒子有任何重叠区域。滚出面板列表的行仍在屏幕上,
|
|
1036
|
+
* 但已不再绘制,因此钉在该行上的浮层会指向空处。
|
|
1037
|
+
* @param rect - 待检测的已测量盒子。
|
|
1038
|
+
* @param bounds - 必须在视觉上包含该盒子的裁剪盒子。
|
|
1039
|
+
* @returns 盒子的一部分位于 bounds 内时为 true。
|
|
1040
|
+
*/
|
|
1041
|
+
function intersectsBounds(rect, bounds) {
|
|
1042
|
+
return rect.bottom > bounds.top && rect.top < bounds.bottom && rect.right > bounds.left && rect.left < bounds.right;
|
|
1043
|
+
}
|
|
1044
|
+
/**
|
|
1045
|
+
* 调整尺寸之后的一段时间窗口,在此期间滚动事件被视为重排的副作用,而不是
|
|
1046
|
+
* 用户滚动。调整尺寸会重排页面并可能移动滚动容器,若因此关闭面板,拖动窗口
|
|
1047
|
+
* 时面板就会消失。
|
|
1048
|
+
*/
|
|
1049
|
+
const RESIZE_SCROLL_GRACE_MS = 250;
|
|
1050
|
+
/** 面板的首选宽度;视口较窄时通过内联样式将其调低。 */
|
|
1051
|
+
const PANEL_WIDTH = 360;
|
|
1052
|
+
/** 首选高度上限;触发按钮周围的空间可能将其进一步调低。 */
|
|
1053
|
+
const PANEL_MAX_HEIGHT = 560;
|
|
1054
|
+
/** 面板边缘与视口边缘之间保留的间距。 */
|
|
1055
|
+
const PANEL_MARGIN = 12;
|
|
1056
|
+
/** 将值限制到 `[min, max]` 内,容忍区间反向(此时返回 min)。 */
|
|
1057
|
+
function clamp(value, min, max) {
|
|
1058
|
+
return Math.min(Math.max(value, min), Math.max(min, max));
|
|
1059
|
+
}
|
|
1060
|
+
/**
|
|
1061
|
+
* 一次关闭是否要把焦点还给触发按钮。
|
|
1062
|
+
*
|
|
1063
|
+
* 只有键盘触发才需要:焦点是用户用键盘移进面板的,面板卸载后不还给按钮就会
|
|
1064
|
+
* 落到 `body`,下一次 Tab 从页首重新开始。指针点击不需要,而且不能还 ——
|
|
1065
|
+
* `focus()` 会让挂在按钮上的 `Tooltip` 立刻弹出气泡(焦点与悬停是它两条独立
|
|
1066
|
+
* 的触发通道,见 `packages/client/ui-primitives/src/Tooltip.tsx`),用户刚用
|
|
1067
|
+
* 鼠标选完模型就会被一个说明气泡挡住。
|
|
1068
|
+
*
|
|
1069
|
+
* 键盘激活的 click 其 `detail` 为 0,指针点击至少为 1。用这个值而不是自己
|
|
1070
|
+
* 记录上一次的输入设备:那需要额外的全局监听器,还会与触摸合成的兼容鼠标
|
|
1071
|
+
* 事件纠缠,是本文件已有触摸防护要处理的同一类麻烦。
|
|
1072
|
+
* @param event - 触发选中或关闭的点击事件。
|
|
1073
|
+
* @returns 由键盘触发时为 true,此时关闭应当恢复焦点。
|
|
1074
|
+
*/
|
|
1075
|
+
function shouldRestoreFocus(event) {
|
|
1076
|
+
return event.detail === 0;
|
|
1077
|
+
}
|
|
1078
|
+
/**
|
|
1079
|
+
* 把一位小数写成不带多余零的形式(`32`、`393.2`、`1`)。
|
|
1080
|
+
*
|
|
1081
|
+
* 先取整到一位小数再判断是否为整数:判断原值会让 `1.0486` 先被判为非整数、再
|
|
1082
|
+
* 由 `toFixed(1)` 写成 `1.0`,正好留下本函数要消除的那个零。
|
|
1083
|
+
* @param value - 待写的数。
|
|
1084
|
+
* @returns 取整后为整数时无小数位,否则固定一位小数。
|
|
1085
|
+
*/
|
|
1086
|
+
function trimDecimal(value) {
|
|
1087
|
+
const rounded = Math.round(value * 10) / 10;
|
|
1088
|
+
return Number.isInteger(rounded) ? String(rounded) : rounded.toFixed(1);
|
|
1089
|
+
}
|
|
1090
|
+
/**
|
|
1091
|
+
* 紧凑地格式化 token 数量(`32k`、`393.2k`、`1M`)。
|
|
1092
|
+
*
|
|
1093
|
+
* 单位内保留一位小数,而不是只换算能整除的值:目录对同一字段给出 32000、
|
|
1094
|
+
* 64000 与 393216,只换算能整除的那个会让最后一项退回 `393,216`,于是同一列
|
|
1095
|
+
* 里出现两种记法。先按显示粒度取整再选单位,避免 `999950` 这类数得到 `1000k`
|
|
1096
|
+
* 与相邻的 `1M` 并存。
|
|
1097
|
+
* @param value - token 数量;缺省时无法表述。
|
|
1098
|
+
* @returns 带单位的紧凑写法,取不到值时为一个占位符。
|
|
1099
|
+
*/
|
|
1100
|
+
function formatTokens(value) {
|
|
1101
|
+
if (value === void 0) return "—";
|
|
1102
|
+
if (value < 1e3) return value.toLocaleString();
|
|
1103
|
+
const thousands = Math.round(value / 100) / 10;
|
|
1104
|
+
if (thousands < 1e3) return `${trimDecimal(thousands)}k`;
|
|
1105
|
+
return `${trimDecimal(thousands / 1e3)}M`;
|
|
1106
|
+
}
|
|
1107
|
+
/**
|
|
1108
|
+
* 输入框允许的字面量:空,或至少一位数字后跟可选的 `k`/`m` 单位后缀。
|
|
1109
|
+
*
|
|
1110
|
+
* 后缀在保存时换算(`5k` → 5000、`1m` → 1000000),因此打字途中必须先允许字母。
|
|
1111
|
+
* 要求后缀前至少有一位数字:否则光打一个 `k` 也会通过校验,而它换算后是 0 ——
|
|
1112
|
+
* 保存即等于清除覆盖,用户却以为自己输了点什么。负号与 `e`(指数记法)不接受:
|
|
1113
|
+
* 这两个上限都只有正数含义,而 `e` 还会让 `Number()` 接受 `1e9` 这类表面很短、
|
|
1114
|
+
* 实际越界的值。空串必须放行,它是"不设覆盖"的输入形式。
|
|
1115
|
+
*/
|
|
1116
|
+
const DRAFT_PATTERN = /^(?:\d+[kKmM]?)?$/;
|
|
1117
|
+
/** 换算草稿中的单位后缀;k/m 各为千倍与百万倍,不区分大小写。 */
|
|
1118
|
+
function draftMultiplier(text) {
|
|
1119
|
+
const suffix = text.slice(-1).toLowerCase();
|
|
1120
|
+
if (suffix === "k") return 1e3;
|
|
1121
|
+
if (suffix === "m") return 1e6;
|
|
1122
|
+
return 1;
|
|
1123
|
+
}
|
|
1124
|
+
/**
|
|
1125
|
+
* 从共享目录里取某个模型公布的思考等级。
|
|
1126
|
+
*
|
|
1127
|
+
* 只认 `codebuddy` 组:同名模型可能由别的 provider 提供,而推理等级由各自的适配器
|
|
1128
|
+
* 声明,混用会把一个提供方的等级显示给另一个。
|
|
1129
|
+
* @param directory - 共享目录快照。
|
|
1130
|
+
* @param modelId - 模型 id。
|
|
1131
|
+
* @returns 该模型的等级列表;未公布时为 undefined。
|
|
1132
|
+
*/
|
|
1133
|
+
function reasoningFor(directory, modelId) {
|
|
1134
|
+
for (const group of directory.groups) {
|
|
1135
|
+
if (group.id !== "codebuddy") continue;
|
|
1136
|
+
for (const model of group.models) if (model.id === modelId) return model.reasoning;
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
/**
|
|
1140
|
+
* 按当前生效值起草编辑框内容。
|
|
1141
|
+
*
|
|
1142
|
+
* 三个入口共用它(打开浮层、点「编辑」、点「取消」),因此每次进入编辑态都从
|
|
1143
|
+
* 该行此刻的生效值开始,而不是沿用上一次编辑留下的草稿。
|
|
1144
|
+
* @param row - 目录中的模型行(已含覆盖值)。
|
|
1145
|
+
* @param editing - 是否直接进入编辑态。
|
|
1146
|
+
* @returns 新的提示框编辑状态。
|
|
1147
|
+
*/
|
|
1148
|
+
function draftsFor(row, editing) {
|
|
1149
|
+
return {
|
|
1150
|
+
contextDraft: row.contextWindow === void 0 ? "" : String(row.contextWindow),
|
|
1151
|
+
outputDraft: row.maxOutputTokens === void 0 ? "" : String(row.maxOutputTokens),
|
|
1152
|
+
saving: false,
|
|
1153
|
+
editing
|
|
1154
|
+
};
|
|
1155
|
+
}
|
|
1156
|
+
/** 模型行的 locale 特性列表(图片输入 / 推理)。 */
|
|
1157
|
+
function featureLabels(row, t) {
|
|
1158
|
+
const features = [];
|
|
1159
|
+
if (row.supportsImages === true) features.push(t("modelPanelFeatureImage"));
|
|
1160
|
+
if (row.supportsReasoning === true) features.push(t("modelPanelFeatureReasoning"));
|
|
1161
|
+
return features.join("、");
|
|
1162
|
+
}
|
|
1163
|
+
/**
|
|
1164
|
+
* CodeBuddy 模型面板的触发按钮 + 弹出层。
|
|
1165
|
+
* @param props - 带注入 face 的插槽 props。
|
|
1166
|
+
* @returns 触发按钮、打开时的面板,以及行的提示框。
|
|
1167
|
+
*/
|
|
1168
|
+
function ModelPanel(props) {
|
|
1169
|
+
const { t, useDirectory } = props;
|
|
1170
|
+
const directory = useDirectory((value) => value);
|
|
1171
|
+
const [state, setState] = (0, react.useState)({
|
|
1172
|
+
open: false,
|
|
1173
|
+
error: null
|
|
1174
|
+
});
|
|
1175
|
+
const [models, setModels] = (0, react.useState)([]);
|
|
1176
|
+
const [tip, setTip] = (0, react.useState)(null);
|
|
1177
|
+
const [tipEdit, setTipEdit] = (0, react.useState)(null);
|
|
1178
|
+
const [effortOpen, setEffortOpen] = (0, react.useState)(false);
|
|
1179
|
+
/**
|
|
1180
|
+
* 固定面板的视口坐标,在首次绘制前应用。在这些坐标存在之前面板保持隐藏:
|
|
1181
|
+
* 没有坐标的固定元素会停在它的静态位置,于是会在打开到 layout effect 之间的
|
|
1182
|
+
* 那一帧闪现在错误的地方。
|
|
1183
|
+
*/
|
|
1184
|
+
const [panelStyle, setPanelStyle] = (0, react.useState)(void 0);
|
|
1185
|
+
const rootRef = (0, react.useRef)(null);
|
|
1186
|
+
const triggerRef = (0, react.useRef)(null);
|
|
1187
|
+
const panelRef = (0, react.useRef)(null);
|
|
1188
|
+
const listRef = (0, react.useRef)(null);
|
|
1189
|
+
const tipRef = (0, react.useRef)(null);
|
|
1190
|
+
const activeRow = (0, react.useRef)(null);
|
|
1191
|
+
const overTipRef = (0, react.useRef)(false);
|
|
1192
|
+
/** 最后一次触摸的时间戳,用于忽略其合成的鼠标事件。 */
|
|
1193
|
+
const lastTouchRef = (0, react.useRef)(0);
|
|
1194
|
+
/**
|
|
1195
|
+
* 当前触摸的起始位置,好让浮层在抬起(一次点按)时打开而不是在按下时打开:
|
|
1196
|
+
* 按下即打开会在手指仍按住时就显示出来,而一次转为列表滚动的按压也会把它
|
|
1197
|
+
* 打开。
|
|
1198
|
+
*/
|
|
1199
|
+
const touchOriginRef = (0, react.useRef)(null);
|
|
1200
|
+
/**
|
|
1201
|
+
* 视口上次变化的时间,以及当时的大小。它们放在 ref 里而不是关闭 effect 的
|
|
1202
|
+
* 闭包中,因为重新钉住气泡会重新渲染并重新运行该 effect,这会重置局部标志
|
|
1203
|
+
* 并取消它的定时器——从而让下一次重排滚动关闭浮层。
|
|
1204
|
+
*/
|
|
1205
|
+
const resizedAtRef = (0, react.useRef)(0);
|
|
1206
|
+
const viewportRef = (0, react.useRef)({
|
|
1207
|
+
width: 0,
|
|
1208
|
+
height: 0
|
|
1209
|
+
});
|
|
1210
|
+
const dismissTimer = (0, react.useRef)(void 0);
|
|
1211
|
+
/**
|
|
1212
|
+
* 浮层是否正处于编辑状态。
|
|
1213
|
+
*
|
|
1214
|
+
* 放在 ref 而不是关闭逻辑那个 effect 的闭包里:该 effect 依赖 `tip`,而重新
|
|
1215
|
+
* 钉住气泡会重建它,闭包里的值会被重置回上一次渲染时的旧值。编辑期间指针移出
|
|
1216
|
+
* 浮层不得关闭它(否则刚点开编辑、鼠标一偏就前功尽弃),而这条判断要一直读到
|
|
1217
|
+
* 最新状态,所以用 ref。
|
|
1218
|
+
*/
|
|
1219
|
+
const editingRef = (0, react.useRef)(false);
|
|
1220
|
+
editingRef.current = tipEdit?.editing ?? false;
|
|
1221
|
+
/**
|
|
1222
|
+
* 思考等级下拉是否展开。
|
|
1223
|
+
*
|
|
1224
|
+
* 与 `editingRef` 同因、同用途:指针移出浮层时不得关闭它,否则用户刚点开下拉、
|
|
1225
|
+
* 指针一移向候选项,承载它的浮层就先被收掉了。用 ref 而非闭包变量,理由同上。
|
|
1226
|
+
*/
|
|
1227
|
+
const effortOpenRef = (0, react.useRef)(false);
|
|
1228
|
+
effortOpenRef.current = effortOpen;
|
|
1229
|
+
const selected = directory.current;
|
|
1230
|
+
const currentModel = selected !== null && selected.provider === "codebuddy" ? selected.model : null;
|
|
1231
|
+
/**
|
|
1232
|
+
* 当前模型是否属于其他 provider。此时触发按钮会被调暗,以免被读作当前所用
|
|
1233
|
+
* 模型的控件;它仍可操作,因为本面板是用户回到 CodeBuddy 的途径。尚未解析出
|
|
1234
|
+
* 结果的目录(`null`)保持原样,而不是凭猜测调暗。
|
|
1235
|
+
*/
|
|
1236
|
+
const otherProviderSelected = selected !== null && selected.provider !== "codebuddy";
|
|
1237
|
+
/**
|
|
1238
|
+
* 触发按钮上显示的当前倍率:生效促销给了折后价时取折后价,否则取目录标价。与
|
|
1239
|
+
* 面板行同源 —— `replace` 形态本就不展示原价。该模型尚未出现在目录中(首次
|
|
1240
|
+
* 读取还没回来,或它不在 CodeBuddy 目录里)时没有可显示的值。
|
|
1241
|
+
*/
|
|
1242
|
+
const currentRow = currentModel === null ? void 0 : models.find((row) => row.id === currentModel);
|
|
1243
|
+
const discounted = currentRow?.promotion?.discountedCredits;
|
|
1244
|
+
const rate = discounted ?? currentRow?.credits ?? null;
|
|
1245
|
+
const rateColor = discounted === void 0 ? void 0 : currentRow?.promotion?.color;
|
|
1246
|
+
/**
|
|
1247
|
+
* 悬停提示与 `aria-label` 只显示模型名与倍率,不加修饰文字:这两个值用户已在
|
|
1248
|
+
* 按钮上见到,再包一层说明只会让人读两遍。`aria-label` 会覆盖按钮内的可见文本,
|
|
1249
|
+
* 读屏用户听不到倍率,因此两者共用同一份文案。模型不在目录里(首次读取未回或
|
|
1250
|
+
* 当前模型属于别的提供方)时没有可说的,退回到插槽的名称。
|
|
1251
|
+
*/
|
|
1252
|
+
const modelLabel = currentRow?.name ?? currentModel ?? void 0;
|
|
1253
|
+
const triggerLabel = modelLabel === void 0 ? t("modelPanelTrigger") : rate === null ? modelLabel : `${modelLabel} ${rate}`;
|
|
1254
|
+
const loadData = async () => {
|
|
1255
|
+
try {
|
|
1256
|
+
setModels(await props.fetchModels());
|
|
1257
|
+
} catch {
|
|
1258
|
+
setModels([]);
|
|
1259
|
+
}
|
|
1260
|
+
};
|
|
1261
|
+
(0, react.useEffect)(() => {
|
|
1262
|
+
if (currentModel === null && !state.open) return;
|
|
1263
|
+
const read = () => {
|
|
1264
|
+
loadData();
|
|
1265
|
+
};
|
|
1266
|
+
read();
|
|
1267
|
+
const timer = window.setInterval(read, RATE_REFRESH_MS);
|
|
1268
|
+
return () => {
|
|
1269
|
+
window.clearInterval(timer);
|
|
1270
|
+
};
|
|
1271
|
+
}, [currentModel, state.open]);
|
|
1272
|
+
/** 为某一行在列表旁显示提示框,通过翻转和限制使其保持在视口内。
|
|
1273
|
+
* 同时记录它的打开方式。 */
|
|
1274
|
+
const showTip = (row, anchor, source) => {
|
|
1275
|
+
if (dismissTimer.current !== void 0) {
|
|
1276
|
+
window.clearTimeout(dismissTimer.current);
|
|
1277
|
+
dismissTimer.current = void 0;
|
|
1278
|
+
}
|
|
1279
|
+
activeRow.current = anchor;
|
|
1280
|
+
const listRect = anchor.parentElement?.getBoundingClientRect();
|
|
1281
|
+
const rowRect = anchor.getBoundingClientRect();
|
|
1282
|
+
if (listRect === void 0) return;
|
|
1283
|
+
const margin = TIP_MARGIN;
|
|
1284
|
+
const width = Math.min(TIP_WIDTH, window.innerWidth - margin * 2);
|
|
1285
|
+
const setEdit = () => {
|
|
1286
|
+
setTipEdit(() => draftsFor(row, false));
|
|
1287
|
+
setEffortOpen(false);
|
|
1288
|
+
};
|
|
1289
|
+
if (!(listRect.right + margin + width <= window.innerWidth - margin || listRect.left - margin - width >= margin)) {
|
|
1290
|
+
setTip({
|
|
1291
|
+
row,
|
|
1292
|
+
left: clamp(rowRect.left, margin, window.innerWidth - width - margin),
|
|
1293
|
+
top: clamp(rowRect.bottom + margin, margin, window.innerHeight - TIP_EST_HEIGHT - margin),
|
|
1294
|
+
width,
|
|
1295
|
+
source
|
|
1296
|
+
});
|
|
1297
|
+
setEdit();
|
|
1298
|
+
return;
|
|
1299
|
+
}
|
|
1300
|
+
let left = listRect.right + margin;
|
|
1301
|
+
if (left + width > window.innerWidth - margin) left = listRect.left - width - margin;
|
|
1302
|
+
left = clamp(left, margin, window.innerWidth - width - margin);
|
|
1303
|
+
let top = rowRect.top;
|
|
1304
|
+
if (top + TIP_EST_HEIGHT > window.innerHeight - margin) top = rowRect.bottom - TIP_EST_HEIGHT;
|
|
1305
|
+
top = clamp(top, margin, window.innerHeight - TIP_EST_HEIGHT - margin);
|
|
1306
|
+
setTip({
|
|
1307
|
+
row,
|
|
1308
|
+
left,
|
|
1309
|
+
top,
|
|
1310
|
+
width,
|
|
1311
|
+
source
|
|
1312
|
+
});
|
|
1313
|
+
setEdit();
|
|
1314
|
+
};
|
|
1315
|
+
/** 隐藏提示框并清除它的归属。 */
|
|
1316
|
+
const dismissTip = () => {
|
|
1317
|
+
setTip(null);
|
|
1318
|
+
setTipEdit(null);
|
|
1319
|
+
setEffortOpen(false);
|
|
1320
|
+
activeRow.current = null;
|
|
1321
|
+
overTipRef.current = false;
|
|
1322
|
+
};
|
|
1323
|
+
(0, react.useEffect)(() => {
|
|
1324
|
+
if (tip === null) return;
|
|
1325
|
+
const isTouch = tip.source === "touch";
|
|
1326
|
+
const reflowing = () => Date.now() - resizedAtRef.current < RESIZE_SCROLL_GRACE_MS;
|
|
1327
|
+
const noteViewport = () => {
|
|
1328
|
+
viewportRef.current = {
|
|
1329
|
+
width: window.innerWidth,
|
|
1330
|
+
height: window.innerHeight
|
|
1331
|
+
};
|
|
1332
|
+
};
|
|
1333
|
+
noteViewport();
|
|
1334
|
+
const onResize = () => {
|
|
1335
|
+
resizedAtRef.current = Date.now();
|
|
1336
|
+
noteViewport();
|
|
1337
|
+
if (dismissTimer.current !== void 0) {
|
|
1338
|
+
window.clearTimeout(dismissTimer.current);
|
|
1339
|
+
dismissTimer.current = void 0;
|
|
1340
|
+
}
|
|
1341
|
+
pinTip();
|
|
1342
|
+
};
|
|
1343
|
+
const onScroll = (event) => {
|
|
1344
|
+
const target = event.target;
|
|
1345
|
+
if (target instanceof Node && tipRef.current?.contains(target) === true) return;
|
|
1346
|
+
const sizeChanged = window.innerWidth !== viewportRef.current.width || window.innerHeight !== viewportRef.current.height;
|
|
1347
|
+
if (sizeChanged) noteViewport();
|
|
1348
|
+
if (sizeChanged || reflowing()) {
|
|
1349
|
+
resizedAtRef.current = Date.now();
|
|
1350
|
+
pinTip();
|
|
1351
|
+
return;
|
|
1352
|
+
}
|
|
1353
|
+
const row = activeRow.current;
|
|
1354
|
+
const rowRect = row?.getBoundingClientRect();
|
|
1355
|
+
const listRect = row?.parentElement?.getBoundingClientRect();
|
|
1356
|
+
if (rowRect === void 0 || listRect === void 0 || !intersectsViewport(rowRect) || !intersectsBounds(rowRect, listRect)) {
|
|
1357
|
+
dismissTip();
|
|
1358
|
+
return;
|
|
1359
|
+
}
|
|
1360
|
+
pinTip();
|
|
1361
|
+
};
|
|
1362
|
+
const onMove = (event) => {
|
|
1363
|
+
if (reflowing()) return;
|
|
1364
|
+
const rowRect = activeRow.current?.getBoundingClientRect();
|
|
1365
|
+
const tipRect = tipRef.current?.getBoundingClientRect();
|
|
1366
|
+
const insideRow = rowRect !== void 0 && rowRect.left <= event.clientX && event.clientX <= rowRect.right && rowRect.top <= event.clientY && event.clientY <= rowRect.bottom;
|
|
1367
|
+
const insideTip = tipRect !== void 0 && tipRect.left <= event.clientX && event.clientX <= tipRect.right && tipRect.top <= event.clientY && event.clientY <= tipRect.bottom;
|
|
1368
|
+
overTipRef.current = insideTip;
|
|
1369
|
+
if (insideRow || insideTip) {
|
|
1370
|
+
if (dismissTimer.current !== void 0) {
|
|
1371
|
+
window.clearTimeout(dismissTimer.current);
|
|
1372
|
+
dismissTimer.current = void 0;
|
|
1373
|
+
}
|
|
1374
|
+
return;
|
|
1375
|
+
}
|
|
1376
|
+
if (editingRef.current || effortOpenRef.current) {
|
|
1377
|
+
if (dismissTimer.current !== void 0) {
|
|
1378
|
+
window.clearTimeout(dismissTimer.current);
|
|
1379
|
+
dismissTimer.current = void 0;
|
|
1380
|
+
}
|
|
1381
|
+
return;
|
|
1382
|
+
}
|
|
1383
|
+
if (dismissTimer.current === void 0) dismissTimer.current = window.setTimeout(() => {
|
|
1384
|
+
dismissTimer.current = void 0;
|
|
1385
|
+
dismissTip();
|
|
1386
|
+
}, 500);
|
|
1387
|
+
};
|
|
1388
|
+
window.addEventListener("resize", onResize);
|
|
1389
|
+
document.addEventListener("scroll", onScroll, true);
|
|
1390
|
+
if (!isTouch) document.addEventListener("mousemove", onMove);
|
|
1391
|
+
return () => {
|
|
1392
|
+
window.removeEventListener("resize", onResize);
|
|
1393
|
+
document.removeEventListener("scroll", onScroll, true);
|
|
1394
|
+
if (!isTouch) document.removeEventListener("mousemove", onMove);
|
|
1395
|
+
};
|
|
1396
|
+
}, [tip]);
|
|
1397
|
+
/**
|
|
1398
|
+
* 用实时 DOM 测量把已打开的浮层重新钉到它所在的行:按当前视口重新计算宽度
|
|
1399
|
+
* 并限制两个轴。用于渲染之后(第一遍是估算高度)和调整尺寸时(行移动了,
|
|
1400
|
+
* 视口可能更窄,因此旧的坐标和宽度已失效)。
|
|
1401
|
+
*/
|
|
1402
|
+
const pinTip = () => {
|
|
1403
|
+
if (tipRef.current === null || activeRow.current === null) return;
|
|
1404
|
+
const rowRect = activeRow.current.getBoundingClientRect();
|
|
1405
|
+
const bubble = tipRef.current.getBoundingClientRect();
|
|
1406
|
+
const listRect = activeRow.current.parentElement?.getBoundingClientRect();
|
|
1407
|
+
const margin = TIP_MARGIN;
|
|
1408
|
+
const width = Math.min(bubble.width > 0 ? bubble.width : TIP_WIDTH, window.innerWidth - margin * 2);
|
|
1409
|
+
const declaredWidth = Math.min(TIP_WIDTH, window.innerWidth - margin * 2);
|
|
1410
|
+
const besideFits = listRect !== void 0 && (listRect.right + margin + width <= window.innerWidth - margin || listRect.left - margin - width >= margin);
|
|
1411
|
+
let left;
|
|
1412
|
+
let top;
|
|
1413
|
+
if (listRect !== void 0 && besideFits) {
|
|
1414
|
+
left = listRect.right + margin;
|
|
1415
|
+
if (left + width > window.innerWidth - margin) left = listRect.left - width - margin;
|
|
1416
|
+
top = rowRect.top;
|
|
1417
|
+
if (top + bubble.height > window.innerHeight - margin) top = rowRect.bottom - bubble.height;
|
|
1418
|
+
} else {
|
|
1419
|
+
left = rowRect.left;
|
|
1420
|
+
top = rowRect.bottom + margin;
|
|
1421
|
+
if (top + bubble.height > window.innerHeight - margin) top = rowRect.top - bubble.height - margin;
|
|
1422
|
+
}
|
|
1423
|
+
left = clamp(left, margin, window.innerWidth - width - margin);
|
|
1424
|
+
top = clamp(top, margin, window.innerHeight - bubble.height - margin);
|
|
1425
|
+
setTip((prev) => prev === null || Math.abs(prev.left - left) < 1 && Math.abs(prev.top - top) < 1 && prev.width === declaredWidth ? prev : {
|
|
1426
|
+
...prev,
|
|
1427
|
+
left,
|
|
1428
|
+
top,
|
|
1429
|
+
width: declaredWidth
|
|
1430
|
+
});
|
|
1431
|
+
};
|
|
1432
|
+
(0, react.useLayoutEffect)(() => {
|
|
1433
|
+
if (tip === null) return;
|
|
1434
|
+
pinTip();
|
|
1435
|
+
}, [tip]);
|
|
1436
|
+
/**
|
|
1437
|
+
* 把固定面板贴着触发按钮放置,并完全位于视口内。
|
|
1438
|
+
*
|
|
1439
|
+
* 水平方向:右边缘与触发按钮对齐,然后限制整个跨度——只限制右边缘会在触发
|
|
1440
|
+
* 按钮靠近左边缘时把面板推出屏幕左侧。
|
|
1441
|
+
*
|
|
1442
|
+
* 垂直方向:从触发按钮向上打开,当上方空间容纳不下面板时改为向下打开——
|
|
1443
|
+
* 若把底边钉在靠近视口顶部的触发按钮上方并采用固定高度,顶边会落到屏幕
|
|
1444
|
+
* 之外。
|
|
1445
|
+
*/
|
|
1446
|
+
const positionPanel = () => {
|
|
1447
|
+
const rect = rootRef.current?.getBoundingClientRect();
|
|
1448
|
+
if (rect === void 0) return;
|
|
1449
|
+
const margin = PANEL_MARGIN;
|
|
1450
|
+
const width = Math.min(PANEL_WIDTH, window.innerWidth - margin * 2);
|
|
1451
|
+
const clampWidth = Math.max(width, panelRef.current?.offsetWidth ?? 0);
|
|
1452
|
+
const right = clamp(window.innerWidth - rect.right, margin, window.innerWidth - clampWidth - margin);
|
|
1453
|
+
const above = rect.top - PANEL_GAP - margin;
|
|
1454
|
+
const below = window.innerHeight - rect.bottom - PANEL_GAP - margin;
|
|
1455
|
+
const maxHeight = Math.min(PANEL_MAX_HEIGHT, window.innerHeight * .7, Math.max(above, below));
|
|
1456
|
+
setPanelStyle(above >= below ? {
|
|
1457
|
+
right,
|
|
1458
|
+
bottom: window.innerHeight - rect.top + PANEL_GAP,
|
|
1459
|
+
top: "auto",
|
|
1460
|
+
maxHeight,
|
|
1461
|
+
width
|
|
1462
|
+
} : {
|
|
1463
|
+
right,
|
|
1464
|
+
top: rect.bottom + PANEL_GAP,
|
|
1465
|
+
bottom: "auto",
|
|
1466
|
+
maxHeight,
|
|
1467
|
+
width
|
|
1468
|
+
});
|
|
1469
|
+
};
|
|
1470
|
+
const open = () => {
|
|
1471
|
+
setState((p) => ({
|
|
1472
|
+
...p,
|
|
1473
|
+
open: true,
|
|
1474
|
+
error: null
|
|
1475
|
+
}));
|
|
1476
|
+
props.load();
|
|
1477
|
+
};
|
|
1478
|
+
/**
|
|
1479
|
+
* 本次打开是否已经滚动过。effect 依赖 `models`,因为首次打开时列表可能还没到;
|
|
1480
|
+
* 但数据每次刷新都会重建 `models`,没有这道记录时,列表在打开期间会被反复拽回
|
|
1481
|
+
* 选中行那里。
|
|
1482
|
+
*/
|
|
1483
|
+
const scrolledForOpen = (0, react.useRef)(false);
|
|
1484
|
+
(0, react.useLayoutEffect)(() => {
|
|
1485
|
+
if (!state.open) {
|
|
1486
|
+
scrolledForOpen.current = false;
|
|
1487
|
+
return;
|
|
1488
|
+
}
|
|
1489
|
+
if (scrolledForOpen.current) return;
|
|
1490
|
+
const selected = listRef.current?.querySelector("[data-selected]") ?? null;
|
|
1491
|
+
if (selected === null) return;
|
|
1492
|
+
selected.scrollIntoView({ block: "nearest" });
|
|
1493
|
+
scrolledForOpen.current = true;
|
|
1494
|
+
}, [state.open, models]);
|
|
1495
|
+
(0, react.useLayoutEffect)(() => {
|
|
1496
|
+
if (!state.open) return;
|
|
1497
|
+
positionPanel();
|
|
1498
|
+
const noteViewport = () => {
|
|
1499
|
+
viewportRef.current = {
|
|
1500
|
+
width: window.innerWidth,
|
|
1501
|
+
height: window.innerHeight
|
|
1502
|
+
};
|
|
1503
|
+
};
|
|
1504
|
+
noteViewport();
|
|
1505
|
+
const reflowing = () => Date.now() - resizedAtRef.current < RESIZE_SCROLL_GRACE_MS;
|
|
1506
|
+
const onResize = () => {
|
|
1507
|
+
resizedAtRef.current = Date.now();
|
|
1508
|
+
noteViewport();
|
|
1509
|
+
positionPanel();
|
|
1510
|
+
};
|
|
1511
|
+
const onScroll = (event) => {
|
|
1512
|
+
const target = event.target;
|
|
1513
|
+
if (target instanceof Node && rootRef.current?.contains(target) === true) return;
|
|
1514
|
+
const sizeChanged = window.innerWidth !== viewportRef.current.width || window.innerHeight !== viewportRef.current.height;
|
|
1515
|
+
if (sizeChanged) noteViewport();
|
|
1516
|
+
if (sizeChanged || reflowing()) {
|
|
1517
|
+
resizedAtRef.current = Date.now();
|
|
1518
|
+
positionPanel();
|
|
1519
|
+
return;
|
|
1520
|
+
}
|
|
1521
|
+
const rect = rootRef.current?.getBoundingClientRect();
|
|
1522
|
+
if (rect !== void 0 && !intersectsViewport(rect)) {
|
|
1523
|
+
close();
|
|
1524
|
+
return;
|
|
1525
|
+
}
|
|
1526
|
+
positionPanel();
|
|
1527
|
+
};
|
|
1528
|
+
window.addEventListener("resize", onResize);
|
|
1529
|
+
document.addEventListener("scroll", onScroll, true);
|
|
1530
|
+
return () => {
|
|
1531
|
+
window.removeEventListener("resize", onResize);
|
|
1532
|
+
document.removeEventListener("scroll", onScroll, true);
|
|
1533
|
+
};
|
|
1534
|
+
}, [state.open]);
|
|
1535
|
+
const close = (restoreFocus = false) => {
|
|
1536
|
+
if (dismissTimer.current !== void 0) {
|
|
1537
|
+
window.clearTimeout(dismissTimer.current);
|
|
1538
|
+
dismissTimer.current = void 0;
|
|
1539
|
+
}
|
|
1540
|
+
setState((p) => ({
|
|
1541
|
+
...p,
|
|
1542
|
+
open: false
|
|
1543
|
+
}));
|
|
1544
|
+
setPanelStyle(void 0);
|
|
1545
|
+
setTip(null);
|
|
1546
|
+
setTipEdit(null);
|
|
1547
|
+
if (restoreFocus) queueMicrotask(() => {
|
|
1548
|
+
triggerRef.current?.focus();
|
|
1549
|
+
});
|
|
1550
|
+
};
|
|
1551
|
+
(0, react.useEffect)(() => {
|
|
1552
|
+
if (!state.open) return;
|
|
1553
|
+
const onPointerDown = (event) => {
|
|
1554
|
+
const target = event.target;
|
|
1555
|
+
if (rootRef.current?.contains(target) === true) return;
|
|
1556
|
+
if (tipRef.current?.contains(target) === true) return;
|
|
1557
|
+
if (effortOpenRef.current && target instanceof Element && target.closest("[role=\"menu\"]") !== null) return;
|
|
1558
|
+
setState((p) => ({
|
|
1559
|
+
...p,
|
|
1560
|
+
open: false
|
|
1561
|
+
}));
|
|
1562
|
+
dismissTip();
|
|
1563
|
+
};
|
|
1564
|
+
document.addEventListener("pointerdown", onPointerDown);
|
|
1565
|
+
return () => {
|
|
1566
|
+
document.removeEventListener("pointerdown", onPointerDown);
|
|
1567
|
+
};
|
|
1568
|
+
}, [state.open]);
|
|
1569
|
+
/** 提交一次模型选择("去使用"操作)。 */
|
|
1570
|
+
const choose = (row, restoreFocus = false) => {
|
|
1571
|
+
const selection = {
|
|
1572
|
+
provider: "codebuddy",
|
|
1573
|
+
model: row.id
|
|
1574
|
+
};
|
|
1575
|
+
if (currentModel === row.id) {
|
|
1576
|
+
close(restoreFocus);
|
|
1577
|
+
return;
|
|
1578
|
+
}
|
|
1579
|
+
props.select(selection).then((accepted) => {
|
|
1580
|
+
if (accepted) close(restoreFocus);
|
|
1581
|
+
else setState((p) => ({
|
|
1582
|
+
...p,
|
|
1583
|
+
error: t("modelPanelSelectError")
|
|
1584
|
+
}));
|
|
1585
|
+
});
|
|
1586
|
+
};
|
|
1587
|
+
/**
|
|
1588
|
+
* 执行一个活动或档位操作:`upgrade` 操作打开定价页,其他任何操作都选中该
|
|
1589
|
+
* 模型。操作是传入的而不是从行重新解析的,因为一行可能同时带有促销操作和
|
|
1590
|
+
* 升级档位,而每个按钮各自拥有自己的操作。
|
|
1591
|
+
*/
|
|
1592
|
+
const runAction = (row, action, restoreFocus = false) => {
|
|
1593
|
+
if (action?.type === "upgrade") {
|
|
1594
|
+
window.open(PRICING_URL, "_blank", "noopener");
|
|
1595
|
+
return;
|
|
1596
|
+
}
|
|
1597
|
+
choose(row, restoreFocus);
|
|
1598
|
+
};
|
|
1599
|
+
/**
|
|
1600
|
+
* 提交一次思考等级选择。
|
|
1601
|
+
*
|
|
1602
|
+
* 走与官方选择器**同一个** `ModelDirectory.select`:两条入口共用一份会话目录,
|
|
1603
|
+
* 因此在任一处切换,另一处随即显示同一结果。`undefined` 表示回到提供方默认 ——
|
|
1604
|
+
* 与官方那侧的「默认」项同义。
|
|
1605
|
+
* @param row - 该行模型。
|
|
1606
|
+
* @param effort - 选中的等级 id;undefined 表示提供方默认。
|
|
1607
|
+
*/
|
|
1608
|
+
const chooseEffort = (row, effort) => {
|
|
1609
|
+
const selection = {
|
|
1610
|
+
provider: "codebuddy",
|
|
1611
|
+
model: row.id,
|
|
1612
|
+
...effort === void 0 ? {} : { reasoningEffort: effort }
|
|
1613
|
+
};
|
|
1614
|
+
props.select(selection).then((accepted) => {
|
|
1615
|
+
if (accepted) setEffortOpen(false);
|
|
1616
|
+
else setState((p) => ({
|
|
1617
|
+
...p,
|
|
1618
|
+
error: t("modelPanelSelectError")
|
|
1619
|
+
}));
|
|
1620
|
+
});
|
|
1621
|
+
};
|
|
1622
|
+
/**
|
|
1623
|
+
* 把草稿文本换算为可保存的上限值。
|
|
1624
|
+
*
|
|
1625
|
+
* 空文本表示"不设覆盖",而不是 0:0 在宿主侧等同于清除(`setOverride` 把
|
|
1626
|
+
* 非正数当作清除),而回到目录值会让输入框看起来"清空后变成了模型最大值"。
|
|
1627
|
+
* 带 `k`/`m` 后缀的草稿先换算再加倍;超过上限钳到上限 —— 面板公布的就是
|
|
1628
|
+
* 可达的最大值。
|
|
1629
|
+
* @param text - 输入框草稿。
|
|
1630
|
+
* @param cap - 目录公布的上限;未公布时不设上限。
|
|
1631
|
+
* @returns 可保存的值;空文本时为 undefined。
|
|
1632
|
+
*/
|
|
1633
|
+
const clampDraft = (text, cap) => {
|
|
1634
|
+
const trimmed = text.trim();
|
|
1635
|
+
if (trimmed.length === 0) return void 0;
|
|
1636
|
+
const multiplier = draftMultiplier(trimmed);
|
|
1637
|
+
const body = multiplier === 1 ? trimmed : trimmed.slice(0, -1);
|
|
1638
|
+
const scaled = Number(body);
|
|
1639
|
+
if (!Number.isFinite(scaled)) return void 0;
|
|
1640
|
+
const value = Math.max(0, Math.floor(scaled * multiplier));
|
|
1641
|
+
return cap !== void 0 && value > cap ? cap : value;
|
|
1642
|
+
};
|
|
1643
|
+
/**
|
|
1644
|
+
* 输入过程中的即时规整。
|
|
1645
|
+
*
|
|
1646
|
+
* 合法且未越界的草稿原样保留 —— 若每次都规范化成换算后的数字,用户打的 `8k`
|
|
1647
|
+
* 会在按键之间就变成 `8000`,后缀根本留不住。只有越界时才落成上限数字,让
|
|
1648
|
+
* "超上限就是最大值"当场可见。
|
|
1649
|
+
* @param text - 输入框的当前取值。
|
|
1650
|
+
* @param cap - 目录公布的上限。
|
|
1651
|
+
* @returns 应当写回输入框的文本。
|
|
1652
|
+
*/
|
|
1653
|
+
const clampOnInput = (text, cap) => {
|
|
1654
|
+
const value = clampDraft(text, cap);
|
|
1655
|
+
if (value === void 0) return text;
|
|
1656
|
+
const scaled = clampDraft(text, void 0);
|
|
1657
|
+
return scaled !== void 0 && value < scaled ? String(value) : text;
|
|
1658
|
+
};
|
|
1659
|
+
/**
|
|
1660
|
+
* 保存提示框中按行的覆盖值草稿。
|
|
1661
|
+
* @param row - 覆盖值所属的模型行。
|
|
1662
|
+
* @param edit - 当前草稿。
|
|
1663
|
+
*/
|
|
1664
|
+
const saveTipEdit = async (row, edit) => {
|
|
1665
|
+
const contextWindow = clampDraft(edit.contextDraft, row.contextWindowCap);
|
|
1666
|
+
const maxOutputTokens = clampDraft(edit.outputDraft, row.maxOutputTokensCap);
|
|
1667
|
+
const next = {
|
|
1668
|
+
...contextWindow === void 0 ? {} : { contextWindow },
|
|
1669
|
+
...maxOutputTokens === void 0 ? {} : { maxOutputTokens }
|
|
1670
|
+
};
|
|
1671
|
+
setTipEdit({
|
|
1672
|
+
...edit,
|
|
1673
|
+
saving: true
|
|
1674
|
+
});
|
|
1675
|
+
await props.saveOverrides(row.id, next).then(() => {
|
|
1676
|
+
setTipEdit(draftsFor({
|
|
1677
|
+
...row,
|
|
1678
|
+
...contextWindow === void 0 ? {} : { contextWindow },
|
|
1679
|
+
...maxOutputTokens === void 0 ? {} : { maxOutputTokens }
|
|
1680
|
+
}, false));
|
|
1681
|
+
loadData();
|
|
1682
|
+
}, () => {
|
|
1683
|
+
setTipEdit({
|
|
1684
|
+
...edit,
|
|
1685
|
+
saving: false
|
|
1686
|
+
});
|
|
1687
|
+
});
|
|
1688
|
+
};
|
|
1689
|
+
/**
|
|
1690
|
+
* 清除该模型的覆盖值,回到目录公布的容量。
|
|
1691
|
+
*
|
|
1692
|
+
* 传空覆盖让宿主走它自己的清除分支(`setOverride` 收到非正数即清除该条目),
|
|
1693
|
+
* 而不是在这里把目录值当作显式覆盖写进去 —— 后者会永久冻结今天的目录数字。
|
|
1694
|
+
* @param row - 覆盖值所属的模型行。
|
|
1695
|
+
*/
|
|
1696
|
+
const restoreDefault = async (row) => {
|
|
1697
|
+
setTipEdit(tipEdit === null ? null : {
|
|
1698
|
+
...tipEdit,
|
|
1699
|
+
saving: true
|
|
1700
|
+
});
|
|
1701
|
+
await props.saveOverrides(row.id, {}).then(() => {
|
|
1702
|
+
setTipEdit(draftsFor(row, false));
|
|
1703
|
+
loadData();
|
|
1704
|
+
}, () => {
|
|
1705
|
+
setTipEdit(tipEdit === null ? null : {
|
|
1706
|
+
...tipEdit,
|
|
1707
|
+
saving: false
|
|
1708
|
+
});
|
|
1709
|
+
});
|
|
1710
|
+
};
|
|
1711
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1712
|
+
ref: rootRef,
|
|
1713
|
+
className: ModelPanel_module_css_default.root,
|
|
1714
|
+
children: [
|
|
1715
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
|
|
1716
|
+
label: triggerLabel,
|
|
1717
|
+
side: "top",
|
|
1718
|
+
delayMs: 500,
|
|
1719
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
1720
|
+
ref: triggerRef,
|
|
1721
|
+
type: "button",
|
|
1722
|
+
className: ModelPanel_module_css_default.trigger,
|
|
1723
|
+
"data-inactive": otherProviderSelected ? "" : void 0,
|
|
1724
|
+
"aria-label": triggerLabel,
|
|
1725
|
+
"aria-haspopup": "dialog",
|
|
1726
|
+
"aria-expanded": state.open,
|
|
1727
|
+
onClick: () => {
|
|
1728
|
+
if (state.open) close();
|
|
1729
|
+
else open();
|
|
1730
|
+
},
|
|
1731
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(CodeBuddyLogo, { size: 16 }), rate !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1732
|
+
className: ModelPanel_module_css_default.rate,
|
|
1733
|
+
style: rateColor === void 0 ? void 0 : { color: rateColor },
|
|
1734
|
+
children: rate
|
|
1735
|
+
})]
|
|
1736
|
+
})
|
|
1737
|
+
}),
|
|
1738
|
+
state.open && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1739
|
+
ref: panelRef,
|
|
1740
|
+
className: ModelPanel_module_css_default.panel,
|
|
1741
|
+
style: panelStyle,
|
|
1742
|
+
"data-positioned": panelStyle === void 0 ? void 0 : "",
|
|
1743
|
+
role: "dialog",
|
|
1744
|
+
"aria-label": t("modelPanelTitle"),
|
|
1745
|
+
children: [
|
|
1746
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1747
|
+
className: ModelPanel_module_css_default.header,
|
|
1748
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1749
|
+
className: ModelPanel_module_css_default.headerTitle,
|
|
1750
|
+
children: t("modelPanelTitle")
|
|
1751
|
+
})
|
|
1752
|
+
}),
|
|
1753
|
+
state.error !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1754
|
+
className: ModelPanel_module_css_default.error,
|
|
1755
|
+
role: "alert",
|
|
1756
|
+
children: state.error
|
|
1757
|
+
}),
|
|
1758
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1759
|
+
className: ModelPanel_module_css_default.list,
|
|
1760
|
+
ref: listRef,
|
|
1761
|
+
children: [models.map((row) => {
|
|
1762
|
+
const selected = currentModel === row.id;
|
|
1763
|
+
const promotion = row.promotion;
|
|
1764
|
+
const discounted = promotion?.discountedCredits;
|
|
1765
|
+
const strikeOriginal = discounted !== void 0 && promotion?.displayMode !== "replace";
|
|
1766
|
+
const hasCredits = row.credits !== void 0 && row.credits.length > 0 || discounted !== void 0;
|
|
1767
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1768
|
+
className: ModelPanel_module_css_default.row,
|
|
1769
|
+
"data-selected": selected || void 0,
|
|
1770
|
+
onPointerEnter: (event) => {
|
|
1771
|
+
if (event.pointerType !== "mouse") return;
|
|
1772
|
+
if (Date.now() - lastTouchRef.current < TOUCH_GHOST_MS) return;
|
|
1773
|
+
if (!overTipRef.current) showTip(row, event.currentTarget, "mouse");
|
|
1774
|
+
},
|
|
1775
|
+
onTouchStart: (event) => {
|
|
1776
|
+
lastTouchRef.current = Date.now();
|
|
1777
|
+
const touch = event.touches[0];
|
|
1778
|
+
touchOriginRef.current = touch === void 0 ? null : {
|
|
1779
|
+
x: touch.clientX,
|
|
1780
|
+
y: touch.clientY,
|
|
1781
|
+
row: event.currentTarget
|
|
1782
|
+
};
|
|
1783
|
+
},
|
|
1784
|
+
onTouchEnd: (event) => {
|
|
1785
|
+
const origin = touchOriginRef.current;
|
|
1786
|
+
touchOriginRef.current = null;
|
|
1787
|
+
if (origin === null || origin.row !== event.currentTarget) return;
|
|
1788
|
+
const touch = event.changedTouches[0];
|
|
1789
|
+
if (touch === void 0) return;
|
|
1790
|
+
if (Math.hypot(touch.clientX - origin.x, touch.clientY - origin.y) > TOUCH_TAP_SLOP) return;
|
|
1791
|
+
if (tip !== null && activeRow.current === origin.row) dismissTip();
|
|
1792
|
+
else showTip(row, origin.row, "touch");
|
|
1793
|
+
},
|
|
1794
|
+
onTouchCancel: () => {
|
|
1795
|
+
touchOriginRef.current = null;
|
|
1796
|
+
},
|
|
1797
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
1798
|
+
type: "button",
|
|
1799
|
+
className: `${ModelPanel_module_css_default.rowMain} ${selected ? ModelPanel_module_css_default.rowSelected : ""}`,
|
|
1800
|
+
onClick: (event) => {
|
|
1801
|
+
if (Date.now() - lastTouchRef.current < TOUCH_GHOST_MS) return;
|
|
1802
|
+
choose(row, shouldRestoreFocus(event));
|
|
1803
|
+
},
|
|
1804
|
+
children: [
|
|
1805
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1806
|
+
className: ModelPanel_module_css_default.rowName,
|
|
1807
|
+
children: row.name
|
|
1808
|
+
}),
|
|
1809
|
+
promotion !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1810
|
+
className: ModelPanel_module_css_default.tag,
|
|
1811
|
+
style: {
|
|
1812
|
+
color: promotion.color,
|
|
1813
|
+
borderColor: promotion.color
|
|
1814
|
+
},
|
|
1815
|
+
children: promotion.label
|
|
1816
|
+
}),
|
|
1817
|
+
row.tier !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1818
|
+
className: `${ModelPanel_module_css_default.tag} ${ModelPanel_module_css_default.tagTier}`,
|
|
1819
|
+
children: row.tier.label
|
|
1820
|
+
}),
|
|
1821
|
+
(selected || hasCredits) && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1822
|
+
className: ModelPanel_module_css_default.rowTail,
|
|
1823
|
+
children: [
|
|
1824
|
+
selected && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1825
|
+
className: ModelPanel_module_css_default.check,
|
|
1826
|
+
"aria-hidden": "true",
|
|
1827
|
+
children: "✓"
|
|
1828
|
+
}),
|
|
1829
|
+
hasCredits && discounted === void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1830
|
+
className: ModelPanel_module_css_default.credits,
|
|
1831
|
+
children: row.credits
|
|
1832
|
+
}),
|
|
1833
|
+
hasCredits && strikeOriginal && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1834
|
+
className: ModelPanel_module_css_default.creditsStruck,
|
|
1835
|
+
children: row.credits
|
|
1836
|
+
}),
|
|
1837
|
+
discounted !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1838
|
+
className: ModelPanel_module_css_default.discounted,
|
|
1839
|
+
style: promotion?.color === void 0 ? void 0 : { color: promotion.color },
|
|
1840
|
+
children: discounted
|
|
1841
|
+
})
|
|
1842
|
+
]
|
|
1843
|
+
})
|
|
1844
|
+
]
|
|
1845
|
+
})
|
|
1846
|
+
}, row.id);
|
|
1847
|
+
}), models.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1848
|
+
className: ModelPanel_module_css_default.empty,
|
|
1849
|
+
children: t("modelPanelEmpty")
|
|
1850
|
+
})]
|
|
1851
|
+
})
|
|
1852
|
+
]
|
|
1853
|
+
}),
|
|
1854
|
+
tip !== null && tipEdit !== null && (() => {
|
|
1855
|
+
const row = models.find((candidate) => candidate.id === tip.row.id) ?? tip.row;
|
|
1856
|
+
const zh = t("modelPanelTitle") === "CodeBuddy 模型";
|
|
1857
|
+
const efforts = reasoningFor(directory, row.id);
|
|
1858
|
+
const selectedEffort = selected !== null && selected.provider === "codebuddy" && selected.model === row.id ? selected.reasoningEffort ?? efforts?.defaultEffort : efforts?.defaultEffort;
|
|
1859
|
+
const effortLabel = efforts === void 0 ? void 0 : selectedEffort === void 0 ? t("modelPanelEffortDefault") : efforts.efforts.find((effort) => effort.id === selectedEffort)?.name ?? selectedEffort;
|
|
1860
|
+
const effortItems = efforts === void 0 ? [] : [...efforts.defaultEffort === void 0 ? [{
|
|
1861
|
+
id: EFFORT_DEFAULT_ID,
|
|
1862
|
+
label: t("modelPanelEffortDefault")
|
|
1863
|
+
}] : [], ...efforts.efforts.map((effort) => ({
|
|
1864
|
+
id: effort.id,
|
|
1865
|
+
label: effort.name
|
|
1866
|
+
}))];
|
|
1867
|
+
const description = zh ? row.descriptionZh ?? row.descriptionEn : row.descriptionEn ?? row.descriptionZh;
|
|
1868
|
+
const promotionText = zh ? row.promotion?.textZh ?? row.promotion?.textEn : row.promotion?.textEn ?? row.promotion?.textZh;
|
|
1869
|
+
const tierText = zh ? row.tier?.textZh ?? row.tier?.textEn : row.tier?.textEn ?? row.tier?.textZh;
|
|
1870
|
+
const tierAction = row.tier?.action;
|
|
1871
|
+
const promotionAction = row.promotion?.action;
|
|
1872
|
+
const hasMidSection = row.credits !== void 0 && row.credits.length > 0 || promotionText !== void 0 || promotionAction !== void 0;
|
|
1873
|
+
const tipDiscounted = row.promotion?.discountedCredits;
|
|
1874
|
+
const tipStrikeOriginal = tipDiscounted !== void 0 && row.promotion?.displayMode !== "replace";
|
|
1875
|
+
return (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1876
|
+
ref: tipRef,
|
|
1877
|
+
className: ModelPanel_module_css_default.tip,
|
|
1878
|
+
role: "tooltip",
|
|
1879
|
+
style: {
|
|
1880
|
+
left: tip.left,
|
|
1881
|
+
top: tip.top,
|
|
1882
|
+
width: tip.width
|
|
1883
|
+
},
|
|
1884
|
+
children: [
|
|
1885
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1886
|
+
className: ModelPanel_module_css_default.tipName,
|
|
1887
|
+
children: row.name
|
|
1888
|
+
}),
|
|
1889
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1890
|
+
className: ModelPanel_module_css_default.tipDesc,
|
|
1891
|
+
children: description ?? t("modelPanelDescriptionMissing")
|
|
1892
|
+
}),
|
|
1893
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1894
|
+
className: ModelPanel_module_css_default.tipFeatures,
|
|
1895
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1896
|
+
className: ModelPanel_module_css_default.tipLabel,
|
|
1897
|
+
children: t("modelPanelFeatures")
|
|
1898
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1899
|
+
className: ModelPanel_module_css_default.tipVal,
|
|
1900
|
+
children: featureLabels(row, t)
|
|
1901
|
+
})]
|
|
1902
|
+
}),
|
|
1903
|
+
tierText !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1904
|
+
className: ModelPanel_module_css_default.tipTier,
|
|
1905
|
+
children: tierText
|
|
1906
|
+
}),
|
|
1907
|
+
tierAction !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
1908
|
+
type: "button",
|
|
1909
|
+
className: ModelPanel_module_css_default.tipLink,
|
|
1910
|
+
onClick: (event) => {
|
|
1911
|
+
runAction(row, tierAction, shouldRestoreFocus(event));
|
|
1912
|
+
},
|
|
1913
|
+
children: [t("modelPanelUpgrade"), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1914
|
+
className: ModelPanel_module_css_default.tipLinkChevron,
|
|
1915
|
+
"aria-hidden": "true",
|
|
1916
|
+
children: "›"
|
|
1917
|
+
})]
|
|
1918
|
+
}),
|
|
1919
|
+
hasMidSection && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: ModelPanel_module_css_default.tipDivider }),
|
|
1920
|
+
row.credits !== void 0 && row.credits.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1921
|
+
className: ModelPanel_module_css_default.tipSpend,
|
|
1922
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1923
|
+
className: ModelPanel_module_css_default.tipLabel,
|
|
1924
|
+
children: t("modelPanelSpendRate")
|
|
1925
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1926
|
+
className: ModelPanel_module_css_default.tipVal,
|
|
1927
|
+
children: [
|
|
1928
|
+
tipDiscounted === void 0 && row.credits,
|
|
1929
|
+
tipStrikeOriginal && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1930
|
+
className: ModelPanel_module_css_default.tipCreditsStruck,
|
|
1931
|
+
children: row.credits
|
|
1932
|
+
}),
|
|
1933
|
+
tipDiscounted !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1934
|
+
className: ModelPanel_module_css_default.tipDiscounted,
|
|
1935
|
+
style: row.promotion?.color === void 0 ? void 0 : { color: row.promotion.color },
|
|
1936
|
+
children: tipDiscounted
|
|
1937
|
+
})
|
|
1938
|
+
]
|
|
1939
|
+
})]
|
|
1940
|
+
}),
|
|
1941
|
+
promotionText !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1942
|
+
className: ModelPanel_module_css_default.tipPromo,
|
|
1943
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: promotionText })
|
|
1944
|
+
}),
|
|
1945
|
+
(promotionAction !== void 0 || tip.source === "touch") && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1946
|
+
type: "button",
|
|
1947
|
+
className: ModelPanel_module_css_default.tipAction,
|
|
1948
|
+
onClick: (event) => {
|
|
1949
|
+
runAction(row, promotionAction, shouldRestoreFocus(event));
|
|
1950
|
+
},
|
|
1951
|
+
children: t("modelPanelUse")
|
|
1952
|
+
}),
|
|
1953
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: ModelPanel_module_css_default.tipDivider }),
|
|
1954
|
+
efforts !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1955
|
+
className: ModelPanel_module_css_default.tipCaps,
|
|
1956
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1957
|
+
className: ModelPanel_module_css_default.tipLabel,
|
|
1958
|
+
children: t("modelPanelEffort")
|
|
1959
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Menu, {
|
|
1960
|
+
open: effortOpen,
|
|
1961
|
+
portal: true,
|
|
1962
|
+
dense: true,
|
|
1963
|
+
side: "bottom",
|
|
1964
|
+
align: "end",
|
|
1965
|
+
selectedId: selectedEffort === void 0 ? EFFORT_DEFAULT_ID : selectedEffort,
|
|
1966
|
+
items: effortItems,
|
|
1967
|
+
onSelect: (id) => {
|
|
1968
|
+
chooseEffort(row, id === EFFORT_DEFAULT_ID ? void 0 : id);
|
|
1969
|
+
},
|
|
1970
|
+
onClose: () => {
|
|
1971
|
+
setEffortOpen(false);
|
|
1972
|
+
},
|
|
1973
|
+
className: ModelPanel_module_css_default.tipEffortMenu,
|
|
1974
|
+
anchor: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
1975
|
+
type: "button",
|
|
1976
|
+
className: ModelPanel_module_css_default.tipEffort,
|
|
1977
|
+
"aria-haspopup": "menu",
|
|
1978
|
+
"aria-expanded": effortOpen,
|
|
1979
|
+
onClick: () => {
|
|
1980
|
+
setEffortOpen((open) => !open);
|
|
1981
|
+
},
|
|
1982
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1983
|
+
className: ModelPanel_module_css_default.tipEffortValue,
|
|
1984
|
+
children: effortLabel
|
|
1985
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1986
|
+
className: ModelPanel_module_css_default.tipEffortChevron,
|
|
1987
|
+
"aria-hidden": true,
|
|
1988
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutline14, {})
|
|
1989
|
+
})]
|
|
1990
|
+
})
|
|
1991
|
+
})]
|
|
1992
|
+
}),
|
|
1993
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1994
|
+
className: ModelPanel_module_css_default.tipCaps,
|
|
1995
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1996
|
+
className: ModelPanel_module_css_default.tipLabel,
|
|
1997
|
+
children: t("modelPanelContextWindow")
|
|
1998
|
+
}), tipEdit.editing ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1999
|
+
type: "text",
|
|
2000
|
+
inputMode: "text",
|
|
2001
|
+
className: `${ModelPanel_module_css_default.tipInput} ${ModelPanel_module_css_default.tipVal}`,
|
|
2002
|
+
autoFocus: true,
|
|
2003
|
+
value: tipEdit.contextDraft,
|
|
2004
|
+
onChange: (event) => {
|
|
2005
|
+
const next = event.currentTarget.value;
|
|
2006
|
+
if (!DRAFT_PATTERN.test(next)) return;
|
|
2007
|
+
setTipEdit({
|
|
2008
|
+
...tipEdit,
|
|
2009
|
+
contextDraft: clampOnInput(next, row.contextWindowCap)
|
|
2010
|
+
});
|
|
2011
|
+
},
|
|
2012
|
+
onKeyDown: (event) => {
|
|
2013
|
+
if (event.key !== "Enter") return;
|
|
2014
|
+
event.preventDefault();
|
|
2015
|
+
saveTipEdit(row, tipEdit);
|
|
2016
|
+
}
|
|
2017
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2018
|
+
className: `${ModelPanel_module_css_default.tipValue} ${ModelPanel_module_css_default.tipVal}`,
|
|
2019
|
+
children: formatTokens(row.contextWindow)
|
|
2020
|
+
})]
|
|
2021
|
+
}),
|
|
2022
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2023
|
+
className: ModelPanel_module_css_default.tipCaps,
|
|
2024
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2025
|
+
className: ModelPanel_module_css_default.tipLabel,
|
|
2026
|
+
children: t("modelPanelMaxOutput")
|
|
2027
|
+
}), tipEdit.editing ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
2028
|
+
type: "text",
|
|
2029
|
+
inputMode: "text",
|
|
2030
|
+
className: `${ModelPanel_module_css_default.tipInput} ${ModelPanel_module_css_default.tipVal}`,
|
|
2031
|
+
value: tipEdit.outputDraft,
|
|
2032
|
+
onChange: (event) => {
|
|
2033
|
+
const next = event.currentTarget.value;
|
|
2034
|
+
if (!DRAFT_PATTERN.test(next)) return;
|
|
2035
|
+
setTipEdit({
|
|
2036
|
+
...tipEdit,
|
|
2037
|
+
outputDraft: clampOnInput(next, row.maxOutputTokensCap)
|
|
2038
|
+
});
|
|
2039
|
+
},
|
|
2040
|
+
onKeyDown: (event) => {
|
|
2041
|
+
if (event.key !== "Enter") return;
|
|
2042
|
+
event.preventDefault();
|
|
2043
|
+
saveTipEdit(row, tipEdit);
|
|
2044
|
+
}
|
|
2045
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2046
|
+
className: `${ModelPanel_module_css_default.tipValue} ${ModelPanel_module_css_default.tipVal}`,
|
|
2047
|
+
children: formatTokens(row.maxOutputTokens)
|
|
2048
|
+
})]
|
|
2049
|
+
}),
|
|
2050
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2051
|
+
className: ModelPanel_module_css_default.tipActions,
|
|
2052
|
+
children: [
|
|
2053
|
+
tipEdit.editing && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2054
|
+
type: "button",
|
|
2055
|
+
className: ModelPanel_module_css_default.tipAction,
|
|
2056
|
+
disabled: tipEdit.saving,
|
|
2057
|
+
onClick: () => {
|
|
2058
|
+
restoreDefault(row);
|
|
2059
|
+
},
|
|
2060
|
+
children: t("modelPanelRestoreDefault")
|
|
2061
|
+
}),
|
|
2062
|
+
tipEdit.editing && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2063
|
+
type: "button",
|
|
2064
|
+
className: ModelPanel_module_css_default.tipAction,
|
|
2065
|
+
disabled: tipEdit.saving,
|
|
2066
|
+
onClick: () => {
|
|
2067
|
+
setTipEdit(draftsFor(row, false));
|
|
2068
|
+
},
|
|
2069
|
+
children: t("cancel")
|
|
2070
|
+
}),
|
|
2071
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2072
|
+
type: "button",
|
|
2073
|
+
className: ModelPanel_module_css_default.tipSave,
|
|
2074
|
+
disabled: tipEdit.saving,
|
|
2075
|
+
onClick: () => {
|
|
2076
|
+
if (tipEdit.editing) saveTipEdit(row, tipEdit);
|
|
2077
|
+
else setTipEdit(draftsFor(row, true));
|
|
2078
|
+
},
|
|
2079
|
+
children: tipEdit.editing ? tipEdit.saving ? t("modelPanelSaving") : t("modelPanelSave") : t("modelPanelEdit")
|
|
2080
|
+
})
|
|
2081
|
+
]
|
|
2082
|
+
})
|
|
2083
|
+
]
|
|
2084
|
+
}), document.body);
|
|
2085
|
+
})()
|
|
2086
|
+
]
|
|
2087
|
+
});
|
|
2088
|
+
}
|
|
2089
|
+
//#endregion
|
|
2090
|
+
//#region \0dsh-css:D:\data\code\deepseek-harness\packages\my\dsh-llm-codebuddy-power\src\client\ComposerControls.module.css.mjs
|
|
2091
|
+
const css$1 = ".YRyTOa_root{flex:none;align-items:center;gap:4px;display:inline-flex}";
|
|
2092
|
+
const tagId$1 = "dsh-llm-codebuddy-power/ComposerControls.module.css";
|
|
2093
|
+
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$1) + "]") === null) {
|
|
2094
|
+
const tag = document.createElement("style");
|
|
2095
|
+
tag.dataset.plugin = "dsh-llm-codebuddy-power";
|
|
2096
|
+
tag.dataset.pluginCss = tagId$1;
|
|
2097
|
+
tag.textContent = css$1;
|
|
2098
|
+
document.head.appendChild(tag);
|
|
2099
|
+
}
|
|
2100
|
+
var ComposerControls_module_css_default = { "root": "YRyTOa_root" };
|
|
2101
|
+
//#endregion
|
|
2102
|
+
//#region src/client/ComposerControls.tsx
|
|
2103
|
+
/**
|
|
2104
|
+
* 把额度圆环、账号切换按钮与模型面板拼进同一个容器。
|
|
2105
|
+
*
|
|
2106
|
+
* 两个子控件各自接受 runtime 份额与自己的注入成员,因此整份 props 原样透传即可:
|
|
2107
|
+
* 它们看到的正是自己声明的那部分。
|
|
2108
|
+
* @param props - 组合后的 slot props。
|
|
2109
|
+
* @returns 控件组元素。
|
|
2110
|
+
*/
|
|
2111
|
+
function ComposerControls(props) {
|
|
2112
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2113
|
+
className: ComposerControls_module_css_default.root,
|
|
2114
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(AccountSwitcher, { ...props }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelPanel, { ...props })]
|
|
2115
|
+
});
|
|
2116
|
+
}
|
|
2117
|
+
//#endregion
|
|
2118
|
+
//#region \0dsh-css:D:\data\code\deepseek-harness\packages\my\dsh-llm-codebuddy-power\src\client\DrawToolCard.module.css.mjs
|
|
2119
|
+
const css = ".pJa0sG_card{flex-direction:column;gap:6px;padding:8px 0;display:flex}.pJa0sG_imageButton{cursor:zoom-in;background:0 0;border:0;border-radius:8px;width:fit-content;max-width:100%;padding:0;display:block}.pJa0sG_imageButton:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:2px}.pJa0sG_image{object-fit:contain;border-radius:8px;max-width:min(480px,100%);max-height:480px;display:block}.pJa0sG_label{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:1.4}.pJa0sG_previewDialog{width:min(1200px,100vw - 48px);max-height:calc(100vh - 48px)}.pJa0sG_previewContent{overflow:auto}.pJa0sG_previewImage{object-fit:contain;border-radius:12px;max-width:100%;max-height:calc(100vh - 180px);margin:0 auto;display:block}";
|
|
2120
|
+
const tagId = "dsh-llm-codebuddy-power/DrawToolCard.module.css";
|
|
2121
|
+
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
|
|
2122
|
+
const tag = document.createElement("style");
|
|
2123
|
+
tag.dataset.plugin = "dsh-llm-codebuddy-power";
|
|
2124
|
+
tag.dataset.pluginCss = tagId;
|
|
2125
|
+
tag.textContent = css;
|
|
2126
|
+
document.head.appendChild(tag);
|
|
2127
|
+
}
|
|
2128
|
+
var DrawToolCard_module_css_default = {
|
|
2129
|
+
"card": "pJa0sG_card",
|
|
2130
|
+
"image": "pJa0sG_image",
|
|
2131
|
+
"imageButton": "pJa0sG_imageButton",
|
|
2132
|
+
"label": "pJa0sG_label",
|
|
2133
|
+
"previewContent": "pJa0sG_previewContent",
|
|
2134
|
+
"previewDialog": "pJa0sG_previewDialog",
|
|
2135
|
+
"previewImage": "pJa0sG_previewImage"
|
|
2136
|
+
};
|
|
2137
|
+
//#endregion
|
|
2138
|
+
//#region src/client/DrawToolCard.tsx
|
|
2139
|
+
/**
|
|
2140
|
+
* codebuddy-draw 工具的浏览器卡片:按已结算工具结果 content 里的标准
|
|
2141
|
+
* ImageBlock,经系统附件通道(loadImage → uiConversation.imageUrl →
|
|
2142
|
+
* session.readAttachment)读取图片并在对话中内联展示,图下方标注提示词
|
|
2143
|
+
* 与实际扣分。重启后图片仍可读(附件持久化于磁盘,读经 host 鉴权通道)。
|
|
2144
|
+
*
|
|
2145
|
+
* @module dsh-llm-codebuddy-power/client/DrawToolCard
|
|
2146
|
+
*/
|
|
2147
|
+
const modalCss = DrawToolCard_module_css_default;
|
|
2148
|
+
/** 从未知持久化值窄化出卡片可用的描述。 */
|
|
2149
|
+
function drawMeta(value) {
|
|
2150
|
+
if (typeof value !== "object" || value === null) return void 0;
|
|
2151
|
+
const meta = value;
|
|
2152
|
+
if (meta.kind !== "codebuddy-draw") return void 0;
|
|
2153
|
+
if (typeof meta.prompt !== "string" || typeof meta.usedCredits !== "number") return void 0;
|
|
2154
|
+
return {
|
|
2155
|
+
kind: "codebuddy-draw",
|
|
2156
|
+
prompt: meta.prompt,
|
|
2157
|
+
usedCredits: meta.usedCredits
|
|
2158
|
+
};
|
|
2159
|
+
}
|
|
2160
|
+
/** 从已结算的工具结果节点读取描述。 */
|
|
2161
|
+
function settledDrawMeta(block) {
|
|
2162
|
+
return drawMeta(block.meta);
|
|
2163
|
+
}
|
|
2164
|
+
/** 收集工具结果 content 里的标准 ImageBlock 附件引用。 */
|
|
2165
|
+
function imageRefsOf(block) {
|
|
2166
|
+
const refs = [];
|
|
2167
|
+
for (const content of block.content) if (content.type === "image") refs.push(content.attachment);
|
|
2168
|
+
return refs;
|
|
2169
|
+
}
|
|
2170
|
+
/**
|
|
2171
|
+
* PTC 模式下 codebuddy-draw 作为 run_code 嵌套子调用,presentationMeta
|
|
2172
|
+
* 不被投影(tool/code-dispatch 事件无 meta),但子调用的 arguments 与
|
|
2173
|
+
* render 的文本信封都完整。此处从调用参数与文本信封降级解析提示词与
|
|
2174
|
+
* 扣分,让 PTC 对话卡片与标准模式一样显示完整信息。
|
|
2175
|
+
*/
|
|
2176
|
+
function settledDrawFacts(block) {
|
|
2177
|
+
const meta = settledDrawMeta(block);
|
|
2178
|
+
if (meta !== void 0) return {
|
|
2179
|
+
prompt: meta.prompt,
|
|
2180
|
+
usedCredits: meta.usedCredits
|
|
2181
|
+
};
|
|
2182
|
+
let prompt = "";
|
|
2183
|
+
try {
|
|
2184
|
+
const args = JSON.parse(block.call?.argsRaw ?? "{}");
|
|
2185
|
+
if (typeof args.prompt === "string") prompt = args.prompt;
|
|
2186
|
+
} catch {}
|
|
2187
|
+
let usedCredits = 0;
|
|
2188
|
+
for (const content of block.content) {
|
|
2189
|
+
if (content.type !== "text") continue;
|
|
2190
|
+
const match = content.text.match(/([\d.]+) CodeBuddy credits spent/);
|
|
2191
|
+
if (match !== null) {
|
|
2192
|
+
const parsed = Number(match[1]);
|
|
2193
|
+
if (Number.isFinite(parsed)) usedCredits = parsed;
|
|
2194
|
+
break;
|
|
2195
|
+
}
|
|
2196
|
+
}
|
|
2197
|
+
return {
|
|
2198
|
+
prompt,
|
|
2199
|
+
usedCredits
|
|
2200
|
+
};
|
|
2201
|
+
}
|
|
2202
|
+
/** 一个附件引用解析为浏览器 URL(经标准 loadImage 通道)。 */
|
|
2203
|
+
function useImageUrl(loadImage, attachment) {
|
|
2204
|
+
const [url, setUrl] = (0, react.useState)(void 0);
|
|
2205
|
+
(0, react.useEffect)(() => {
|
|
2206
|
+
if (loadImage === void 0) return;
|
|
2207
|
+
let live = true;
|
|
2208
|
+
loadImage(attachment).then((resolved) => {
|
|
2209
|
+
if (live) setUrl(resolved);
|
|
2210
|
+
}, () => {
|
|
2211
|
+
if (live) setUrl(void 0);
|
|
2212
|
+
});
|
|
2213
|
+
return () => {
|
|
2214
|
+
live = false;
|
|
2215
|
+
};
|
|
2216
|
+
}, [loadImage, attachment]);
|
|
2217
|
+
return url;
|
|
2218
|
+
}
|
|
2219
|
+
/**
|
|
2220
|
+
* codebuddy-draw 的 keyed toolview 组件。运行中的调用显示占位;已结算
|
|
2221
|
+
* 结果按 content 里的标准 ImageBlock 经 loadImage 渲染图片,meta 缺失时
|
|
2222
|
+
* (如无卡片客户端回放)返回空。
|
|
2223
|
+
*/
|
|
2224
|
+
function DrawToolCard(props) {
|
|
2225
|
+
const block = props.block;
|
|
2226
|
+
if ("kind" in block) {
|
|
2227
|
+
const facts = settledDrawFacts(block);
|
|
2228
|
+
const refs = imageRefsOf(block);
|
|
2229
|
+
if (facts.prompt.length === 0 && refs.length === 0) return null;
|
|
2230
|
+
const credits = facts.usedCredits > 0 ? `${facts.usedCredits}` : "0";
|
|
2231
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2232
|
+
className: DrawToolCard_module_css_default.card,
|
|
2233
|
+
children: [refs.map((ref, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DrawImage, {
|
|
2234
|
+
loadImage: props.loadImage,
|
|
2235
|
+
attachment: ref,
|
|
2236
|
+
prompt: facts.prompt,
|
|
2237
|
+
t: props.t
|
|
2238
|
+
}, index)), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2239
|
+
className: DrawToolCard_module_css_default.label,
|
|
2240
|
+
children: props.t("drawGenerated", {
|
|
2241
|
+
prompt: facts.prompt,
|
|
2242
|
+
credits
|
|
2243
|
+
})
|
|
2244
|
+
})]
|
|
2245
|
+
});
|
|
2246
|
+
}
|
|
2247
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2248
|
+
className: DrawToolCard_module_css_default.label,
|
|
2249
|
+
children: props.t("drawGenerating")
|
|
2250
|
+
});
|
|
2251
|
+
}
|
|
2252
|
+
/** 单张生成图片的缩略图与内置大图预览。 */
|
|
2253
|
+
function DrawImage(props) {
|
|
2254
|
+
const url = useImageUrl(props.loadImage, props.attachment);
|
|
2255
|
+
const [open, setOpen] = (0, react.useState)(false);
|
|
2256
|
+
const close = (0, react.useCallback)(() => {
|
|
2257
|
+
setOpen(false);
|
|
2258
|
+
}, []);
|
|
2259
|
+
if (url === void 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2260
|
+
className: DrawToolCard_module_css_default.label,
|
|
2261
|
+
children: props.t("drawImageLoading")
|
|
2262
|
+
});
|
|
2263
|
+
const alt = props.prompt === "" ? props.t("drawImageAlt") : props.prompt;
|
|
2264
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2265
|
+
type: "button",
|
|
2266
|
+
className: DrawToolCard_module_css_default.imageButton,
|
|
2267
|
+
title: props.t("drawImageOpen"),
|
|
2268
|
+
"aria-label": props.t("drawImageOpen"),
|
|
2269
|
+
onClick: () => {
|
|
2270
|
+
setOpen(true);
|
|
2271
|
+
},
|
|
2272
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
|
|
2273
|
+
src: url,
|
|
2274
|
+
alt,
|
|
2275
|
+
className: DrawToolCard_module_css_default.image
|
|
2276
|
+
})
|
|
2277
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Modal, {
|
|
2278
|
+
open,
|
|
2279
|
+
onClose: close,
|
|
2280
|
+
title: props.t("drawImagePreviewTitle"),
|
|
2281
|
+
closeLabel: props.t("drawImagePreviewClose"),
|
|
2282
|
+
description: props.prompt,
|
|
2283
|
+
className: modalCss.previewDialog,
|
|
2284
|
+
contentClassName: modalCss.previewContent,
|
|
2285
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
|
|
2286
|
+
src: url,
|
|
2287
|
+
alt,
|
|
2288
|
+
className: DrawToolCard_module_css_default.previewImage
|
|
2289
|
+
})
|
|
2290
|
+
})] });
|
|
2291
|
+
}
|
|
2292
|
+
//#endregion
|
|
2293
|
+
//#region src/client/TurnCreditPill.tsx
|
|
2294
|
+
/**
|
|
2295
|
+
* codebuddy 每轮积分的按钮 pill,插在官方用量/耗时 pill 同一行的
|
|
2296
|
+
* `conversation.chat.assistant-actions` seat(copy/branch 之间)。
|
|
2297
|
+
*
|
|
2298
|
+
* owner 只给 durable `messageId`,轮号由 `useChat` 快照里匹配的 assistant
|
|
2299
|
+
* 节点反查(`AssistantMessageNode.turn`);再经注入的 RPC 查该轮积分,有记录
|
|
2300
|
+
* 则渲染,无记录(非 CodeBuddy 轮)渲染为空。与官方 `TurnUsagePanel` 同构:
|
|
2301
|
+
* 图标 + 文字标签,点击无详情面板(仅展示一条合计)。
|
|
2302
|
+
*
|
|
2303
|
+
* 悬停提示附上本轮用过的账号:账号在扣分那一刻随记录写入(见 host 的
|
|
2304
|
+
* `recordCredit`),不是悬停时现查 —— 一轮里用户可以切换账号,现查会把整轮
|
|
2305
|
+
* 说成最后那个账号。
|
|
2306
|
+
*
|
|
2307
|
+
* @module dsh-llm-codebuddy-power/client/TurnCreditPill
|
|
2308
|
+
*/
|
|
2309
|
+
const pillStyle = {
|
|
2310
|
+
display: "inline-flex",
|
|
2311
|
+
alignItems: "center",
|
|
2312
|
+
gap: 4,
|
|
2313
|
+
fontSize: 12,
|
|
2314
|
+
color: "var(--dsw-alias-label-secondary)",
|
|
2315
|
+
lineHeight: 1.2,
|
|
2316
|
+
padding: "2px 6px",
|
|
2317
|
+
borderRadius: 999,
|
|
2318
|
+
border: "1px solid var(--dsw-alias-border)",
|
|
2319
|
+
background: "var(--dsw-alias-surface-raised)",
|
|
2320
|
+
whiteSpace: "nowrap"
|
|
2321
|
+
};
|
|
2322
|
+
/**
|
|
2323
|
+
* 每轮积分 pill。messageId 反查轮号后查该轮记录;无记录渲染为空(未用
|
|
2324
|
+
* CodeBuddy 模型即一轮 0 分也渲染,有记录即证明用了 CodeBuddy)。只显示
|
|
2325
|
+
* 积分,模型由 dsh 用量自带;悬停给出本轮用过的账号。
|
|
2326
|
+
*/
|
|
2327
|
+
function TurnCreditPill(props) {
|
|
2328
|
+
const { messageId, useChat, sessionId, creditOf, t } = props;
|
|
2329
|
+
const [record, setRecord] = (0, react.useState)(void 0);
|
|
2330
|
+
const nodes = useChat((snapshot) => snapshot.legacy.nodes);
|
|
2331
|
+
const turn = (0, react.useMemo)(() => {
|
|
2332
|
+
for (const node of nodes) {
|
|
2333
|
+
if (node.kind !== "assistant" || node.messageId !== messageId) continue;
|
|
2334
|
+
return typeof node.turn === "number" ? node.turn : null;
|
|
2335
|
+
}
|
|
2336
|
+
return null;
|
|
2337
|
+
}, [messageId, nodes]);
|
|
2338
|
+
(0, react.useEffect)(() => {
|
|
2339
|
+
if (turn === null) {
|
|
2340
|
+
setRecord(void 0);
|
|
2341
|
+
return;
|
|
2342
|
+
}
|
|
2343
|
+
let live = true;
|
|
2344
|
+
creditOf(String(sessionId), turn).then((value) => {
|
|
2345
|
+
if (live) setRecord(value);
|
|
2346
|
+
}, () => {
|
|
2347
|
+
if (live) setRecord(void 0);
|
|
2348
|
+
});
|
|
2349
|
+
return () => {
|
|
2350
|
+
live = false;
|
|
2351
|
+
};
|
|
2352
|
+
}, [
|
|
2353
|
+
creditOf,
|
|
2354
|
+
sessionId,
|
|
2355
|
+
turn
|
|
2356
|
+
]);
|
|
2357
|
+
if (record === void 0) return null;
|
|
2358
|
+
const creditText = (Math.round(record.credit * 100) / 100).toLocaleString(void 0, {
|
|
2359
|
+
maximumFractionDigits: 2,
|
|
2360
|
+
useGrouping: false
|
|
2361
|
+
});
|
|
2362
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
|
|
2363
|
+
label: [t("turnCreditTip"), ...record.accounts.map((account) => t("turnCreditAccount", {
|
|
2364
|
+
nickname: account.nickname,
|
|
2365
|
+
uid: account.uidPrefix
|
|
2366
|
+
}))].join("\n"),
|
|
2367
|
+
side: "bottom",
|
|
2368
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2369
|
+
style: pillStyle,
|
|
2370
|
+
children: t("turnCredit", { credit: creditText })
|
|
2371
|
+
})
|
|
2372
|
+
});
|
|
2373
|
+
}
|
|
2374
|
+
//#endregion
|
|
2375
|
+
//#region src/client/locales.ts
|
|
2376
|
+
/** CodeBuddy 设置分区与对话内账号控件的文案字典。 */
|
|
2377
|
+
/** 英文字符串(zh/en 键集合的事实来源)。 */
|
|
2378
|
+
const en = {
|
|
2379
|
+
nav: "CodeBuddy",
|
|
2380
|
+
intro: "Sign in with your Tencent CodeBuddy account. You can keep several accounts signed in and choose one per conversation.",
|
|
2381
|
+
loading: "Loading…",
|
|
2382
|
+
notSignedIn: "Not signed in.",
|
|
2383
|
+
waiting: "Waiting for the browser sign-in to complete…",
|
|
2384
|
+
signIn: "Sign in",
|
|
2385
|
+
signingIn: "Signing in…",
|
|
2386
|
+
copyLoginLink: "Copy sign-in link",
|
|
2387
|
+
copyLoginLinkFailed: "Could not copy the sign-in link. Check the browser clipboard permission and try again.",
|
|
2388
|
+
copied: "Copied",
|
|
2389
|
+
cancelLogin: "Cancel sign-in",
|
|
2390
|
+
timeout: "Sign-in timed out. Please try again.",
|
|
2391
|
+
multiSelect: "Multi-select",
|
|
2392
|
+
multiSelectDone: "Done",
|
|
2393
|
+
selectAccount: "Select this account",
|
|
2394
|
+
logoutSelectedTip: "Sign out selected accounts",
|
|
2395
|
+
logoutTitle: "Sign out",
|
|
2396
|
+
logoutSelectedTitle: "Sign out selected accounts",
|
|
2397
|
+
logoutDesc: "Signing out removes the stored credential; {name} must sign in again to be used.",
|
|
2398
|
+
logoutSelectedDesc: "Signing out removes the stored credentials; the {count} selected accounts must sign in again to be used.",
|
|
2399
|
+
logoutConfirm: "Sign out",
|
|
2400
|
+
cancel: "Cancel",
|
|
2401
|
+
chooseDefault: "Choose default",
|
|
2402
|
+
clearDefault: "Remove default",
|
|
2403
|
+
defaultTag: "Default",
|
|
2404
|
+
nickname: "Nickname",
|
|
2405
|
+
uid: "UID",
|
|
2406
|
+
loginExpires: "Sign-in expires",
|
|
2407
|
+
uin: "UIN",
|
|
2408
|
+
enterprise: "Enterprise",
|
|
2409
|
+
enterpriseId: "Enterprise ID",
|
|
2410
|
+
enterpriseUser: "Enterprise user",
|
|
2411
|
+
department: "Department",
|
|
2412
|
+
switchAccount: "Switch account",
|
|
2413
|
+
showUsage: "Show usage allowance",
|
|
2414
|
+
showUsageDesc: "Display the used-allowance ring in the composer, next to the account button.",
|
|
2415
|
+
drawToolMinimal: "Draw tool in minimal mode",
|
|
2416
|
+
drawToolMinimalDesc: "Inject the codebuddy-draw tool into minimal-mode sessions.",
|
|
2417
|
+
dangerPct: "Low-allowance alert",
|
|
2418
|
+
dangerPctDesc: "The ring turns red once used usage reaches this percentage. Defaults to 90%.",
|
|
2419
|
+
usageUsed: "Total used",
|
|
2420
|
+
usageMain: "Main allowance",
|
|
2421
|
+
usageGift: "Gifted allowance",
|
|
2422
|
+
usageResets: "Resets at",
|
|
2423
|
+
drawGenerating: "Generating image…",
|
|
2424
|
+
drawGenerated: "Generated “{prompt}” · {credits} CodeBuddy credits spent (x5.00 billing multiplier)",
|
|
2425
|
+
drawImageLoading: "Loading image…",
|
|
2426
|
+
drawImageAlt: "Generated image",
|
|
2427
|
+
drawImageOpen: "Open full-size generated image",
|
|
2428
|
+
drawImagePreviewTitle: "Generated image",
|
|
2429
|
+
drawImagePreviewClose: "Close image preview",
|
|
2430
|
+
turnCredit: "{credit} credits",
|
|
2431
|
+
turnCreditTip: "CodeBuddy credits spent this turn",
|
|
2432
|
+
turnCreditAccount: "Account: {nickname} ({uid}…)",
|
|
2433
|
+
modelPanelTrigger: "CodeBuddy models",
|
|
2434
|
+
modelPanelTitle: "CodeBuddy models",
|
|
2435
|
+
modelPanelEmpty: "No CodeBuddy models available.",
|
|
2436
|
+
modelPanelSelectError: "Could not apply the model selection.",
|
|
2437
|
+
modelPanelContextWindow: "Context window",
|
|
2438
|
+
modelPanelMaxOutput: "Max output tokens",
|
|
2439
|
+
modelPanelFeatures: "Available features",
|
|
2440
|
+
modelPanelSpendRate: "Spend rate",
|
|
2441
|
+
modelPanelUse: "Use",
|
|
2442
|
+
modelPanelUpgrade: "Upgrade",
|
|
2443
|
+
modelPanelEdit: "Edit",
|
|
2444
|
+
modelPanelDescriptionMissing: "No description.",
|
|
2445
|
+
modelPanelSave: "Save",
|
|
2446
|
+
modelPanelSaving: "Saving…",
|
|
2447
|
+
modelPanelRestoreDefault: "Reset to default",
|
|
2448
|
+
modelPanelEffort: "Reasoning effort",
|
|
2449
|
+
modelPanelEffortDefault: "Default",
|
|
2450
|
+
modelPanelFeatureImage: "Image input",
|
|
2451
|
+
modelPanelFeatureReasoning: "Reasoning"
|
|
2452
|
+
};
|
|
2453
|
+
/** 中文字符串(键与 {@link en} 相同)。 */
|
|
2454
|
+
const zh = {
|
|
2455
|
+
nav: "CodeBuddy",
|
|
2456
|
+
intro: "使用腾讯 CodeBuddy 账号登录。可以同时登录多个账号,再按会话选用其中一个。",
|
|
2457
|
+
loading: "加载中…",
|
|
2458
|
+
notSignedIn: "未登录。",
|
|
2459
|
+
waiting: "等待浏览器登录完成…",
|
|
2460
|
+
signIn: "登录",
|
|
2461
|
+
signingIn: "登录中…",
|
|
2462
|
+
copyLoginLink: "复制登录链接",
|
|
2463
|
+
copyLoginLinkFailed: "复制登录链接失败,请检查浏览器剪贴板权限后重试。",
|
|
2464
|
+
copied: "已复制",
|
|
2465
|
+
cancelLogin: "取消登录",
|
|
2466
|
+
timeout: "登录超时,请重试。",
|
|
2467
|
+
multiSelect: "多选",
|
|
2468
|
+
multiSelectDone: "完成",
|
|
2469
|
+
selectAccount: "选中此账号",
|
|
2470
|
+
logoutSelectedTip: "退出选中账号",
|
|
2471
|
+
logoutTitle: "退出账号",
|
|
2472
|
+
logoutSelectedTitle: "退出选中账号",
|
|
2473
|
+
logoutDesc: "退出会删除已存凭据,{name} 需要重新登录才能继续使用。",
|
|
2474
|
+
logoutSelectedDesc: "退出会删除已存凭据,选中的 {count} 个账号需要重新登录才能继续使用。",
|
|
2475
|
+
logoutConfirm: "退出",
|
|
2476
|
+
cancel: "取消",
|
|
2477
|
+
chooseDefault: "选择默认",
|
|
2478
|
+
clearDefault: "取消默认",
|
|
2479
|
+
defaultTag: "默认",
|
|
2480
|
+
nickname: "昵称",
|
|
2481
|
+
uid: "UID",
|
|
2482
|
+
loginExpires: "登录到期",
|
|
2483
|
+
uin: "UIN",
|
|
2484
|
+
enterprise: "企业",
|
|
2485
|
+
enterpriseId: "企业 ID",
|
|
2486
|
+
enterpriseUser: "企业用户名",
|
|
2487
|
+
department: "部门",
|
|
2488
|
+
switchAccount: "切换账号",
|
|
2489
|
+
showUsage: "显示额度余量",
|
|
2490
|
+
showUsageDesc: "在对话框输入区、账号按钮旁显示已用额度圆环。",
|
|
2491
|
+
drawToolMinimal: "极简模式画图工具",
|
|
2492
|
+
drawToolMinimalDesc: "在极简模式会话中注入 codebuddy-draw 工具。",
|
|
2493
|
+
dangerPct: "余量告警百分比",
|
|
2494
|
+
dangerPctDesc: "已用百分比达到此值时,圆环变为红色提醒。默认 90%。",
|
|
2495
|
+
usageUsed: "合计已用",
|
|
2496
|
+
usageMain: "主额度",
|
|
2497
|
+
usageGift: "赠送额度",
|
|
2498
|
+
usageResets: "重置时间",
|
|
2499
|
+
drawGenerating: "正在生成图片…",
|
|
2500
|
+
drawGenerated: "已生成“{prompt}” · 消耗 {credits} CodeBuddy 积分(x5.00 计费倍率)",
|
|
2501
|
+
drawImageLoading: "正在加载图片…",
|
|
2502
|
+
drawImageAlt: "生成的图片",
|
|
2503
|
+
drawImageOpen: "查看生成图片原图",
|
|
2504
|
+
drawImagePreviewTitle: "生成的图片",
|
|
2505
|
+
drawImagePreviewClose: "关闭图片预览",
|
|
2506
|
+
turnCredit: "{credit} 分",
|
|
2507
|
+
turnCreditTip: "本轮消耗的 CodeBuddy 积分",
|
|
2508
|
+
turnCreditAccount: "账号:{nickname}({uid}…)",
|
|
2509
|
+
modelPanelTrigger: "CodeBuddy 模型",
|
|
2510
|
+
modelPanelTitle: "CodeBuddy 模型",
|
|
2511
|
+
modelPanelEmpty: "暂无可用 CodeBuddy 模型。",
|
|
2512
|
+
modelPanelSelectError: "模型选择未能生效。",
|
|
2513
|
+
modelPanelContextWindow: "上下文窗口",
|
|
2514
|
+
modelPanelMaxOutput: "最大输出 token",
|
|
2515
|
+
modelPanelSave: "保存",
|
|
2516
|
+
modelPanelSaving: "保存中…",
|
|
2517
|
+
modelPanelRestoreDefault: "还原默认",
|
|
2518
|
+
modelPanelEffort: "思考程度",
|
|
2519
|
+
modelPanelEffortDefault: "默认",
|
|
2520
|
+
modelPanelFeatures: "可用功能",
|
|
2521
|
+
modelPanelSpendRate: "消耗速度",
|
|
2522
|
+
modelPanelUse: "去使用",
|
|
2523
|
+
modelPanelUpgrade: "去升级",
|
|
2524
|
+
modelPanelEdit: "编辑",
|
|
2525
|
+
modelPanelDescriptionMissing: "暂无介绍。",
|
|
2526
|
+
modelPanelFeatureImage: "图片输入",
|
|
2527
|
+
modelPanelFeatureReasoning: "推理"
|
|
2528
|
+
};
|
|
2529
|
+
//#endregion
|
|
2530
|
+
//#region src/client/store.ts
|
|
2531
|
+
/**
|
|
2532
|
+
* CodeBuddy 页面 store:设置分区共享的账号快照。
|
|
2533
|
+
*
|
|
2534
|
+
* 客户端插件在设置页条目下挂载本句柄,因此在设置分区里完成的登录、退出或改
|
|
2535
|
+
* 默认账号无需跨组件接线即可到达同一页面的各个徽章。
|
|
2536
|
+
*
|
|
2537
|
+
* **界面偏好不在这里**:它们是 harness 用户设置文档里的一段(见 `../prefs.ts`),
|
|
2538
|
+
* 由 `./index.ts` 经 `ctx.settingsScope.bind()` 读取,并以一个派生的活快照经
|
|
2539
|
+
* 注入面的 `hooks` 隔舱同时下发给设置页与对话输入区的控件组 —— 后者处在另一个
|
|
2540
|
+
* 作用域,共享不了本句柄。偏好此前存在 `localStorage` 并由输入区按 5 秒轮询
|
|
2541
|
+
* 读取,导致开关与圆环之间有最多一个轮询周期的延迟。
|
|
2542
|
+
*
|
|
2543
|
+
* @module dsh-llm-codebuddy-power/client/store
|
|
2544
|
+
*/
|
|
2545
|
+
/**
|
|
2546
|
+
* 声明 CodeBuddy 页面状态与写入接口。
|
|
2547
|
+
* @returns store 句柄(在 apply 中挂载于设置页条目下)。
|
|
2548
|
+
*/
|
|
2549
|
+
function createCodeBuddyStore() {
|
|
2550
|
+
return (0, _deepseek_ai_dsh_client_store.defineStore)({
|
|
2551
|
+
init: () => ({
|
|
2552
|
+
phase: "loading",
|
|
2553
|
+
error: null,
|
|
2554
|
+
accounts: [],
|
|
2555
|
+
defaultUid: null,
|
|
2556
|
+
loginState: null
|
|
2557
|
+
}),
|
|
2558
|
+
actions: {
|
|
2559
|
+
start: (draft) => {
|
|
2560
|
+
draft.phase = "loading";
|
|
2561
|
+
draft.error = null;
|
|
2562
|
+
},
|
|
2563
|
+
setAccounts: (draft, result) => {
|
|
2564
|
+
draft.phase = "idle";
|
|
2565
|
+
draft.error = null;
|
|
2566
|
+
draft.accounts = result.accounts;
|
|
2567
|
+
draft.defaultUid = result.defaultUid ?? null;
|
|
2568
|
+
},
|
|
2569
|
+
fail: (draft, message) => {
|
|
2570
|
+
draft.phase = "error";
|
|
2571
|
+
draft.error = message;
|
|
2572
|
+
},
|
|
2573
|
+
setLoginState: (draft, state) => {
|
|
2574
|
+
draft.loginState = state;
|
|
2575
|
+
}
|
|
2576
|
+
}
|
|
2577
|
+
});
|
|
2578
|
+
}
|
|
2579
|
+
//#endregion
|
|
2580
|
+
//#region src/client/wire.ts
|
|
2581
|
+
/** host 鉴权服务监听的 RPC 通道(与 host 常量保持一致)。 */
|
|
2582
|
+
const AUTH_CHANNEL = "/codebuddy";
|
|
2583
|
+
/**
|
|
2584
|
+
* 在共享通道上调用一个鉴权 endpoint。该通道调用按设计不做类型约束;
|
|
2585
|
+
* endpoint 所记录的 payload 结构在此唯一的 wire 边界处恢复。
|
|
2586
|
+
*/
|
|
2587
|
+
function authCall(rpc, endpoint, payload = {}) {
|
|
2588
|
+
return rpc.call(AUTH_CHANNEL, endpoint, payload).then((result) => result);
|
|
2589
|
+
}
|
|
2590
|
+
//#endregion
|
|
2591
|
+
//#region src/client/index.ts
|
|
2592
|
+
/** 本插件拥有的字典命名空间。 */
|
|
2593
|
+
const NS = "settings.codebuddy";
|
|
2594
|
+
/** 必需服务:插槽注册表、locale 服务、wire、共享模型目录服务(该面板读取
|
|
2595
|
+
* 与官方选择器相同的 store)、uiConversation(附件图片 URL),以及设置作用域
|
|
2596
|
+
* (界面偏好所在的命名空间)。
|
|
2597
|
+
*
|
|
2598
|
+
* 声明 `sessions`/`remote`/`remote.session` 是因为
|
|
2599
|
+
* `modelDirectories.directoryFor()` 在本插件的 Context 下运行,并在构建
|
|
2600
|
+
* 会话目录时读取这些服务。 */
|
|
2601
|
+
const inject = [
|
|
2602
|
+
"slots",
|
|
2603
|
+
"locale",
|
|
2604
|
+
"connection",
|
|
2605
|
+
"modelDirectories",
|
|
2606
|
+
"sessions",
|
|
2607
|
+
"remote",
|
|
2608
|
+
"remote.session",
|
|
2609
|
+
"uiConversation",
|
|
2610
|
+
"settingsScope"
|
|
2611
|
+
];
|
|
2612
|
+
/**
|
|
2613
|
+
* 在插槽声明后注册 CodeBuddy 设置分区与对话内账号控件。
|
|
2614
|
+
* @param ctx - 客户端根 context。
|
|
2615
|
+
*/
|
|
2616
|
+
function apply(ctx) {
|
|
2617
|
+
ctx.effect(() => ctx.locale.register(NS, {
|
|
2618
|
+
zh,
|
|
2619
|
+
en
|
|
2620
|
+
}), "dsh-llm-codebuddy-power: settings copy");
|
|
2621
|
+
const rpc = ctx.get("connection").rpc;
|
|
2622
|
+
const store = createCodeBuddyStore();
|
|
2623
|
+
const t = ctx.locale.bind(NS);
|
|
2624
|
+
const prefsScope = ctx.settingsScope.bind({ namespace: CODEBUDDY_SETTINGS_NAMESPACE });
|
|
2625
|
+
const prefsStore = (0, _deepseek_ai_dsh_client_store.createSnapshotStore)(DEFAULT_CODEBUDDY_SETTINGS);
|
|
2626
|
+
const publishPrefs = () => {
|
|
2627
|
+
const section = prefsScope.getSnapshot().value;
|
|
2628
|
+
if (section === void 0) return;
|
|
2629
|
+
const current = prefsStore.getSnapshot();
|
|
2630
|
+
if (current.showUsage === section.showUsage && current.dangerPct === section.dangerPct && current.drawToolInMinimal === section.drawToolInMinimal) return;
|
|
2631
|
+
prefsStore.set({
|
|
2632
|
+
showUsage: section.showUsage,
|
|
2633
|
+
dangerPct: section.dangerPct,
|
|
2634
|
+
drawToolInMinimal: section.drawToolInMinimal
|
|
2635
|
+
});
|
|
2636
|
+
};
|
|
2637
|
+
ctx.effect(() => {
|
|
2638
|
+
const off = prefsScope.subscribe(publishPrefs);
|
|
2639
|
+
publishPrefs();
|
|
2640
|
+
return off;
|
|
2641
|
+
}, "dsh-llm-codebuddy-power: settings prefs");
|
|
2642
|
+
/** 写入一个偏好字段;用户层覆盖部署级 base。 */
|
|
2643
|
+
const setPref = (field, value) => {
|
|
2644
|
+
prefsScope.set(field, value);
|
|
2645
|
+
};
|
|
2646
|
+
/** 读取账号集合并发布到设置页的 store。 */
|
|
2647
|
+
const refreshAccounts = async (actions) => {
|
|
2648
|
+
actions.start();
|
|
2649
|
+
try {
|
|
2650
|
+
const result = await authCall(rpc, "accounts");
|
|
2651
|
+
if (!result.ok) {
|
|
2652
|
+
actions.fail(`${result.error.code}: ${result.error.message}`);
|
|
2653
|
+
return;
|
|
2654
|
+
}
|
|
2655
|
+
if (result.value.accounts.length === 0) {
|
|
2656
|
+
const retry = await authCall(rpc, "accounts");
|
|
2657
|
+
if (retry.ok && retry.value.accounts.length > 0) {
|
|
2658
|
+
actions.setAccounts(retry.value);
|
|
2659
|
+
return;
|
|
2660
|
+
}
|
|
2661
|
+
}
|
|
2662
|
+
actions.setAccounts(result.value);
|
|
2663
|
+
} catch (error) {
|
|
2664
|
+
actions.fail(error instanceof Error ? error.message : String(error));
|
|
2665
|
+
}
|
|
2666
|
+
};
|
|
2667
|
+
const injected = (actions) => {
|
|
2668
|
+
/**
|
|
2669
|
+
* 发起一次登录握手。
|
|
2670
|
+
* @param openBrowser - true 时在新标签页打开鉴权 URL,false 时只把它复制到剪贴板。
|
|
2671
|
+
* @returns 复制模式下链接是否已进剪贴板;打开模式恒为 true。
|
|
2672
|
+
*/
|
|
2673
|
+
const beginLogin = async (openBrowser) => {
|
|
2674
|
+
const result = await authCall(rpc, "startLogin");
|
|
2675
|
+
if (!result.ok) {
|
|
2676
|
+
actions.fail(`${result.error.code}: ${result.error.message}`);
|
|
2677
|
+
return false;
|
|
2678
|
+
}
|
|
2679
|
+
if (openBrowser) window.open(result.value.authUrl, "_blank", "noopener");
|
|
2680
|
+
else if (!await (0, _deepseek_ai_dsh_client_ui_primitives.writeClipboard)(result.value.authUrl)) {
|
|
2681
|
+
actions.fail(t("copyLoginLinkFailed"));
|
|
2682
|
+
return false;
|
|
2683
|
+
}
|
|
2684
|
+
actions.setLoginState({
|
|
2685
|
+
state: result.value.state,
|
|
2686
|
+
startedAt: Date.now()
|
|
2687
|
+
});
|
|
2688
|
+
return true;
|
|
2689
|
+
};
|
|
2690
|
+
const pollLogin = async (state) => {
|
|
2691
|
+
const result = await authCall(rpc, "pollLogin", { state });
|
|
2692
|
+
if (!result.ok || !result.value.done) return false;
|
|
2693
|
+
actions.setLoginState(null);
|
|
2694
|
+
await refreshAccounts(actions);
|
|
2695
|
+
return true;
|
|
2696
|
+
};
|
|
2697
|
+
const logoutAccounts = async (uids) => {
|
|
2698
|
+
const result = await authCall(rpc, "logout", { uids });
|
|
2699
|
+
if (result.ok) await refreshAccounts(actions);
|
|
2700
|
+
else actions.fail(`${result.error.code}: ${result.error.message}`);
|
|
2701
|
+
};
|
|
2702
|
+
const reorderAccounts = async (uids) => {
|
|
2703
|
+
const result = await authCall(rpc, "reorder", { uids });
|
|
2704
|
+
if (result.ok) await refreshAccounts(actions);
|
|
2705
|
+
else actions.fail(`${result.error.code}: ${result.error.message}`);
|
|
2706
|
+
};
|
|
2707
|
+
const setDefault = async (uid) => {
|
|
2708
|
+
const result = await authCall(rpc, "setDefault", { uid: uid ?? "" });
|
|
2709
|
+
if (result.ok) await refreshAccounts(actions);
|
|
2710
|
+
else actions.fail(`${result.error.code}: ${result.error.message}`);
|
|
2711
|
+
};
|
|
2712
|
+
return {
|
|
2713
|
+
hooks: { prefs: prefsStore },
|
|
2714
|
+
refreshAccounts: () => refreshAccounts(actions),
|
|
2715
|
+
startLogin: async () => {
|
|
2716
|
+
await beginLogin(true);
|
|
2717
|
+
},
|
|
2718
|
+
copyLoginLink: () => beginLogin(false),
|
|
2719
|
+
cancelLogin: async (state) => {
|
|
2720
|
+
return (await authCall(rpc, "cancelLogin", { state })).ok;
|
|
2721
|
+
},
|
|
2722
|
+
pollLogin,
|
|
2723
|
+
logoutAccounts,
|
|
2724
|
+
reorderAccounts,
|
|
2725
|
+
setDefault,
|
|
2726
|
+
setPref
|
|
2727
|
+
};
|
|
2728
|
+
};
|
|
2729
|
+
ctx.slots.inject("settings.section", () => ctx.slots.register({
|
|
2730
|
+
name: "settings.section",
|
|
2731
|
+
id: "codebuddy",
|
|
2732
|
+
order: 25,
|
|
2733
|
+
label: () => t("nav"),
|
|
2734
|
+
store,
|
|
2735
|
+
locale: NS,
|
|
2736
|
+
inject: injected
|
|
2737
|
+
}, CodeBuddySection));
|
|
2738
|
+
const modelDirectories = ctx.get("modelDirectories");
|
|
2739
|
+
ctx.slots.inject("conversation.input.right", () => ctx.slots.register({
|
|
2740
|
+
name: "conversation.input.right",
|
|
2741
|
+
id: "codebuddy-input",
|
|
2742
|
+
order: 1e3,
|
|
2743
|
+
locale: NS,
|
|
2744
|
+
inject: (sessionId) => {
|
|
2745
|
+
const directory = modelDirectories.directoryFor(sessionId);
|
|
2746
|
+
return {
|
|
2747
|
+
fetchAccounts: async () => {
|
|
2748
|
+
const result = await authCall(rpc, "accounts");
|
|
2749
|
+
if (!result.ok) return null;
|
|
2750
|
+
return {
|
|
2751
|
+
accounts: result.value.accounts,
|
|
2752
|
+
defaultUid: result.value.defaultUid ?? null
|
|
2753
|
+
};
|
|
2754
|
+
},
|
|
2755
|
+
fetchSessionAccount: async () => {
|
|
2756
|
+
const result = await authCall(rpc, "sessionAccount", { sessionId });
|
|
2757
|
+
return result.ok ? result.value.uid ?? null : null;
|
|
2758
|
+
},
|
|
2759
|
+
activate: async (uid) => {
|
|
2760
|
+
return (await authCall(rpc, "activate", {
|
|
2761
|
+
sessionId,
|
|
2762
|
+
uid
|
|
2763
|
+
})).ok;
|
|
2764
|
+
},
|
|
2765
|
+
fetchUsage: async () => {
|
|
2766
|
+
const result = await authCall(rpc, "usage", { sessionId });
|
|
2767
|
+
return result.ok && result.value.loggedIn ? result.value : null;
|
|
2768
|
+
},
|
|
2769
|
+
hooks: {
|
|
2770
|
+
directory: directory.store,
|
|
2771
|
+
prefs: prefsStore
|
|
2772
|
+
},
|
|
2773
|
+
load: () => {
|
|
2774
|
+
directory.load().catch(() => {});
|
|
2775
|
+
},
|
|
2776
|
+
select: (selection) => directory.select(selection).then(() => true, () => false),
|
|
2777
|
+
fetchModels: async () => {
|
|
2778
|
+
const result = await authCall(rpc, "models", { sessionId });
|
|
2779
|
+
return result.ok && result.value !== void 0 ? result.value.models : [];
|
|
2780
|
+
},
|
|
2781
|
+
saveOverrides: async (modelId, overrides) => {
|
|
2782
|
+
await authCall(rpc, "setOverride", {
|
|
2783
|
+
modelId,
|
|
2784
|
+
...overrides
|
|
2785
|
+
});
|
|
2786
|
+
}
|
|
2787
|
+
};
|
|
2788
|
+
}
|
|
2789
|
+
}, ComposerControls));
|
|
2790
|
+
ctx.slots.inject("tool.call.toolview", () => ctx.slots.register({
|
|
2791
|
+
name: "tool.call.toolview",
|
|
2792
|
+
key: "codebuddy-draw",
|
|
2793
|
+
locale: NS,
|
|
2794
|
+
inject: (sessionId) => ({ loadImage: (attachment) => ctx.uiConversation.imageUrl(sessionId, attachment) })
|
|
2795
|
+
}, DrawToolCard));
|
|
2796
|
+
ctx.slots.inject("conversation.chat.assistant-actions", () => ctx.slots.register({
|
|
2797
|
+
name: "conversation.chat.assistant-actions",
|
|
2798
|
+
id: "codebuddy-credit",
|
|
2799
|
+
order: 30,
|
|
2800
|
+
locale: NS,
|
|
2801
|
+
inject: () => ({ creditOf: (sessionId, turn) => authCall(rpc, "creditOf", {
|
|
2802
|
+
sessionId,
|
|
2803
|
+
turn
|
|
2804
|
+
}).then((result) => result.ok ? result.value : void 0) })
|
|
2805
|
+
}, TurnCreditPill));
|
|
2806
|
+
}
|
|
2807
|
+
//#endregion
|
|
2808
|
+
exports.apply = apply;
|
|
2809
|
+
exports.inject = inject;
|
|
2810
|
+
return module.exports;
|
|
2811
|
+
}
|
|
2812
|
+
});
|
|
2813
|
+
|
|
2814
|
+
//# sourceMappingURL=client.js.map
|