@shgroup/dsh-serenity-hooks 1.26.0 → 1.26.2

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/dsh.plugin.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "id": "dsh-serenity-hooks",
3
- "version": "1.26.0",
3
+ "version": "1.26.2",
4
4
  "main": "lib/index.js",
5
5
  "description": "宁静号 ACC harness(Native Cordis 插件):真实 DSH 工具 cc_fs/session/acc_msm/eap/neat/cce/handyman/session_rebuild/skiff_admin + 拦截缝机械约束(safe-mode/路径守卫)+ 高级设定面板(双端口网关/账号管理)+ Skiff 认知子集角色(实验性)。适配 DSH 公开版(0.1.0-rc,deepseek-ai/deepseek-harness)。",
6
6
  "engines": {
package/lib/client.js CHANGED
@@ -3245,7 +3245,7 @@ window.__ModuleLoader__.load({
3245
3245
  if (!res.ok) return null;
3246
3246
  return (await res.json()).config ?? null;
3247
3247
  }
3248
- /** PUT 配置(账号 patch + 工作区白名单 + persona 彩蛋)→ 返回保存后的 wire */
3248
+ /** PUT 配置(账号 patch + 工作区白名单 + persona 彩蛋 + 开放容器白名单)→ 返回保存后的 wire */
3249
3249
  async function saveConfig(patch) {
3250
3250
  const res = await fetch(CONFIG_PATH, {
3251
3251
  method: "PUT",
@@ -3263,7 +3263,7 @@ window.__ModuleLoader__.load({
3263
3263
  }
3264
3264
  //#endregion
3265
3265
  //#region \0sp-css:/home/yh/home/home-serenity/AI_LAB/dsh-serenity-plugin/hooks/dsh-serenity-hooks/src/client/AccountsEditor
3266
- const css$2 = "/* 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/* v1.24.6:TOTP 绑定二维码(扫码即录入;scalable SVG,白底保证扫码识别) */\n.ae-totpQr {\n flex-basis: 100%;\n display: inline-flex;\n justify-content: center;\n padding: 8px;\n background: #fff;\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: 8px;\n}\n\n.ae-totpQr svg {\n width: 132px;\n height: 132px;\n display: block;\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/* v1.24.7:绑定提示(去确认码后——扫码或手动录入,保存即绑定) */\n.ae-totpHint {\n flex-basis: 100%;\n font-size: 11px;\n color: var(--dsw-alias-label-tertiary);\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";
3266
+ const css$2 = "/* 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/* v1.24.6:TOTP 绑定二维码(扫码即录入;scalable SVG,白底保证扫码识别) */\n.ae-totpQr {\n flex-basis: 100%;\n display: inline-flex;\n justify-content: center;\n padding: 8px;\n background: #fff;\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: 8px;\n}\n\n.ae-totpQr svg {\n width: 132px;\n height: 132px;\n display: block;\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/* v1.24.7:绑定提示(去确认码后——扫码或手动录入,保存即绑定) */\n.ae-totpHint {\n flex-basis: 100%;\n font-size: 11px;\n color: var(--dsw-alias-label-tertiary);\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\n/* ── v1.26.2 建议问答页配置(PublicAskEditor:开放容器白名单 chips + key/地址展示)── */\n\n.ae-chips {\n display: flex;\n flex-wrap: wrap;\n gap: 6px;\n margin: 8px 0 4px;\n}\n\n.ae-chip {\n padding: 4px 12px;\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: 999px;\n background: transparent;\n color: var(--dsw-alias-text-primary);\n font-size: 12px;\n cursor: pointer;\n transition: background 0.15s ease, border-color 0.15s ease;\n}\n\n.ae-chip:hover {\n border-color: var(--dsw-alias-accent, #0ba875);\n}\n\n.ae-chipOn {\n background: var(--dsw-alias-accent, #0ba875);\n border-color: var(--dsw-alias-accent, #0ba875);\n color: #fff;\n}\n\n.ae-saved {\n margin-left: 10px;\n font-size: 12px;\n color: var(--dsw-alias-state-success-fg, #4ade80);\n}\n\n.ae-err {\n margin-left: 10px;\n font-size: 12px;\n color: var(--dsw-alias-state-danger-fg, #f87171);\n}\n\n.ae-key {\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n font-size: 12px;\n padding: 2px 8px;\n border-radius: 6px;\n background: var(--dsw-alias-bg-l2);\n color: var(--dsw-alias-text-primary);\n word-break: break-all;\n user-select: all;\n}\n\n.ae-copy {\n flex: none;\n margin-left: 8px;\n padding: 3px 10px;\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: 6px;\n background: transparent;\n color: var(--dsw-alias-text-primary);\n font-size: 12px;\n cursor: pointer;\n}\n\n.ae-copy:hover {\n border-color: var(--dsw-alias-accent, #0ba875);\n color: var(--dsw-alias-accent, #0ba875);\n}\n\n.ae-rows {\n display: flex;\n flex-direction: column;\n gap: 6px;\n margin-top: 8px;\n}\n\n.ae-labelDim {\n opacity: 0.65;\n min-width: 96px;\n}\n";
3267
3267
  if (typeof document !== "undefined" && document.querySelector("style[data-sp-css=\"AccountsEditor\"]") === null) {
3268
3268
  const tag = document.createElement("style");
3269
3269
  tag.setAttribute("data-sp-css", "AccountsEditor");
@@ -3818,6 +3818,251 @@ Instruction-following style:
3818
3818
  });
3819
3819
  }
3820
3820
  //#endregion
3821
+ //#region src/client/PublicAskEditor.tsx
3822
+ /**
3823
+ * PublicAskEditor.tsx — 建议问答页配置(v1.26.2,S142 用户需求)
3824
+ *
3825
+ * 功能:
3826
+ * - **开放容器白名单**:从 /serenity/cccs 拉取候选容器,chips 选择哪些开放;
3827
+ * 空白名单 = 全部容器开放(向后兼容 v1.26.1 全局开放语义)。
3828
+ * - **key + 地址展示**:经 /serenity/public-ask 获取访问 key 与各开放容器 URL
3829
+ * (用户:配置处需能获取 key 和地址——管理员复制分享给使用者)。
3830
+ *
3831
+ * 数据通道(plugin 全局):
3832
+ * - GET /serenity/cccs 候选容器(node half discoverCccs)
3833
+ * - GET /serenity/public-ask key + 地址(x-serenity-ui 头)
3834
+ * - PUT /serenity/config publicAsk.allowed 白名单保存(accounts-api saveConfig)
3835
+ */
3836
+ function PublicAskEditor(props) {
3837
+ const { publicAskOn } = props;
3838
+ const [candidates, setCandidates] = (0, react.useState)([]);
3839
+ const [allowed, setAllowed] = (0, react.useState)(null);
3840
+ const [info, setInfo] = (0, react.useState)(null);
3841
+ const [busy, setBusy] = (0, react.useState)(false);
3842
+ const [error, setError] = (0, react.useState)(null);
3843
+ const [saved, setSaved] = (0, react.useState)(false);
3844
+ (0, react.useEffect)(() => {
3845
+ let alive = true;
3846
+ (async () => {
3847
+ try {
3848
+ const cfg = await fetchConfig();
3849
+ if (!alive) return;
3850
+ setAllowed(cfg?.publicAsk?.allowed ?? []);
3851
+ } catch {}
3852
+ })();
3853
+ return () => {
3854
+ alive = false;
3855
+ };
3856
+ }, []);
3857
+ (0, react.useEffect)(() => {
3858
+ let alive = true;
3859
+ (async () => {
3860
+ try {
3861
+ const res = await fetch("/serenity/cccs", { headers: { accept: "application/json" } });
3862
+ if (!res.ok) return;
3863
+ const body = await res.json();
3864
+ if (alive) setCandidates(body?.cccs ?? []);
3865
+ } catch {}
3866
+ })();
3867
+ return () => {
3868
+ alive = false;
3869
+ };
3870
+ }, []);
3871
+ (0, react.useEffect)(() => {
3872
+ if (!publicAskOn) return;
3873
+ let alive = true;
3874
+ (async () => {
3875
+ try {
3876
+ const res = await fetch("/serenity/public-ask", { headers: {
3877
+ accept: "application/json",
3878
+ "x-serenity-ui": "1"
3879
+ } });
3880
+ if (!res.ok) return;
3881
+ const body = await res.json();
3882
+ if (alive) setInfo(body);
3883
+ } catch {}
3884
+ })();
3885
+ return () => {
3886
+ alive = false;
3887
+ };
3888
+ }, [publicAskOn]);
3889
+ const toggleAllowed = (name) => {
3890
+ setAllowed((cur) => {
3891
+ const base = cur ?? [];
3892
+ return base.includes(name) ? base.filter((n) => n !== name) : [...base, name];
3893
+ });
3894
+ };
3895
+ const save = async () => {
3896
+ if (allowed === null) return;
3897
+ setBusy(true);
3898
+ setError(null);
3899
+ setSaved(false);
3900
+ try {
3901
+ const cfg = await saveConfig({ publicAsk: { allowed } });
3902
+ if (cfg) {
3903
+ setAllowed(cfg.publicAsk.allowed ?? []);
3904
+ setSaved(true);
3905
+ setTimeout(() => setSaved(false), 2e3);
3906
+ }
3907
+ } catch (err) {
3908
+ setError(err instanceof Error ? err.message : String(err));
3909
+ } finally {
3910
+ setBusy(false);
3911
+ }
3912
+ };
3913
+ const allOpen = (allowed ?? []).length === 0;
3914
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3915
+ className: "ae-root",
3916
+ children: [
3917
+ !publicAskOn && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3918
+ className: "ae-warn",
3919
+ children: "⚠️ 建议问答页当前关闭——先开启上方「建议问答页」开关,外部访问才生效。"
3920
+ }),
3921
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3922
+ className: "ae-block",
3923
+ children: [
3924
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", {
3925
+ className: "ae-title",
3926
+ children: "开放容器"
3927
+ }),
3928
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
3929
+ className: "ae-desc",
3930
+ children: [
3931
+ "允许外部问答的认知容器(URL:/c/<容器名>)。",
3932
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: "未选择 = 全部容器开放" }),
3933
+ "; 选择后仅白名单内容器可访问。"
3934
+ ]
3935
+ }),
3936
+ candidates.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3937
+ className: "ae-note",
3938
+ children: "暂无可选容器(/serenity/cccs 未返回条目——先在工作区打开一个 CCC)"
3939
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3940
+ className: "ae-chips",
3941
+ children: candidates.map((c) => {
3942
+ const on = (allowed ?? []).includes(c.name);
3943
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
3944
+ type: "button",
3945
+ className: `ae-chip${on ? " ae-chipOn" : ""}`,
3946
+ onClick: () => toggleAllowed(c.name),
3947
+ title: `${c.root}(${c.roles.length} 角色)`,
3948
+ children: [c.name, on ? " ✓" : ""]
3949
+ }, c.name);
3950
+ })
3951
+ }),
3952
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3953
+ className: "ae-actions",
3954
+ children: [
3955
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3956
+ type: "button",
3957
+ className: "ae-save",
3958
+ disabled: busy || allowed === null,
3959
+ onClick: () => void save(),
3960
+ children: busy ? "保存中…" : "保存白名单"
3961
+ }),
3962
+ saved && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3963
+ className: "ae-saved",
3964
+ children: "✓ 已保存"
3965
+ }),
3966
+ error && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3967
+ className: "ae-err",
3968
+ children: error
3969
+ })
3970
+ ]
3971
+ }),
3972
+ allOpen && allowed !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3973
+ className: "ae-note",
3974
+ children: "当前为「全部开放」——所有发现到的认知容器都可问答。"
3975
+ })
3976
+ ]
3977
+ }),
3978
+ publicAskOn && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3979
+ className: "ae-block",
3980
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", {
3981
+ className: "ae-title",
3982
+ children: "访问 Key 与地址"
3983
+ }), info ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
3984
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3985
+ className: "ae-desc",
3986
+ children: "把下面的 key 与地址分享给使用者;key 已自动保存在其浏览器(localStorage)。"
3987
+ }),
3988
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3989
+ className: "ae-row",
3990
+ children: [
3991
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3992
+ className: "ae-label",
3993
+ children: "访问 Key"
3994
+ }),
3995
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
3996
+ className: "ae-key",
3997
+ children: info.key
3998
+ }),
3999
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4000
+ type: "button",
4001
+ className: "ae-copy",
4002
+ onClick: () => {
4003
+ navigator.clipboard?.writeText(info.key);
4004
+ },
4005
+ children: "复制"
4006
+ })
4007
+ ]
4008
+ }),
4009
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4010
+ className: "ae-row",
4011
+ children: [
4012
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4013
+ className: "ae-label",
4014
+ children: "容器列表页"
4015
+ }),
4016
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
4017
+ className: "ae-key",
4018
+ children: info.listUrl
4019
+ }),
4020
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4021
+ type: "button",
4022
+ className: "ae-copy",
4023
+ onClick: () => {
4024
+ navigator.clipboard?.writeText(info.listUrl);
4025
+ },
4026
+ children: "复制"
4027
+ })
4028
+ ]
4029
+ }),
4030
+ info.urls.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4031
+ className: "ae-rows",
4032
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4033
+ className: "ae-label",
4034
+ children: "各容器地址"
4035
+ }), info.urls.map((u) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4036
+ className: "ae-row",
4037
+ children: [
4038
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4039
+ className: "ae-label ae-labelDim",
4040
+ children: u.name
4041
+ }),
4042
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
4043
+ className: "ae-key",
4044
+ children: u.url
4045
+ }),
4046
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4047
+ type: "button",
4048
+ className: "ae-copy",
4049
+ onClick: () => {
4050
+ navigator.clipboard?.writeText(u.url);
4051
+ },
4052
+ children: "复制"
4053
+ })
4054
+ ]
4055
+ }, u.name))]
4056
+ })
4057
+ ] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
4058
+ className: "ae-note",
4059
+ children: "开启后此处显示访问 key 与地址…"
4060
+ })]
4061
+ })
4062
+ ]
4063
+ });
4064
+ }
4065
+ //#endregion
3821
4066
  //#region \0sp-css:/home/yh/home/home-serenity/AI_LAB/dsh-serenity-plugin/hooks/dsh-serenity-hooks/src/client/SettingsSection
3822
4067
  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/* Skiff 调试端口输入(number;关闭时跟随 disabled 半透明) */\n.ss-portInput {\n width: 96px;\n padding: 6px 8px;\n border-radius: 8px;\n border: 1px solid var(--dsw-alias-border-l2);\n background: var(--dsw-alias-fill-l1, rgba(128, 128, 128, 0.08));\n color: var(--dsw-alias-label-primary);\n font-size: 13px;\n font-variant-numeric: tabular-nums;\n text-align: right;\n}\n\n.ss-portInput:disabled {\n opacity: 0.4;\n cursor: default;\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";
3823
4068
  if (typeof document !== "undefined" && document.querySelector("style[data-sp-css=\"SettingsSection\"]") === null) {
@@ -3913,6 +4158,7 @@ Instruction-following style:
3913
4158
  const skiffPort = value?.skiffDebugPort ?? 3099;
3914
4159
  const acpOn = value?.acpEnabled ?? false;
3915
4160
  const acpPort = value?.acpHttpPort ?? 3100;
4161
+ const publicAskOn = value?.publicAskEnabled ?? false;
3916
4162
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3917
4163
  className: "ss-section",
3918
4164
  children: [
@@ -4002,30 +4248,48 @@ Instruction-following style:
4002
4248
  }) })]
4003
4249
  }),
4004
4250
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Group, {
4005
- title: "ACP",
4006
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RowCard, {
4007
- title: "ACP JSON-RPC 端点",
4008
- desc: "程序化调用 Skiff 角色(指定 CCC+角色+会话;实验性,仅监听 127.0.0.1)",
4009
- control: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Toggle, {
4010
- checked: acpOn,
4011
- onChange: (on) => toggle("acpEnabled", on)
4012
- })
4013
- }) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RowCard, {
4014
- title: "HTTP 端口",
4015
- desc: "ACP JSON-RPC 端口(1024 ~ 65535,默认 3100)",
4016
- control: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4017
- className: "ss-threshold",
4018
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
4019
- className: "ss-portInput",
4020
- type: "number",
4021
- min: 1024,
4022
- max: 65535,
4023
- value: acpPort,
4024
- disabled: !acpOn,
4025
- onChange: (e) => setAcpPort(Number(e.target.value))
4251
+ title: "ACP / 建议问答",
4252
+ children: [
4253
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RowCard, {
4254
+ title: "ACP JSON-RPC 端点",
4255
+ desc: "程序化调用 Skiff 角色(指定 CCC+角色+会话;实验性,仅监听 127.0.0.1)",
4256
+ control: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Toggle, {
4257
+ checked: acpOn,
4258
+ onChange: (on) => toggle("acpEnabled", on)
4026
4259
  })
4027
- })
4028
- }) })]
4260
+ }) }),
4261
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RowCard, {
4262
+ title: "建议问答页",
4263
+ desc: "按认知容器暴露问答页供他人验证(需 key;首次开启自动生成固定 key,见 ~/.dsh/serenity-hooks.json)",
4264
+ control: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Toggle, {
4265
+ checked: publicAskOn,
4266
+ onChange: (on) => toggle("publicAskEnabled", on)
4267
+ })
4268
+ }) }),
4269
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RowCard, {
4270
+ title: "HTTP 端口",
4271
+ desc: "ACP JSON-RPC + 问答页共用端口(1024 ~ 65535,默认 3100)",
4272
+ control: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4273
+ className: "ss-threshold",
4274
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
4275
+ className: "ss-portInput",
4276
+ type: "number",
4277
+ min: 1024,
4278
+ max: 65535,
4279
+ value: acpPort,
4280
+ disabled: !acpOn && !publicAskOn,
4281
+ onChange: (e) => setAcpPort(Number(e.target.value))
4282
+ })
4283
+ })
4284
+ }) })
4285
+ ]
4286
+ }),
4287
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4288
+ className: "ss-group",
4289
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
4290
+ className: "ss-groupTitle",
4291
+ children: "建议问答"
4292
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PublicAskEditor, { publicAskOn })]
4029
4293
  }),
4030
4294
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4031
4295
  className: "ss-group",
package/lib/index.js CHANGED
@@ -11,6 +11,18 @@ import { createUserMessage } from "@deepseek-ai/dsh-llm";
11
11
  import { createHmac, randomBytes, randomUUID, scryptSync, timingSafeEqual } from "node:crypto";
12
12
  import { deriveEventMessage } from "@deepseek-ai/dsh-session";
13
13
  import { createServer, request } from "node:http";
14
+ //#region \0rolldown/runtime.js
15
+ var __defProp = Object.defineProperty;
16
+ var __exportAll = (all, no_symbols) => {
17
+ let target = {};
18
+ for (var name in all) __defProp(target, name, {
19
+ get: all[name],
20
+ enumerable: true
21
+ });
22
+ if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
23
+ return target;
24
+ };
25
+ //#endregion
14
26
  //#region src/ccc.ts
15
27
  /**
16
28
  * ccc.ts — CCC 纯逻辑层(零 DSH 依赖,可独立单测)
@@ -5248,6 +5260,14 @@ function installSettingsSection(ctx, ns, schema, entry, hooks) {
5248
5260
  }
5249
5261
  //#endregion
5250
5262
  //#region src/settings-section.ts
5263
+ var settings_section_exports = /* @__PURE__ */ __exportAll({
5264
+ SERENITY_SETTINGS_NS: () => SERENITY_SETTINGS_NS,
5265
+ defaultSimpleSettings: () => defaultSimpleSettings,
5266
+ entryDefaults: () => entryDefaults,
5267
+ readSimpleSettings: () => readSimpleSettings,
5268
+ registerSettingsSection: () => registerSettingsSection,
5269
+ simpleSettingsSchema: () => simpleSettingsSchema
5270
+ });
5251
5271
  /** 简单配置命名空间(dsh 设置面板的 section id / settings.yaml section) */
5252
5272
  const SERENITY_SETTINGS_NS = "serenity-hooks";
5253
5273
  /** schemastery schema(与 DSH 各插件 Config 同款) */
@@ -5259,7 +5279,8 @@ const simpleSettingsSchema = z.object({
5259
5279
  skiffEnabled: z.boolean().default(false),
5260
5280
  skiffDebugPort: z.number().min(1024).max(65535).default(3099),
5261
5281
  acpEnabled: z.boolean().default(false),
5262
- acpHttpPort: z.number().min(1024).max(65535).default(3100)
5282
+ acpHttpPort: z.number().min(1024).max(65535).default(3100),
5283
+ publicAskEnabled: z.boolean().default(false)
5263
5284
  });
5264
5285
  /** 从插件 Config 提取 entry 默认(settings base 层) */
5265
5286
  function entryDefaults(config) {
@@ -5271,7 +5292,8 @@ function entryDefaults(config) {
5271
5292
  skiffEnabled: config.skiff?.enabled ?? false,
5272
5293
  skiffDebugPort: config.skiff?.debugPort ?? 3099,
5273
5294
  acpEnabled: config.acp?.enabled ?? false,
5274
- acpHttpPort: config.acp?.httpPort ?? 3100
5295
+ acpHttpPort: config.acp?.httpPort ?? 3100,
5296
+ publicAskEnabled: config.publicAsk?.enabled ?? false
5275
5297
  };
5276
5298
  }
5277
5299
  /** 运行时源(installSettingsSection 注入:settings scope 或 entry fallback) */
@@ -5286,7 +5308,8 @@ function defaultSimpleSettings() {
5286
5308
  skiffEnabled: false,
5287
5309
  skiffDebugPort: 3099,
5288
5310
  acpEnabled: false,
5289
- acpHttpPort: 3100
5311
+ acpHttpPort: 3100,
5312
+ publicAskEnabled: false
5290
5313
  };
5291
5314
  }
5292
5315
  /**
@@ -7369,6 +7392,10 @@ function defaultAdvancedSettings() {
7369
7392
  persona: {
7370
7393
  mode: "",
7371
7394
  overrideText: ""
7395
+ },
7396
+ publicAsk: {
7397
+ key: "",
7398
+ allowed: []
7372
7399
  }
7373
7400
  };
7374
7401
  }
@@ -7420,6 +7447,7 @@ function mergeWithDefaults(raw) {
7420
7447
  const rebuild = o.rebuild ?? {};
7421
7448
  const naming = o.naming ?? {};
7422
7449
  const persona = o.persona ?? {};
7450
+ const publicAsk = o.publicAsk ?? {};
7423
7451
  return {
7424
7452
  gateway: {
7425
7453
  enabled: typeof gateway.enabled === "boolean" ? gateway.enabled : def.gateway.enabled,
@@ -7439,6 +7467,10 @@ function mergeWithDefaults(raw) {
7439
7467
  persona: {
7440
7468
  mode: typeof persona.mode === "string" ? persona.mode : def.persona.mode,
7441
7469
  overrideText: typeof persona.overrideText === "string" ? persona.overrideText : def.persona.overrideText
7470
+ },
7471
+ publicAsk: {
7472
+ key: typeof publicAsk.key === "string" ? publicAsk.key : def.publicAsk.key,
7473
+ allowed: Array.isArray(publicAsk.allowed) ? publicAsk.allowed.filter((n) => typeof n === "string" && n !== "") : def.publicAsk.allowed
7442
7474
  }
7443
7475
  };
7444
7476
  }
@@ -7479,12 +7511,39 @@ function updateAdvancedSettings(patch) {
7479
7511
  persona: ps !== void 0 ? {
7480
7512
  mode: typeof ps.mode === "string" ? ps.mode : current.persona.mode,
7481
7513
  overrideText: typeof ps.overrideText === "string" ? ps.overrideText : current.persona.overrideText
7482
- } : current.persona
7514
+ } : current.persona,
7515
+ publicAsk: patch.publicAsk !== void 0 ? {
7516
+ key: typeof patch.publicAsk.key === "string" ? patch.publicAsk.key : current.publicAsk.key,
7517
+ allowed: Array.isArray(patch.publicAsk.allowed) ? patch.publicAsk.allowed.filter((n) => typeof n === "string" && n !== "") : current.publicAsk.allowed
7518
+ } : current.publicAsk
7483
7519
  };
7484
7520
  writeAdvancedSettings(next);
7485
7521
  return next;
7486
7522
  }
7487
7523
  /**
7524
+ * 确保问答页访问 key 存在(首次启用自动生成 + 写回固定,用户拍板"配置时生成"):
7525
+ * 读取全局配置 publicAsk.key;为空 → 生成 32 字节随机 hex(64 字符)→ 写回持久化。
7526
+ * @returns 当前固定 key(已生成则返回既有值——**幂等**,不会覆盖用户手改的 key)
7527
+ */
7528
+ function ensurePublicAskKey() {
7529
+ const current = readAdvancedSettings();
7530
+ if (current.publicAsk.key !== "") return current.publicAsk.key;
7531
+ const key = randomBytes(32).toString("hex");
7532
+ updateAdvancedSettings({ publicAsk: {
7533
+ key,
7534
+ allowed: current.publicAsk.allowed
7535
+ } });
7536
+ return key;
7537
+ }
7538
+ /** 校验问答页访问 key(timing-safe;key 未生成 → 恒 false——没有 key 不工作) */
7539
+ function verifyPublicAskKey(provided) {
7540
+ const expected = readAdvancedSettings().publicAsk.key;
7541
+ if (!expected || typeof provided !== "string" || provided === "") return false;
7542
+ const a = Buffer.from(provided);
7543
+ const b = Buffer.from(expected);
7544
+ return a.length === b.length && timingSafeEqual(a, b);
7545
+ }
7546
+ /**
7488
7547
  * 一次性迁移(v1.21.x → v1.22):旧版把 `serenityAdvanced` 存在 CCC localstore.json;
7489
7548
  * 新版归 plugin 全局文件。全局文件已存在 → 跳过(幂等);localstore 无旧节 → 跳过。
7490
7549
  * @param root - CCC 根(localstore.json 所在目录)
@@ -7519,7 +7578,8 @@ function toWire(settings) {
7519
7578
  },
7520
7579
  rebuild: settings.rebuild,
7521
7580
  naming: settings.naming,
7522
- persona: settings.persona
7581
+ persona: settings.persona,
7582
+ publicAsk: { allowed: [...settings.publicAsk.allowed] }
7523
7583
  };
7524
7584
  }
7525
7585
  /**
@@ -7583,6 +7643,11 @@ function applyWirePatch(wire) {
7583
7643
  if (typeof wire.persona.overrideText === "string") psPatch.overrideText = wire.persona.overrideText;
7584
7644
  patch.persona = psPatch;
7585
7645
  }
7646
+ if (wire.publicAsk !== void 0 && Array.isArray(wire.publicAsk.allowed)) {
7647
+ const paPatch = { ...current.publicAsk };
7648
+ paPatch.allowed = wire.publicAsk.allowed.filter((n) => typeof n === "string" && n !== "");
7649
+ patch.publicAsk = paPatch;
7650
+ }
7586
7651
  return updateAdvancedSettings(patch);
7587
7652
  }
7588
7653
  //#endregion
@@ -8246,6 +8311,8 @@ const HANDYMEN_PATH = "/serenity/handymen";
8246
8311
  const UPLOAD_PATH = "/serenity/image-upload";
8247
8312
  const FILE_UPLOAD_PATH = "/serenity/file-upload";
8248
8313
  const CONFIG_PATH = "/serenity/config";
8314
+ const CCCS_PATH = "/serenity/cccs";
8315
+ const PUBLIC_ASK_PATH = "/serenity/public-ask";
8249
8316
  /** 图片落盘目录(CCC 根相对;S142 图片自动识别基础设施——粘贴图片落盘供 agent 经 CCC vlm MSM 自主处理) */
8250
8317
  const IMAGE_UPLOAD_DIR = "_tmp/images_from_user";
8251
8318
  const IMAGE_MEDIA_TYPES = /* @__PURE__ */ new Set([
@@ -8536,6 +8603,63 @@ function registerStatusApi(ctx, opts = {}) {
8536
8603
  }
8537
8604
  }
8538
8605
  });
8606
+ ctx.webServer.register({
8607
+ kind: "exact",
8608
+ path: CCCS_PATH,
8609
+ handler: async (req, res) => {
8610
+ try {
8611
+ if (req.method !== "GET") {
8612
+ sendJson$2(res, 405, { error: "method not allowed" });
8613
+ return;
8614
+ }
8615
+ const url = new URL(req.url ?? "/", "http://127.0.0.1");
8616
+ const root = findSerenityRoot(resolveWorkspace(ctx, {
8617
+ sessionId: url.searchParams.get("sessionId") ?? void 0,
8618
+ workspace: url.searchParams.get("workspace") ?? void 0
8619
+ })) ?? "";
8620
+ const { discoverCccs } = await Promise.resolve().then(() => skiff_debug_exports);
8621
+ sendJson$2(res, 200, { cccs: await discoverCccs(ctx, root) });
8622
+ } catch (err) {
8623
+ sendJson$2(res, 400, { error: err.message ?? String(err) });
8624
+ }
8625
+ }
8626
+ });
8627
+ ctx.webServer.register({
8628
+ kind: "exact",
8629
+ path: PUBLIC_ASK_PATH,
8630
+ handler: async (req, res) => {
8631
+ try {
8632
+ if (req.method !== "GET") {
8633
+ sendJson$2(res, 405, { error: "method not allowed" });
8634
+ return;
8635
+ }
8636
+ if (req.headers["x-serenity-ui"] !== "1") {
8637
+ sendJson$2(res, 403, { error: "仅限 WebUI(key 属敏感凭据)" });
8638
+ return;
8639
+ }
8640
+ const settings = readAdvancedSettings();
8641
+ const { readSimpleSettings } = await Promise.resolve().then(() => settings_section_exports);
8642
+ const simple = readSimpleSettings();
8643
+ const allowed = settings.publicAsk.allowed;
8644
+ const port = simple.acpHttpPort ?? 3100;
8645
+ const base = `http://127.0.0.1:${port}`;
8646
+ const containerUrls = allowed.map((name) => ({
8647
+ name,
8648
+ url: `${base}/c/${encodeURIComponent(name)}`
8649
+ }));
8650
+ sendJson$2(res, 200, {
8651
+ enabled: simple.publicAskEnabled ?? false,
8652
+ port,
8653
+ key: ensurePublicAskKey(),
8654
+ allowed,
8655
+ urls: containerUrls,
8656
+ listUrl: `${base}/`
8657
+ });
8658
+ } catch (err) {
8659
+ sendJson$2(res, 400, { error: err.message ?? String(err) });
8660
+ }
8661
+ }
8662
+ });
8539
8663
  }
8540
8664
  //#endregion
8541
8665
  //#region src/seams/env.ts
@@ -11108,6 +11232,13 @@ x.lex;
11108
11232
  *
11109
11233
  * 实验性质:未开启时零资源占用(无监听、无 agent 创建)。
11110
11234
  */
11235
+ var skiff_debug_exports = /* @__PURE__ */ __exportAll({
11236
+ discoverCccs: () => discoverCccs,
11237
+ renderSkiffMarkdown: () => renderSkiffMarkdown,
11238
+ skiffDebugPage: () => skiffDebugPage,
11239
+ startSkiffDebugServer: () => startSkiffDebugServer,
11240
+ stopSkiffDebugServer: () => stopSkiffDebugServer
11241
+ });
11111
11242
  /** 运行中的调试服务(单实例;进程级) */
11112
11243
  let active$1 = null;
11113
11244
  function readBody$1(req) {
@@ -11126,7 +11257,7 @@ function sendJson$1(res, status, payload) {
11126
11257
  });
11127
11258
  res.end(body);
11128
11259
  }
11129
- function sendHtml(res, html) {
11260
+ function sendHtml$1(res, html) {
11130
11261
  res.writeHead(200, {
11131
11262
  "Content-Type": "text/html; charset=utf-8",
11132
11263
  "Cache-Control": "no-store"
@@ -11411,7 +11542,7 @@ async function handle$1(ctx, defaultRoot, webPort, req, res) {
11411
11542
  try {
11412
11543
  const url = (req.url ?? "/").split("?")[0] ?? "/";
11413
11544
  if (req.method === "GET" && url === "/") {
11414
- sendHtml(res, skiffDebugPage(await discoverCccs(ctx, defaultRoot), defaultRoot, webPort));
11545
+ sendHtml$1(res, skiffDebugPage(await discoverCccs(ctx, defaultRoot), defaultRoot, webPort));
11415
11546
  return;
11416
11547
  }
11417
11548
  if (req.method === "POST" && url === "/ask") {
@@ -11653,12 +11784,16 @@ function rpcError(id, code, message) {
11653
11784
  //#endregion
11654
11785
  //#region src/acp-http.ts
11655
11786
  /**
11656
- * acp-http.ts — ACP HTTP JSON-RPC 端点(F4c,v1.26.0 实验性)
11787
+ * acp-http.ts — ACP HTTP JSON-RPC + 建议问答页(F4c/F4d,v1.26.x 实验性)
11657
11788
  *
11658
11789
  * node:http 服务(默认关,仅监听 127.0.0.1;启停 = 人工——设置面板「Serenity」
11659
- * 页 ACP 区块开关,不随插件加载自动启动;仿 skiff 调试服务模式)。
11790
+ * 页 ACP 区块开关 / 问答页开关,不随插件加载自动启动;仿 skiff 调试服务模式)。
11660
11791
  *
11661
- * - POST / → JSON-RPC 2.0 单帧/批处理(body JSON)→ JSON-RPC 响应数组
11792
+ * - POST / → JSON-RPC 2.0 单帧/批处理(acpEnabled 门控;ACP 程序化面)
11793
+ * - GET / → 容器列表页(publicAskEnabled 门控;v1.26.2:只列已开放容器)
11794
+ * - GET /c/<name> → 单容器问答页(v1.26.2:URL 体现容器名,用户只输 key)
11795
+ * - POST /c/<name>/ask → 该容器问答(key 校验 + allowed 白名单校验)
11796
+ * - POST /ask → 兼容旧形态 { key, ccc, ... }(key 校验 + allowed 校验)
11662
11797
  *
11663
11798
  * 与 skiff 调试页(人工测试面)共享同一会话核心(acp-core → skiff-core);
11664
11799
  * 企业微信桥(F4c-2)同进程直调 acp-core 处理器函数,不经本端点。
@@ -11684,15 +11819,23 @@ function sendJson(res, status, payload) {
11684
11819
  });
11685
11820
  res.end(body);
11686
11821
  }
11822
+ function sendHtml(res, html) {
11823
+ res.writeHead(200, {
11824
+ "Content-Type": "text/html; charset=utf-8",
11825
+ "Cache-Control": "no-store"
11826
+ });
11827
+ res.end(html);
11828
+ }
11687
11829
  /**
11688
- * 启动 ACP HTTP JSON-RPC 端点(单实例;重复启动幂等返回既有实例)。
11830
+ * 启动 ACP HTTP JSON-RPC + 建议问答页服务(单实例;重复启动幂等返回既有实例)。
11689
11831
  * @param ctx 插件上下文(AcpServer 内部经 skiff-core 创建 agent)
11690
11832
  * @param port 监听端口(仅 127.0.0.1)
11833
+ * @param defaultRoot 默认 CCC 根(问答页候选发现兜底;resolveSkiffRoot 传入)
11691
11834
  */
11692
- async function startAcpHttpServer(ctx, port) {
11835
+ async function startAcpHttpServer(ctx, port, defaultRoot) {
11693
11836
  if (active) return;
11694
11837
  const server = createServer((req, res) => {
11695
- handle(ctx, req, res);
11838
+ handle(ctx, defaultRoot ?? "", req, res);
11696
11839
  });
11697
11840
  await new Promise((resolve, reject) => {
11698
11841
  server.once("error", reject);
@@ -11702,7 +11845,11 @@ async function startAcpHttpServer(ctx, port) {
11702
11845
  server,
11703
11846
  port
11704
11847
  };
11705
- console.log(`[serenity-hooks] ACP HTTP JSON-RPC: http://127.0.0.1:${port}(session/new 支持 {ccc, role, sessionId?})`);
11848
+ const s = readSimpleSettings();
11849
+ const faces = [];
11850
+ if (s.acpEnabled) faces.push("JSON-RPC");
11851
+ if (s.publicAskEnabled) faces.push("问答页(key 认证)");
11852
+ console.log(`[serenity-hooks] ✓ ACP HTTP: http://127.0.0.1:${port}(${faces.join(" + ") || "未开启任何面"};session/new 支持 {ccc, role, sessionId?})`);
11706
11853
  }
11707
11854
  function stopAcpHttpServer() {
11708
11855
  if (!active) return;
@@ -11711,10 +11858,15 @@ function stopAcpHttpServer() {
11711
11858
  } catch {}
11712
11859
  active = null;
11713
11860
  }
11714
- async function handle(ctx, req, res) {
11861
+ async function handle(ctx, defaultRoot, req, res) {
11715
11862
  try {
11716
11863
  const url = (req.url ?? "/").split("?")[0] ?? "/";
11864
+ const s = readSimpleSettings();
11717
11865
  if (req.method === "POST" && url === "/") {
11866
+ if (!s.acpEnabled) {
11867
+ sendJson(res, 403, { error: "ACP JSON-RPC disabled (enable in Serenity settings)" });
11868
+ return;
11869
+ }
11718
11870
  let body;
11719
11871
  try {
11720
11872
  body = JSON.parse(await readBody(req));
@@ -11732,11 +11884,299 @@ async function handle(ctx, req, res) {
11732
11884
  sendJson(res, 200, await dispatchRpc(new AcpServer(ctx), body));
11733
11885
  return;
11734
11886
  }
11887
+ if (!s.publicAskEnabled) {
11888
+ if (req.method === "GET" && (url === "/" || url.startsWith("/c/"))) {
11889
+ sendHtml(res, publicAskDisabledHtml());
11890
+ return;
11891
+ }
11892
+ if (req.method === "POST" && (url === "/ask" || url.startsWith("/c/"))) {
11893
+ sendJson(res, 403, { error: "public ask disabled (enable in Serenity settings)" });
11894
+ return;
11895
+ }
11896
+ sendJson(res, 404, { error: "not found" });
11897
+ return;
11898
+ }
11899
+ const containerAllowed = (name) => {
11900
+ const allowed = readAdvancedSettings().publicAsk.allowed;
11901
+ return allowed.length === 0 || allowed.includes(name);
11902
+ };
11903
+ const cMatch = /^\/c\/([^/]+)$/.exec(url);
11904
+ if (req.method === "GET" && cMatch) {
11905
+ const name = decodeURIComponent(cMatch[1] ?? "");
11906
+ const ccc = (await discoverCccs(ctx, defaultRoot || (findSerenityRoot(process.cwd()) ?? ""))).find((c) => c.name === name);
11907
+ if (!ccc) {
11908
+ sendHtml(res, publicAskContainerUnknownHtml(name));
11909
+ return;
11910
+ }
11911
+ if (!containerAllowed(name)) {
11912
+ sendHtml(res, publicAskContainerClosedHtml(name));
11913
+ return;
11914
+ }
11915
+ sendHtml(res, publicAskContainerPage(ccc));
11916
+ return;
11917
+ }
11918
+ if (req.method === "GET" && url === "/") {
11919
+ sendHtml(res, publicAskListPage((await discoverCccs(ctx, defaultRoot || (findSerenityRoot(process.cwd()) ?? ""))).filter((c) => containerAllowed(c.name))));
11920
+ return;
11921
+ }
11922
+ const aMatch = /^\/c\/([^/]+)\/ask$/.exec(url);
11923
+ if (req.method === "POST" && aMatch) {
11924
+ const name = decodeURIComponent(aMatch[1] ?? "");
11925
+ const ccc = (await discoverCccs(ctx, defaultRoot || (findSerenityRoot(process.cwd()) ?? ""))).find((c) => c.name === name);
11926
+ if (!ccc || !containerAllowed(name)) {
11927
+ sendJson(res, 403, { error: `container "${name}" is not open for public ask` });
11928
+ return;
11929
+ }
11930
+ let body;
11931
+ try {
11932
+ const parsed = JSON.parse(await readBody(req));
11933
+ if (parsed === null || typeof parsed !== "object") throw new Error("not an object");
11934
+ body = parsed;
11935
+ } catch {
11936
+ sendJson(res, 400, { error: "invalid JSON body" });
11937
+ return;
11938
+ }
11939
+ await handleAskParsed(ctx, ccc, body, res);
11940
+ return;
11941
+ }
11942
+ if (req.method === "POST" && url === "/ask") {
11943
+ let body;
11944
+ try {
11945
+ const parsed = JSON.parse(await readBody(req));
11946
+ if (parsed === null || typeof parsed !== "object") throw new Error("not an object");
11947
+ body = parsed;
11948
+ } catch {
11949
+ sendJson(res, 400, { error: "invalid JSON body" });
11950
+ return;
11951
+ }
11952
+ const cccValue = typeof body.ccc === "string" && body.ccc !== "" ? body.ccc : void 0;
11953
+ const nameValue = typeof body.name === "string" && body.name !== "" ? body.name : void 0;
11954
+ let ccc;
11955
+ if (nameValue) ccc = (await discoverCccs(ctx, defaultRoot || (findSerenityRoot(process.cwd()) ?? ""))).find((c) => c.name === nameValue);
11956
+ else if (cccValue) {
11957
+ ccc = (await discoverCccs(ctx, defaultRoot || (findSerenityRoot(process.cwd()) ?? ""))).find((c) => c.root === cccValue);
11958
+ if (!ccc && cccValue !== "") ccc = {
11959
+ root: cccValue,
11960
+ name: cccValue.split("/").filter(Boolean).pop() ?? cccValue,
11961
+ roles: [...readSkiffRoles(cccValue).keys()]
11962
+ };
11963
+ }
11964
+ if (!ccc || !containerAllowed(ccc.name)) {
11965
+ sendJson(res, 403, { error: "container is not open for public ask" });
11966
+ return;
11967
+ }
11968
+ await handleAskParsed(ctx, ccc, body, res);
11969
+ return;
11970
+ }
11735
11971
  sendJson(res, 404, { error: "not found" });
11736
11972
  } catch (err) {
11737
11973
  sendJson(res, 500, { error: err?.message ?? String(err) });
11738
11974
  }
11739
11975
  }
11976
+ /**
11977
+ * 单容器问答核心(/c/<name>/ask 与 /ask 共用):
11978
+ * body { key, role?, question, sessionId? } → key 校验(401)→ 角色缺省取 CCC 第一个
11979
+ * → 会话延续(v1.25.10 语义,静默新建)→ askSkiff(0 全量轨迹) → { answer, answer_html, sessionId, continued, trajectory }
11980
+ */
11981
+ async function handleAskParsed(ctx, ccc, body, res) {
11982
+ if (!verifyPublicAskKey(typeof body.key === "string" ? body.key : void 0)) {
11983
+ sendJson(res, 401, { error: "invalid or missing key" });
11984
+ return;
11985
+ }
11986
+ const root = ccc.root;
11987
+ const roleName = typeof body.role === "string" && body.role !== "" ? body.role : void 0;
11988
+ const question = typeof body.question === "string" ? body.question : "";
11989
+ const sessionId = typeof body.sessionId === "string" && body.sessionId !== "" ? body.sessionId : void 0;
11990
+ if (!question.trim()) {
11991
+ sendJson(res, 400, { error: "empty question" });
11992
+ return;
11993
+ }
11994
+ const roles = readSkiffRoles(root);
11995
+ const explicit = roleName ? roles.get(roleName) : void 0;
11996
+ const effectiveRole = roleName && explicit ? roleName : roles.keys().next().value;
11997
+ const effectiveCfg = (roleName && explicit ? explicit : effectiveRole ? roles.get(effectiveRole) : void 0) ?? void 0;
11998
+ if (!effectiveRole || !effectiveCfg) {
11999
+ sendJson(res, 400, { error: `no skiff role available in ccc: ${root}` });
12000
+ return;
12001
+ }
12002
+ let agent;
12003
+ let continued = false;
12004
+ if (sessionId) {
12005
+ const info = skiffSessionInfo(sessionId);
12006
+ const live = getSkiffAgent(sessionId);
12007
+ if (info && live && info.role === effectiveRole && info.ccc === root) {
12008
+ agent = live;
12009
+ continued = true;
12010
+ }
12011
+ }
12012
+ if (!agent) agent = (await createSkiffAgent(ctx, root, effectiveRole, effectiveCfg, readHandymanConfig(root)?.defaultModel)).agent;
12013
+ const result = await askSkiff(ctx, agent, question, 0);
12014
+ sendJson(res, 200, {
12015
+ answer: result.answer,
12016
+ answer_html: renderSkiffMarkdown(result.answer),
12017
+ sessionId: result.sessionId,
12018
+ continued,
12019
+ trajectory: result.trajectory
12020
+ });
12021
+ }
12022
+ /** 问答页未启用提示页 */
12023
+ function publicAskDisabledHtml() {
12024
+ return `<!DOCTYPE html>
12025
+ <html lang="zh"><head><meta charset="utf-8"><title>Serenity Public Ask</title></head>
12026
+ <body style="font-family:system-ui;padding:48px;text-align:center;color:#57606a">
12027
+ <h2>Serenity Public Ask</h2>
12028
+ <p>问答页未启用(请在 dsh 设置面板「Serenity」页开启「建议问答」)。</p>
12029
+ </body></html>`;
12030
+ }
12031
+ /** 容器不存在提示页(URL 容器名不在已发现 CCC 中) */
12032
+ function publicAskContainerUnknownHtml(name) {
12033
+ return `<!DOCTYPE html>
12034
+ <html lang="zh"><head><meta charset="utf-8"><title>Serenity Public Ask</title></head>
12035
+ <body style="font-family:system-ui;padding:48px;text-align:center;color:#57606a">
12036
+ <h2>Serenity Public Ask</h2>
12037
+ <p>认知容器「${name}」不存在。</p>
12038
+ <p style="font-size:13px"><a href="/" style="color:#0ba875">返回容器列表</a></p>
12039
+ </body></html>`;
12040
+ }
12041
+ /** 容器未开放提示页(白名单外) */
12042
+ function publicAskContainerClosedHtml(name) {
12043
+ return `<!DOCTYPE html>
12044
+ <html lang="zh"><head><meta charset="utf-8"><title>Serenity Public Ask</title></head>
12045
+ <body style="font-family:system-ui;padding:48px;text-align:center;color:#57606a">
12046
+ <h2>Serenity Public Ask</h2>
12047
+ <p>认知容器「${name}」当前未开放问答(管理员在设置面板「开放容器」中配置)。</p>
12048
+ <p style="font-size:13px"><a href="/" style="color:#0ba875">返回容器列表</a></p>
12049
+ </body></html>`;
12050
+ }
12051
+ /** 页面基础样式(列表页 + 单容器页共用) */
12052
+ const ASK_CSS = `
12053
+ :root { color-scheme: light dark; }
12054
+ body { font-family: system-ui, -apple-system, sans-serif; margin: 0; padding: 24px; background: #f6f7f9; color: #1f2328; }
12055
+ @media (prefers-color-scheme: dark) { body { background: #1a1b1e; color: #e6e6e6; } }
12056
+ main { max-width: 720px; margin: 0 auto; }
12057
+ h1 { font-size: 18px; margin: 0 0 4px; }
12058
+ .sub { opacity: .65; font-size: 13px; margin-bottom: 16px; }
12059
+ label { font-size: 13px; font-weight: 600; display: block; margin: 12px 0 4px; }
12060
+ input, select, textarea { width: 100%; box-sizing: border-box; padding: 8px 10px; border-radius: 8px; border: 1px solid #d0d7de; background: #fff; color: inherit; font-size: 14px; }
12061
+ @media (prefers-color-scheme: dark) { input, select, textarea { background: #26282c; border-color: #3a3d42; } }
12062
+ textarea { min-height: 72px; resize: vertical; }
12063
+ button { margin-top: 12px; padding: 9px 18px; border-radius: 8px; border: 0; background: #0ba875; color: #fff; font-size: 14px; font-weight: 600; cursor: pointer; }
12064
+ button:disabled { opacity: .55; cursor: wait; }
12065
+ .card { background: #fff; border: 1px solid #d0d7de; border-radius: 10px; padding: 14px 16px; margin-bottom: 10px; display: block; text-decoration: none; color: inherit; }
12066
+ @media (prefers-color-scheme: dark) { .card { background: #26282c; border-color: #3a3d42; } }
12067
+ .card:hover { border-color: #0ba875; }
12068
+ .card .cname { font-weight: 600; font-size: 15px; }
12069
+ .card .cmeta { opacity: .6; font-size: 12px; margin-top: 2px; }
12070
+ #answer { background: #fff; border: 1px solid #d0d7de; border-radius: 8px; padding: 12px; margin-top: 16px; font-size: 14px; line-height: 1.6; min-height: 48px; word-break: break-word; }
12071
+ @media (prefers-color-scheme: dark) { #answer { background: #26282c; border-color: #3a3d42; } }
12072
+ #answer p { margin: 6px 0; }
12073
+ #answer h1, #answer h2, #answer h3 { margin: 12px 0 6px; font-size: 15px; }
12074
+ #answer code { background: rgba(127,127,127,.14); border-radius: 4px; padding: 1px 5px; font-size: 13px; font-family: ui-monospace, monospace; }
12075
+ #answer pre { background: rgba(127,127,127,.1); border-radius: 8px; padding: 10px 12px; overflow-x: auto; }
12076
+ #answer ul, #answer ol { margin: 6px 0; padding-left: 22px; }
12077
+ .err { color: #cf222e; white-space: pre-wrap; }
12078
+ .muted { opacity: .6; font-size: 13px; }
12079
+ details.think { margin: 8px 0; border: 1px solid rgba(210,153,34,.4); border-radius: 8px; background: rgba(210,153,34,.06); }
12080
+ details.think summary { cursor: pointer; padding: 6px 10px; font-size: 12px; color: #d29922; font-weight: 600; }
12081
+ .empty { text-align: center; opacity: .7; padding: 24px 0; }
12082
+ .back { display: inline-block; margin-bottom: 12px; font-size: 13px; color: #0ba875; text-decoration: none; }
12083
+ .ccname { font-weight: 600; color: #0ba875; }
12084
+ `;
12085
+ /** 容器列表页(v1.26.2):只列已开放容器,点击进入 /c/<name> */
12086
+ function publicAskListPage(cccs) {
12087
+ const cards = cccs.map((c) => `
12088
+ <a class="card" href="/c/${encodeURIComponent(c.name)}">
12089
+ <div class="cname">${c.name}</div>
12090
+ <div class="cmeta">${c.roles.length} 个问答角色 · 点击进入</div>
12091
+ </a>`).join("") || "<div class=\"empty\">暂无可问答的认知容器(管理员尚未开放)</div>";
12092
+ return `<!DOCTYPE html>
12093
+ <html lang="zh">
12094
+ <head>
12095
+ <meta charset="utf-8">
12096
+ <meta name="viewport" content="width=device-width, initial-scale=1">
12097
+ <title>Serenity Public Ask</title>
12098
+ <style>${ASK_CSS}</style>
12099
+ </head>
12100
+ <body>
12101
+ <main>
12102
+ <h1>Serenity Public Ask</h1>
12103
+ <div class="sub">选择认知容器开始提问(访问需 key,key 由管理员提供)</div>
12104
+ ${cards}
12105
+ </main>
12106
+ </body>
12107
+ </html>`;
12108
+ }
12109
+ /** 单容器问答页(v1.26.2):URL 锁定容器,key + 角色;key 自动存 localStorage */
12110
+ function publicAskContainerPage(ccc) {
12111
+ const data = JSON.stringify({
12112
+ name: ccc.name,
12113
+ root: ccc.root,
12114
+ roles: ccc.roles
12115
+ }).replace(/</g, "\\u003c");
12116
+ return `<!DOCTYPE html>
12117
+ <html lang="zh">
12118
+ <head>
12119
+ <meta charset="utf-8">
12120
+ <meta name="viewport" content="width=device-width, initial-scale=1">
12121
+ <title>Serenity Public Ask · ${ccc.name}</title>
12122
+ <style>${ASK_CSS}</style>
12123
+ </head>
12124
+ <body>
12125
+ <main>
12126
+ <a class="back" href="/">← 容器列表</a>
12127
+ <h1>Serenity Public Ask</h1>
12128
+ <div class="sub">认知容器:<span class="ccname">${ccc.name}</span></div>
12129
+ <label for="key">访问 Key</label>
12130
+ <input id="key" type="password" placeholder="请输入访问 key" autocomplete="off">
12131
+ <label for="role">问答角色</label>
12132
+ <select id="role"></select>
12133
+ <label for="q">问题</label>
12134
+ <textarea id="q" placeholder="向该认知容器提问…"></textarea>
12135
+ <button id="ask">提问</button>
12136
+ <div id="answer" class="muted">等待提问…</div>
12137
+ </main>
12138
+ <script id="ask-data" type="application/json">${data}<\/script>
12139
+ <script>
12140
+ const CCC = JSON.parse(document.getElementById('ask-data').textContent)
12141
+ const KEY_STORE = 'serenity-public-ask-key'
12142
+ const keyInput = document.getElementById('key')
12143
+ const roleSel = document.getElementById('role')
12144
+ const btn = document.getElementById('ask')
12145
+ const answer = document.getElementById('answer')
12146
+ let sessionId = ''
12147
+ // 角色下拉(v1.26.2 用户:3100 应能选择角色)
12148
+ roleSel.innerHTML = (CCC.roles || []).map((r) => '<option value="' + r + '">' + r + '</option>').join('') || '<option value="">(无角色)</option>'
12149
+ // key 自动从 localStorage 恢复(v1.26.2:输入一次后浏览器记住,无需每次粘贴)
12150
+ try { const saved = localStorage.getItem(KEY_STORE); if (saved) keyInput.value = saved } catch {}
12151
+ btn.addEventListener('click', async () => {
12152
+ const q = document.getElementById('q').value.trim()
12153
+ const key = keyInput.value.trim()
12154
+ const role = roleSel.value || undefined
12155
+ if (!q) { answer.className = 'err'; answer.textContent = '请输入问题'; return }
12156
+ if (!key) { answer.className = 'err'; answer.textContent = '请输入访问 key(没有 key 无法使用)'; return }
12157
+ // 输入即记住(保存到 localStorage;下次自动填充)
12158
+ try { localStorage.setItem(KEY_STORE, key) } catch {}
12159
+ btn.disabled = true
12160
+ answer.className = 'muted'
12161
+ answer.textContent = '运行中…'
12162
+ try {
12163
+ const res = await fetch('/c/' + encodeURIComponent(CCC.name) + '/ask', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ key, role, question: q, sessionId: sessionId || undefined }) })
12164
+ const data = await res.json()
12165
+ if (!res.ok) { sessionId = ''; throw new Error(data.error || ('HTTP ' + res.status)) }
12166
+ answer.className = ''
12167
+ answer.innerHTML = data.answer_html || data.answer || '(空回答)'
12168
+ sessionId = data.sessionId || ''
12169
+ } catch (err) {
12170
+ answer.className = 'err'
12171
+ answer.textContent = String(err.message || err)
12172
+ } finally {
12173
+ btn.disabled = false
12174
+ }
12175
+ })
12176
+ <\/script>
12177
+ </body>
12178
+ </html>`;
12179
+ }
11740
12180
  //#endregion
11741
12181
  //#region src/index.ts
11742
12182
  const name = "dsh-serenity-hooks";
@@ -11882,20 +12322,24 @@ function readWebPort(ctx) {
11882
12322
  }
11883
12323
  }
11884
12324
  /**
11885
- * F4c ACP HTTP JSON-RPC 端点装配(v1.26.0 实验性):启停 = 人工(设置面板
11886
- * 「Serenity」页 ACP 区块开关,settings 持久化)。会话创建/延续走 acp-core →
11887
- * skiff-core(与调试页同核心);仅监听 127.0.0.1;未开启零资源占用。
12325
+ * F4c ACP HTTP + F4d 建议问答页装配(v1.26.x 实验性):启停 = 人工(设置面板
12326
+ * 「Serenity」页 ACP / 问答页开关,settings 持久化)。任一面开启即启动服务
12327
+ * (同一端口:POST / = JSON-RPC 需 acpEnabled;GET / + /ask = 问答页需 publicAskEnabled)。
12328
+ * 会话创建/延续走 acp-core → skiff-core;仅监听 127.0.0.1;未开启零资源占用。
11888
12329
  */
11889
12330
  function registerAcp(ctx) {
11890
12331
  let started = false;
11891
12332
  const sync = () => {
11892
12333
  const s = readSimpleSettings();
11893
- if (s.acpEnabled && !started) startAcpHttpServer(ctx, s.acpHttpPort).then(() => {
11894
- started = true;
11895
- }).catch((err) => {
11896
- console.error(`[serenity-hooks] ACP HTTP 端点启动失败: ${String(err?.message ?? err)}`);
11897
- });
11898
- else if (!s.acpEnabled && started) {
12334
+ const anyFace = s.acpEnabled || s.publicAskEnabled;
12335
+ if (anyFace && !started) {
12336
+ const root = resolveSkiffRoot(ctx);
12337
+ startAcpHttpServer(ctx, s.acpHttpPort, root ?? void 0).then(() => {
12338
+ started = true;
12339
+ }).catch((err) => {
12340
+ console.error(`[serenity-hooks] ✗ ACP HTTP 服务启动失败: ${String(err?.message ?? err)}`);
12341
+ });
12342
+ } else if (!anyFace && started) {
11899
12343
  stopAcpHttpServer();
11900
12344
  started = false;
11901
12345
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shgroup/dsh-serenity-hooks",
3
- "version": "1.26.0",
3
+ "version": "1.26.2",
4
4
  "description": "宁静号 ACC harness — Native Cordis 插件(DeepSeek Harness 运行时)。真实 DSH 工具注册(cc_fs/session/acc_msm 等 9 工具)+ 拦截缝机械约束(safe-mode/路径守卫/会话落盘)+ 系统提示词注入(ACC/CCE/Constraints/SKILL/Session 五块)。适配 DSH 公开版(deepseek-ai/deepseek-harness 0.1.0-rc)。",
5
5
  "license": "MIT",
6
6
  "repository": {