@shgroup/dsh-serenity-hooks 1.21.1 → 1.23.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/lib/client.js CHANGED
@@ -7,266 +7,12 @@ window.__ModuleLoader__.load({
7
7
  let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
8
8
  let react = require("react");
9
9
  let react_jsx_runtime = require("react/jsx-runtime");
10
- //#region src/client/accounts-api.ts
11
- /** wire 账号 → 本地编辑行 */
12
- function accountDraftFromWire(a) {
13
- return {
14
- id: a.id,
15
- user: a.user,
16
- pass: "",
17
- isNew: false,
18
- hasPassword: a.hasPassword
19
- };
20
- }
21
- /** 生成新账号 id(本地编辑用;提交时若为空由服务端拒绝) */
22
- function newAccountId() {
23
- return `a-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`;
24
- }
25
- /** 本地编辑行 → wire 账号(提交时;pass 字段携带) */
26
- function accountToWire(d) {
27
- return {
28
- id: d.id,
29
- user: d.user,
30
- pass: d.pass
31
- };
32
- }
33
- /** 校验本地编辑行:user 非空;新账号必须设密码 */
34
- function validateDraft(d) {
35
- if (d.user.trim() === "") return "用户名不能为空";
36
- if (d.isNew && d.pass === "") return "新账号必须设置密码";
37
- return null;
38
- }
39
- const CONFIG_PATH = "/serenity/config";
40
- /** GET 配置(含账号列表;无 hash) */
41
- async function fetchConfig(sessionId) {
42
- const qs = sessionId ? `?sessionId=${encodeURIComponent(sessionId)}` : "";
43
- const res = await fetch(`${CONFIG_PATH}${qs}`, { headers: {
44
- accept: "application/json",
45
- "x-serenity-ui": "1"
46
- } });
47
- if (!res.ok) return null;
48
- return (await res.json()).config ?? null;
49
- }
50
- /** PUT 配置(账号 patch)→ 返回保存后的 wire */
51
- async function saveConfig(sessionId, patch) {
52
- const res = await fetch(CONFIG_PATH, {
53
- method: "PUT",
54
- headers: {
55
- "content-type": "application/json",
56
- "x-serenity-ui": "1"
57
- },
58
- body: JSON.stringify({
59
- sessionId,
60
- config: patch
61
- })
62
- });
63
- if (!res.ok) {
64
- const body = await res.json().catch(() => null);
65
- throw new Error(body?.error ?? `保存失败(${res.status})`);
66
- }
67
- return (await res.json()).config ?? null;
68
- }
69
- //#endregion
70
- //#region \0sp-css:/home/yh/home/home-serenity/AI_LAB/dsh-serenity-plugin/hooks/dsh-serenity-hooks/src/client/AccountsTab
71
- const css$2 = "/* AccountsTab — 高级面板账号 tab(v1.21;--dsw-alias-* 语义 token,明暗自适应) */\n\n.at-root {\n display: flex;\n flex-direction: column;\n gap: 8px;\n padding: 6px 2px;\n font-size: 13px;\n}\n\n.at-note {\n margin: 0;\n font-size: 12px;\n opacity: 0.65;\n}\n\n.at-row {\n display: flex;\n align-items: center;\n gap: 8px;\n}\n\n.at-label {\n font-weight: 600;\n white-space: nowrap;\n}\n\n.at-input {\n flex: 1;\n min-width: 0;\n padding: 5px 8px;\n border: 1px solid var(--dsw-alias-border, #333);\n border-radius: 6px;\n background: var(--dsw-alias-field, #222);\n color: var(--dsw-alias-text, #eee);\n font-size: 13px;\n}\n\n.at-input.at-port {\n flex: none;\n width: 72px;\n}\n\n.at-list {\n display: flex;\n flex-direction: column;\n gap: 4px;\n}\n\n.at-head {\n display: flex;\n gap: 8px;\n font-size: 12px;\n opacity: 0.6;\n padding: 0 2px;\n}\n\n.at-item {\n display: flex;\n gap: 8px;\n align-items: center;\n}\n\n.at-colUser { flex: 1.2; }\n.at-colPass { flex: 1.6; }\n.at-colOp { flex: none; width: 48px; }\n\n.at-del {\n flex: none;\n padding: 4px 10px;\n border: 1px solid var(--dsw-alias-border, #444);\n border-radius: 6px;\n background: transparent;\n color: var(--dsw-alias-danger, #f87171);\n font-size: 12px;\n cursor: pointer;\n}\n\n.at-add {\n align-self: flex-start;\n padding: 5px 12px;\n border: 1px dashed var(--dsw-alias-border, #444);\n border-radius: 6px;\n background: transparent;\n color: var(--dsw-alias-text, #ccc);\n font-size: 12px;\n cursor: pointer;\n}\n\n.at-error {\n margin: 0;\n font-size: 12px;\n color: var(--dsw-alias-danger, #f87171);\n}\n\n.at-actions {\n display: flex;\n justify-content: flex-end;\n padding-top: 4px;\n}\n\n.at-save {\n padding: 6px 22px;\n border: 0;\n border-radius: 6px;\n background: var(--dsw-alias-accent, #3b82f6);\n color: #fff;\n font-size: 13px;\n cursor: pointer;\n}\n\n.at-save:disabled {\n opacity: 0.6;\n cursor: default;\n}\n";
72
- if (typeof document !== "undefined" && document.querySelector("style[data-sp-css]") === null) {
73
- const tag = document.createElement("style");
74
- tag.setAttribute("data-sp-css", "1");
75
- tag.textContent = css$2;
76
- document.head.appendChild(tag);
77
- }
78
- //#endregion
79
- //#region src/client/AccountsTab.tsx
80
- /**
81
- * AccountsTab.tsx — 高级面板「账号」tab(v1.21 复杂配置层)
82
- *
83
- * 承载复杂配置:gateway 监听 host/port + 账号列表 CRUD(含密码)。
84
- * 简单配置(开关/阈值)在 dsh 原生设置面板(SettingsSection)——本 tab 仅提示引导。
85
- * 数据:GET/PUT /serenity/config(accounts-api.ts);无滚动条约束(紧凑布局)。
86
- */
87
- function AccountsTab(props) {
88
- const { sessionId } = props;
89
- const [config, setConfig] = (0, react.useState)(null);
90
- const [drafts, setDrafts] = (0, react.useState)([]);
91
- const [host, setHost] = (0, react.useState)("0.0.0.0");
92
- const [port, setPort] = (0, react.useState)(3081);
93
- const [error, setError] = (0, react.useState)(null);
94
- const [busy, setBusy] = (0, react.useState)(false);
95
- const [loaded, setLoaded] = (0, react.useState)(false);
96
- (0, react.useEffect)(() => {
97
- let alive = true;
98
- fetchConfig(sessionId).then((cfg) => {
99
- if (!alive) return;
100
- if (cfg) {
101
- setConfig(cfg);
102
- setHost(cfg.gateway.host);
103
- setPort(cfg.gateway.port);
104
- setDrafts(cfg.gateway.accounts.map(accountDraftFromWire));
105
- }
106
- setLoaded(true);
107
- });
108
- return () => {
109
- alive = false;
110
- };
111
- }, [sessionId]);
112
- const updateDraft = (id, patch) => {
113
- setDrafts((ds) => ds.map((d) => d.id === id ? {
114
- ...d,
115
- ...patch
116
- } : d));
117
- };
118
- const addRow = () => {
119
- setDrafts((ds) => [...ds, {
120
- id: newAccountId(),
121
- user: "",
122
- pass: "",
123
- isNew: true,
124
- hasPassword: false
125
- }]);
126
- };
127
- const removeRow = (id) => {
128
- setDrafts((ds) => ds.filter((d) => d.id !== id));
129
- };
130
- const save = async () => {
131
- for (const d of drafts) {
132
- const err = validateDraft(d);
133
- if (err) {
134
- setError(err);
135
- return;
136
- }
137
- }
138
- setBusy(true);
139
- setError(null);
140
- try {
141
- const cfg = await saveConfig(sessionId, { gateway: {
142
- host,
143
- port,
144
- accounts: drafts.map(accountToWire)
145
- } });
146
- if (cfg) {
147
- setConfig(cfg);
148
- setDrafts(cfg.gateway.accounts.map(accountDraftFromWire));
149
- }
150
- } catch (err) {
151
- setError(err instanceof Error ? err.message : String(err));
152
- } finally {
153
- setBusy(false);
154
- }
155
- };
156
- if (!loaded) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
157
- className: "at-root",
158
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
159
- className: "at-note",
160
- children: "加载中…"
161
- })
162
- });
163
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
164
- className: "at-root",
165
- children: [
166
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
167
- className: "at-note",
168
- children: "复杂配置(账号密码)。开关/阈值等简单配置请到 dsh 设置面板「Serenity」页。"
169
- }),
170
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
171
- className: "at-row",
172
- children: [
173
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
174
- className: "at-label",
175
- children: "监听地址"
176
- }),
177
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
178
- className: "at-input",
179
- value: host,
180
- onChange: (e) => setHost(e.target.value),
181
- placeholder: "0.0.0.0"
182
- }),
183
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
184
- className: "at-label",
185
- children: "端口"
186
- }),
187
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
188
- className: "at-input at-port",
189
- type: "number",
190
- value: port,
191
- min: 1,
192
- max: 65535,
193
- onChange: (e) => setPort(Number(e.target.value) || 0)
194
- })
195
- ]
196
- }),
197
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
198
- className: "at-list",
199
- children: [
200
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
201
- className: "at-head",
202
- children: [
203
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
204
- className: "at-col at-colUser",
205
- children: "用户名"
206
- }),
207
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
208
- className: "at-col at-colPass",
209
- children: "密码"
210
- }),
211
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "at-col at-colOp" })
212
- ]
213
- }),
214
- drafts.map((d) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
215
- className: "at-item",
216
- children: [
217
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
218
- className: "at-input at-colUser",
219
- value: d.user,
220
- placeholder: "用户名",
221
- onChange: (e) => updateDraft(d.id, { user: e.target.value })
222
- }),
223
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
224
- className: "at-input at-colPass",
225
- type: "password",
226
- value: d.pass,
227
- placeholder: d.isNew ? "必填" : d.hasPassword ? "留空保留原密码" : "未设置",
228
- onChange: (e) => updateDraft(d.id, { pass: e.target.value })
229
- }),
230
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
231
- type: "button",
232
- className: "at-del",
233
- onClick: () => removeRow(d.id),
234
- children: "删除"
235
- })
236
- ]
237
- }, d.id)),
238
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
239
- type: "button",
240
- className: "at-add",
241
- onClick: addRow,
242
- children: "+ 添加账号"
243
- })
244
- ]
245
- }),
246
- error !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
247
- className: "at-error",
248
- children: error
249
- }),
250
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
251
- className: "at-actions",
252
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
253
- type: "button",
254
- className: "at-save",
255
- disabled: busy,
256
- onClick: () => void save(),
257
- children: busy ? "保存中…" : "保存"
258
- })
259
- })
260
- ]
261
- });
262
- }
263
- //#endregion
264
10
  //#region \0sp-css:/home/yh/home/home-serenity/AI_LAB/dsh-serenity-plugin/hooks/dsh-serenity-hooks/src/client/SafeModePanel
265
- const css$1 = "/* SafeModePanel — 会话头部 Serenity 状态卡片(v1.21 UX 重构)\n * 卡片 → 点击 → 官方 Modal(body portal 遮罩 + 居中)\n * 遵循 web-styling.md:颜色只用 --dsw-alias-* 语义 token(明暗自适应);\n * 前缀 sp- 防冲突。 */\n\n/* ── 头部状态卡片(可点击) ── */\n\n.sp-card {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n height: 26px;\n padding: 0 10px;\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: 8px;\n background: var(--dsw-alias-bg-module-platform);\n color: inherit;\n font-size: 12px;\n cursor: pointer;\n transition: background var(--ds-transition-duration-fast, 0.1s) var(--ds-ease-in-out, ease),\n border-color var(--ds-transition-duration-fast, 0.1s) var(--ds-ease-in-out, ease);\n}\n\n.sp-card:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n border-color: var(--dsw-alias-border-l2);\n}\n\n.sp-card:focus-visible {\n outline: 2px solid var(--dsw-alias-button-ghost-active-border);\n outline-offset: 1px;\n}\n\n.sp-card:disabled {\n opacity: 0.6;\n cursor: default;\n}\n\n.sp-dot {\n flex: none;\n width: 7px;\n height: 7px;\n border-radius: 50%;\n}\n\n.sp-dotOn {\n background: var(--dsw-alias-state-success-primary);\n}\n\n.sp-dotOff {\n background: var(--dsw-alias-label-dimmed);\n}\n\n.sp-brand {\n color: var(--dsw-alias-label-primary);\n font-weight: 500;\n}\n\n.sp-chev {\n color: var(--dsw-alias-label-dimmed);\n}\n\n/* safe-mode 徽标(卡片本体直接可见;ON 红 / OFF 灰) */\n.sp-sm {\n display: inline-flex;\n align-items: center;\n gap: 3px;\n height: 18px;\n padding: 0 7px;\n border-radius: 9px;\n font-size: 11px;\n font-weight: 600;\n line-height: 1;\n}\n\n.sp-smOn {\n background: var(--dsw-alias-state-warning-bg, rgba(245, 158, 11, 0.18));\n color: var(--dsw-alias-state-warning-fg, #f59e0b);\n border: 1px solid var(--dsw-alias-state-warning-border, rgba(245, 158, 11, 0.4));\n}\n\n.sp-smOff {\n background: var(--dsw-alias-interactive-bg-active, rgba(128, 128, 128, 0.16));\n color: var(--dsw-alias-label-dimmed);\n border: 1px solid var(--dsw-alias-border-l1);\n}\n\n.sp-smIcon {\n flex: none;\n}\n\n/* ── 官方 Modal 定制(宽度 + 无滚动条) ── */\n\n.sp-modal {\n width: 480px;\n max-width: calc(100vw - 48px);\n /* 无滚动条约束:内容在视口内完整容纳(紧凑布局,溢出不滚动) */\n}\n\n/* ── tab 栏 ── */\n\n.sp-tabs {\n display: flex;\n gap: 4px;\n padding: 0 2px 10px;\n border-bottom: 1px solid var(--dsw-alias-border-l1);\n}\n\n.sp-tab {\n flex: 1;\n padding: 7px 0;\n border: none;\n border-radius: 6px;\n background: transparent;\n color: var(--dsw-alias-label-secondary);\n font-size: 13px;\n cursor: pointer;\n}\n\n.sp-tab:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n.sp-tabOn {\n background: var(--dsw-alias-interactive-bg-active, rgba(128, 128, 128, 0.2));\n color: var(--dsw-alias-label-primary);\n font-weight: 600;\n}\n\n/* ── 模态内容(紧凑布局,无滚动条) ── */\n\n.sp-modalBody {\n display: flex;\n flex-direction: column;\n gap: 8px;\n padding-top: 12px;\n /* 内容超出视口时 body 区滚动(兜底——不再截断);整卡不滚动 */\n overflow-y: auto;\n max-height: calc(100vh - 200px);\n}\n\n.sp-row {\n display: flex;\n align-items: baseline;\n gap: 10px;\n min-width: 0;\n}\n\n.sp-label {\n flex: none;\n width: 40px;\n font-size: 12px;\n color: var(--dsw-alias-label-caption);\n}\n\n.sp-value {\n flex: 1;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n font-size: 12px;\n color: var(--dsw-alias-label-secondary);\n font-family: var(--ds-font-family-code, monospace);\n word-break: break-all;\n}\n\n.sp-actions {\n display: flex;\n justify-content: flex-end;\n padding-top: 4px;\n border-top: 1px solid var(--dsw-alias-border-l1);\n margin-top: 2px;\n}\n\n/* safe-mode 开关(大) */\n\n.sp-toggle {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n height: 24px;\n padding: 0 10px;\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: 6px;\n font-size: 12px;\n cursor: pointer;\n}\n\n.sp-toggleLg {\n height: 30px;\n padding: 0 16px;\n font-size: 13px;\n}\n\n.sp-toggleOn {\n background: var(--dsw-alias-state-warning-bg, rgba(245, 158, 11, 0.15));\n color: var(--dsw-alias-state-warning-fg, #f59e0b);\n border-color: var(--dsw-alias-state-warning-border, rgba(245, 158, 11, 0.4));\n}\n\n.sp-toggleOff {\n background: var(--dsw-alias-interactive-bg-hover);\n color: var(--dsw-alias-label-secondary);\n}\n\n.sp-toggleIcon {\n flex: none;\n}\n\n/* loop 运行状态区块(并行任务各自一行) */\n\n.sp-section {\n display: flex;\n flex-direction: column;\n gap: 4px;\n padding-top: 6px;\n border-top: 1px solid var(--dsw-alias-border-l1);\n margin-top: 2px;\n}\n\n.sp-loopItem {\n display: flex;\n flex-direction: column;\n gap: 2px;\n padding: 2px 0 2px 50px;\n min-width: 0;\n}\n\n.sp-loopHead {\n display: flex;\n align-items: baseline;\n gap: 6px;\n min-width: 0;\n}\n\n.sp-loopLabel {\n flex: 1;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n font-size: 12px;\n color: var(--dsw-alias-label-primary);\n}\n\n.sp-loopRound {\n flex: none;\n font-size: 11px;\n color: var(--dsw-alias-label-dimmed);\n}\n\n.sp-loopTime {\n flex: none;\n font-size: 11px;\n color: var(--dsw-alias-label-dimmed);\n}\n\n.sp-loopResp {\n font-size: 11px;\n color: var(--dsw-alias-label-secondary);\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n/* loop 详情折叠控件 */\n.sp-loopsToggle {\n flex: none;\n padding: 2px 8px;\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: 5px;\n background: transparent;\n color: var(--dsw-alias-label-secondary);\n font-size: 11px;\n cursor: pointer;\n}\n\n.sp-loopsToggle:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n.sp-loopsList {\n display: flex;\n flex-direction: column;\n gap: 4px;\n margin-top: 2px;\n}\n\n.sp-loopsMore {\n font-size: 11px;\n color: var(--dsw-alias-label-dimmed);\n padding: 2px 0 2px 50px;\n}\n\n";
11
+ const css$2 = "/* SafeModePanel — 会话头部 Serenity 状态卡片(v1.22 UX 重构)\n * 卡片 → 点击 → 官方 Modal(body portal 遮罩 + 居中)\n * 模态内:tab 栏 + 分组(groupTitle 标题 + rowCard 行卡)——看齐官方 settings 设计语言\n * 遵循 web-styling.md:颜色只用 --dsw-alias-* 语义 token(明暗自适应);\n * 前缀 sp- 防冲突。 */\n\n/* ── 头部状态卡片(可点击) ── */\n\n.sp-card {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n height: 26px;\n padding: 0 10px;\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: 8px;\n background: var(--dsw-alias-bg-module-platform);\n color: inherit;\n font-size: 12px;\n cursor: pointer;\n transition: background var(--ds-transition-duration-fast, 0.1s) var(--ds-ease-in-out, ease),\n border-color var(--ds-transition-duration-fast, 0.1s) var(--ds-ease-in-out, ease);\n}\n\n.sp-card:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n border-color: var(--dsw-alias-border-l2);\n}\n\n.sp-card:focus-visible {\n outline: 2px solid var(--dsw-alias-button-ghost-active-border);\n outline-offset: 1px;\n}\n\n.sp-card:disabled {\n opacity: 0.6;\n cursor: default;\n}\n\n.sp-dot {\n flex: none;\n width: 7px;\n height: 7px;\n border-radius: 50%;\n}\n\n.sp-dotOn {\n background: var(--dsw-alias-state-success-primary);\n}\n\n.sp-dotOff {\n background: var(--dsw-alias-label-dimmed);\n}\n\n.sp-brand {\n color: var(--dsw-alias-label-primary);\n font-weight: 500;\n}\n\n.sp-chev {\n color: var(--dsw-alias-label-dimmed);\n}\n\n/* safe-mode 徽标(卡片本体直接可见;ON 红 / OFF 灰) */\n.sp-sm {\n display: inline-flex;\n align-items: center;\n gap: 3px;\n height: 18px;\n padding: 0 7px;\n border-radius: 9px;\n font-size: 11px;\n font-weight: 600;\n line-height: 1;\n}\n\n.sp-smOn {\n background: var(--dsw-alias-state-warning-bg, rgba(245, 158, 11, 0.18));\n color: var(--dsw-alias-state-warning-fg, #f59e0b);\n border: 1px solid var(--dsw-alias-state-warning-border, rgba(245, 158, 11, 0.4));\n}\n\n.sp-smOff {\n background: var(--dsw-alias-interactive-bg-active, rgba(128, 128, 128, 0.16));\n color: var(--dsw-alias-label-dimmed);\n border: 1px solid var(--dsw-alias-border-l1);\n}\n\n.sp-smIcon {\n flex: none;\n}\n\n/* ── 官方 Modal 定制(宽度 + 内容紧凑) ── */\n\n.sp-modal {\n width: 520px;\n max-width: calc(100vw - 48px);\n}\n\n/* ── tab 栏 ── */\n\n.sp-tabs {\n display: flex;\n gap: 6px;\n padding: 0 2px 12px;\n border-bottom: 1px solid var(--dsw-alias-border-l1);\n}\n\n.sp-tab {\n flex: 1;\n padding: 8px 0;\n border: none;\n border-radius: 8px;\n background: transparent;\n color: var(--dsw-alias-label-secondary);\n font-size: 13px;\n font-weight: 500;\n cursor: pointer;\n transition: background var(--ds-transition-duration-fast, 0.1s) var(--ds-ease-in-out, ease);\n}\n\n.sp-tab:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n.sp-tabOn {\n background: var(--dsw-alias-interactive-bg-active, rgba(128, 128, 128, 0.2));\n color: var(--dsw-alias-label-primary);\n font-weight: 600;\n}\n\n/* ── 模态内容 ── */\n\n.sp-modalBody {\n display: flex;\n flex-direction: column;\n gap: 16px;\n padding-top: 14px;\n /* 内容超出视口时 body 区滚动(兜底——不再截断);整卡不滚动 */\n overflow-y: auto;\n max-height: calc(100vh - 200px);\n}\n\n/* ── 分组(官方 settings 语言:groupTitle + rows) ── */\n\n.sp-group {\n display: flex;\n flex-direction: column;\n gap: 8px;\n}\n\n.sp-groupHead {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 8px;\n}\n\n.sp-groupTitle {\n margin: 0;\n font-size: 12px;\n line-height: 18px;\n font-weight: 600;\n letter-spacing: 0.02em;\n text-transform: uppercase;\n color: var(--dsw-alias-label-secondary);\n}\n\n.sp-rows {\n display: flex;\n flex-direction: column;\n gap: 6px;\n}\n\n/* 行卡(rowCard:border + radius + padding;值与 label 分离、长内容可换行) */\n.sp-row {\n display: flex;\n align-items: baseline;\n gap: 12px;\n min-width: 0;\n padding: 8px 12px;\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: 10px;\n background: var(--dsw-alias-bg-module);\n}\n\n.sp-label {\n flex: none;\n width: 76px;\n font-size: 12px;\n font-weight: 500;\n color: var(--dsw-alias-label-secondary);\n white-space: nowrap;\n}\n\n.sp-value {\n flex: 1;\n min-width: 0;\n font-size: 12px;\n line-height: 18px;\n color: var(--dsw-alias-label-primary);\n font-family: var(--ds-font-family-code, monospace);\n word-break: break-all;\n overflow-wrap: anywhere;\n}\n\n/* 安全模式动作区 */\n.sp-actions {\n display: flex;\n align-items: center;\n gap: 12px;\n padding: 8px 12px;\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: 10px;\n background: var(--dsw-alias-bg-module);\n}\n\n.sp-toggle {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n height: 24px;\n padding: 0 10px;\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: 6px;\n font-size: 12px;\n cursor: pointer;\n transition: background var(--ds-transition-duration-fast, 0.1s) var(--ds-ease-in-out, ease);\n}\n\n.sp-toggleLg {\n height: 30px;\n padding: 0 16px;\n font-size: 13px;\n}\n\n.sp-toggleOn {\n background: var(--dsw-alias-state-warning-bg, rgba(245, 158, 11, 0.15));\n color: var(--dsw-alias-state-warning-fg, #f59e0b);\n border-color: var(--dsw-alias-state-warning-border, rgba(245, 158, 11, 0.4));\n}\n\n.sp-toggleOff {\n background: var(--dsw-alias-interactive-bg-hover);\n color: var(--dsw-alias-label-secondary);\n}\n\n.sp-toggleIcon {\n flex: none;\n}\n\n.sp-toggleDesc {\n flex: 1;\n font-size: 12px;\n line-height: 18px;\n color: var(--dsw-alias-label-tertiary);\n}\n\n/* loop 运行状态区块 */\n\n.sp-loopsToggle {\n flex: none;\n padding: 2px 10px;\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: 6px;\n background: transparent;\n color: var(--dsw-alias-label-secondary);\n font-size: 11px;\n cursor: pointer;\n}\n\n.sp-loopsToggle:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n.sp-loopsList {\n display: flex;\n flex-direction: column;\n gap: 4px;\n margin-top: 2px;\n}\n\n.sp-loopItem {\n display: flex;\n flex-direction: column;\n gap: 2px;\n padding: 8px 12px;\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: 8px;\n min-width: 0;\n}\n\n.sp-loopHead {\n display: flex;\n align-items: baseline;\n gap: 8px;\n min-width: 0;\n}\n\n.sp-loopLabel {\n flex: 1;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n font-size: 12px;\n font-weight: 500;\n color: var(--dsw-alias-label-primary);\n}\n\n.sp-loopRound {\n flex: none;\n font-size: 11px;\n color: var(--dsw-alias-label-dimmed);\n}\n\n.sp-loopTime {\n flex: none;\n font-size: 11px;\n color: var(--dsw-alias-label-dimmed);\n}\n\n.sp-loopResp {\n font-size: 11px;\n line-height: 16px;\n color: var(--dsw-alias-label-secondary);\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.sp-loopsMore {\n font-size: 11px;\n color: var(--dsw-alias-label-dimmed);\n padding: 4px 12px;\n}\n\n/* 底部提示(配置入口引导) */\n.sp-hint {\n margin: 0;\n font-size: 12px;\n line-height: 18px;\n color: var(--dsw-alias-label-tertiary);\n word-break: break-word;\n overflow-wrap: anywhere;\n padding: 8px 12px;\n border: 1px dashed var(--dsw-alias-border-l2);\n border-radius: 10px;\n}\n";
266
12
  if (typeof document !== "undefined" && document.querySelector("style[data-sp-css]") === null) {
267
13
  const tag = document.createElement("style");
268
14
  tag.setAttribute("data-sp-css", "1");
269
- tag.textContent = css$1;
15
+ tag.textContent = css$2;
270
16
  document.head.appendChild(tag);
271
17
  }
272
18
  //#endregion
@@ -277,12 +23,11 @@ window.__ModuleLoader__.load({
277
23
  function cx(...parts) {
278
24
  return parts.filter(Boolean).join(" ");
279
25
  }
280
- /** 会话头部:Serenity 状态卡片(点击 → 官方 Modal 居中,双 tab) */
26
+ /** 会话头部:CCC 状态栏卡片(点击 → 官方 Modal 居中,纯状态展示) */
281
27
  function SafeModePanel(props) {
282
28
  const [status, setStatus] = (0, react.useState)(null);
283
29
  const [busy, setBusy] = (0, react.useState)(false);
284
30
  const [open, setOpen] = (0, react.useState)(false);
285
- const [tab, setTab] = (0, react.useState)("status");
286
31
  const [loops, setLoops] = (0, react.useState)([]);
287
32
  const [loopsOpen, setLoopsOpen] = (0, react.useState)(false);
288
33
  const sessionId = props.sessionId;
@@ -346,6 +91,7 @@ window.__ModuleLoader__.load({
346
91
  })]
347
92
  });
348
93
  const inCcc = status.root !== null;
94
+ const runningLoops = loops.filter((l) => !l.done);
349
95
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
350
96
  type: "button",
351
97
  className: cx("sp-card"),
@@ -372,149 +118,182 @@ window.__ModuleLoader__.load({
372
118
  className: cx("sp-chev")
373
119
  })
374
120
  ]
375
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(_deepseek_ai_dsh_client_ui_primitives.Modal, {
121
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Modal, {
376
122
  open,
377
123
  onClose: () => setOpen(false),
378
- title: "Serenity 高级设定",
124
+ title: "CCC 状态栏",
379
125
  closeLabel: "关闭",
380
126
  className: cx("sp-modal"),
381
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
382
- className: cx("sp-tabs"),
383
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
384
- type: "button",
385
- className: cx("sp-tab", tab === "status" && "sp-tabOn"),
386
- onClick: () => setTab("status"),
387
- children: "状态"
388
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
389
- type: "button",
390
- className: cx("sp-tab", tab === "accounts" && "sp-tabOn"),
391
- onClick: () => setTab("accounts"),
392
- children: "账号"
393
- })]
394
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
127
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
395
128
  className: cx("sp-modalBody"),
396
- children: tab === "accounts" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AccountsTab, { sessionId: String(sessionId ?? "") }) : inCcc ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
129
+ children: inCcc ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
397
130
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
398
- className: cx("sp-row"),
399
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
400
- className: cx("sp-label"),
401
- children: "CCC"
402
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
403
- className: cx("sp-value"),
404
- title: status.root ?? void 0,
405
- children: status.root
406
- })]
407
- }),
408
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
409
- className: cx("sp-row"),
410
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
411
- className: cx("sp-label"),
412
- children: "loop"
413
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
414
- className: cx("sp-value"),
415
- title: status.loopModel ? `loop.defaultModel: ${status.loopModel}` : void 0,
416
- children: status.loopModel ?? "未配置"
417
- })]
418
- }),
419
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
420
- className: cx("sp-row"),
421
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
422
- className: cx("sp-label"),
423
- children: "守卫"
424
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
425
- className: cx("sp-value"),
131
+ className: cx("sp-group"),
132
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
133
+ className: cx("sp-groupTitle"),
134
+ children: "运行环境"
135
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
136
+ className: cx("sp-rows"),
426
137
  children: [
427
- "blacklist ",
428
- status.blacklist.length,
429
- " 条",
430
- status.threshold !== null ? ` · keeper 阈值 ${status.threshold}` : ""
138
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
139
+ className: cx("sp-row"),
140
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
141
+ className: cx("sp-label"),
142
+ children: "CCC"
143
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
144
+ className: cx("sp-value"),
145
+ title: status.root ?? void 0,
146
+ children: status.root
147
+ })]
148
+ }),
149
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
150
+ className: cx("sp-row"),
151
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
152
+ className: cx("sp-label"),
153
+ children: "Loop 模型"
154
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
155
+ className: cx("sp-value"),
156
+ title: status.loopModel ? `loop.defaultModel: ${status.loopModel}` : void 0,
157
+ children: status.loopModel ?? "未配置"
158
+ })]
159
+ }),
160
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
161
+ className: cx("sp-row"),
162
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
163
+ className: cx("sp-label"),
164
+ children: "守卫"
165
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
166
+ className: cx("sp-value"),
167
+ children: [
168
+ "blacklist ",
169
+ status.blacklist.length,
170
+ " 条",
171
+ status.threshold !== null ? ` · keeper 阈值 ${status.threshold}` : ""
172
+ ]
173
+ })]
174
+ })
431
175
  ]
432
176
  })]
433
177
  }),
434
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
435
- className: cx("sp-actions"),
436
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
437
- type: "button",
438
- className: cx("sp-toggle", status.safeModeOn ? "sp-toggleOn" : "sp-toggleOff", "sp-toggleLg"),
439
- disabled: busy,
440
- onClick: () => void toggle(!status.safeModeOn),
441
- children: [
442
- status.safeModeOn && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconWarningOutline16, {
443
- size: 14,
444
- className: cx("sp-toggleIcon")
445
- }),
446
- "safe-mode ",
447
- status.safeModeOn ? "ON" : "OFF"
448
- ]
449
- })
178
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
179
+ className: cx("sp-group"),
180
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
181
+ className: cx("sp-groupTitle"),
182
+ children: "安全模式"
183
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
184
+ className: cx("sp-actions"),
185
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
186
+ type: "button",
187
+ className: cx("sp-toggle", status.safeModeOn ? "sp-toggleOn" : "sp-toggleOff", "sp-toggleLg"),
188
+ disabled: busy,
189
+ onClick: () => void toggle(!status.safeModeOn),
190
+ children: [
191
+ status.safeModeOn && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconWarningOutline16, {
192
+ size: 14,
193
+ className: cx("sp-toggleIcon")
194
+ }),
195
+ "safe-mode ",
196
+ status.safeModeOn ? "ON" : "OFF"
197
+ ]
198
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
199
+ className: cx("sp-toggleDesc"),
200
+ children: status.safeModeOn ? "写工具已隐藏,无人值守安全航行" : "写工具可用(点击开启)"
201
+ })]
202
+ })]
450
203
  }),
451
204
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
452
- className: cx("sp-section"),
453
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
454
- className: cx("sp-row"),
455
- children: [
456
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
457
- className: cx("sp-label"),
458
- children: "loops"
459
- }),
460
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
461
- className: cx("sp-value"),
462
- children: loops.filter((l) => !l.done).length > 0 ? `运行中 ${loops.filter((l) => !l.done).length}${loops.length > 0 ? ` / 共 ${loops.length}` : ""}` : "无运行中 loop"
463
- }),
464
- loops.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
205
+ className: cx("sp-group"),
206
+ children: [
207
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
208
+ className: cx("sp-groupHead"),
209
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
210
+ className: cx("sp-groupTitle"),
211
+ children: "Loop 运行"
212
+ }), loops.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
465
213
  type: "button",
466
214
  className: cx("sp-loopsToggle"),
467
215
  onClick: () => setLoopsOpen((v) => !v),
468
216
  children: loopsOpen ? "收起" : "详情"
217
+ })]
218
+ }),
219
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
220
+ className: cx("sp-rows"),
221
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
222
+ className: cx("sp-row"),
223
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
224
+ className: cx("sp-label"),
225
+ children: "状态"
226
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
227
+ className: cx("sp-value"),
228
+ children: runningLoops.length > 0 ? `运行中 ${runningLoops.length}${loops.length > 0 ? ` / 共 ${loops.length}` : ""}` : "无运行中 loop"
229
+ })]
469
230
  })
470
- ]
471
- }), loopsOpen && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
472
- className: cx("sp-loopsList"),
473
- children: [loops.slice(0, 3).map((l) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
474
- className: cx("sp-loopItem"),
475
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
476
- className: cx("sp-loopHead"),
231
+ }),
232
+ loopsOpen && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
233
+ className: cx("sp-loopsList"),
234
+ children: [loops.slice(0, 3).map((l) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
235
+ className: cx("sp-loopItem"),
236
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
237
+ className: cx("sp-loopHead"),
238
+ children: [
239
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
240
+ className: cx("sp-loopLabel"),
241
+ title: l.label,
242
+ children: l.label
243
+ }),
244
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
245
+ className: cx("sp-loopRound"),
246
+ children: l.done ? "✓ 完成" : `R${l.round}`
247
+ }),
248
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
249
+ className: cx("sp-loopTime"),
250
+ children: new Date(l.updated).toLocaleTimeString()
251
+ })
252
+ ]
253
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
254
+ className: cx("sp-loopResp"),
255
+ title: l.lastResponse,
256
+ children: l.lastResponse.slice(0, 80) || (l.done ? "已完成" : "(等待首轮响应)")
257
+ })]
258
+ }, l.label)), loops.length > 3 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
259
+ className: cx("sp-loopsMore"),
477
260
  children: [
478
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
479
- className: cx("sp-loopLabel"),
480
- title: l.label,
481
- children: l.label
482
- }),
483
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
484
- className: cx("sp-loopRound"),
485
- children: l.done ? "✓" : `R${l.round}`
486
- }),
487
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
488
- className: cx("sp-loopTime"),
489
- children: new Date(l.updated).toLocaleTimeString()
490
- })
261
+ "… 共 ",
262
+ loops.length,
263
+ " 个 loop(其余见等待界面)"
491
264
  ]
492
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
493
- className: cx("sp-loopResp"),
494
- title: l.lastResponse,
495
- children: l.lastResponse.slice(0, 80) || (l.done ? "已完成" : "(等待首轮响应)")
496
265
  })]
497
- }, l.label)), loops.length > 3 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
498
- className: cx("sp-loopsMore"),
499
- children: [
500
- "… 共 ",
501
- loops.length,
502
- " 个 loop(其余见等待界面)"
503
- ]
504
- })]
505
- })]
266
+ })
267
+ ]
268
+ }),
269
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
270
+ className: cx("sp-hint"),
271
+ children: [
272
+ "💡 开关/阈值/账号/工作区白名单请在 ",
273
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: "设置 → Serenity" }),
274
+ " 中配置。"
275
+ ]
506
276
  })
507
277
  ] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
508
- className: cx("sp-row"),
509
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
510
- className: cx("sp-label"),
278
+ className: cx("sp-group"),
279
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
280
+ className: cx("sp-groupTitle"),
511
281
  children: "状态"
512
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
513
- className: cx("sp-value"),
514
- children: "ACC 未激活(工作区不在 CCC 内)"
282
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
283
+ className: cx("sp-rows"),
284
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
285
+ className: cx("sp-row"),
286
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
287
+ className: cx("sp-label"),
288
+ children: "ACC"
289
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
290
+ className: cx("sp-value"),
291
+ children: "未激活(工作区不在 CCC 内)"
292
+ })]
293
+ })
515
294
  })]
516
295
  })
517
- })]
296
+ })
518
297
  })] });
519
298
  }
520
299
  //#endregion
@@ -528,8 +307,8 @@ window.__ModuleLoader__.load({
528
307
  * 单图:用户提供了一张图片(路径:...);多图:每张一行路径
529
308
  */
530
309
  function imageNoteTemplate(paths) {
531
- if (paths.length === 1) return `用户提供了一张图片(路径:${paths[0]})`;
532
- return `用户提供了 ${paths.length} 张图片:\n${paths.map((p) => `- ${p}`).join("\n")}`;
310
+ if (paths.length === 1) return `The user provided an image (path: ${paths[0]})`;
311
+ return `The user provided ${paths.length} images:\n${paths.map((p) => `- ${p}`).join("\n")}`;
533
312
  }
534
313
  /** 浏览器 File → base64(与 ui-conversation serializeImages 等价的最小实现) */
535
314
  function fileToBase64(file) {
@@ -641,6 +420,533 @@ window.__ModuleLoader__.load({
641
420
  return null;
642
421
  }
643
422
  //#endregion
423
+ //#region src/client/accounts-api.ts
424
+ /** wire 账号 → 本地编辑行 */
425
+ function accountDraftFromWire(a) {
426
+ return {
427
+ id: a.id,
428
+ user: a.user,
429
+ pass: "",
430
+ isNew: false,
431
+ hasPassword: a.hasPassword,
432
+ hasTotp: a.hasTotp,
433
+ totpState: "none"
434
+ };
435
+ }
436
+ /** 生成新账号 id(本地编辑用;提交时若为空由服务端拒绝) */
437
+ function newAccountId() {
438
+ return `a-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`;
439
+ }
440
+ const B32_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
441
+ /** 字节 → base32(无填充) */
442
+ function bytesToBase32(bytes) {
443
+ let bits = 0;
444
+ let value = 0;
445
+ let out = "";
446
+ for (const byte of bytes) {
447
+ value = value << 8 | byte;
448
+ bits += 8;
449
+ while (bits >= 5) {
450
+ out += B32_ALPHABET[value >>> bits - 5 & 31];
451
+ bits -= 5;
452
+ }
453
+ }
454
+ if (bits > 0) out += B32_ALPHABET[value << 5 - bits & 31];
455
+ return out;
456
+ }
457
+ /** 生成 20 字节随机 TOTP secret(Web Crypto getRandomValues) */
458
+ function newTotpSecret() {
459
+ const bytes = /* @__PURE__ */ new Uint8Array(20);
460
+ if (typeof crypto !== "undefined" && typeof crypto.getRandomValues === "function") crypto.getRandomValues(bytes);
461
+ else for (let i = 0; i < bytes.length; i++) bytes[i] = Math.floor(Math.random() * 256);
462
+ return bytesToBase32(bytes);
463
+ }
464
+ /** 构造 otpauth URI(Authenticator 扫码/手动录入) */
465
+ function otpauthUriClient(secret, label, issuer = "Serenity Home") {
466
+ const params = new URLSearchParams({
467
+ secret,
468
+ issuer,
469
+ algorithm: "SHA1",
470
+ digits: "6",
471
+ period: "30"
472
+ });
473
+ return `otpauth://totp/${encodeURIComponent(label)}?${params.toString()}`;
474
+ }
475
+ /** 本地编辑行 → wire 账号(提交时;pass 字段携带) */
476
+ function accountToWire(d) {
477
+ return {
478
+ id: d.id,
479
+ user: d.user,
480
+ pass: d.pass,
481
+ ...d.totpState === "pending" && d.totpSecret !== void 0 && d.totpSecret !== "" ? { totpSecret: d.totpSecret } : d.totpState === "clear" ? { totpReset: true } : {}
482
+ };
483
+ }
484
+ /** 校验本地编辑行:user 非空;新账号必须设密码;pending TOTP 需确认码 */
485
+ function validateDraft(d) {
486
+ if (d.user.trim() === "") return "用户名不能为空";
487
+ if (d.isNew && d.pass === "") return "新账号必须设置密码";
488
+ if (d.totpState === "pending" && !/^\d{6}$/.test(d.totpConfirm ?? "")) return `绑定 ${d.user} 的验证器需输入 6 位确认码`;
489
+ return null;
490
+ }
491
+ const CONFIG_PATH = "/serenity/config";
492
+ /**
493
+ * 获取已有工作区列表(需求 1:白名单从已有工作区选择而非手输)。
494
+ * 走 DSH 原生 RPC `POST /api/workspace.list`(JSON RPC 形态)。
495
+ * 信封必须是完整 ClientRequest:`{ type: 'client-request', rpcId, method, payload }`
496
+ * (api/rpc.ts wire 契约——缺 type/method → clientRequestSchema 校验失败 → bad-request)。
497
+ * 返回 { path, title }[];失败返回空数组(面板显示"暂无可选工作区")。
498
+ */
499
+ async function fetchWorkspaces() {
500
+ try {
501
+ const res = await fetch("/api/workspace.list", {
502
+ method: "POST",
503
+ headers: { "content-type": "application/json" },
504
+ body: JSON.stringify({
505
+ type: "client-request",
506
+ rpcId: `ws-${Date.now().toString(36)}`,
507
+ method: "workspace.list",
508
+ payload: {}
509
+ })
510
+ });
511
+ if (!res.ok) return [];
512
+ const items = (await res.json())?.result?.value?.items;
513
+ if (!Array.isArray(items)) return [];
514
+ return items.filter((i) => typeof i.path === "string" && i.path !== "").map((i) => ({
515
+ path: i.path,
516
+ title: typeof i.title === "string" && i.title !== "" ? i.title : i.path
517
+ }));
518
+ } catch {
519
+ return [];
520
+ }
521
+ }
522
+ /** GET 配置(含账号列表;无 hash) */
523
+ async function fetchConfig() {
524
+ const res = await fetch(CONFIG_PATH, { headers: {
525
+ accept: "application/json",
526
+ "x-serenity-ui": "1"
527
+ } });
528
+ if (!res.ok) return null;
529
+ return (await res.json()).config ?? null;
530
+ }
531
+ /** PUT 配置(账号 patch + 工作区白名单)→ 返回保存后的 wire */
532
+ async function saveConfig(patch) {
533
+ const res = await fetch(CONFIG_PATH, {
534
+ method: "PUT",
535
+ headers: {
536
+ "content-type": "application/json",
537
+ "x-serenity-ui": "1"
538
+ },
539
+ body: JSON.stringify({ config: patch })
540
+ });
541
+ if (!res.ok) {
542
+ const body = await res.json().catch(() => null);
543
+ throw new Error(body?.error ?? `保存失败(${res.status})`);
544
+ }
545
+ return (await res.json()).config ?? null;
546
+ }
547
+ //#endregion
548
+ //#region \0sp-css:/home/yh/home/home-serenity/AI_LAB/dsh-serenity-plugin/hooks/dsh-serenity-hooks/src/client/AccountsEditor
549
+ const css$1 = "/* AccountsEditor — DSH 设置面板「外部访问」区块(v1.22)\n * 与 SettingsSection ss-* 同设计语言:--dsw-alias-* 语义 token,明暗自适应。 */\n\n.ae-root {\n display: flex;\n flex-direction: column;\n gap: 16px;\n padding: 12px 14px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 12px;\n font-size: 13px;\n}\n\n.ae-warn {\n margin: 0;\n font-size: 12px;\n line-height: 18px;\n color: var(--dsw-alias-state-warning-fg, #f59e0b);\n word-break: break-word;\n}\n\n.ae-block {\n display: flex;\n flex-direction: column;\n gap: 8px;\n}\n\n.ae-title {\n margin: 0;\n font-size: 12px;\n line-height: 18px;\n font-weight: 600;\n letter-spacing: 0.02em;\n text-transform: uppercase;\n color: var(--dsw-alias-label-secondary);\n}\n\n.ae-desc {\n margin: 0;\n font-size: 12px;\n line-height: 18px;\n color: var(--dsw-alias-label-tertiary);\n word-break: break-word;\n overflow-wrap: anywhere;\n}\n\n.ae-note {\n margin: 0;\n font-size: 12px;\n line-height: 18px;\n color: var(--dsw-alias-label-tertiary);\n}\n\n/* ── 监听设置 / 白名单输入行 ── */\n\n.ae-row {\n display: flex;\n align-items: center;\n gap: 8px;\n}\n\n.ae-label {\n flex: none;\n font-size: 12px;\n font-weight: 500;\n color: var(--dsw-alias-label-secondary);\n white-space: nowrap;\n}\n\n.ae-input {\n flex: 1;\n min-width: 0;\n padding: 6px 10px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 8px;\n background: var(--dsw-alias-bg-module);\n color: var(--dsw-alias-label-primary);\n font-size: 13px;\n outline: none;\n}\n\n.ae-input:focus {\n border-color: var(--dsw-alias-button-primary-bg, var(--dsw-alias-accent, #3b82f6));\n}\n\n.ae-input::placeholder {\n color: var(--dsw-alias-label-dimmed);\n}\n\n.ae-input.ae-port {\n flex: none;\n width: 76px;\n}\n\n/* ── 账号列表 ── */\n\n.ae-list {\n display: flex;\n flex-direction: column;\n gap: 4px;\n}\n\n.ae-head {\n display: flex;\n gap: 8px;\n font-size: 11px;\n font-weight: 500;\n color: var(--dsw-alias-label-dimmed);\n padding: 0 2px;\n text-transform: uppercase;\n letter-spacing: 0.02em;\n}\n\n.ae-item {\n display: flex;\n gap: 8px;\n align-items: center;\n padding: 6px 2px;\n}\n\n.ae-colUser { flex: 1.2; }\n.ae-colPass { flex: 1.6; }\n.ae-colTotp { flex: 2.2; min-width: 0; }\n.ae-colOp { flex: none; width: 48px; }\n\n/* ── v1.22.4 TOTP 第二因素 ── */\n\n.ae-totpBtn {\n flex: none;\n padding: 4px 10px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 6px;\n background: transparent;\n color: var(--dsw-alias-label-secondary);\n font-size: 12px;\n cursor: pointer;\n white-space: nowrap;\n}\n\n.ae-totpBtn:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n.ae-totpPending {\n display: flex;\n flex-wrap: wrap;\n gap: 6px;\n align-items: center;\n min-width: 0;\n}\n\n.ae-totpSecret {\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n font-size: 11px;\n background: var(--dsw-alias-surface-raised, rgba(0, 0, 0, 0.25));\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: 4px;\n padding: 2px 6px;\n color: var(--dsw-alias-label-secondary);\n word-break: break-all;\n}\n\n.ae-totpUri {\n font-size: 11px;\n color: var(--dsw-alias-accent, #3b82f6);\n text-decoration: none;\n}\n\n.ae-totpUri:hover {\n text-decoration: underline;\n}\n\n.ae-totpConfirm {\n max-width: 120px;\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n letter-spacing: 0.2em;\n}\n\n.ae-totpBadge {\n font-size: 12px;\n color: var(--dsw-alias-state-warning-fg, #f59e0b);\n}\n\n.ae-totpClear {\n display: flex;\n gap: 6px;\n align-items: center;\n}\n\n.ae-check {\n display: flex;\n gap: 8px;\n align-items: center;\n font-size: 12px;\n color: var(--dsw-alias-label-secondary);\n margin-top: 10px;\n cursor: pointer;\n}\n\n.ae-check input {\n accent-color: var(--dsw-alias-accent, #3b82f6);\n}\n\n.ae-del {\n flex: none;\n padding: 4px 10px;\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: 6px;\n background: transparent;\n color: var(--dsw-alias-state-danger-fg, #f87171);\n font-size: 12px;\n cursor: pointer;\n}\n\n.ae-del:hover {\n background: var(--dsw-alias-state-danger-bg, rgba(248, 113, 113, 0.12));\n}\n\n.ae-add {\n align-self: flex-start;\n padding: 5px 12px;\n border: 1px dashed var(--dsw-alias-border-l2);\n border-radius: 8px;\n background: transparent;\n color: var(--dsw-alias-label-secondary);\n font-size: 12px;\n cursor: pointer;\n}\n\n.ae-add:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n.ae-wsSelect {\n margin-top: 8px;\n max-width: 100%;\n cursor: pointer;\n}\n\n/* ── 工作区白名单 chips ── */\n\n.ae-wsList {\n display: flex;\n flex-wrap: wrap;\n gap: 6px;\n}\n\n.ae-wsChip {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n padding: 3px 8px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 8px;\n background: var(--dsw-alias-interactive-bg-active, rgba(128, 128, 128, 0.14));\n}\n\n.ae-wsPath {\n font-size: 12px;\n color: var(--dsw-alias-label-primary);\n font-family: var(--ds-font-family-code, monospace);\n word-break: break-all;\n}\n\n.ae-wsDel {\n flex: none;\n width: 18px;\n height: 18px;\n border: none;\n border-radius: 50%;\n background: transparent;\n color: var(--dsw-alias-label-dimmed);\n font-size: 13px;\n line-height: 1;\n cursor: pointer;\n}\n\n.ae-wsDel:hover {\n background: var(--dsw-alias-state-danger-bg, rgba(248, 113, 113, 0.18));\n color: var(--dsw-alias-state-danger-fg, #f87171);\n}\n\n/* ── 错误 / 操作区 ── */\n\n.ae-error {\n margin: 0;\n font-size: 12px;\n line-height: 18px;\n color: var(--dsw-alias-state-danger-fg, #f87171);\n word-break: break-word;\n}\n\n.ae-actions {\n display: flex;\n justify-content: flex-end;\n padding-top: 4px;\n border-top: 1px solid var(--dsw-alias-border-l1);\n}\n\n.ae-save {\n padding: 7px 24px;\n border: 0;\n border-radius: 8px;\n background: var(--dsw-alias-button-primary-bg, var(--dsw-alias-accent, #3b82f6));\n color: var(--dsw-alias-button-primary-fg, #fff);\n font-size: 13px;\n font-weight: 500;\n cursor: pointer;\n}\n\n.ae-save:hover:not(:disabled) {\n filter: brightness(1.08);\n}\n\n.ae-save:disabled {\n opacity: 0.6;\n cursor: default;\n}\n";
550
+ if (typeof document !== "undefined" && document.querySelector("style[data-sp-css]") === null) {
551
+ const tag = document.createElement("style");
552
+ tag.setAttribute("data-sp-css", "1");
553
+ tag.textContent = css$1;
554
+ document.head.appendChild(tag);
555
+ }
556
+ //#endregion
557
+ //#region src/client/AccountsEditor.tsx
558
+ /**
559
+ * AccountsEditor.tsx — DSH 设置面板「外部访问」区块(v1.22 重构 + v1.22.4 完善)
560
+ *
561
+ * 归属原则(v1.22 用户拍板):账号密码/工作区白名单是 **plugin 级配置**,
562
+ * 统一在 DSH 设置面板(plugin 层)设定——本组件即该设定面。
563
+ * 数据:GET/PUT /serenity/config(accounts-api.ts;服务端 scrypt hash,
564
+ * 存 ~/.dsh/serenity-hooks.json plugin 全局文件)。
565
+ *
566
+ * 内容:
567
+ * - 监听设置:gateway host/port + Secure Cookie(HTTPS 反代时)
568
+ * - 外部能力:允许新建工作区 / 启用 Authenticator 第二因素
569
+ * - 登录账号:账号列表 CRUD(含密码 + TOTP 绑定/解绑)
570
+ * - 工作区白名单:**从已有工作区选择**(v1.22.4 需求 1;非手输)
571
+ */
572
+ function AccountsEditor(props) {
573
+ const { gatewayOn } = props;
574
+ const [config, setConfig] = (0, react.useState)(null);
575
+ const [drafts, setDrafts] = (0, react.useState)([]);
576
+ const [host, setHost] = (0, react.useState)("0.0.0.0");
577
+ const [port, setPort] = (0, react.useState)(3081);
578
+ const [workspaces, setWorkspaces] = (0, react.useState)([]);
579
+ const [cookieSecure, setCookieSecure] = (0, react.useState)(false);
580
+ const [allowWorkspaceCreate, setAllowWorkspaceCreate] = (0, react.useState)(true);
581
+ const [totpEnabled, setTotpEnabled] = (0, react.useState)(false);
582
+ const [knownWorkspaces, setKnownWorkspaces] = (0, react.useState)([]);
583
+ const [error, setError] = (0, react.useState)(null);
584
+ const [busy, setBusy] = (0, react.useState)(false);
585
+ const [loaded, setLoaded] = (0, react.useState)(false);
586
+ const [saved, setSaved] = (0, react.useState)(false);
587
+ (0, react.useEffect)(() => {
588
+ let alive = true;
589
+ Promise.all([fetchConfig(), fetchWorkspaces()]).then(([cfg, wss]) => {
590
+ if (!alive) return;
591
+ setKnownWorkspaces(wss);
592
+ if (cfg) {
593
+ setConfig(cfg);
594
+ setHost(cfg.gateway.host);
595
+ setPort(cfg.gateway.port);
596
+ setDrafts(cfg.gateway.accounts.map(accountDraftFromWire));
597
+ setWorkspaces(cfg.gateway.workspaces ?? []);
598
+ setCookieSecure(cfg.gateway.cookieSecure === true);
599
+ setAllowWorkspaceCreate(cfg.gateway.allowWorkspaceCreate !== false);
600
+ setTotpEnabled(cfg.gateway.totpEnabled === true);
601
+ }
602
+ setLoaded(true);
603
+ });
604
+ return () => {
605
+ alive = false;
606
+ };
607
+ }, []);
608
+ const updateDraft = (id, patch) => {
609
+ setDrafts((ds) => ds.map((d) => d.id === id ? {
610
+ ...d,
611
+ ...patch
612
+ } : d));
613
+ };
614
+ const addRow = () => {
615
+ setDrafts((ds) => [...ds, {
616
+ id: newAccountId(),
617
+ user: "",
618
+ pass: "",
619
+ isNew: true,
620
+ hasPassword: false,
621
+ hasTotp: false,
622
+ totpState: "none"
623
+ }]);
624
+ };
625
+ const removeRow = (id) => {
626
+ setDrafts((ds) => ds.filter((d) => d.id !== id));
627
+ };
628
+ const startTotpBind = (d) => {
629
+ const secret = newTotpSecret();
630
+ updateDraft(d.id, {
631
+ totpState: "pending",
632
+ totpSecret: secret,
633
+ totpConfirm: ""
634
+ });
635
+ };
636
+ const cancelTotpBind = (d) => {
637
+ updateDraft(d.id, {
638
+ totpState: d.hasTotp ? "none" : "none",
639
+ totpSecret: void 0,
640
+ totpConfirm: ""
641
+ });
642
+ };
643
+ const markTotpClear = (d) => {
644
+ updateDraft(d.id, {
645
+ totpState: "clear",
646
+ totpSecret: void 0,
647
+ totpConfirm: ""
648
+ });
649
+ };
650
+ const toggleWorkspace = (path) => {
651
+ setWorkspaces((ws) => ws.includes(path) ? ws.filter((w) => w !== path) : [...ws, path]);
652
+ };
653
+ const removeWorkspace = (v) => {
654
+ setWorkspaces((ws) => ws.filter((w) => w !== v));
655
+ };
656
+ const save = async () => {
657
+ for (const d of drafts) {
658
+ const err = validateDraft(d);
659
+ if (err) {
660
+ setError(err);
661
+ return;
662
+ }
663
+ }
664
+ setBusy(true);
665
+ setError(null);
666
+ setSaved(false);
667
+ try {
668
+ const cfg = await saveConfig({ gateway: {
669
+ host,
670
+ port,
671
+ accounts: drafts.map(accountToWire),
672
+ workspaces,
673
+ cookieSecure,
674
+ allowWorkspaceCreate,
675
+ totpEnabled
676
+ } });
677
+ if (cfg) {
678
+ setConfig(cfg);
679
+ setDrafts(cfg.gateway.accounts.map(accountDraftFromWire));
680
+ setWorkspaces(cfg.gateway.workspaces ?? []);
681
+ setSaved(true);
682
+ setTimeout(() => setSaved(false), 2e3);
683
+ }
684
+ } catch (err) {
685
+ setError(err instanceof Error ? err.message : String(err));
686
+ } finally {
687
+ setBusy(false);
688
+ }
689
+ };
690
+ if (!loaded) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
691
+ className: "ae-note",
692
+ children: "加载中…"
693
+ });
694
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
695
+ className: "ae-root",
696
+ children: [
697
+ !gatewayOn && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
698
+ className: "ae-warn",
699
+ children: "⚠️ 双端口网关当前关闭——先开启上方「双端口网关」开关,外部访问才生效。"
700
+ }),
701
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
702
+ className: "ae-block",
703
+ children: [
704
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", {
705
+ className: "ae-title",
706
+ children: "监听设置"
707
+ }),
708
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
709
+ className: "ae-row",
710
+ children: [
711
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
712
+ className: "ae-label",
713
+ children: "监听地址"
714
+ }),
715
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
716
+ className: "ae-input",
717
+ value: host,
718
+ onChange: (e) => setHost(e.target.value),
719
+ placeholder: "0.0.0.0"
720
+ }),
721
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
722
+ className: "ae-label",
723
+ children: "端口"
724
+ }),
725
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
726
+ className: "ae-input ae-port",
727
+ type: "number",
728
+ value: port,
729
+ min: 1,
730
+ max: 65535,
731
+ onChange: (e) => setPort(Number(e.target.value) || 0)
732
+ })
733
+ ]
734
+ }),
735
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
736
+ className: "ae-check",
737
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
738
+ type: "checkbox",
739
+ checked: cookieSecure,
740
+ onChange: (e) => setCookieSecure(e.target.checked)
741
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: "Secure Cookie(仅 HTTPS 反代时开启;明文 HTTP 下关闭,否则登录态不生效)" })]
742
+ })
743
+ ]
744
+ }),
745
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
746
+ className: "ae-block",
747
+ children: [
748
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", {
749
+ className: "ae-title",
750
+ children: "外部能力"
751
+ }),
752
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
753
+ className: "ae-check",
754
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
755
+ type: "checkbox",
756
+ checked: allowWorkspaceCreate,
757
+ onChange: (e) => setAllowWorkspaceCreate(e.target.checked)
758
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: "允许外部新建工作区(关闭后经外部网关的 workspace.create 一律拒绝;仅使用已有工作区)" })]
759
+ }),
760
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
761
+ className: "ae-check",
762
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
763
+ type: "checkbox",
764
+ checked: totpEnabled,
765
+ onChange: (e) => setTotpEnabled(e.target.checked)
766
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: "启用 Authenticator 第二因素(关闭时 TOTP 完全禁用;开启后可对账号绑定验证器,登录需输入 6 位码)" })]
767
+ })
768
+ ]
769
+ }),
770
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
771
+ className: "ae-block",
772
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", {
773
+ className: "ae-title",
774
+ children: "登录账号"
775
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
776
+ className: "ae-list",
777
+ children: [
778
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
779
+ className: "ae-head",
780
+ children: [
781
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
782
+ className: "ae-col ae-colUser",
783
+ children: "用户名"
784
+ }),
785
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
786
+ className: "ae-col ae-colPass",
787
+ children: "密码"
788
+ }),
789
+ totpEnabled && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
790
+ className: "ae-col ae-colTotp",
791
+ children: "第二因素"
792
+ }),
793
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "ae-col ae-colOp" })
794
+ ]
795
+ }),
796
+ drafts.map((d) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
797
+ className: "ae-item",
798
+ children: [
799
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
800
+ className: "ae-input ae-colUser",
801
+ value: d.user,
802
+ placeholder: "用户名",
803
+ onChange: (e) => updateDraft(d.id, { user: e.target.value })
804
+ }),
805
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
806
+ className: "ae-input ae-colPass",
807
+ type: "password",
808
+ value: d.pass,
809
+ placeholder: d.isNew ? "必填" : d.hasPassword ? "留空保留原密码" : "未设置",
810
+ onChange: (e) => updateDraft(d.id, { pass: e.target.value })
811
+ }),
812
+ totpEnabled && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
813
+ className: "ae-col ae-colTotp",
814
+ children: d.totpState === "pending" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
815
+ className: "ae-totpPending",
816
+ children: [
817
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
818
+ className: "ae-totpSecret",
819
+ title: "录入到 Authenticator(Google Authenticator / 1Password / Aegis)",
820
+ children: d.totpSecret
821
+ }),
822
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
823
+ className: "ae-totpUri",
824
+ href: d.totpSecret ? otpauthUriClient(d.totpSecret, `${d.user.trim() || "user"}@serenity`) : "#",
825
+ target: "_blank",
826
+ rel: "noreferrer",
827
+ children: "otpauth://"
828
+ }),
829
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
830
+ className: "ae-input ae-totpConfirm",
831
+ type: "text",
832
+ inputMode: "numeric",
833
+ pattern: "[0-9]{6}",
834
+ maxLength: 6,
835
+ value: d.totpConfirm ?? "",
836
+ placeholder: "输入验证码确认",
837
+ onChange: (e) => updateDraft(d.id, { totpConfirm: e.target.value })
838
+ }),
839
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
840
+ type: "button",
841
+ className: "ae-del",
842
+ onClick: () => cancelTotpBind(d),
843
+ children: "取消"
844
+ })
845
+ ]
846
+ }) : d.totpState === "clear" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
847
+ className: "ae-totpClear",
848
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
849
+ className: "ae-totpBadge",
850
+ children: "将解绑"
851
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
852
+ type: "button",
853
+ className: "ae-del",
854
+ onClick: () => updateDraft(d.id, { totpState: "none" }),
855
+ children: "撤销"
856
+ })]
857
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
858
+ type: "button",
859
+ className: "ae-totpBtn",
860
+ onClick: () => d.hasTotp ? markTotpClear(d) : startTotpBind(d),
861
+ title: d.hasTotp ? "已绑定验证器——点击解绑" : "绑定 Authenticator 第二因素",
862
+ children: d.hasTotp ? "✓ 已绑定" : "绑定验证器"
863
+ })
864
+ }),
865
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
866
+ type: "button",
867
+ className: "ae-del",
868
+ onClick: () => removeRow(d.id),
869
+ children: "删除"
870
+ })
871
+ ]
872
+ }, d.id)),
873
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
874
+ type: "button",
875
+ className: "ae-add",
876
+ onClick: addRow,
877
+ children: "+ 添加账号"
878
+ })
879
+ ]
880
+ })]
881
+ }),
882
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
883
+ className: "ae-block",
884
+ children: [
885
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", {
886
+ className: "ae-title",
887
+ children: "工作区白名单"
888
+ }),
889
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
890
+ className: "ae-desc",
891
+ children: "允许外部访问的已有工作区;未选择 = 全部允许。登录后仅白名单内的工作区可见/可用。"
892
+ }),
893
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
894
+ className: "ae-wsList",
895
+ children: workspaces.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
896
+ className: "ae-note",
897
+ children: "(全部允许)"
898
+ }) : workspaces.map((w) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
899
+ className: "ae-wsChip",
900
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
901
+ className: "ae-wsPath",
902
+ children: w
903
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
904
+ type: "button",
905
+ className: "ae-wsDel",
906
+ onClick: () => removeWorkspace(w),
907
+ "aria-label": `移除 ${w}`,
908
+ children: "×"
909
+ })]
910
+ }, w))
911
+ }),
912
+ knownWorkspaces.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
913
+ className: "ae-note",
914
+ children: "暂无可选工作区(workspace.list 未返回条目)"
915
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
916
+ className: "ae-input ae-wsSelect",
917
+ value: "",
918
+ onChange: (e) => {
919
+ if (e.target.value !== "") toggleWorkspace(e.target.value);
920
+ },
921
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
922
+ value: "",
923
+ disabled: true,
924
+ children: "选择已有工作区…"
925
+ }), knownWorkspaces.map((w) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
926
+ value: w.path,
927
+ children: w.title
928
+ }, w.path))]
929
+ })
930
+ ]
931
+ }),
932
+ error !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
933
+ className: "ae-error",
934
+ children: error
935
+ }),
936
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
937
+ className: "ae-actions",
938
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
939
+ type: "button",
940
+ className: "ae-save",
941
+ disabled: busy,
942
+ onClick: () => void save(),
943
+ children: busy ? "保存中…" : saved ? "✓ 已保存" : "保存"
944
+ })
945
+ })
946
+ ]
947
+ });
948
+ }
949
+ //#endregion
644
950
  //#region \0sp-css:/home/yh/home/home-serenity/AI_LAB/dsh-serenity-plugin/hooks/dsh-serenity-hooks/src/client/SettingsSection
645
951
  const css = "/* SettingsSection — dsh 原生设置面板 serenity-hooks 简单配置页\n v1.21.1 看齐 dsh 自身:官方 settings 设计语言——\n section > title + intro + group(groupTitle) + rowCard 行卡;\n 全部 --dsw-alias-* token(与 ui-settings-models 同词汇表)。 */\n\n/* 页面容器(官方 .section:flex column gap 12) */\n.ss-section {\n display: flex;\n flex-direction: column;\n gap: 14px;\n max-width: 720px;\n color: var(--dsw-alias-label-primary);\n}\n\n/* 页面标题(官方 .title:16px/24 500) */\n.ss-title {\n margin: 0;\n font-size: 16px;\n line-height: 24px;\n font-weight: 500;\n color: var(--dsw-alias-label-primary);\n}\n\n/* 页面说明(官方 .intro:14px/22 tertiary) */\n.ss-intro {\n margin: 0;\n font-size: 14px;\n line-height: 22px;\n color: var(--dsw-alias-label-tertiary);\n word-break: break-word;\n overflow-wrap: anywhere;\n}\n\n/* 分组(多级标题:分组标题 + 行卡列表) */\n.ss-group {\n display: flex;\n flex-direction: column;\n gap: 8px;\n}\n\n/* 分组标题(二级:12px/18 500 secondary——官方 fieldLabel 风格) */\n.ss-groupTitle {\n margin: 0;\n font-size: 12px;\n line-height: 18px;\n font-weight: 600;\n letter-spacing: 0.02em;\n text-transform: uppercase;\n color: var(--dsw-alias-label-secondary);\n}\n\n/* 行卡列表(官方 .rows:无 marker,gap 8) */\n.ss-rows {\n list-style: none;\n margin: 0;\n padding: 0;\n display: flex;\n flex-direction: column;\n gap: 8px;\n}\n\n/* 行卡(官方 .rowCard:border-l2 r12 padding 12/14) */\n.ss-rowCard {\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 12px;\n padding: 12px 14px;\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 16px;\n}\n\n/* 行卡文本(标题 + 说明 左列) */\n.ss-rowText {\n display: flex;\n flex-direction: column;\n gap: 3px;\n min-width: 0;\n flex: 1;\n}\n\n/* 行卡标题(官方 .rowName:14px/22 500) */\n.ss-rowName {\n font-size: 14px;\n line-height: 22px;\n font-weight: 500;\n color: var(--dsw-alias-label-primary);\n}\n\n/* 行卡说明(12px/18 tertiary) */\n.ss-rowDesc {\n font-size: 12px;\n line-height: 18px;\n color: var(--dsw-alias-label-tertiary);\n word-break: break-word;\n overflow-wrap: anywhere;\n}\n\n/* 行卡控件(右置) */\n.ss-rowControl {\n flex: none;\n display: flex;\n align-items: center;\n}\n\n/* 开关(官方 token 的自绘 toggle:track 22px,thumb 16px) */\n.ss-switch {\n position: relative;\n flex: none;\n width: 40px;\n height: 22px;\n cursor: pointer;\n}\n\n.ss-switch input {\n position: absolute;\n opacity: 0;\n width: 100%;\n height: 100%;\n margin: 0;\n cursor: pointer;\n}\n\n.ss-switchTrack {\n position: absolute;\n inset: 0;\n border-radius: 11px;\n background: var(--dsw-alias-interactive-bg-active, rgba(128, 128, 128, 0.3));\n transition: background var(--ds-transition-duration-fast, 0.15s) var(--ds-ease-in-out, ease);\n}\n\n.ss-switchTrack::after {\n content: '';\n position: absolute;\n top: 3px;\n left: 3px;\n width: 16px;\n height: 16px;\n border-radius: 50%;\n background: var(--dsw-alias-label-primary, #eee);\n transition: transform var(--ds-transition-duration-fast, 0.15s) var(--ds-ease-in-out, ease);\n}\n\n.ss-switch input:checked + .ss-switchTrack {\n background: var(--dsw-alias-state-success-primary, #22c55e);\n}\n\n.ss-switch input:checked + .ss-switchTrack::after {\n transform: translateX(18px);\n}\n\n.ss-switch input:disabled + .ss-switchTrack {\n opacity: 0.4;\n cursor: default;\n}\n\n/* 阈值行(滑块 + 值) */\n.ss-threshold {\n display: flex;\n align-items: center;\n gap: 12px;\n}\n\n.ss-threshold input[type='range'] {\n width: 160px;\n}\n\n.ss-value {\n min-width: 40px;\n font-variant-numeric: tabular-nums;\n font-size: 13px;\n font-weight: 600;\n color: var(--dsw-alias-label-primary);\n text-align: right;\n}\n\n/* 底部帮助行 */\n.ss-hint {\n margin: 0;\n font-size: 12px;\n line-height: 18px;\n color: var(--dsw-alias-label-tertiary);\n word-break: break-word;\n overflow-wrap: anywhere;\n}\n\n/* 降级提示 */\n.ss-degrade {\n margin: 0;\n font-size: 12px;\n line-height: 18px;\n color: var(--dsw-alias-state-warn-label);\n word-break: break-word;\n overflow-wrap: anywhere;\n}\n";
646
952
  if (typeof document !== "undefined" && document.querySelector("style[data-sp-css]") === null) {
@@ -652,7 +958,7 @@ window.__ModuleLoader__.load({
652
958
  //#endregion
653
959
  //#region src/client/SettingsSection.tsx
654
960
  /** 降级提示文案(旧 RC 白名单时引导去宁静号面板) */
655
- const DEGRADE_NOTE = "当前运行版本未暴露 serenity-hooks 配置(旧 RC 白名单)。请在会话头部 Serenity 徽章的「高级设定」面板中管理,或升级 DSH。";
961
+ const DEGRADE_NOTE = "当前运行版本未暴露 serenity-hooks 配置(旧 RC 白名单)。请升级 DSH 或使用会话头部 Serenity 状态栏。";
656
962
  /** 开关(官方 settings 语言:label 右置 toggle,--dsw-alias-* token) */
657
963
  function Toggle(props) {
658
964
  const { checked, disabled, onChange } = props;
@@ -700,7 +1006,7 @@ window.__ModuleLoader__.load({
700
1006
  })]
701
1007
  });
702
1008
  }
703
- /** dsh 原生设置面板:serenity-hooks 简单配置页(官方 settings 设计语言,多级标题) */
1009
+ /** dsh 原生设置面板:serenity-hooks 配置页(官方 settings 设计语言,多级标题) */
704
1010
  function SettingsSection(props) {
705
1011
  const { scope } = props;
706
1012
  const [snapshot, setSnapshot] = (0, react.useState)(() => scope.getSnapshot());
@@ -735,7 +1041,7 @@ window.__ModuleLoader__.load({
735
1041
  }),
736
1042
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
737
1043
  className: "ss-intro",
738
- children: "宁静号的开关与阈值,改动即时保存(DSH settings.yaml)。账号密码等复杂配置请到会话头部「Serenity」卡片 → 高级设定 → 账号。"
1044
+ children: "宁静号的 plugin 级配置(全局生效,不依赖具体 CCC)。改动即时保存。"
739
1045
  }),
740
1046
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Group, {
741
1047
  title: "访问",
@@ -788,9 +1094,12 @@ window.__ModuleLoader__.load({
788
1094
  })
789
1095
  }) })
790
1096
  }),
791
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
792
- className: "ss-hint",
793
- children: "💡 双端口网关需先在「Serenity 卡片 → 账号」里添加账号并设置监听端口。"
1097
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1098
+ className: "ss-group",
1099
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
1100
+ className: "ss-groupTitle",
1101
+ children: "外部访问"
1102
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AccountsEditor, { gatewayOn })]
794
1103
  })
795
1104
  ]
796
1105
  });