@shgroup/dsh-serenity-hooks 1.26.4 → 1.26.6

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.4",
3
+ "version": "1.26.6",
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
@@ -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\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";
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/* v1.26.5 轮换 key(警示色:旧 key 立即失效) */\n.ae-rotate {\n color: var(--dsw-alias-state-danger-fg, #f87171);\n border-color: rgba(248, 113, 113, 0.35);\n}\n\n.ae-rotate:hover {\n border-color: var(--dsw-alias-state-danger-fg, #f87171);\n color: var(--dsw-alias-state-danger-fg, #f87171);\n}\n\n.ae-rotate:disabled {\n opacity: 0.6;\n cursor: wait;\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");
@@ -3841,6 +3841,34 @@ Instruction-following style:
3841
3841
  const [busy, setBusy] = (0, react.useState)(false);
3842
3842
  const [error, setError] = (0, react.useState)(null);
3843
3843
  const [saved, setSaved] = (0, react.useState)(false);
3844
+ const [rotating, setRotating] = (0, react.useState)(false);
3845
+ const rotateKey = async () => {
3846
+ if (!window.confirm("重新生成访问 Key?旧 Key 将立即失效,需要重新分享给使用者。")) return;
3847
+ setRotating(true);
3848
+ setError(null);
3849
+ try {
3850
+ const res = await fetch("/serenity/public-ask", {
3851
+ method: "PUT",
3852
+ headers: {
3853
+ "content-type": "application/json",
3854
+ "x-serenity-ui": "1"
3855
+ },
3856
+ body: JSON.stringify({ action: "rotate" })
3857
+ });
3858
+ if (!res.ok) throw new Error(`轮换失败(${res.status})`);
3859
+ const body = await res.json();
3860
+ setInfo((cur) => cur ? {
3861
+ ...cur,
3862
+ key: body.key
3863
+ } : cur);
3864
+ setSaved(true);
3865
+ setTimeout(() => setSaved(false), 2500);
3866
+ } catch (err) {
3867
+ setError(err instanceof Error ? err.message : String(err));
3868
+ } finally {
3869
+ setRotating(false);
3870
+ }
3871
+ };
3844
3872
  (0, react.useEffect)(() => {
3845
3873
  let alive = true;
3846
3874
  (async () => {
@@ -4003,6 +4031,13 @@ Instruction-following style:
4003
4031
  navigator.clipboard?.writeText(info.key);
4004
4032
  },
4005
4033
  children: "复制"
4034
+ }),
4035
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4036
+ type: "button",
4037
+ className: "ae-copy ae-rotate",
4038
+ disabled: rotating,
4039
+ onClick: () => void rotateKey(),
4040
+ children: rotating ? "生成中…" : "重新生成"
4006
4041
  })
4007
4042
  ]
4008
4043
  }),
package/lib/index.js CHANGED
@@ -7559,6 +7559,62 @@ function verifyPublicAskKey(provided) {
7559
7559
  return a.length === b.length && timingSafeEqual(a, b);
7560
7560
  }
7561
7561
  /**
7562
+ * 轮换问答页访问 key(v1.26.5,S142 用户:公网开放 + 不好说要换 key 呢):
7563
+ * 生成新 32 字节随机 hex(64 字符)→ 覆盖写回(**强制替换**——旧 key 立即失效)。
7564
+ * @returns 新 key(面板展示给管理员,重新分享给使用者)
7565
+ */
7566
+ function rotatePublicAskKey() {
7567
+ const current = readAdvancedSettings();
7568
+ const key = randomBytes(32).toString("hex");
7569
+ updateAdvancedSettings({ publicAsk: {
7570
+ key,
7571
+ allowed: current.publicAsk.allowed
7572
+ } });
7573
+ return key;
7574
+ }
7575
+ /** 首次锁定基础时长(指数退避底数) */
7576
+ const PUBLIC_ASK_LOCK_BASE_MS = 9e5;
7577
+ /** 锁定上限 */
7578
+ const PUBLIC_ASK_LOCK_MAX_MS = 144e5;
7579
+ const ipFailStates = /* @__PURE__ */ new Map();
7580
+ /** 重置 IP 失败状态(测试/管理员解封) */
7581
+ function resetPublicAskIpFail(ip) {
7582
+ ipFailStates.delete(ip);
7583
+ }
7584
+ /** IP 当前是否锁定(到期自动解锁) */
7585
+ function isPublicAskIpLocked(ip) {
7586
+ const st = ipFailStates.get(ip);
7587
+ if (!st || st.lockedUntil === 0) return false;
7588
+ if (Date.now() >= st.lockedUntil) {
7589
+ st.lockedUntil = 0;
7590
+ st.count = 0;
7591
+ return false;
7592
+ }
7593
+ return true;
7594
+ }
7595
+ /** 记录一次 key 校验失败;达到阈值 → 锁定(指数退避)。返回锁定剩余毫秒(0 = 未锁定) */
7596
+ function recordPublicAskFail(ip) {
7597
+ let st = ipFailStates.get(ip);
7598
+ if (!st) {
7599
+ st = {
7600
+ count: 0,
7601
+ lockedUntil: 0,
7602
+ lockRound: 0
7603
+ };
7604
+ ipFailStates.set(ip, st);
7605
+ }
7606
+ if (st.lockedUntil > 0 && Date.now() < st.lockedUntil) return st.lockedUntil - Date.now();
7607
+ st.count += 1;
7608
+ if (st.count >= 5) {
7609
+ const base = PUBLIC_ASK_LOCK_BASE_MS * 2 ** st.lockRound;
7610
+ st.lockedUntil = Date.now() + Math.min(base, PUBLIC_ASK_LOCK_MAX_MS);
7611
+ st.lockRound += 1;
7612
+ st.count = 0;
7613
+ return st.lockedUntil - Date.now();
7614
+ }
7615
+ return 0;
7616
+ }
7617
+ /**
7562
7618
  * 一次性迁移(v1.21.x → v1.22):旧版把 `serenityAdvanced` 存在 CCC localstore.json;
7563
7619
  * 新版归 plugin 全局文件。全局文件已存在 → 跳过(幂等);localstore 无旧节 → 跳过。
7564
7620
  * @param root - CCC 根(localstore.json 所在目录)
@@ -8644,14 +8700,23 @@ function registerStatusApi(ctx, opts = {}) {
8644
8700
  path: PUBLIC_ASK_PATH,
8645
8701
  handler: async (req, res) => {
8646
8702
  try {
8647
- if (req.method !== "GET") {
8648
- sendJson$2(res, 405, { error: "method not allowed" });
8649
- return;
8650
- }
8651
8703
  if (req.headers["x-serenity-ui"] !== "1") {
8652
8704
  sendJson$2(res, 403, { error: "仅限 WebUI(key 属敏感凭据)" });
8653
8705
  return;
8654
8706
  }
8707
+ if (req.method === "PUT") {
8708
+ const raw = await readBody$3(req, 16384);
8709
+ if (JSON.parse(raw).action !== "rotate") {
8710
+ sendJson$2(res, 400, { error: "unsupported action (expected \"rotate\")" });
8711
+ return;
8712
+ }
8713
+ sendJson$2(res, 200, { key: rotatePublicAskKey() });
8714
+ return;
8715
+ }
8716
+ if (req.method !== "GET") {
8717
+ sendJson$2(res, 405, { error: "method not allowed" });
8718
+ return;
8719
+ }
8655
8720
  const settings = readAdvancedSettings();
8656
8721
  const { readSimpleSettings } = await Promise.resolve().then(() => settings_section_exports);
8657
8722
  const simple = readSimpleSettings();
@@ -12045,6 +12110,7 @@ async function handle(ctx, defaultRoot, req, res) {
12045
12110
  try {
12046
12111
  const url = (req.url ?? "/").split("?")[0] ?? "/";
12047
12112
  const s = readSimpleSettings();
12113
+ const ip = req.headers["x-forwarded-for"]?.split(",")[0]?.trim() || req.socket.remoteAddress || "unknown";
12048
12114
  if (req.method === "POST" && url === "/") {
12049
12115
  if (!s.acpEnabled) {
12050
12116
  sendJson(res, 403, { error: "ACP JSON-RPC disabled (enable in Serenity settings)" });
@@ -12119,7 +12185,7 @@ async function handle(ctx, defaultRoot, req, res) {
12119
12185
  sendJson(res, 400, { error: "invalid JSON body" });
12120
12186
  return;
12121
12187
  }
12122
- await handleAskParsed(ctx, ccc, body, res);
12188
+ await handleAskParsed(ctx, ccc, body, res, ip);
12123
12189
  return;
12124
12190
  }
12125
12191
  if (req.method === "POST" && url === "/ask") {
@@ -12148,7 +12214,7 @@ async function handle(ctx, defaultRoot, req, res) {
12148
12214
  sendJson(res, 403, { error: "container is not open for public ask" });
12149
12215
  return;
12150
12216
  }
12151
- await handleAskParsed(ctx, ccc, body, res);
12217
+ await handleAskParsed(ctx, ccc, body, res, ip);
12152
12218
  return;
12153
12219
  }
12154
12220
  sendJson(res, 404, { error: "not found" });
@@ -12158,14 +12224,22 @@ async function handle(ctx, defaultRoot, req, res) {
12158
12224
  }
12159
12225
  /**
12160
12226
  * 单容器问答核心(/c/<name>/ask 与 /ask 共用):
12161
- * body { key, role?, question, sessionId? } → key 校验(401)→ 角色缺省取 CCC 第一个
12227
+ * body { key, role?, question, sessionId? } → key 校验(401 + IP 失败锁定)→ 角色缺省取 CCC 第一个
12162
12228
  * → 会话延续(v1.25.10 语义,静默新建)→ askSkiff(0 全量轨迹) → { answer, answer_html, sessionId, continued, trajectory }
12229
+ * @param ip 请求来源 IP(v1.26.5 公网 key 失败锁定:X-Forwarded-For 优先——公网反代,回退 remoteAddress)
12163
12230
  */
12164
- async function handleAskParsed(ctx, ccc, body, res) {
12165
- if (!verifyPublicAskKey(typeof body.key === "string" ? body.key : void 0)) {
12166
- sendJson(res, 401, { error: "invalid or missing key" });
12231
+ async function handleAskParsed(ctx, ccc, body, res, ip) {
12232
+ const key = typeof body.key === "string" ? body.key : void 0;
12233
+ if (isPublicAskIpLocked(ip)) {
12234
+ sendJson(res, 429, { error: "too many failed attempts — temporarily locked (retry later)" });
12235
+ return;
12236
+ }
12237
+ if (!verifyPublicAskKey(key)) {
12238
+ const remaining = recordPublicAskFail(ip);
12239
+ sendJson(res, remaining > 0 ? 429 : 401, { error: remaining > 0 ? `too many failed attempts — locked for ${Math.ceil(remaining / 6e4)} min` : "invalid or missing key" });
12167
12240
  return;
12168
12241
  }
12242
+ resetPublicAskIpFail(ip);
12169
12243
  const root = ccc.root;
12170
12244
  const roleName = typeof body.role === "string" && body.role !== "" ? body.role : void 0;
12171
12245
  const question = typeof body.question === "string" ? body.question : "";
@@ -12248,6 +12322,11 @@ const ASK_CSS = `
12248
12322
  .card .cmeta { opacity: .6; font-size: 12px; margin-top: 2px; }
12249
12323
  .empty { text-align: center; opacity: .7; padding: 24px 0; }
12250
12324
  .ccname { font-weight: 600; color: var(--sp-green); }
12325
+ /* key 门(v1.26.6:选容器前必填) */
12326
+ .keyGate { display: flex; align-items: center; gap: 8px; margin-bottom: 14px; background: rgba(127,127,127,.06); border: 1px solid rgba(127,127,127,.18); border-radius: 10px; padding: 10px 14px; }
12327
+ .keyGate label { font-size: 12px; opacity: .7; white-space: nowrap; }
12328
+ .keyGate input { flex: 1; border: 1px solid rgba(127,127,127,.25); border-radius: 8px; background: transparent; color: inherit; padding: 8px 12px; font-size: 14px; }
12329
+ .keyGate input:focus { outline: none; border-color: var(--sp-green); }
12251
12330
 
12252
12331
  /* ── 聊天布局(v1.26.4 体验升级:参考 ChatGPT/Claude 会话形态)── */
12253
12332
  .chat { display: flex; flex-direction: column; height: 100vh; max-width: 720px; margin: 0 auto; }
@@ -12291,9 +12370,6 @@ const ASK_CSS = `
12291
12370
  .msg .err { padding: 0 4px; }
12292
12371
  /* 输入区 */
12293
12372
  .chatInput { border-top: 1px solid rgba(127,127,127,.18); padding: 10px 16px calc(12px + env(safe-area-inset-bottom)); background: rgba(127,127,127,.04); }
12294
- .chatInput .keyRow { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }
12295
- .chatInput .keyRow label { font-size: 12px; opacity: .7; white-space: nowrap; }
12296
- .chatInput .keyRow input { flex: 1; border: 1px solid rgba(127,127,127,.25); border-radius: 8px; background: transparent; color: inherit; padding: 6px 10px; font-size: 13px; }
12297
12373
  .inputRow { display: flex; align-items: flex-end; gap: 8px; }
12298
12374
  .inputRow textarea { flex: 1; border: 1px solid rgba(127,127,127,.25); border-radius: 12px; background: #fff; color: inherit; padding: 10px 12px; font-size: 14px; resize: none; min-height: 44px; max-height: 140px; line-height: 1.5; }
12299
12375
  @media (prefers-color-scheme: dark) { .inputRow textarea { background: #26282c; border-color: #3a3d42; } }
@@ -12302,11 +12378,35 @@ const ASK_CSS = `
12302
12378
  .inputRow .sendBtn:disabled { opacity: .5; cursor: wait; }
12303
12379
  .inputRow .sendBtn:hover:not(:disabled) { background: var(--sp-green-dim); }
12304
12380
  .hint { text-align: center; font-size: 12px; opacity: .55; padding: 20px 0; }
12381
+
12382
+ /* ── 移动端适配(v1.26.6:手机/平板——iOS 防放大 16px / 触控 ≥44px / 100dvh / safe-area)── */
12383
+ @media (max-width: 640px) {
12384
+ main { padding: 20px 16px calc(28px + env(safe-area-inset-bottom)); }
12385
+ .sub { font-size: 14px; margin-bottom: 14px; }
12386
+ .keyGate { flex-wrap: wrap; gap: 6px; }
12387
+ .keyGate input { flex: 1 1 100%; font-size: 16px; padding: 12px 14px; min-height: 48px; } /* 16px 防 iOS 聚焦放大 + 触控 ≥44px */
12388
+ .card { padding: 16px; border-radius: 12px; }
12389
+ .card .cname { font-size: 16px; }
12390
+ .card .cmeta { font-size: 13px; }
12391
+ .chat { height: 100vh; height: 100dvh; } /* dvh:iOS 地址栏收起/展开不跳动 */
12392
+ .chatHead { gap: 8px; padding: 8px 12px calc(8px + env(safe-area-inset-top)); }
12393
+ .chatHead .back { font-size: 14px; padding: 10px 4px; } /* 触控目标 ≥44px */
12394
+ .chatHead .title { font-size: 14px; }
12395
+ .chatHead select { font-size: 16px; padding: 8px 6px; min-height: 44px; } /* 16px 防放大 + 触控 */
12396
+ .chatHead .newBtn { font-size: 14px; padding: 10px 12px; min-height: 44px; }
12397
+ .msgList { padding: 14px 12px calc(20px + env(safe-area-inset-bottom)); gap: 14px; }
12398
+ .msg { max-width: 94%; }
12399
+ .msg .bubble { font-size: 15px; padding: 12px 14px; }
12400
+ .msg .who { font-size: 12px; }
12401
+ .chatInput { padding: 8px 12px calc(10px + env(safe-area-inset-bottom)); }
12402
+ .inputRow textarea { font-size: 16px; padding: 12px 14px; min-height: 48px; } /* 16px 防放大 + 触控 */
12403
+ .inputRow .sendBtn { font-size: 16px; padding: 12px 20px; min-height: 48px; }
12404
+ }
12305
12405
  `;
12306
- /** 容器列表页(v1.26.2):只列已开放容器,点击进入 /c/<name> */
12406
+ /** 容器列表页(v1.26.2 v1.26.6):key 在选容器前填写(v1.26.6 用户拍板:不填 key 不让选容器) */
12307
12407
  function publicAskListPage(cccs) {
12308
12408
  const cards = cccs.map((c) => `
12309
- <a class="card" href="/c/${encodeURIComponent(c.name)}">
12409
+ <a class="card" data-name="${c.name}" href="/c/${encodeURIComponent(c.name)}">
12310
12410
  <div class="cname">${c.name}</div>
12311
12411
  <div class="cmeta">${c.roles.length} 个问答角色 · 点击进入</div>
12312
12412
  </a>`).join("") || "<div class=\"empty\">暂无可问答的认知容器(管理员尚未开放)</div>";
@@ -12314,20 +12414,46 @@ function publicAskListPage(cccs) {
12314
12414
  <html lang="zh">
12315
12415
  <head>
12316
12416
  <meta charset="utf-8">
12317
- <meta name="viewport" content="width=device-width, initial-scale=1">
12417
+ <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
12318
12418
  <title>Serenity Public Ask</title>
12319
12419
  <style>${ASK_CSS}</style>
12320
12420
  </head>
12321
12421
  <body>
12322
12422
  <main>
12323
12423
  <h1>Serenity Public Ask</h1>
12324
- <div class="sub">选择认知容器开始提问(访问需 key,key 由管理员提供)</div>
12325
- ${cards}
12424
+ <div class="sub">先填写访问 key,再选择认知容器开始提问</div>
12425
+ <div class="keyGate">
12426
+ <label for="gateKey">访问 Key</label>
12427
+ <input id="gateKey" type="password" placeholder="请输入访问 key(仅本次浏览器记忆)" autocomplete="off" autocapitalize="none" enterkeyhint="done">
12428
+ </div>
12429
+ <div id="cards">${cards}</div>
12326
12430
  </main>
12431
+ <script>
12432
+ const KEY_STORE = 'serenity-public-ask-key'
12433
+ const gateKey = document.getElementById('gateKey')
12434
+ const cardList = document.getElementById('cards')
12435
+ // 容器卡片(key 未填时禁用——v1.26.6 用户拍板:key 在选容器前填写)
12436
+ function applyGate() {
12437
+ const ok = gateKey.value.trim().length > 0
12438
+ for (const card of cardList.querySelectorAll('a.card')) {
12439
+ card.classList.toggle('locked', !ok)
12440
+ card.setAttribute('aria-disabled', ok ? 'false' : 'true')
12441
+ if (!ok) { card.removeAttribute('href'); card.style.pointerEvents = 'none'; card.style.opacity = '.4' }
12442
+ else { card.setAttribute('href', '/c/' + encodeURIComponent(card.dataset.name)); card.style.pointerEvents = ''; card.style.opacity = '' }
12443
+ }
12444
+ }
12445
+ gateKey.addEventListener('input', applyGate)
12446
+ // key 自动从 localStorage 恢复
12447
+ let hasSavedKey = false
12448
+ try { const saved = localStorage.getItem(KEY_STORE); if (saved) { gateKey.value = saved; hasSavedKey = true } } catch {}
12449
+ if (hasSavedKey) gateKey.placeholder = '访问 Key(已记忆,可修改)'
12450
+ applyGate()
12451
+ gateKey.focus()
12452
+ <\/script>
12327
12453
  </body>
12328
12454
  </html>`;
12329
12455
  }
12330
- /** 单容器问答页(v1.26.2→v1.26.4):URL 锁定容器;聊天 UI——消息流 + 连续对话 + key 自动记忆 */
12456
+ /** 单容器问答页(v1.26.2→v1.26.6):URL 锁定容器;聊天 UI——消息流 + 连续对话 + key 从列表页记忆(v1.26.6 用户拍板:对话页不再填 key) */
12331
12457
  function publicAskContainerPage(ccc) {
12332
12458
  const data = JSON.stringify({
12333
12459
  name: ccc.name,
@@ -12338,7 +12464,7 @@ function publicAskContainerPage(ccc) {
12338
12464
  <html lang="zh">
12339
12465
  <head>
12340
12466
  <meta charset="utf-8">
12341
- <meta name="viewport" content="width=device-width, initial-scale=1">
12467
+ <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
12342
12468
  <title>Serenity · ${ccc.name}</title>
12343
12469
  <style>${ASK_CSS}</style>
12344
12470
  </head>
@@ -12355,12 +12481,8 @@ function publicAskContainerPage(ccc) {
12355
12481
  <div id="msgs" class="msgList"></div>
12356
12482
 
12357
12483
  <div class="chatInput">
12358
- <div class="keyRow" id="keyRow">
12359
- <label for="key">访问 Key</label>
12360
- <input id="key" type="password" placeholder="请输入访问 key(自动记忆)" autocomplete="off">
12361
- </div>
12362
12484
  <div class="inputRow">
12363
- <textarea id="q" placeholder="向 ${ccc.name} 提问…(Enter 发送,Shift+Enter 换行)" rows="1"></textarea>
12485
+ <textarea id="q" placeholder="向 ${ccc.name} 提问…(Enter 发送,Shift+Enter 换行)" rows="1" enterkeyhint="send" autocapitalize="sentences"></textarea>
12364
12486
  <button id="ask" class="sendBtn">发送</button>
12365
12487
  </div>
12366
12488
  </div>
@@ -12369,19 +12491,19 @@ function publicAskContainerPage(ccc) {
12369
12491
  <script>
12370
12492
  const CCC = JSON.parse(document.getElementById('ask-data').textContent)
12371
12493
  const KEY_STORE = 'serenity-public-ask-key'
12372
- const keyInput = document.getElementById('key')
12373
12494
  const roleSel = document.getElementById('role')
12374
12495
  const btn = document.getElementById('ask')
12375
12496
  const msgs = document.getElementById('msgs')
12376
12497
  const qInput = document.getElementById('q')
12377
- const keyRow = document.getElementById('keyRow')
12378
12498
  let sessionId = ''
12379
12499
  let busy = false
12380
12500
 
12381
12501
  // 角色下拉
12382
12502
  roleSel.innerHTML = (CCC.roles || []).map((r) => '<option value="' + r + '">' + r + '</option>').join('') || '<option value="">(无角色)</option>'
12383
- // key 自动从 localStorage 恢复
12384
- try { const saved = localStorage.getItem(KEY_STORE); if (saved) { keyInput.value = saved; keyRow.style.display = 'none' } } catch {}
12503
+ // key localStorage 读取(列表页填写时记忆;对话页不填——v1.26.6 用户拍板)
12504
+ function getStoredKey() {
12505
+ try { return (localStorage.getItem(KEY_STORE) || '').trim() } catch { return '' }
12506
+ }
12385
12507
 
12386
12508
  /** 追加一条消息气泡(role: user|assistant) */
12387
12509
  function addMsg(role, htmlOrText, isHtml) {
@@ -12420,13 +12542,11 @@ function addTyping() {
12420
12542
 
12421
12543
  async function ask() {
12422
12544
  const q = qInput.value.trim()
12423
- const key = keyInput.value.trim()
12545
+ const key = getStoredKey()
12424
12546
  const role = roleSel.value || undefined
12425
12547
  if (!q) return
12426
- if (!key) { keyInput.focus(); return }
12548
+ if (!key) { addMsg('assistant', '⚠️ 未找到访问 key,请先回到 <a href="/">容器列表页</a> 填写 key。', true); return }
12427
12549
  if (busy) return
12428
- // 保存 key(下次自动记忆 + 隐藏 key 行)
12429
- try { localStorage.setItem(KEY_STORE, key); keyRow.style.display = 'none' } catch {}
12430
12550
  // 追加用户消息 + 打字指示
12431
12551
  addMsg('user', q, false)
12432
12552
  qInput.value = ''
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shgroup/dsh-serenity-hooks",
3
- "version": "1.26.4",
3
+ "version": "1.26.6",
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": {