@shgroup/dsh-serenity-hooks 1.26.3 → 1.26.5
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 +1 -1
- package/lib/client.js +36 -1
- package/lib/index.js +267 -68
- package/package.json +1 -1
package/dsh.plugin.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "dsh-serenity-hooks",
|
|
3
|
-
"version": "1.26.
|
|
3
|
+
"version": "1.26.5",
|
|
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();
|
|
@@ -11663,9 +11728,11 @@ function escapeHtml(s) {
|
|
|
11663
11728
|
* ① 提取 `<think>…</think>` 块(占位符 \u0000T<idx>\u0000)
|
|
11664
11729
|
* ② 正文与 think 内容**先 escapeHtml 再 marked.parse**(GFM + breaks)——markdown 语法不受
|
|
11665
11730
|
* 转义影响,原始 HTML 注入被消除(安全);代码块内 `<` 显示为实体(可接受)
|
|
11666
|
-
* ③ think
|
|
11731
|
+
* ③ think 占位符:默认还原为 `<details class="think">` 折叠卡(🧠 思考过程,默认收起);
|
|
11732
|
+
* **hideThink=true(v1.26.4,public 口)→ 直接移除**(思考过程对普通用户不展示)
|
|
11733
|
+
* @param hideThink 为 true 时 `<think>` 内容完全不渲染(public 问答页体验)
|
|
11667
11734
|
*/
|
|
11668
|
-
function renderSkiffMarkdown(raw) {
|
|
11735
|
+
function renderSkiffMarkdown(raw, hideThink = false) {
|
|
11669
11736
|
const thinks = [];
|
|
11670
11737
|
const body = raw.replace(/<think>([\s\S]*?)<\/think>/gi, (_m, inner) => {
|
|
11671
11738
|
const idx = thinks.length;
|
|
@@ -11677,6 +11744,7 @@ function renderSkiffMarkdown(raw) {
|
|
|
11677
11744
|
gfm: true
|
|
11678
11745
|
});
|
|
11679
11746
|
return (typeof parsed === "string" ? parsed : "").replace(/\u0000T(\d+)\u0000/g, (_m, idx) => {
|
|
11747
|
+
if (hideThink) return "";
|
|
11680
11748
|
const inner = thinks[Number(idx)] ?? "";
|
|
11681
11749
|
const innerHtml = inner === "" ? "" : f.parse(escapeHtml(inner), {
|
|
11682
11750
|
breaks: true,
|
|
@@ -12042,6 +12110,7 @@ async function handle(ctx, defaultRoot, req, res) {
|
|
|
12042
12110
|
try {
|
|
12043
12111
|
const url = (req.url ?? "/").split("?")[0] ?? "/";
|
|
12044
12112
|
const s = readSimpleSettings();
|
|
12113
|
+
const ip = req.headers["x-forwarded-for"]?.split(",")[0]?.trim() || req.socket.remoteAddress || "unknown";
|
|
12045
12114
|
if (req.method === "POST" && url === "/") {
|
|
12046
12115
|
if (!s.acpEnabled) {
|
|
12047
12116
|
sendJson(res, 403, { error: "ACP JSON-RPC disabled (enable in Serenity settings)" });
|
|
@@ -12116,7 +12185,7 @@ async function handle(ctx, defaultRoot, req, res) {
|
|
|
12116
12185
|
sendJson(res, 400, { error: "invalid JSON body" });
|
|
12117
12186
|
return;
|
|
12118
12187
|
}
|
|
12119
|
-
await handleAskParsed(ctx, ccc, body, res);
|
|
12188
|
+
await handleAskParsed(ctx, ccc, body, res, ip);
|
|
12120
12189
|
return;
|
|
12121
12190
|
}
|
|
12122
12191
|
if (req.method === "POST" && url === "/ask") {
|
|
@@ -12145,7 +12214,7 @@ async function handle(ctx, defaultRoot, req, res) {
|
|
|
12145
12214
|
sendJson(res, 403, { error: "container is not open for public ask" });
|
|
12146
12215
|
return;
|
|
12147
12216
|
}
|
|
12148
|
-
await handleAskParsed(ctx, ccc, body, res);
|
|
12217
|
+
await handleAskParsed(ctx, ccc, body, res, ip);
|
|
12149
12218
|
return;
|
|
12150
12219
|
}
|
|
12151
12220
|
sendJson(res, 404, { error: "not found" });
|
|
@@ -12155,14 +12224,22 @@ async function handle(ctx, defaultRoot, req, res) {
|
|
|
12155
12224
|
}
|
|
12156
12225
|
/**
|
|
12157
12226
|
* 单容器问答核心(/c/<name>/ask 与 /ask 共用):
|
|
12158
|
-
* body { key, role?, question, sessionId? } → key 校验(401
|
|
12227
|
+
* body { key, role?, question, sessionId? } → key 校验(401 + IP 失败锁定)→ 角色缺省取 CCC 第一个
|
|
12159
12228
|
* → 会话延续(v1.25.10 语义,静默新建)→ askSkiff(0 全量轨迹) → { answer, answer_html, sessionId, continued, trajectory }
|
|
12229
|
+
* @param ip 请求来源 IP(v1.26.5 公网 key 失败锁定:X-Forwarded-For 优先——公网反代,回退 remoteAddress)
|
|
12160
12230
|
*/
|
|
12161
|
-
async function handleAskParsed(ctx, ccc, body, res) {
|
|
12162
|
-
|
|
12163
|
-
|
|
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" });
|
|
12164
12240
|
return;
|
|
12165
12241
|
}
|
|
12242
|
+
resetPublicAskIpFail(ip);
|
|
12166
12243
|
const root = ccc.root;
|
|
12167
12244
|
const roleName = typeof body.role === "string" && body.role !== "" ? body.role : void 0;
|
|
12168
12245
|
const question = typeof body.question === "string" ? body.question : "";
|
|
@@ -12193,7 +12270,7 @@ async function handleAskParsed(ctx, ccc, body, res) {
|
|
|
12193
12270
|
const result = await askSkiff(ctx, agent, question, 0);
|
|
12194
12271
|
sendJson(res, 200, {
|
|
12195
12272
|
answer: result.answer,
|
|
12196
|
-
answer_html: renderSkiffMarkdown(result.answer),
|
|
12273
|
+
answer_html: renderSkiffMarkdown(result.answer, true),
|
|
12197
12274
|
sessionId: result.sessionId,
|
|
12198
12275
|
continued,
|
|
12199
12276
|
trajectory: result.trajectory
|
|
@@ -12228,39 +12305,77 @@ function publicAskContainerClosedHtml(name) {
|
|
|
12228
12305
|
<p style="font-size:13px"><a href="/" style="color:#0ba875">返回容器列表</a></p>
|
|
12229
12306
|
</body></html>`;
|
|
12230
12307
|
}
|
|
12231
|
-
/** 页面基础样式(列表页 +
|
|
12308
|
+
/** 页面基础样式(列表页 + 单容器页共用;v1.26.4 聊天 UI) */
|
|
12232
12309
|
const ASK_CSS = `
|
|
12233
|
-
:root { color-scheme: light dark; }
|
|
12234
|
-
|
|
12235
|
-
|
|
12310
|
+
:root { color-scheme: light dark; --sp-green: #0ba875; --sp-green-dim: #059669; }
|
|
12311
|
+
* { box-sizing: border-box; }
|
|
12312
|
+
html, body { height: 100%; }
|
|
12313
|
+
body { font-family: system-ui, -apple-system, sans-serif; margin: 0; background: #f6f7f9; color: #1f2328; }
|
|
12314
|
+
@media (prefers-color-scheme: dark) { body { background: #16181c; color: #e6e6e6; } }
|
|
12236
12315
|
main { max-width: 720px; margin: 0 auto; }
|
|
12237
12316
|
h1 { font-size: 18px; margin: 0 0 4px; }
|
|
12238
12317
|
.sub { opacity: .65; font-size: 13px; margin-bottom: 16px; }
|
|
12239
|
-
label { font-size: 13px; font-weight: 600; display: block; margin: 12px 0 4px; }
|
|
12240
|
-
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; }
|
|
12241
|
-
@media (prefers-color-scheme: dark) { input, select, textarea { background: #26282c; border-color: #3a3d42; } }
|
|
12242
|
-
textarea { min-height: 72px; resize: vertical; }
|
|
12243
|
-
button { margin-top: 12px; padding: 9px 18px; border-radius: 8px; border: 0; background: #0ba875; color: #fff; font-size: 14px; font-weight: 600; cursor: pointer; }
|
|
12244
|
-
button:disabled { opacity: .55; cursor: wait; }
|
|
12245
12318
|
.card { background: #fff; border: 1px solid #d0d7de; border-radius: 10px; padding: 14px 16px; margin-bottom: 10px; display: block; text-decoration: none; color: inherit; }
|
|
12246
12319
|
@media (prefers-color-scheme: dark) { .card { background: #26282c; border-color: #3a3d42; } }
|
|
12247
|
-
.card:hover { border-color:
|
|
12320
|
+
.card:hover { border-color: var(--sp-green); }
|
|
12248
12321
|
.card .cname { font-weight: 600; font-size: 15px; }
|
|
12249
12322
|
.card .cmeta { opacity: .6; font-size: 12px; margin-top: 2px; }
|
|
12250
|
-
#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; }
|
|
12251
|
-
@media (prefers-color-scheme: dark) { #answer { background: #26282c; border-color: #3a3d42; } }
|
|
12252
|
-
#answer p { margin: 6px 0; }
|
|
12253
|
-
#answer h1, #answer h2, #answer h3 { margin: 12px 0 6px; font-size: 15px; }
|
|
12254
|
-
#answer code { background: rgba(127,127,127,.14); border-radius: 4px; padding: 1px 5px; font-size: 13px; font-family: ui-monospace, monospace; }
|
|
12255
|
-
#answer pre { background: rgba(127,127,127,.1); border-radius: 8px; padding: 10px 12px; overflow-x: auto; }
|
|
12256
|
-
#answer ul, #answer ol { margin: 6px 0; padding-left: 22px; }
|
|
12257
|
-
.err { color: #cf222e; white-space: pre-wrap; }
|
|
12258
|
-
.muted { opacity: .6; font-size: 13px; }
|
|
12259
|
-
details.think { margin: 8px 0; border: 1px solid rgba(210,153,34,.4); border-radius: 8px; background: rgba(210,153,34,.06); }
|
|
12260
|
-
details.think summary { cursor: pointer; padding: 6px 10px; font-size: 12px; color: #d29922; font-weight: 600; }
|
|
12261
12323
|
.empty { text-align: center; opacity: .7; padding: 24px 0; }
|
|
12262
|
-
.
|
|
12263
|
-
|
|
12324
|
+
.ccname { font-weight: 600; color: var(--sp-green); }
|
|
12325
|
+
|
|
12326
|
+
/* ── 聊天布局(v1.26.4 体验升级:参考 ChatGPT/Claude 会话形态)── */
|
|
12327
|
+
.chat { display: flex; flex-direction: column; height: 100vh; max-width: 720px; margin: 0 auto; }
|
|
12328
|
+
.chatHead { display: flex; align-items: center; gap: 10px; padding: 10px 16px; border-bottom: 1px solid rgba(127,127,127,.18); background: rgba(127,127,127,.04); position: sticky; top: 0; z-index: 5; }
|
|
12329
|
+
.chatHead .back { color: var(--sp-green); text-decoration: none; font-size: 13px; white-space: nowrap; }
|
|
12330
|
+
.chatHead .title { font-weight: 600; font-size: 14px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
12331
|
+
.chatHead .spacer { flex: 1; }
|
|
12332
|
+
.chatHead select { border: 1px solid rgba(127,127,127,.25); border-radius: 8px; background: transparent; color: inherit; padding: 4px 8px; font-size: 12px; }
|
|
12333
|
+
.chatHead .newBtn { border: 1px solid rgba(127,127,127,.25); border-radius: 8px; background: transparent; color: inherit; padding: 4px 10px; font-size: 12px; cursor: pointer; white-space: nowrap; }
|
|
12334
|
+
.chatHead .newBtn:hover { border-color: var(--sp-green); color: var(--sp-green); }
|
|
12335
|
+
/* 消息流 */
|
|
12336
|
+
.msgList { flex: 1; overflow-y: auto; padding: 18px 16px; display: flex; flex-direction: column; gap: 16px; }
|
|
12337
|
+
.msg { display: flex; flex-direction: column; max-width: 88%; }
|
|
12338
|
+
.msg.user { align-self: flex-end; align-items: flex-end; }
|
|
12339
|
+
.msg.assistant { align-self: flex-start; align-items: flex-start; }
|
|
12340
|
+
.msg .who { font-size: 11px; opacity: .55; margin-bottom: 4px; padding: 0 4px; }
|
|
12341
|
+
.msg .bubble { padding: 10px 14px; border-radius: 14px; font-size: 14px; line-height: 1.65; word-break: break-word; white-space: normal; }
|
|
12342
|
+
.msg.user .bubble { background: var(--sp-green); color: #fff; border-bottom-right-radius: 4px; }
|
|
12343
|
+
.msg.assistant .bubble { background: #fff; border: 1px solid #d0d7de; border-bottom-left-radius: 4px; }
|
|
12344
|
+
@media (prefers-color-scheme: dark) { .msg.assistant .bubble { background: #26282c; border-color: #3a3d42; } }
|
|
12345
|
+
.bubble p { margin: 6px 0; }
|
|
12346
|
+
.bubble p:first-child { margin-top: 0; }
|
|
12347
|
+
.bubble p:last-child { margin-bottom: 0; }
|
|
12348
|
+
.bubble h1, .bubble h2, .bubble h3, .bubble h4 { margin: 12px 0 6px; font-size: 15px; line-height: 1.4; }
|
|
12349
|
+
.bubble ul, .bubble ol { margin: 6px 0; padding-left: 22px; }
|
|
12350
|
+
.bubble code { background: rgba(127,127,127,.14); border-radius: 4px; padding: 1px 5px; font-size: 13px; font-family: ui-monospace, monospace; }
|
|
12351
|
+
.bubble pre { background: rgba(127,127,127,.1); border-radius: 8px; padding: 10px 12px; overflow-x: auto; margin: 8px 0; }
|
|
12352
|
+
.bubble pre code { background: none; padding: 0; }
|
|
12353
|
+
.bubble blockquote { margin: 6px 0; padding: 2px 12px; border-left: 3px solid rgba(127,127,127,.3); opacity: .85; }
|
|
12354
|
+
.bubble a { color: var(--sp-green); }
|
|
12355
|
+
.bubble table { border-collapse: collapse; margin: 8px 0; }
|
|
12356
|
+
.bubble th, .bubble td { border: 1px solid rgba(127,127,127,.25); padding: 4px 10px; font-size: 13px; }
|
|
12357
|
+
.bubble img { max-width: 100%; border-radius: 8px; }
|
|
12358
|
+
.bubble hr { border: 0; border-top: 1px solid rgba(127,127,127,.2); margin: 10px 0; }
|
|
12359
|
+
.typing { display: inline-flex; align-items: center; gap: 4px; padding: 4px 2px; }
|
|
12360
|
+
.typing i { width: 6px; height: 6px; border-radius: 50%; background: rgba(127,127,127,.5); animation: blink 1.2s infinite; }
|
|
12361
|
+
.typing i:nth-child(2) { animation-delay: .2s; }
|
|
12362
|
+
.typing i:nth-child(3) { animation-delay: .4s; }
|
|
12363
|
+
@keyframes blink { 0%, 80%, 100% { opacity: .3 } 40% { opacity: 1 } }
|
|
12364
|
+
.err { color: #cf222e; white-space: pre-wrap; font-size: 13px; }
|
|
12365
|
+
.msg .err { padding: 0 4px; }
|
|
12366
|
+
/* 输入区 */
|
|
12367
|
+
.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); }
|
|
12368
|
+
.chatInput .keyRow { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }
|
|
12369
|
+
.chatInput .keyRow label { font-size: 12px; opacity: .7; white-space: nowrap; }
|
|
12370
|
+
.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; }
|
|
12371
|
+
.inputRow { display: flex; align-items: flex-end; gap: 8px; }
|
|
12372
|
+
.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; }
|
|
12373
|
+
@media (prefers-color-scheme: dark) { .inputRow textarea { background: #26282c; border-color: #3a3d42; } }
|
|
12374
|
+
.inputRow textarea:focus { outline: none; border-color: var(--sp-green); }
|
|
12375
|
+
.inputRow .sendBtn { border: 0; border-radius: 12px; background: var(--sp-green); color: #fff; padding: 10px 18px; font-size: 14px; font-weight: 600; cursor: pointer; white-space: nowrap; }
|
|
12376
|
+
.inputRow .sendBtn:disabled { opacity: .5; cursor: wait; }
|
|
12377
|
+
.inputRow .sendBtn:hover:not(:disabled) { background: var(--sp-green-dim); }
|
|
12378
|
+
.hint { text-align: center; font-size: 12px; opacity: .55; padding: 20px 0; }
|
|
12264
12379
|
`;
|
|
12265
12380
|
/** 容器列表页(v1.26.2):只列已开放容器,点击进入 /c/<name> */
|
|
12266
12381
|
function publicAskListPage(cccs) {
|
|
@@ -12286,7 +12401,7 @@ function publicAskListPage(cccs) {
|
|
|
12286
12401
|
</body>
|
|
12287
12402
|
</html>`;
|
|
12288
12403
|
}
|
|
12289
|
-
/** 单容器问答页(v1.26.2):URL
|
|
12404
|
+
/** 单容器问答页(v1.26.2→v1.26.4):URL 锁定容器;聊天 UI——消息流 + 连续对话 + key 自动记忆 */
|
|
12290
12405
|
function publicAskContainerPage(ccc) {
|
|
12291
12406
|
const data = JSON.stringify({
|
|
12292
12407
|
name: ccc.name,
|
|
@@ -12298,23 +12413,32 @@ function publicAskContainerPage(ccc) {
|
|
|
12298
12413
|
<head>
|
|
12299
12414
|
<meta charset="utf-8">
|
|
12300
12415
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
12301
|
-
<title>Serenity
|
|
12416
|
+
<title>Serenity · ${ccc.name}</title>
|
|
12302
12417
|
<style>${ASK_CSS}</style>
|
|
12303
12418
|
</head>
|
|
12304
12419
|
<body>
|
|
12305
|
-
<
|
|
12306
|
-
<
|
|
12307
|
-
|
|
12308
|
-
|
|
12309
|
-
|
|
12310
|
-
|
|
12311
|
-
|
|
12312
|
-
|
|
12313
|
-
|
|
12314
|
-
<
|
|
12315
|
-
|
|
12316
|
-
<div
|
|
12317
|
-
|
|
12420
|
+
<div class="chat">
|
|
12421
|
+
<div class="chatHead">
|
|
12422
|
+
<a class="back" href="/">← 容器</a>
|
|
12423
|
+
<span class="title">Serenity · <span class="ccname">${ccc.name}</span></span>
|
|
12424
|
+
<span class="spacer"></span>
|
|
12425
|
+
<select id="role" title="问答角色"></select>
|
|
12426
|
+
<button id="newChat" class="newBtn" title="清空当前对话(不存储,刷新即失)">新对话</button>
|
|
12427
|
+
</div>
|
|
12428
|
+
|
|
12429
|
+
<div id="msgs" class="msgList"></div>
|
|
12430
|
+
|
|
12431
|
+
<div class="chatInput">
|
|
12432
|
+
<div class="keyRow" id="keyRow">
|
|
12433
|
+
<label for="key">访问 Key</label>
|
|
12434
|
+
<input id="key" type="password" placeholder="请输入访问 key(自动记忆)" autocomplete="off">
|
|
12435
|
+
</div>
|
|
12436
|
+
<div class="inputRow">
|
|
12437
|
+
<textarea id="q" placeholder="向 ${ccc.name} 提问…(Enter 发送,Shift+Enter 换行)" rows="1"></textarea>
|
|
12438
|
+
<button id="ask" class="sendBtn">发送</button>
|
|
12439
|
+
</div>
|
|
12440
|
+
</div>
|
|
12441
|
+
</div>
|
|
12318
12442
|
<script id="ask-data" type="application/json">${data}<\/script>
|
|
12319
12443
|
<script>
|
|
12320
12444
|
const CCC = JSON.parse(document.getElementById('ask-data').textContent)
|
|
@@ -12322,37 +12446,112 @@ const KEY_STORE = 'serenity-public-ask-key'
|
|
|
12322
12446
|
const keyInput = document.getElementById('key')
|
|
12323
12447
|
const roleSel = document.getElementById('role')
|
|
12324
12448
|
const btn = document.getElementById('ask')
|
|
12325
|
-
const
|
|
12449
|
+
const msgs = document.getElementById('msgs')
|
|
12450
|
+
const qInput = document.getElementById('q')
|
|
12451
|
+
const keyRow = document.getElementById('keyRow')
|
|
12326
12452
|
let sessionId = ''
|
|
12327
|
-
|
|
12453
|
+
let busy = false
|
|
12454
|
+
|
|
12455
|
+
// 角色下拉
|
|
12328
12456
|
roleSel.innerHTML = (CCC.roles || []).map((r) => '<option value="' + r + '">' + r + '</option>').join('') || '<option value="">(无角色)</option>'
|
|
12329
|
-
// key 自动从 localStorage 恢复(v1.26.
|
|
12330
|
-
|
|
12331
|
-
|
|
12332
|
-
|
|
12457
|
+
// key 自动从 localStorage 恢复(v1.26.5:**常驻可见**——只预填不隐藏,
|
|
12458
|
+
// 用户随时可查看/修改 key;saved 时 placeholder 提示已记忆)
|
|
12459
|
+
let hasSavedKey = false
|
|
12460
|
+
try { const saved = localStorage.getItem(KEY_STORE); if (saved) { keyInput.value = saved; hasSavedKey = true } } catch {}
|
|
12461
|
+
if (hasSavedKey) keyInput.placeholder = '访问 Key(已记忆,可修改)'
|
|
12462
|
+
|
|
12463
|
+
/** 追加一条消息气泡(role: user|assistant) */
|
|
12464
|
+
function addMsg(role, htmlOrText, isHtml) {
|
|
12465
|
+
const m = document.createElement('div')
|
|
12466
|
+
m.className = 'msg ' + role
|
|
12467
|
+
const who = document.createElement('div')
|
|
12468
|
+
who.className = 'who'
|
|
12469
|
+
who.textContent = role === 'user' ? '你' : CCC.name
|
|
12470
|
+
const bubble = document.createElement('div')
|
|
12471
|
+
bubble.className = 'bubble'
|
|
12472
|
+
if (isHtml) bubble.innerHTML = htmlOrText
|
|
12473
|
+
else bubble.textContent = htmlOrText
|
|
12474
|
+
m.appendChild(who)
|
|
12475
|
+
m.appendChild(bubble)
|
|
12476
|
+
msgs.appendChild(m)
|
|
12477
|
+
msgs.scrollTop = msgs.scrollHeight
|
|
12478
|
+
return bubble
|
|
12479
|
+
}
|
|
12480
|
+
|
|
12481
|
+
/** 打字指示器(等待回答时显示) */
|
|
12482
|
+
function addTyping() {
|
|
12483
|
+
const m = document.createElement('div')
|
|
12484
|
+
m.className = 'msg assistant'
|
|
12485
|
+
const who = document.createElement('div')
|
|
12486
|
+
who.className = 'who'
|
|
12487
|
+
who.textContent = CCC.name
|
|
12488
|
+
const bubble = document.createElement('div')
|
|
12489
|
+
bubble.className = 'bubble'
|
|
12490
|
+
bubble.innerHTML = '<span class="typing"><i></i><i></i><i></i></span>'
|
|
12491
|
+
m.appendChild(who)
|
|
12492
|
+
m.appendChild(bubble)
|
|
12493
|
+
msgs.appendChild(m)
|
|
12494
|
+
msgs.scrollTop = msgs.scrollHeight
|
|
12495
|
+
return bubble
|
|
12496
|
+
}
|
|
12497
|
+
|
|
12498
|
+
async function ask() {
|
|
12499
|
+
const q = qInput.value.trim()
|
|
12333
12500
|
const key = keyInput.value.trim()
|
|
12334
12501
|
const role = roleSel.value || undefined
|
|
12335
|
-
if (!q)
|
|
12336
|
-
if (!key) {
|
|
12337
|
-
|
|
12502
|
+
if (!q) return
|
|
12503
|
+
if (!key) { keyInput.focus(); return }
|
|
12504
|
+
if (busy) return
|
|
12505
|
+
// 保存 key(下次自动记忆;v1.26.5:不隐藏 key 行——常驻可见可修改)
|
|
12338
12506
|
try { localStorage.setItem(KEY_STORE, key) } catch {}
|
|
12507
|
+
// 追加用户消息 + 打字指示
|
|
12508
|
+
addMsg('user', q, false)
|
|
12509
|
+
qInput.value = ''
|
|
12510
|
+
autoGrow()
|
|
12511
|
+
const typing = addTyping()
|
|
12512
|
+
busy = true
|
|
12339
12513
|
btn.disabled = true
|
|
12340
|
-
answer.className = 'muted'
|
|
12341
|
-
answer.textContent = '运行中…'
|
|
12342
12514
|
try {
|
|
12343
12515
|
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 }) })
|
|
12344
12516
|
const data = await res.json()
|
|
12345
|
-
|
|
12346
|
-
|
|
12347
|
-
|
|
12517
|
+
typing.remove() // 移除打字指示,替换为真实回答
|
|
12518
|
+
if (!res.ok) {
|
|
12519
|
+
sessionId = ''
|
|
12520
|
+
addMsg('assistant', '⚠️ ' + (data.error || ('HTTP ' + res.status)), true)
|
|
12521
|
+
return
|
|
12522
|
+
}
|
|
12523
|
+
addMsg('assistant', data.answer_html || data.answer || '(空回答)', true)
|
|
12348
12524
|
sessionId = data.sessionId || ''
|
|
12349
12525
|
} catch (err) {
|
|
12350
|
-
|
|
12351
|
-
|
|
12526
|
+
typing.remove()
|
|
12527
|
+
addMsg('assistant', '⚠️ ' + String(err.message || err), true)
|
|
12352
12528
|
} finally {
|
|
12529
|
+
busy = false
|
|
12353
12530
|
btn.disabled = false
|
|
12531
|
+
qInput.focus()
|
|
12354
12532
|
}
|
|
12533
|
+
}
|
|
12534
|
+
|
|
12535
|
+
function autoGrow() {
|
|
12536
|
+
qInput.style.height = 'auto'
|
|
12537
|
+
qInput.style.height = Math.min(qInput.scrollHeight, 140) + 'px'
|
|
12538
|
+
}
|
|
12539
|
+
|
|
12540
|
+
btn.addEventListener('click', () => void ask())
|
|
12541
|
+
qInput.addEventListener('keydown', (e) => {
|
|
12542
|
+
if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); void ask() }
|
|
12355
12543
|
})
|
|
12544
|
+
qInput.addEventListener('input', autoGrow)
|
|
12545
|
+
// 新对话:重置 sessionId + 清空消息流(前端内存,不存储——刷新即失)
|
|
12546
|
+
document.getElementById('newChat').addEventListener('click', () => {
|
|
12547
|
+
sessionId = ''
|
|
12548
|
+
msgs.innerHTML = ''
|
|
12549
|
+
qInput.focus()
|
|
12550
|
+
})
|
|
12551
|
+
|
|
12552
|
+
// 初始欢迎消息(外层模板求值注入容器名;textContent 渲染防注入)
|
|
12553
|
+
addMsg('assistant', '你好,我是「${ccc.name}」的认知助手。有什么可以帮你的?(本对话仅当前页面有效,刷新即失)', false)
|
|
12554
|
+
qInput.focus()
|
|
12356
12555
|
<\/script>
|
|
12357
12556
|
</body>
|
|
12358
12557
|
</html>`;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shgroup/dsh-serenity-hooks",
|
|
3
|
-
"version": "1.26.
|
|
3
|
+
"version": "1.26.5",
|
|
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": {
|