@shgroup/dsh-serenity-hooks 1.26.2 → 1.26.4
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/index.js +375 -72
- 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.4",
|
|
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/index.js
CHANGED
|
@@ -785,6 +785,17 @@ const CC_FS_WRITE_ACTIONS = /* @__PURE__ */ new Set([
|
|
|
785
785
|
"touch",
|
|
786
786
|
"append"
|
|
787
787
|
]);
|
|
788
|
+
/**
|
|
789
|
+
* 凭据文件硬名单(数据面守卫,v1.26.3,S142 用户:localstore.json 需要在 read 工具的黑名单里):
|
|
790
|
+
* **任何工具**(含 read/grep/glob/cc_fs 只读子命令)命中这些相对 CCC 根的路径 → deny。
|
|
791
|
+
*
|
|
792
|
+
* 与 safeMode.blacklist(写黑名单,v1.18.5 决策只拦写)语义独立:
|
|
793
|
+
* - 黑名单 = 用户可配置的写保护(REPOSITORIES/ 等只读参考源不误伤读)
|
|
794
|
+
* - 凭据硬名单 = **结构性数据面边界**(不依赖 safe-mode 开关,凭据文件任何时候不可读)——
|
|
795
|
+
* 对齐 CCE Bounded Space + wardn "structural guarantee, not policy":localstore.json
|
|
796
|
+
* 含 API keys/tokens/SSH 密码,即使 prompt 纪律失败也无值可吐(机械保证)。
|
|
797
|
+
*/
|
|
798
|
+
const SENSITIVE_CREDENTIAL_FILES = /* @__PURE__ */ new Set(["localstore.json"]);
|
|
788
799
|
/** 判定工具是否为写类:普通工具按名;cc_fs 复合工具按子命令 action */
|
|
789
800
|
function isWriteTool(toolName, action) {
|
|
790
801
|
if (!WRITE_TOOLS.has(toolName)) return false;
|
|
@@ -817,8 +828,12 @@ function decideGuard(input) {
|
|
|
817
828
|
deny: `path escape blocked: "${pathArg}" escapes the CCC root`,
|
|
818
829
|
kind: "deny"
|
|
819
830
|
};
|
|
831
|
+
const rel = relative(root, abs).split("\\").join("/");
|
|
832
|
+
if (SENSITIVE_CREDENTIAL_FILES.has(rel)) return {
|
|
833
|
+
deny: `access blocked: "${pathArg}" is a sensitive credential file (${rel})`,
|
|
834
|
+
kind: "deny"
|
|
835
|
+
};
|
|
820
836
|
if (isWriteTool(toolName, action)) {
|
|
821
|
-
const rel = relative(root, abs).split("\\").join("/");
|
|
822
837
|
if (rel === ".serenity-safe-on" || rel === ".serenity" || rel.startsWith(".serenity-safe-on/") || rel.startsWith(".serenity/")) return {
|
|
823
838
|
deny: `CCC governance file "${rel}" is reserved for the user — agent must not write`,
|
|
824
839
|
kind: "deny"
|
|
@@ -2392,7 +2407,7 @@ function listActiveHandymen(root) {
|
|
|
2392
2407
|
}
|
|
2393
2408
|
//#endregion
|
|
2394
2409
|
//#region src/skiff-core.ts
|
|
2395
|
-
const PLUGIN_SOURCE$
|
|
2410
|
+
const PLUGIN_SOURCE$4 = {
|
|
2396
2411
|
kind: "plugin",
|
|
2397
2412
|
plugin: "dsh-serenity-hooks"
|
|
2398
2413
|
};
|
|
@@ -2491,7 +2506,7 @@ function waitIdle$1(ctx, agent) {
|
|
|
2491
2506
|
});
|
|
2492
2507
|
}
|
|
2493
2508
|
/** 读会话最后一个 assistant/message 文本(handyman 同款) */
|
|
2494
|
-
function lastAssistantText$
|
|
2509
|
+
function lastAssistantText$2(agent) {
|
|
2495
2510
|
const events = agent.session.events;
|
|
2496
2511
|
for (let i = events.length - 1; i >= 0; i--) {
|
|
2497
2512
|
const e = events[i];
|
|
@@ -2560,10 +2575,10 @@ async function askSkiff(ctx, agent, question, eventsStart) {
|
|
|
2560
2575
|
type: "text",
|
|
2561
2576
|
text: question
|
|
2562
2577
|
}],
|
|
2563
|
-
source: PLUGIN_SOURCE$
|
|
2578
|
+
source: PLUGIN_SOURCE$4
|
|
2564
2579
|
}));
|
|
2565
2580
|
await waitIdle$1(ctx, agent);
|
|
2566
|
-
const answer = lastAssistantText$
|
|
2581
|
+
const answer = lastAssistantText$2(agent);
|
|
2567
2582
|
const trajectory = eventsToTrajectory(agent.session.events.slice(before));
|
|
2568
2583
|
return {
|
|
2569
2584
|
answer,
|
|
@@ -2973,7 +2988,7 @@ function waitIdle(ctx, agent) {
|
|
|
2973
2988
|
});
|
|
2974
2989
|
}
|
|
2975
2990
|
/** 读取会话最后一个 assistant/message 文本 */
|
|
2976
|
-
function lastAssistantText(agent) {
|
|
2991
|
+
function lastAssistantText$1(agent) {
|
|
2977
2992
|
const events = agent.session.events;
|
|
2978
2993
|
for (let i = events.length - 1; i >= 0; i--) {
|
|
2979
2994
|
const e = events[i];
|
|
@@ -3071,7 +3086,7 @@ async function runHandymanJob(ctx, opts) {
|
|
|
3071
3086
|
}
|
|
3072
3087
|
}));
|
|
3073
3088
|
await waitIdle(ctx, workerAgent);
|
|
3074
|
-
lastResponse = lastAssistantText(workerAgent);
|
|
3089
|
+
lastResponse = lastAssistantText$1(workerAgent);
|
|
3075
3090
|
} catch {
|
|
3076
3091
|
restarts++;
|
|
3077
3092
|
if (restarts > 100) {
|
|
@@ -6567,7 +6582,7 @@ function registerBootstrap(ctx) {
|
|
|
6567
6582
|
}
|
|
6568
6583
|
//#endregion
|
|
6569
6584
|
//#region src/rebuild.ts
|
|
6570
|
-
const PLUGIN_SOURCE$
|
|
6585
|
+
const PLUGIN_SOURCE$3 = {
|
|
6571
6586
|
kind: "plugin",
|
|
6572
6587
|
plugin: "dsh-serenity-hooks"
|
|
6573
6588
|
};
|
|
@@ -6753,7 +6768,7 @@ function registerRebuildTurnHook(ctx) {
|
|
|
6753
6768
|
type: "text",
|
|
6754
6769
|
text: "[TRAJECTORY-REBUILD] The conversation has been cleared and rebuilt. Follow the anchor instructions above now: read the persistent trajectory (SESSION.md) and continue the work automatically from the last checkpoint."
|
|
6755
6770
|
}],
|
|
6756
|
-
source: PLUGIN_SOURCE$
|
|
6771
|
+
source: PLUGIN_SOURCE$3
|
|
6757
6772
|
}));
|
|
6758
6773
|
console.log(`[serenity-hooks] session_rebuild executed with auto-continue (turn ${payload.turn ?? "?"} ended): ${id}`);
|
|
6759
6774
|
}
|
|
@@ -7150,7 +7165,7 @@ function readContextPressure(ctx, session) {
|
|
|
7150
7165
|
return null;
|
|
7151
7166
|
}
|
|
7152
7167
|
}
|
|
7153
|
-
const PLUGIN_SOURCE$
|
|
7168
|
+
const PLUGIN_SOURCE$2 = {
|
|
7154
7169
|
kind: "plugin",
|
|
7155
7170
|
plugin: "dsh-serenity-hooks"
|
|
7156
7171
|
};
|
|
@@ -7213,7 +7228,7 @@ function registerKeeper(ctx, opts = {}) {
|
|
|
7213
7228
|
if (blocks.length === 0) return downstream;
|
|
7214
7229
|
const reminderAll = createUserMessage({
|
|
7215
7230
|
content: blocks,
|
|
7216
|
-
source: PLUGIN_SOURCE$
|
|
7231
|
+
source: PLUGIN_SOURCE$2
|
|
7217
7232
|
});
|
|
7218
7233
|
if (downstream.kind === "block") return {
|
|
7219
7234
|
kind: "block",
|
|
@@ -8168,7 +8183,7 @@ function accIdentityText(root, configPaths = DEFAULT_SERENITY_CONFIG_PATHS, entr
|
|
|
8168
8183
|
}
|
|
8169
8184
|
return lines.join("\n");
|
|
8170
8185
|
}
|
|
8171
|
-
const PLUGIN_SOURCE = {
|
|
8186
|
+
const PLUGIN_SOURCE$1 = {
|
|
8172
8187
|
kind: "plugin",
|
|
8173
8188
|
plugin: "dsh-serenity-hooks"
|
|
8174
8189
|
};
|
|
@@ -8185,7 +8200,7 @@ function accMessage(root, configPaths, entrySkillMaxChars) {
|
|
|
8185
8200
|
}];
|
|
8186
8201
|
return createUserMessage({
|
|
8187
8202
|
content,
|
|
8188
|
-
source: PLUGIN_SOURCE
|
|
8203
|
+
source: PLUGIN_SOURCE$1
|
|
8189
8204
|
});
|
|
8190
8205
|
}
|
|
8191
8206
|
/** 每 agent 是否已注入过(进程内存态) */
|
|
@@ -9562,6 +9577,171 @@ function registerGateway(ctx) {
|
|
|
9562
9577
|
});
|
|
9563
9578
|
}
|
|
9564
9579
|
//#endregion
|
|
9580
|
+
//#region src/output-guard.ts
|
|
9581
|
+
/**
|
|
9582
|
+
* output-guard.ts — 最终输出敏感词检测 + 打回重生成(v1.26.3,S142 用户需求)
|
|
9583
|
+
*
|
|
9584
|
+
* 认知容器对外提供认知结果时,**支撑宁静号自身的凭据与机制**不应透露给用户。
|
|
9585
|
+
* 用户拍板方案(讨论稿定稿):不做工具结果 redaction(那是模型工作内存),
|
|
9586
|
+
* 改为**卡最终输出**——`agent/turn-stopping`(serial)时取 turn 最后 assistant 文本,
|
|
9587
|
+
* 命中敏感词表 → `agent.steer()` 打回重生成,直到合规或达上限。
|
|
9588
|
+
*
|
|
9589
|
+
* 敏感词表(有限,ACC 立场,用户确认"这样比较全面了"):
|
|
9590
|
+
* ① 凭据词 — localstore.json credentials 条目名 + 值精确匹配(运行时读)
|
|
9591
|
+
* ② 机制词 — 内部结构静态词(插件名/配置路径/端口/内部实现词)
|
|
9592
|
+
* ③ MSM 词 — mech-registry.json 注册的工具名(用户补充:msm_list 里的工具名)
|
|
9593
|
+
* **不含** 公开概念(宁静号/Serenity/认知容器/EAP/CCE/Neat——正常交流词,误伤灾难)
|
|
9594
|
+
*
|
|
9595
|
+
* 机制依据(DSH 官方):agent/turn-stopping 注释 "a listener that objects steers and
|
|
9596
|
+
* the machine re-reads its inbox: fresh steering runs another step, none closes the
|
|
9597
|
+
* turn"——steer 打回是官方支持的"重生成"通道(v1.22.5 rebuild 自动继续同款先例)。
|
|
9598
|
+
*/
|
|
9599
|
+
/** 插件/内部实现词(静态内置;命中即视为机制泄露) */
|
|
9600
|
+
const MECHANISM_WORDS = [
|
|
9601
|
+
"dsh-serenity-hooks",
|
|
9602
|
+
"serenity-hooks",
|
|
9603
|
+
"serenity-hooks.json",
|
|
9604
|
+
"mech-registry",
|
|
9605
|
+
"mech-registry.json",
|
|
9606
|
+
"localstore.json",
|
|
9607
|
+
".opencode/serenity.json",
|
|
9608
|
+
"AGENT_SESSIONS",
|
|
9609
|
+
"agent/session-start",
|
|
9610
|
+
"agent/turn-stopping",
|
|
9611
|
+
"tools/pre-execute",
|
|
9612
|
+
"tools/post-execute",
|
|
9613
|
+
"session_rebuild",
|
|
9614
|
+
"Trajectory Steward",
|
|
9615
|
+
"first-anchor",
|
|
9616
|
+
"拦截缝",
|
|
9617
|
+
"装配层",
|
|
9618
|
+
"acp-core",
|
|
9619
|
+
"skiff-core",
|
|
9620
|
+
"skiff-registry",
|
|
9621
|
+
"skiff-role",
|
|
9622
|
+
"output-guard"
|
|
9623
|
+
];
|
|
9624
|
+
/** 内部端口(宁静号服务面) */
|
|
9625
|
+
const MECHANISM_PORTS = [
|
|
9626
|
+
"3080",
|
|
9627
|
+
"3081",
|
|
9628
|
+
"3099",
|
|
9629
|
+
"3100"
|
|
9630
|
+
];
|
|
9631
|
+
/**
|
|
9632
|
+
* 构建敏感词表(每次调用实时构建——词源文件小,开销可接受):
|
|
9633
|
+
* ① localstore.json credentials:条目名(UPPER_SNAKE)+ 值(精确匹配)
|
|
9634
|
+
* ② 静态机制词 + 端口
|
|
9635
|
+
* ③ mech-registry 注册的 MSM 工具名(loadMsmEntries 全注册表去重)
|
|
9636
|
+
* @param root CCC 根(localstore.json / mech-registry 所在)
|
|
9637
|
+
*/
|
|
9638
|
+
function buildSensitiveTable(root) {
|
|
9639
|
+
const exact = /* @__PURE__ */ new Set();
|
|
9640
|
+
const substring = [];
|
|
9641
|
+
try {
|
|
9642
|
+
const p = join(root, "localstore.json");
|
|
9643
|
+
if (existsSync(p)) {
|
|
9644
|
+
const creds = JSON.parse(readFileSync(p, "utf-8").replace(/^\uFEFF/, ""))?.credentials;
|
|
9645
|
+
if (creds && typeof creds === "object") for (const [name, value] of Object.entries(creds)) {
|
|
9646
|
+
if (name) exact.add(name);
|
|
9647
|
+
if (typeof value === "string" && value !== "") exact.add(value);
|
|
9648
|
+
}
|
|
9649
|
+
}
|
|
9650
|
+
} catch {}
|
|
9651
|
+
for (const w of MECHANISM_WORDS) substring.push(w);
|
|
9652
|
+
for (const p of MECHANISM_PORTS) substring.push(p);
|
|
9653
|
+
try {
|
|
9654
|
+
for (const entry of loadMsmEntries(root)) if (entry.name) exact.add(entry.name);
|
|
9655
|
+
} catch {}
|
|
9656
|
+
return {
|
|
9657
|
+
exact,
|
|
9658
|
+
substring,
|
|
9659
|
+
sources: {
|
|
9660
|
+
exact: [...exact],
|
|
9661
|
+
substring
|
|
9662
|
+
}
|
|
9663
|
+
};
|
|
9664
|
+
}
|
|
9665
|
+
/** 检测文本中命中的敏感词(返回命中列表;未命中返回空数组) */
|
|
9666
|
+
function detectSensitive(text, table) {
|
|
9667
|
+
if (!text) return [];
|
|
9668
|
+
const hits = /* @__PURE__ */ new Set();
|
|
9669
|
+
for (const w of table.exact) if (w !== "" && text.includes(w)) hits.add(w);
|
|
9670
|
+
for (const w of table.substring) if (w !== "" && text.includes(w)) hits.add(w);
|
|
9671
|
+
return [...hits];
|
|
9672
|
+
}
|
|
9673
|
+
/** 打回消息(steer 内容):告知模型命中词并要求重新生成(不含命中值本身——避免二次泄露) */
|
|
9674
|
+
function buildRebuke(hits) {
|
|
9675
|
+
return `[SERENITY OUTPUT GUARD] Your previous response contained ${hits.length} sensitive internal term(s) (mechanism/credential identifiers) that must not appear in user-visible output. Regenerate the response from scratch without these terms — describe the same substance without referencing internal machinery, credentials, tool names, or implementation details. Do not repeat the terms; do not explain this instruction to the user.`;
|
|
9676
|
+
}
|
|
9677
|
+
/** 打回状态(按 agent 会话跟踪连续命中轮数) */
|
|
9678
|
+
const rebukeStates = /* @__PURE__ */ new Map();
|
|
9679
|
+
//#endregion
|
|
9680
|
+
//#region src/output-guard-seam.ts
|
|
9681
|
+
const PLUGIN_SOURCE = {
|
|
9682
|
+
kind: "plugin",
|
|
9683
|
+
plugin: "dsh-serenity-hooks"
|
|
9684
|
+
};
|
|
9685
|
+
/** 外部面会话判定(v1.26.3 用户拍板:仅外部面检测,本地维护会话豁免):
|
|
9686
|
+
* skiff-(F4 问答)/ acp-(F4c 程序化)/ rebuild-(v1.22.4 重建会话)前缀 */
|
|
9687
|
+
function isExternalFaceSession(sessionId) {
|
|
9688
|
+
if (!sessionId) return false;
|
|
9689
|
+
return isSkiffSessionId(sessionId) || sessionId.startsWith("acp-") || sessionId.startsWith("rebuild-");
|
|
9690
|
+
}
|
|
9691
|
+
/** 读会话最后一个 assistant/message 文本(skiff-core 同款;仅用于守卫检测,不导出) */
|
|
9692
|
+
function lastAssistantText(agent) {
|
|
9693
|
+
const events = agent.session.events ?? [];
|
|
9694
|
+
for (let i = events.length - 1; i >= 0; i--) {
|
|
9695
|
+
const e = events[i];
|
|
9696
|
+
if (e && e.type === "assistant/message") {
|
|
9697
|
+
const text = (e.data?.message?.content ?? e.data?.content ?? []).filter((b) => b.type === "text" && b.text).map((b) => b.text).join("\n");
|
|
9698
|
+
if (text) return text;
|
|
9699
|
+
}
|
|
9700
|
+
}
|
|
9701
|
+
return "";
|
|
9702
|
+
}
|
|
9703
|
+
/**
|
|
9704
|
+
* 注册输出守卫(index.ts apply 调用):
|
|
9705
|
+
* turn-stopping 时检测最终输出,命中敏感词 → steer 打回重生成(官方机制,零改 DSH)。
|
|
9706
|
+
*/
|
|
9707
|
+
function registerOutputGuardHook(ctx) {
|
|
9708
|
+
ctx.on("agent/turn-stopping", (payload) => {
|
|
9709
|
+
const agent = payload?.agent;
|
|
9710
|
+
if (!agent) return;
|
|
9711
|
+
if (!isExternalFaceSession(String(agent.session.id ?? ""))) return;
|
|
9712
|
+
const root = findSerenityRoot(agent.session.header?.cwd ?? process.cwd());
|
|
9713
|
+
if (!root) return;
|
|
9714
|
+
const id = agent.id;
|
|
9715
|
+
const text = lastAssistantText(agent);
|
|
9716
|
+
if (!text) return;
|
|
9717
|
+
const hits = detectSensitive(text, buildSensitiveTable(root));
|
|
9718
|
+
if (hits.length === 0) {
|
|
9719
|
+
rebukeStates.delete(id);
|
|
9720
|
+
return;
|
|
9721
|
+
}
|
|
9722
|
+
const st = rebukeStates.get(id) ?? { consecutive: 0 };
|
|
9723
|
+
st.consecutive += 1;
|
|
9724
|
+
rebukeStates.set(id, st);
|
|
9725
|
+
if (st.consecutive > 3) {
|
|
9726
|
+
rebukeStates.delete(id);
|
|
9727
|
+
console.warn(`[serenity-hooks] output-guard: giving up after 3 rebukes (agent=${id}, turn=${payload.turn ?? "?"}) — response kept for manual review`);
|
|
9728
|
+
return;
|
|
9729
|
+
}
|
|
9730
|
+
console.log(`[serenity-hooks] output-guard: rebuke ${st.consecutive}/3 (agent=${id}, turn=${payload.turn ?? "?"}, hits=${hits.length})`);
|
|
9731
|
+
try {
|
|
9732
|
+
agent.steer(createUserMessage({
|
|
9733
|
+
content: [{
|
|
9734
|
+
type: "text",
|
|
9735
|
+
text: buildRebuke(hits)
|
|
9736
|
+
}],
|
|
9737
|
+
source: PLUGIN_SOURCE
|
|
9738
|
+
}));
|
|
9739
|
+
} catch (error) {
|
|
9740
|
+
console.warn(`[serenity-hooks] output-guard steer failed: ${String(error?.message ?? error)}`);
|
|
9741
|
+
}
|
|
9742
|
+
});
|
|
9743
|
+
}
|
|
9744
|
+
//#endregion
|
|
9565
9745
|
//#region node_modules/.pnpm/marked@18.0.11/node_modules/marked/lib/marked.esm.js
|
|
9566
9746
|
/**
|
|
9567
9747
|
* marked v18.0.11 - a markdown parser
|
|
@@ -11483,9 +11663,11 @@ function escapeHtml(s) {
|
|
|
11483
11663
|
* ① 提取 `<think>…</think>` 块(占位符 \u0000T<idx>\u0000)
|
|
11484
11664
|
* ② 正文与 think 内容**先 escapeHtml 再 marked.parse**(GFM + breaks)——markdown 语法不受
|
|
11485
11665
|
* 转义影响,原始 HTML 注入被消除(安全);代码块内 `<` 显示为实体(可接受)
|
|
11486
|
-
* ③ think
|
|
11666
|
+
* ③ think 占位符:默认还原为 `<details class="think">` 折叠卡(🧠 思考过程,默认收起);
|
|
11667
|
+
* **hideThink=true(v1.26.4,public 口)→ 直接移除**(思考过程对普通用户不展示)
|
|
11668
|
+
* @param hideThink 为 true 时 `<think>` 内容完全不渲染(public 问答页体验)
|
|
11487
11669
|
*/
|
|
11488
|
-
function renderSkiffMarkdown(raw) {
|
|
11670
|
+
function renderSkiffMarkdown(raw, hideThink = false) {
|
|
11489
11671
|
const thinks = [];
|
|
11490
11672
|
const body = raw.replace(/<think>([\s\S]*?)<\/think>/gi, (_m, inner) => {
|
|
11491
11673
|
const idx = thinks.length;
|
|
@@ -11497,6 +11679,7 @@ function renderSkiffMarkdown(raw) {
|
|
|
11497
11679
|
gfm: true
|
|
11498
11680
|
});
|
|
11499
11681
|
return (typeof parsed === "string" ? parsed : "").replace(/\u0000T(\d+)\u0000/g, (_m, idx) => {
|
|
11682
|
+
if (hideThink) return "";
|
|
11500
11683
|
const inner = thinks[Number(idx)] ?? "";
|
|
11501
11684
|
const innerHtml = inner === "" ? "" : f.parse(escapeHtml(inner), {
|
|
11502
11685
|
breaks: true,
|
|
@@ -12013,7 +12196,7 @@ async function handleAskParsed(ctx, ccc, body, res) {
|
|
|
12013
12196
|
const result = await askSkiff(ctx, agent, question, 0);
|
|
12014
12197
|
sendJson(res, 200, {
|
|
12015
12198
|
answer: result.answer,
|
|
12016
|
-
answer_html: renderSkiffMarkdown(result.answer),
|
|
12199
|
+
answer_html: renderSkiffMarkdown(result.answer, true),
|
|
12017
12200
|
sessionId: result.sessionId,
|
|
12018
12201
|
continued,
|
|
12019
12202
|
trajectory: result.trajectory
|
|
@@ -12048,39 +12231,77 @@ function publicAskContainerClosedHtml(name) {
|
|
|
12048
12231
|
<p style="font-size:13px"><a href="/" style="color:#0ba875">返回容器列表</a></p>
|
|
12049
12232
|
</body></html>`;
|
|
12050
12233
|
}
|
|
12051
|
-
/** 页面基础样式(列表页 +
|
|
12234
|
+
/** 页面基础样式(列表页 + 单容器页共用;v1.26.4 聊天 UI) */
|
|
12052
12235
|
const ASK_CSS = `
|
|
12053
|
-
:root { color-scheme: light dark; }
|
|
12054
|
-
|
|
12055
|
-
|
|
12236
|
+
:root { color-scheme: light dark; --sp-green: #0ba875; --sp-green-dim: #059669; }
|
|
12237
|
+
* { box-sizing: border-box; }
|
|
12238
|
+
html, body { height: 100%; }
|
|
12239
|
+
body { font-family: system-ui, -apple-system, sans-serif; margin: 0; background: #f6f7f9; color: #1f2328; }
|
|
12240
|
+
@media (prefers-color-scheme: dark) { body { background: #16181c; color: #e6e6e6; } }
|
|
12056
12241
|
main { max-width: 720px; margin: 0 auto; }
|
|
12057
12242
|
h1 { font-size: 18px; margin: 0 0 4px; }
|
|
12058
12243
|
.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
12244
|
.card { background: #fff; border: 1px solid #d0d7de; border-radius: 10px; padding: 14px 16px; margin-bottom: 10px; display: block; text-decoration: none; color: inherit; }
|
|
12066
12245
|
@media (prefers-color-scheme: dark) { .card { background: #26282c; border-color: #3a3d42; } }
|
|
12067
|
-
.card:hover { border-color:
|
|
12246
|
+
.card:hover { border-color: var(--sp-green); }
|
|
12068
12247
|
.card .cname { font-weight: 600; font-size: 15px; }
|
|
12069
12248
|
.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
12249
|
.empty { text-align: center; opacity: .7; padding: 24px 0; }
|
|
12082
|
-
.
|
|
12083
|
-
|
|
12250
|
+
.ccname { font-weight: 600; color: var(--sp-green); }
|
|
12251
|
+
|
|
12252
|
+
/* ── 聊天布局(v1.26.4 体验升级:参考 ChatGPT/Claude 会话形态)── */
|
|
12253
|
+
.chat { display: flex; flex-direction: column; height: 100vh; max-width: 720px; margin: 0 auto; }
|
|
12254
|
+
.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; }
|
|
12255
|
+
.chatHead .back { color: var(--sp-green); text-decoration: none; font-size: 13px; white-space: nowrap; }
|
|
12256
|
+
.chatHead .title { font-weight: 600; font-size: 14px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
12257
|
+
.chatHead .spacer { flex: 1; }
|
|
12258
|
+
.chatHead select { border: 1px solid rgba(127,127,127,.25); border-radius: 8px; background: transparent; color: inherit; padding: 4px 8px; font-size: 12px; }
|
|
12259
|
+
.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; }
|
|
12260
|
+
.chatHead .newBtn:hover { border-color: var(--sp-green); color: var(--sp-green); }
|
|
12261
|
+
/* 消息流 */
|
|
12262
|
+
.msgList { flex: 1; overflow-y: auto; padding: 18px 16px; display: flex; flex-direction: column; gap: 16px; }
|
|
12263
|
+
.msg { display: flex; flex-direction: column; max-width: 88%; }
|
|
12264
|
+
.msg.user { align-self: flex-end; align-items: flex-end; }
|
|
12265
|
+
.msg.assistant { align-self: flex-start; align-items: flex-start; }
|
|
12266
|
+
.msg .who { font-size: 11px; opacity: .55; margin-bottom: 4px; padding: 0 4px; }
|
|
12267
|
+
.msg .bubble { padding: 10px 14px; border-radius: 14px; font-size: 14px; line-height: 1.65; word-break: break-word; white-space: normal; }
|
|
12268
|
+
.msg.user .bubble { background: var(--sp-green); color: #fff; border-bottom-right-radius: 4px; }
|
|
12269
|
+
.msg.assistant .bubble { background: #fff; border: 1px solid #d0d7de; border-bottom-left-radius: 4px; }
|
|
12270
|
+
@media (prefers-color-scheme: dark) { .msg.assistant .bubble { background: #26282c; border-color: #3a3d42; } }
|
|
12271
|
+
.bubble p { margin: 6px 0; }
|
|
12272
|
+
.bubble p:first-child { margin-top: 0; }
|
|
12273
|
+
.bubble p:last-child { margin-bottom: 0; }
|
|
12274
|
+
.bubble h1, .bubble h2, .bubble h3, .bubble h4 { margin: 12px 0 6px; font-size: 15px; line-height: 1.4; }
|
|
12275
|
+
.bubble ul, .bubble ol { margin: 6px 0; padding-left: 22px; }
|
|
12276
|
+
.bubble code { background: rgba(127,127,127,.14); border-radius: 4px; padding: 1px 5px; font-size: 13px; font-family: ui-monospace, monospace; }
|
|
12277
|
+
.bubble pre { background: rgba(127,127,127,.1); border-radius: 8px; padding: 10px 12px; overflow-x: auto; margin: 8px 0; }
|
|
12278
|
+
.bubble pre code { background: none; padding: 0; }
|
|
12279
|
+
.bubble blockquote { margin: 6px 0; padding: 2px 12px; border-left: 3px solid rgba(127,127,127,.3); opacity: .85; }
|
|
12280
|
+
.bubble a { color: var(--sp-green); }
|
|
12281
|
+
.bubble table { border-collapse: collapse; margin: 8px 0; }
|
|
12282
|
+
.bubble th, .bubble td { border: 1px solid rgba(127,127,127,.25); padding: 4px 10px; font-size: 13px; }
|
|
12283
|
+
.bubble img { max-width: 100%; border-radius: 8px; }
|
|
12284
|
+
.bubble hr { border: 0; border-top: 1px solid rgba(127,127,127,.2); margin: 10px 0; }
|
|
12285
|
+
.typing { display: inline-flex; align-items: center; gap: 4px; padding: 4px 2px; }
|
|
12286
|
+
.typing i { width: 6px; height: 6px; border-radius: 50%; background: rgba(127,127,127,.5); animation: blink 1.2s infinite; }
|
|
12287
|
+
.typing i:nth-child(2) { animation-delay: .2s; }
|
|
12288
|
+
.typing i:nth-child(3) { animation-delay: .4s; }
|
|
12289
|
+
@keyframes blink { 0%, 80%, 100% { opacity: .3 } 40% { opacity: 1 } }
|
|
12290
|
+
.err { color: #cf222e; white-space: pre-wrap; font-size: 13px; }
|
|
12291
|
+
.msg .err { padding: 0 4px; }
|
|
12292
|
+
/* 输入区 */
|
|
12293
|
+
.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
|
+
.inputRow { display: flex; align-items: flex-end; gap: 8px; }
|
|
12298
|
+
.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
|
+
@media (prefers-color-scheme: dark) { .inputRow textarea { background: #26282c; border-color: #3a3d42; } }
|
|
12300
|
+
.inputRow textarea:focus { outline: none; border-color: var(--sp-green); }
|
|
12301
|
+
.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; }
|
|
12302
|
+
.inputRow .sendBtn:disabled { opacity: .5; cursor: wait; }
|
|
12303
|
+
.inputRow .sendBtn:hover:not(:disabled) { background: var(--sp-green-dim); }
|
|
12304
|
+
.hint { text-align: center; font-size: 12px; opacity: .55; padding: 20px 0; }
|
|
12084
12305
|
`;
|
|
12085
12306
|
/** 容器列表页(v1.26.2):只列已开放容器,点击进入 /c/<name> */
|
|
12086
12307
|
function publicAskListPage(cccs) {
|
|
@@ -12106,7 +12327,7 @@ function publicAskListPage(cccs) {
|
|
|
12106
12327
|
</body>
|
|
12107
12328
|
</html>`;
|
|
12108
12329
|
}
|
|
12109
|
-
/** 单容器问答页(v1.26.2):URL
|
|
12330
|
+
/** 单容器问答页(v1.26.2→v1.26.4):URL 锁定容器;聊天 UI——消息流 + 连续对话 + key 自动记忆 */
|
|
12110
12331
|
function publicAskContainerPage(ccc) {
|
|
12111
12332
|
const data = JSON.stringify({
|
|
12112
12333
|
name: ccc.name,
|
|
@@ -12118,23 +12339,32 @@ function publicAskContainerPage(ccc) {
|
|
|
12118
12339
|
<head>
|
|
12119
12340
|
<meta charset="utf-8">
|
|
12120
12341
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
12121
|
-
<title>Serenity
|
|
12342
|
+
<title>Serenity · ${ccc.name}</title>
|
|
12122
12343
|
<style>${ASK_CSS}</style>
|
|
12123
12344
|
</head>
|
|
12124
12345
|
<body>
|
|
12125
|
-
<
|
|
12126
|
-
<
|
|
12127
|
-
|
|
12128
|
-
|
|
12129
|
-
|
|
12130
|
-
|
|
12131
|
-
|
|
12132
|
-
|
|
12133
|
-
|
|
12134
|
-
<
|
|
12135
|
-
|
|
12136
|
-
<div
|
|
12137
|
-
|
|
12346
|
+
<div class="chat">
|
|
12347
|
+
<div class="chatHead">
|
|
12348
|
+
<a class="back" href="/">← 容器</a>
|
|
12349
|
+
<span class="title">Serenity · <span class="ccname">${ccc.name}</span></span>
|
|
12350
|
+
<span class="spacer"></span>
|
|
12351
|
+
<select id="role" title="问答角色"></select>
|
|
12352
|
+
<button id="newChat" class="newBtn" title="清空当前对话(不存储,刷新即失)">新对话</button>
|
|
12353
|
+
</div>
|
|
12354
|
+
|
|
12355
|
+
<div id="msgs" class="msgList"></div>
|
|
12356
|
+
|
|
12357
|
+
<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
|
+
<div class="inputRow">
|
|
12363
|
+
<textarea id="q" placeholder="向 ${ccc.name} 提问…(Enter 发送,Shift+Enter 换行)" rows="1"></textarea>
|
|
12364
|
+
<button id="ask" class="sendBtn">发送</button>
|
|
12365
|
+
</div>
|
|
12366
|
+
</div>
|
|
12367
|
+
</div>
|
|
12138
12368
|
<script id="ask-data" type="application/json">${data}<\/script>
|
|
12139
12369
|
<script>
|
|
12140
12370
|
const CCC = JSON.parse(document.getElementById('ask-data').textContent)
|
|
@@ -12142,37 +12372,109 @@ const KEY_STORE = 'serenity-public-ask-key'
|
|
|
12142
12372
|
const keyInput = document.getElementById('key')
|
|
12143
12373
|
const roleSel = document.getElementById('role')
|
|
12144
12374
|
const btn = document.getElementById('ask')
|
|
12145
|
-
const
|
|
12375
|
+
const msgs = document.getElementById('msgs')
|
|
12376
|
+
const qInput = document.getElementById('q')
|
|
12377
|
+
const keyRow = document.getElementById('keyRow')
|
|
12146
12378
|
let sessionId = ''
|
|
12147
|
-
|
|
12379
|
+
let busy = false
|
|
12380
|
+
|
|
12381
|
+
// 角色下拉
|
|
12148
12382
|
roleSel.innerHTML = (CCC.roles || []).map((r) => '<option value="' + r + '">' + r + '</option>').join('') || '<option value="">(无角色)</option>'
|
|
12149
|
-
// key 自动从 localStorage
|
|
12150
|
-
try { const saved = localStorage.getItem(KEY_STORE); if (saved) keyInput.value = saved } catch {}
|
|
12151
|
-
|
|
12152
|
-
|
|
12383
|
+
// key 自动从 localStorage 恢复
|
|
12384
|
+
try { const saved = localStorage.getItem(KEY_STORE); if (saved) { keyInput.value = saved; keyRow.style.display = 'none' } } catch {}
|
|
12385
|
+
|
|
12386
|
+
/** 追加一条消息气泡(role: user|assistant) */
|
|
12387
|
+
function addMsg(role, htmlOrText, isHtml) {
|
|
12388
|
+
const m = document.createElement('div')
|
|
12389
|
+
m.className = 'msg ' + role
|
|
12390
|
+
const who = document.createElement('div')
|
|
12391
|
+
who.className = 'who'
|
|
12392
|
+
who.textContent = role === 'user' ? '你' : CCC.name
|
|
12393
|
+
const bubble = document.createElement('div')
|
|
12394
|
+
bubble.className = 'bubble'
|
|
12395
|
+
if (isHtml) bubble.innerHTML = htmlOrText
|
|
12396
|
+
else bubble.textContent = htmlOrText
|
|
12397
|
+
m.appendChild(who)
|
|
12398
|
+
m.appendChild(bubble)
|
|
12399
|
+
msgs.appendChild(m)
|
|
12400
|
+
msgs.scrollTop = msgs.scrollHeight
|
|
12401
|
+
return bubble
|
|
12402
|
+
}
|
|
12403
|
+
|
|
12404
|
+
/** 打字指示器(等待回答时显示) */
|
|
12405
|
+
function addTyping() {
|
|
12406
|
+
const m = document.createElement('div')
|
|
12407
|
+
m.className = 'msg assistant'
|
|
12408
|
+
const who = document.createElement('div')
|
|
12409
|
+
who.className = 'who'
|
|
12410
|
+
who.textContent = CCC.name
|
|
12411
|
+
const bubble = document.createElement('div')
|
|
12412
|
+
bubble.className = 'bubble'
|
|
12413
|
+
bubble.innerHTML = '<span class="typing"><i></i><i></i><i></i></span>'
|
|
12414
|
+
m.appendChild(who)
|
|
12415
|
+
m.appendChild(bubble)
|
|
12416
|
+
msgs.appendChild(m)
|
|
12417
|
+
msgs.scrollTop = msgs.scrollHeight
|
|
12418
|
+
return bubble
|
|
12419
|
+
}
|
|
12420
|
+
|
|
12421
|
+
async function ask() {
|
|
12422
|
+
const q = qInput.value.trim()
|
|
12153
12423
|
const key = keyInput.value.trim()
|
|
12154
12424
|
const role = roleSel.value || undefined
|
|
12155
|
-
if (!q)
|
|
12156
|
-
if (!key) {
|
|
12157
|
-
|
|
12158
|
-
|
|
12425
|
+
if (!q) return
|
|
12426
|
+
if (!key) { keyInput.focus(); return }
|
|
12427
|
+
if (busy) return
|
|
12428
|
+
// 保存 key(下次自动记忆 + 隐藏 key 行)
|
|
12429
|
+
try { localStorage.setItem(KEY_STORE, key); keyRow.style.display = 'none' } catch {}
|
|
12430
|
+
// 追加用户消息 + 打字指示
|
|
12431
|
+
addMsg('user', q, false)
|
|
12432
|
+
qInput.value = ''
|
|
12433
|
+
autoGrow()
|
|
12434
|
+
const typing = addTyping()
|
|
12435
|
+
busy = true
|
|
12159
12436
|
btn.disabled = true
|
|
12160
|
-
answer.className = 'muted'
|
|
12161
|
-
answer.textContent = '运行中…'
|
|
12162
12437
|
try {
|
|
12163
12438
|
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
12439
|
const data = await res.json()
|
|
12165
|
-
|
|
12166
|
-
|
|
12167
|
-
|
|
12440
|
+
typing.remove() // 移除打字指示,替换为真实回答
|
|
12441
|
+
if (!res.ok) {
|
|
12442
|
+
sessionId = ''
|
|
12443
|
+
addMsg('assistant', '⚠️ ' + (data.error || ('HTTP ' + res.status)), true)
|
|
12444
|
+
return
|
|
12445
|
+
}
|
|
12446
|
+
addMsg('assistant', data.answer_html || data.answer || '(空回答)', true)
|
|
12168
12447
|
sessionId = data.sessionId || ''
|
|
12169
12448
|
} catch (err) {
|
|
12170
|
-
|
|
12171
|
-
|
|
12449
|
+
typing.remove()
|
|
12450
|
+
addMsg('assistant', '⚠️ ' + String(err.message || err), true)
|
|
12172
12451
|
} finally {
|
|
12452
|
+
busy = false
|
|
12173
12453
|
btn.disabled = false
|
|
12454
|
+
qInput.focus()
|
|
12174
12455
|
}
|
|
12456
|
+
}
|
|
12457
|
+
|
|
12458
|
+
function autoGrow() {
|
|
12459
|
+
qInput.style.height = 'auto'
|
|
12460
|
+
qInput.style.height = Math.min(qInput.scrollHeight, 140) + 'px'
|
|
12461
|
+
}
|
|
12462
|
+
|
|
12463
|
+
btn.addEventListener('click', () => void ask())
|
|
12464
|
+
qInput.addEventListener('keydown', (e) => {
|
|
12465
|
+
if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); void ask() }
|
|
12175
12466
|
})
|
|
12467
|
+
qInput.addEventListener('input', autoGrow)
|
|
12468
|
+
// 新对话:重置 sessionId + 清空消息流(前端内存,不存储——刷新即失)
|
|
12469
|
+
document.getElementById('newChat').addEventListener('click', () => {
|
|
12470
|
+
sessionId = ''
|
|
12471
|
+
msgs.innerHTML = ''
|
|
12472
|
+
qInput.focus()
|
|
12473
|
+
})
|
|
12474
|
+
|
|
12475
|
+
// 初始欢迎消息(外层模板求值注入容器名;textContent 渲染防注入)
|
|
12476
|
+
addMsg('assistant', '你好,我是「${ccc.name}」的认知助手。有什么可以帮你的?(本对话仅当前页面有效,刷新即失)', false)
|
|
12477
|
+
qInput.focus()
|
|
12176
12478
|
<\/script>
|
|
12177
12479
|
</body>
|
|
12178
12480
|
</html>`;
|
|
@@ -12252,6 +12554,7 @@ function apply(ctx, config) {
|
|
|
12252
12554
|
registerSettingsSection(ctx, config);
|
|
12253
12555
|
registerGateway(ctx);
|
|
12254
12556
|
registerRebuildTurnHook(ctx);
|
|
12557
|
+
registerOutputGuardHook(ctx);
|
|
12255
12558
|
if (config.env) registerEnv(ctx);
|
|
12256
12559
|
if (config.opencodeSkills) registerOpencodeSkills(ctx);
|
|
12257
12560
|
registerBootstrap(ctx);
|
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.4",
|
|
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": {
|