@shgroup/dsh-serenity-hooks 1.27.0 → 1.27.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -69,6 +69,13 @@ export declare function jscSafeJsonText(jsonText: string): string;
69
69
  * @param hideThink 为 true 时 `<think>` 内容完全不渲染(public 问答页体验)
70
70
  */
71
71
  export declare function renderSkiffMarkdown(raw: string, hideThink?: boolean): string;
72
+ /**
73
+ * 剥离 `<think>…</think>` 块(v1.27.1,微信桥用户反馈:"过微信桥回复给用户的消息,
74
+ * 要去掉 think 标签,用户不应看到")。复用 extractThinkBlocks 状态机(v1.26.8,
75
+ * 弃正则)——提取后**只保留正文**(占位符替换为空,think 内容丢弃),
76
+ * 未闭合 think 优雅截断。适用于纯文本输出面(微信等不支持 think 折叠的通道)。
77
+ */
78
+ export declare function stripThink(raw: string): string;
72
79
  /**
73
80
  * 启动调试问答服务(单实例;重复启动幂等返回既有实例)。
74
81
  *
@@ -18,8 +18,6 @@ import { defineTool } from '@deepseek-ai/dsh-tools';
18
18
  import type { Context } from 'cordis';
19
19
  import { type ActiveSessionInfo, type CreateSessionResult } from '../session-ops.js';
20
20
  export interface RenameOnUseDeps {
21
- /** naming.enabled 简单配置(DSH settings) */
22
- namingEnabled: boolean;
23
21
  /** sessionTitle 服务可用性 */
24
22
  sessionTitleAvailable: boolean;
25
23
  }
@@ -33,8 +31,8 @@ export interface RenameOnUseDeps {
33
31
  export declare function namingTitleFor(active: ActiveSessionInfo): string;
34
32
  /**
35
33
  * use 激活宁静号会话后,把当前 dsh 会话重命名为命名标题(`S###-日期`)。
36
- * 门控:naming.enabled + sessionTitle 服务存在;失败不静默——返回结果对象
37
- * 而非 null(v1.22.9),调用方决定可见性。
34
+ * v1.27.1:**永远开启**(不再有 naming.enabled 门控)——仅 sessionTitle 服务
35
+ * 存在性守卫;失败不静默——返回结果对象而非 null(v1.22.9),调用方决定可见性。
38
36
  *
39
37
  * v1.23.2 修复(this 绑定):第三参从**解构的裸 rename 函数**改为**整个
40
38
  * sessionTitle 服务对象**——内部以 `titles.rename(session, title)` **方法调用**
@@ -19,7 +19,9 @@ import { type WeixinAccountCredential } from './weixin-route.js';
19
19
  *
20
20
  * 会话语义:`weixinSessionIdFor(fromUserId)` 固定可重建——同用户长期同一会话;
21
21
  * 进程内已有(getSkiffAgent 命中)→ 延续;无(首次/进程重启)→ createSkiffAgent
22
- * 以固定 id 新建(角色不变,记忆从新开始——重启后自动重建,与 3100 问答页同语义)。
22
+ * 固定 id resume-or-create(v1.27.2):磁盘已有持久化 log → resume(历史延续,
23
+ * 重启后记忆保留——真正的"同用户长期延续");无 log(首次)→ create。
24
+ * "新的对话已开始"通知仅真正首次(create)时发送;resume/进程内延续不发。
23
25
  */
24
26
  export declare function handleIncoming(ctx: Context, root: string, _accountId: string, cred: WeixinAccountCredential, msg: {
25
27
  from_user_id?: string;
@@ -282,7 +282,8 @@ function runLocalStore(root, args) {
282
282
  * 职责(零 DSH 依赖,可独立单测):
283
283
  * - 读取 CCC 的 weixin 配置(.opencode/serenity.json weixin 段——结构/路由/开关)
284
284
  * - 读取/写入 CCC localstore 的账号凭据(weixin.accounts.<accountId>.token/.baseUrl/.userId)
285
- * - 微信用户 → skiff 会话 id 映射(固定可重建:skiff-weixin-<sha256(userid)>.slice(16))
285
+ * - 微信用户 → skiff 会话 id 映射(固定可重建:skiff-weixin-<sha256(userid)>.slice(1, 17);
286
+ * v1.27.3 错位一位避开损坏 log)
286
287
  * - 路由匹配(exact user → 通配 * 兜底)
287
288
  *
288
289
  * 配置归属(S142 用户拍板):**CCC 级**——serenity.json 结构 + localstore 凭据分离;
@@ -293,6 +294,7 @@ var weixin_route_exports = /* @__PURE__ */ __exportAll({
293
294
  clearWeixinCredential: () => clearWeixinCredential,
294
295
  extractWeixinText: () => extractWeixinText,
295
296
  matchWeixinRoute: () => matchWeixinRoute,
297
+ nextWeixinAccountId: () => nextWeixinAccountId,
296
298
  readWeixinCredential: () => readWeixinCredential,
297
299
  readWeixinSettings: () => readWeixinSettings,
298
300
  removeWeixinAccount: () => removeWeixinAccount,
@@ -311,9 +313,12 @@ function accountKeyPart(accountId) {
311
313
  }
312
314
  /** 微信桥 skiff 会话 id 前缀(seams 旁路判定;对外面纯净——守卫识别外部面) */
313
315
  const WEIXIN_SESSION_PREFIX = "skiff-weixin-";
314
- /** 微信用户 → 固定会话 id(同用户长期同一会话,记忆延续;多用户天然隔离) */
316
+ /** 微信用户 → 固定会话 id(同用户长期同一会话,记忆延续;多用户天然隔离)。
317
+ * **v1.27.3 错位一位(用户拍板)**:`.slice(0, 16)` → `.slice(1, 17)`——旧规则生成的
318
+ * 固定 id 已与磁盘损坏的持久化 log 绑定(dsh 不可硬删,create 同 id 必撞)→
319
+ * 新规则下同用户生成**全新 id**,避开损坏 log;固定可重建语义不变(同用户恒同 id)。 */
315
320
  function weixinSessionIdFor(fromUserId) {
316
- const digest = createHash("sha256").update(fromUserId).digest("hex").slice(0, 16);
321
+ const digest = createHash("sha256").update(fromUserId).digest("hex").slice(1, 17);
317
322
  return `${WEIXIN_SESSION_PREFIX}${digest}`;
318
323
  }
319
324
  /**
@@ -460,6 +465,17 @@ function saveWeixinRoutes(root, routes) {
460
465
  writeWeixinSection(root, next);
461
466
  return next;
462
467
  }
468
+ /**
469
+ * 生成下一个可用账号 id(`wechat-N`,N 自增取**最小未占用**):
470
+ * 多账号支持(S142 用户反馈 ①"要支持添加多个账号,每个都是扫码")——
471
+ * 移除中间账号后不冲突(wechat-2 被删 → 新账号复用 wechat-2 而非跳到 4)。
472
+ */
473
+ function nextWeixinAccountId(settings) {
474
+ const used = new Set((settings.accounts ?? []).map((a) => a.accountId));
475
+ let n = 1;
476
+ while (used.has(`wechat-${n}`)) n++;
477
+ return `wechat-${n}`;
478
+ }
463
479
  /** 切换总开关 */
464
480
  function setWeixinEnabled(root, enabled) {
465
481
  const next = {
@@ -4,7 +4,8 @@
4
4
  * 职责(零 DSH 依赖,可独立单测):
5
5
  * - 读取 CCC 的 weixin 配置(.opencode/serenity.json weixin 段——结构/路由/开关)
6
6
  * - 读取/写入 CCC localstore 的账号凭据(weixin.accounts.<accountId>.token/.baseUrl/.userId)
7
- * - 微信用户 → skiff 会话 id 映射(固定可重建:skiff-weixin-<sha256(userid)>.slice(16))
7
+ * - 微信用户 → skiff 会话 id 映射(固定可重建:skiff-weixin-<sha256(userid)>.slice(1, 17);
8
+ * v1.27.3 错位一位避开损坏 log)
8
9
  * - 路由匹配(exact user → 通配 * 兜底)
9
10
  *
10
11
  * 配置归属(S142 用户拍板):**CCC 级**——serenity.json 结构 + localstore 凭据分离;
@@ -15,7 +16,10 @@ import { type WeixinSettings, type WeixinRouteConfig, type WeixinAccountConfig }
15
16
  export declare const WEIXIN_SESSION_PREFIX = "skiff-weixin-";
16
17
  /** 判定 sessionId 是否为微信桥会话(外部面——输出守卫/轨迹隐藏生效) */
17
18
  export declare function isWeixinSessionId(sessionId: string | undefined): boolean;
18
- /** 微信用户 → 固定会话 id(同用户长期同一会话,记忆延续;多用户天然隔离) */
19
+ /** 微信用户 → 固定会话 id(同用户长期同一会话,记忆延续;多用户天然隔离)。
20
+ * **v1.27.3 错位一位(用户拍板)**:`.slice(0, 16)` → `.slice(1, 17)`——旧规则生成的
21
+ * 固定 id 已与磁盘损坏的持久化 log 绑定(dsh 不可硬删,create 同 id 必撞)→
22
+ * 新规则下同用户生成**全新 id**,避开损坏 log;固定可重建语义不变(同用户恒同 id)。 */
19
23
  export declare function weixinSessionIdFor(fromUserId: string): string;
20
24
  /**
21
25
  * 读取 CCC 微信桥配置(未配置/未启用 → 归一默认)。
@@ -57,5 +61,11 @@ export declare function upsertWeixinAccount(root: string, account: WeixinAccount
57
61
  export declare function removeWeixinAccount(root: string, accountId: string): WeixinSettings;
58
62
  /** 保存路由表(整体替换) */
59
63
  export declare function saveWeixinRoutes(root: string, routes: WeixinRouteConfig[]): WeixinSettings;
64
+ /**
65
+ * 生成下一个可用账号 id(`wechat-N`,N 自增取**最小未占用**):
66
+ * 多账号支持(S142 用户反馈 ①"要支持添加多个账号,每个都是扫码")——
67
+ * 移除中间账号后不冲突(wechat-2 被删 → 新账号复用 wechat-2 而非跳到 4)。
68
+ */
69
+ export declare function nextWeixinAccountId(settings: WeixinSettings): string;
60
70
  /** 切换总开关 */
61
71
  export declare function setWeixinEnabled(root: string, enabled: boolean): WeixinSettings;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shgroup/dsh-serenity-hooks",
3
- "version": "1.27.0",
3
+ "version": "1.27.2",
4
4
  "description": "宁静号 ACC harness — Native Cordis 插件(DeepSeek Harness 运行时)。真实 DSH 工具注册(cc_fs/session/acc_msm 等 9 工具)+ 拦截缝机械约束(safe-mode/路径守卫/会话落盘)+ 系统提示词注入(ACC/CCE/Constraints/SKILL/Session 五块)。适配 DSH 公开版(deepseek-ai/deepseek-harness 0.1.0-rc)。",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -1,424 +0,0 @@
1
- import { a as resolveRoleSystemPrompt, i as readSkiffRoles, n as buildSkiffBasePrompt, o as roleMsmWhitelist, r as isSkiffSessionId, u as trajectorySubset } from "./skiff-role-Dw7UKCUm.js";
2
- import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
3
- import { join } from "node:path";
4
- import { createUserMessage } from "@deepseek-ai/dsh-llm";
5
- import { randomBytes, randomUUID } from "node:crypto";
6
- //#region src/skiff-registry.ts
7
- const skiffSessions = /* @__PURE__ */ new Map();
8
- /** 查 sessionId 的 Skiff 角色名(无 → null)——向后兼容(guards/seams 依赖) */
9
- function skiffRoleFor$1(sessionId) {
10
- return skiffSessions.get(sessionId)?.role ?? null;
11
- }
12
- /** 查 sessionId 的完整绑定(role + ccc;无 → null)——v1.25.10 会话追问校验 */
13
- function skiffSessionInfo$1(sessionId) {
14
- return skiffSessions.get(sessionId) ?? null;
15
- }
16
- function registerSkiffSession$1(sessionId, role, ccc) {
17
- skiffSessions.set(sessionId, {
18
- role,
19
- ccc
20
- });
21
- }
22
- function unregisterSkiffSession$1(sessionId) {
23
- skiffSessions.delete(sessionId);
24
- }
25
- /** 测试/调试:注册表快照(sessionId → {role, ccc}) */
26
- function skiffSessionSnapshot$1() {
27
- return new Map(skiffSessions);
28
- }
29
- //#endregion
30
- //#region src/handyman-ops.ts
31
- /**
32
- * handyman-ops.ts — handyman(杂工)纯逻辑层(零 DSH 依赖,可独立单测)
33
- *
34
- * v1.24.0:loop(牛马)→ handyman(杂工)重命名。语义对齐 osp loop:
35
- * 进度文件(handyman-<label>.md/.json)、续跑、轮次 prompt 结构、stop token。
36
- * 不兼容旧 loop- 进度文件(用户拍板:仅新 handyman- 前缀)。
37
- */
38
- /** label 脱敏(Windows 审计问题 17):非法字符 → '-',去尾点/空格,限长(按码点截断,修复代理对切散 U+FFFD) */
39
- function sanitizeLabel(label) {
40
- return [...label.replace(/[<>:"/\\|?*\u0000-\u001f]/g, "-").replace(/[ .]+$/g, "")].slice(0, 50).join("");
41
- }
42
- function handymanProgressPaths(root, label) {
43
- const dir = join(root, "AGENT_SESSIONS");
44
- const safe = sanitizeLabel(label);
45
- return {
46
- md: join(dir, `handyman-${safe}.md`),
47
- json: join(dir, `handyman-${safe}.json`)
48
- };
49
- }
50
- /** 读取进度(续跑);无文件返回 round 0 */
51
- function readProgress(root, label) {
52
- const { json } = handymanProgressPaths(root, label);
53
- if (!existsSync(json)) return null;
54
- try {
55
- return JSON.parse(readFileSync(json, "utf-8"));
56
- } catch {
57
- return null;
58
- }
59
- }
60
- function writeProgress(root, label, p) {
61
- const { md, json } = handymanProgressPaths(root, label);
62
- mkdirSync(join(root, "AGENT_SESSIONS"), { recursive: true });
63
- writeFileSync(json, JSON.stringify({
64
- ...p,
65
- status: p.status ?? "running",
66
- updated: (/* @__PURE__ */ new Date()).toISOString()
67
- }, null, 2) + "\n", "utf-8");
68
- const lines = [
69
- `# handyman: ${label}`,
70
- `- Model: ${p.model}`,
71
- `- Round: ${p.round}`,
72
- `- Done: ${p.done}`,
73
- "",
74
- `## Latest response`,
75
- "",
76
- p.lastResponse,
77
- ""
78
- ];
79
- writeFileSync(md, lines.join("\n"), "utf-8");
80
- }
81
- /** 失败状态落盘(对齐 osp writeFailedStatus:done=true / status=failed / errorCode) */
82
- function writeFailedStatus(root, label, info) {
83
- const { json } = handymanProgressPaths(root, label);
84
- mkdirSync(join(root, "AGENT_SESSIONS"), { recursive: true });
85
- const prev = readProgress(root, label);
86
- writeFileSync(json, JSON.stringify({
87
- round: prev?.round ?? 0,
88
- done: true,
89
- label,
90
- model: prev?.model ?? "",
91
- status: "failed",
92
- errorCode: info.errorCode,
93
- errorMessage: info.errorMessage,
94
- updated: (/* @__PURE__ */ new Date()).toISOString(),
95
- lastResponse: prev?.lastResponse ?? ""
96
- }, null, 2) + "\n", "utf-8");
97
- }
98
- function newStopToken() {
99
- return `SERENITY_HANDYMAN_DONE_${randomBytes(8).toString("hex")}`;
100
- }
101
- /** 解析 model 字符串(provider/model)→ {provider, model};无 / 视为 model-only */
102
- function splitModel(model) {
103
- const idx = model.indexOf("/");
104
- if (idx < 0) return {
105
- provider: void 0,
106
- model
107
- };
108
- return {
109
- provider: model.slice(0, idx),
110
- model: model.slice(idx + 1)
111
- };
112
- }
113
- /** 校验模型在白名单内;不在 → 抛错(用户拍板:只能使用 CCC 配置的模型) */
114
- function requireWhitelistedModel(model, models) {
115
- if (!models.includes(model)) throw new Error(`handyman: model "${model}" is not in the CCC whitelist. Configure .opencode/serenity.json "handyman.models" with one of: ${models.join(", ")}`);
116
- }
117
- /** 轮次 prompt(对齐老 loop 结构:回顾进度 → 自由工作 → 汇报;S134 EAP 化:固定详尽) */
118
- function buildRoundPrompt(opts) {
119
- const { root, session, label, round, stopToken, progress, task } = opts;
120
- const resumeNote = progress && progress.round > 0 ? `Previous round (round ${progress.round}) completed: ${progress.lastResponse.slice(0, 300)}\nAlways continue from where you left off; never redo completed work.` : "This is the first round.";
121
- return `# ${label} — handyman round ${round}
122
-
123
- CCC root: ${root}
124
- ${session ? `Work session: ${session} (progress recorded in AGENT_SESSIONS/${session}/SESSION.md)` : ""}
125
- ${task ? `Task: ${task}` : `Task: follow the work corresponding to label "${label}" (if a work session exists, read SESSION.md first to clarify the goal)`}
126
- ${resumeNote}
127
-
128
- ## Work rules (fixed every round, must follow)
129
- 1. Work freely within this round: read files, modify code, execute commands — use every means to advance the task.
130
- 2. If this task is **reading/curating or text-writing work** (extracting from files, summarizing, writing docs, generating text, etc.),
131
- first load eap (acc-eap skill) and organize output per the EAP standard:
132
- - E↑ Explicit: entities/variables clearly defined, relationships with direction and cardinality, boundaries drawn, no ambiguous words
133
- - R↓ Reconstructable: key conclusions record sources and reasoning, rebuildable by later agents
134
- - S↑ Stable: output structure regenerates repeatably, no reliance on implicit context
135
- 3. Reports must be concrete and verifiable — no filler.
136
-
137
- ## Per-round report (fixed format, answer each item)
138
- 1. What was done this round (concrete)
139
- 2. Next-step plan
140
- 3. Whether the task is complete (if complete, output only ${stopToken})
141
-
142
- If the task is complete, output only ${stopToken}.`;
143
- }
144
- /**
145
- * handyman 规模化使用指引(guide 子命令输出;S134 继承 + v1.24.0 更新):
146
- * 使用 handyman 前必须先加载 eap 设计方案;并行策略(jobs 编排);提示词规范(详尽固定 EAP);
147
- * 阅读/文字编写类 handyman 内部也加载 eap。
148
- */
149
- const HANDYMAN_GUIDE = `# handyman — Scale-Up Usage Guide (guide)
150
-
151
- ## ⚠️ Before using: load eap and design the plan
152
- Before calling handyman, load eap (acc-eap skill) and design the "scale-up handyman plan" based on the EAP framework.
153
-
154
- ### 1. Task decomposition (E↑ Explicit)
155
- - Split large tasks into explicit subtasks: each subtask defines goal / input / boundaries (what to do, what not to do) / acceptance criteria
156
- - Make dependencies explicit: dependent tasks run serially, independent ones can run in parallel
157
-
158
- ### 2. Prompt design (handyman's task parameter)
159
- - task must be detailed, fixed, and EAP-compliant: clear goal, drawn boundaries, decidable acceptance criteria
160
- - Anti-example "handle this file" — ambiguous; good example "read <path>, extract all rows of the「关键决策」table,
161
- output a JSON array (fields id/conclusion/evidence), do not modify the original file"
162
- - Reading/curating or text-writing work (extracting from files, summarizing, writing docs, generating text, etc.):
163
- the handyman-internal agent is also required to load eap and organize output per the EAP standard
164
-
165
- ### 3. Model whitelist (CCC-configured, mandatory)
166
- - handyman only uses models listed in .opencode/serenity.json "handyman.models" — never arbitrary models
167
- - Recursive subagents inside a handyman inherit the handyman's model automatically (DSH native)
168
- - Keep the subagent tool instance free of a fixed agentOptions, or model inheritance breaks
169
-
170
- ### 4. Parallel strategy (jobs orchestration, workflow capability)
171
- - Independent subtasks can run in parallel via handyman(jobs=[...]): each job gets its own label + task + stop token + progress file
172
- - Concurrency safety guaranteed: unique sessionId (handyman-<label>-<uuid>), progress files isolated per label
173
- (AGENT_SESSIONS/handyman-<label>.json) — same label resumes, different labels never interfere
174
- - Parallel cap: handyman.maxParallel (default 10 — cheap models are cheap)
175
- - Aggregation: after each parallel job produces progress, the main agent merges (or spawns one aggregation handyman)
176
- - For programmable pipeline/phase orchestration at scale, use the platform's workflow tool instead
177
-
178
- ## Completion criteria (osp loop standard)
179
- - The only completion condition = the handyman-internal agent echoes this round's random verification code (stop token); dialogue round cap (default 100, osp fail-safe, forced stop beyond the cap, resumable)
180
- - Automatic restart on abnormal agent stop (≤100 restarts, anti-infinite-loop)
181
-
182
- ## Waiting UI
183
- - The WebUI session-header Serenity detail card shows running handymen's progress (label / round / last response), one line per parallel job, ~3s refresh
184
- `;
185
- /** 列出 AGENT_SESSIONS/handyman-*.json 的全部进度(按 updated 倒序;坏文件跳过) */
186
- function listActiveHandymen(root) {
187
- const dir = join(root, "AGENT_SESSIONS");
188
- if (!existsSync(dir)) return [];
189
- const out = [];
190
- for (const entry of readdirSync(dir)) {
191
- if (!entry.startsWith("handyman-") || !entry.endsWith(".json")) continue;
192
- try {
193
- const data = JSON.parse(readFileSync(join(dir, entry), "utf-8"));
194
- if (typeof data.label !== "string" || typeof data.round !== "number") continue;
195
- out.push({
196
- label: data.label,
197
- round: data.round,
198
- done: data.done === true,
199
- model: typeof data.model === "string" ? data.model : "",
200
- updated: typeof data.updated === "string" ? data.updated : "",
201
- lastResponse: typeof data.lastResponse === "string" ? data.lastResponse : ""
202
- });
203
- } catch {}
204
- }
205
- out.sort((a, b) => a.updated < b.updated ? 1 : -1);
206
- return out;
207
- }
208
- //#endregion
209
- //#region src/skiff-core.ts
210
- const PLUGIN_SOURCE = {
211
- kind: "plugin",
212
- plugin: "dsh-serenity-hooks"
213
- };
214
- const skiffAgents = /* @__PURE__ */ new Map();
215
- /** 查 sessionId 的 Skiff 角色名(无 → null) */
216
- function skiffRoleFor(sessionId) {
217
- return skiffRoleFor$1(sessionId);
218
- }
219
- /** 查 sessionId 的会话绑定(role + ccc;无 → null)——v1.25.10 追问校验 */
220
- function skiffSessionInfo(sessionId) {
221
- return skiffSessionInfo$1(sessionId);
222
- }
223
- /** 查 sessionId 的活体 agent(进程内会话延续用;未注册/已清理 → undefined) */
224
- function getSkiffAgent(sessionId) {
225
- return skiffAgents.get(sessionId);
226
- }
227
- function registerSkiffSession(sessionId, role, ccc, agent) {
228
- skiffAgents.set(sessionId, agent);
229
- registerSkiffSession$1(sessionId, role, ccc);
230
- }
231
- function unregisterSkiffSession(sessionId) {
232
- skiffAgents.delete(sessionId);
233
- unregisterSkiffSession$1(sessionId);
234
- }
235
- /** 测试/调试:注册表快照(sessionId → {role} 兼容展示) */
236
- function skiffSessionSnapshot() {
237
- const out = /* @__PURE__ */ new Map();
238
- for (const [id, b] of skiffSessionSnapshot$1()) out.set(id, {
239
- role: b.role,
240
- ccc: b.ccc
241
- });
242
- return out;
243
- }
244
- /** Skiff agent 挂载的 DSH preset(v1.25.3 修复:read/grep/glob 等平台工具由 preset 决定工具面;
245
- * handyman 经 composeFrom 继承父、skiff 无父上下文——直接挂 DSH 默认 standard preset;
246
- * guard 角色白名单再按角色过滤可见/可用面——白名单外工具仍 deny) */
247
- const SKIFF_PRESET = "standard";
248
- /**
249
- * 创建 Skiff agent:标准 DSH agent + cwd=CCC root + 角色模型 +
250
- * standard preset(平台工具面)+ scoped 系统提示词(基础提示词 + CCC 定义段,全替换 ACC 默认注入)。
251
- * @param sessionId 指定会话 id(可选;微信桥等外部面用固定 id 实现用户↔会话长期映射——
252
- * 不传则随机生成 skiff-<role>-<uuid>,ACP/调试页默认路径)
253
- */
254
- async function createSkiffAgent(ctx, root, roleName, role, defaultModel, sessionId) {
255
- if (!ctx.agents) throw new Error("skiff: ctx.agents unavailable");
256
- const model = role.model?.trim() || defaultModel || "";
257
- const id = sessionId ?? `skiff-${roleName}-${randomUUID()}`;
258
- const handle = await ctx.agents.create({
259
- sessionId: id,
260
- meta: {
261
- cwd: root,
262
- agentPreset: SKIFF_PRESET
263
- },
264
- setup: async (agentCtx) => {
265
- try {
266
- await agentCtx.get("agentPresets")?.mount?.(agentCtx, SKIFF_PRESET);
267
- } catch {}
268
- },
269
- ...model ? { agentOptions: splitModel(model) } : {}
270
- });
271
- const agent = handle.agent;
272
- let cccPrompt = "";
273
- try {
274
- cccPrompt = resolveRoleSystemPrompt(root, role);
275
- } catch (err) {
276
- console.warn(`[serenity-hooks] skiff 角色 "${roleName}" 系统提示词解析失败(回退仅基础段): ${String(err?.message ?? err)}`);
277
- }
278
- try {
279
- agent.ctx.systemPrompt.section({
280
- name: "serenity-skiff",
281
- order: -60,
282
- text: () => [buildSkiffBasePrompt(roleName, role), cccPrompt].filter(Boolean).join("\n")
283
- });
284
- } catch (err) {
285
- console.warn(`[serenity-hooks] skiff 系统提示词注册失败: ${String(err?.message ?? err)}`);
286
- }
287
- registerSkiffSession(id, roleName, root, agent);
288
- return {
289
- handle,
290
- agent,
291
- sessionId: id
292
- };
293
- }
294
- /** 等待 agent 空闲(agent/status → idle);无超时(agent 工作多久等多久,handyman 同款) */
295
- function waitIdle(ctx, agent) {
296
- return new Promise((resolve) => {
297
- let settled = false;
298
- let dispose = () => {};
299
- const finish = () => {
300
- if (settled) return;
301
- settled = true;
302
- dispose();
303
- resolve();
304
- };
305
- dispose = ctx.on("agent/status", (payload) => {
306
- if (payload.agent === agent && payload.status === "idle") finish();
307
- });
308
- });
309
- }
310
- /** 读会话最后一个 assistant/message 文本(handyman 同款) */
311
- function lastAssistantText(agent) {
312
- const events = agent.session.events;
313
- for (let i = events.length - 1; i >= 0; i--) {
314
- const e = events[i];
315
- if (e && e.type === "assistant/message") {
316
- const text = (e.data?.message?.content ?? e.data?.content ?? []).filter((b) => b.type === "text" && b.text).map((b) => b.text).join("\n");
317
- if (text) return text;
318
- }
319
- }
320
- return "";
321
- }
322
- /** events → 可读轨迹(user/assistant 文本 + 工具调用 + 工具结果;单条解析失败跳过) */
323
- function eventsToTrajectory(events) {
324
- const out = [];
325
- for (const raw of events) try {
326
- const ev = raw;
327
- if (ev.type === "user/message") {
328
- const text = extractText(ev.data);
329
- if (text) out.push({
330
- role: "user",
331
- text
332
- });
333
- } else if (ev.type === "assistant/message") {
334
- const d = ev.data;
335
- const text = (d?.message?.content ?? d?.content ?? []).filter((b) => b.type === "text" && b.text).map((b) => b.text).join("\n");
336
- const calls = d?.message?.tool_calls ?? [];
337
- if (text) out.push({
338
- role: "assistant",
339
- text
340
- });
341
- for (const c of calls ?? []) {
342
- const args = typeof c.arguments === "string" ? c.arguments : JSON.stringify(c.arguments ?? {});
343
- out.push({
344
- role: "assistant",
345
- text: `→ ${c.name ?? "(tool)"} ${truncate(args, 300)}`,
346
- tool: c.name
347
- });
348
- }
349
- } else if (ev.type === "tool/result") {
350
- const outText = extractText(ev.data);
351
- if (outText) out.push({
352
- role: "tool",
353
- text: truncate(outText, 500),
354
- tool: String(ev.data?.name ?? "")
355
- });
356
- }
357
- } catch {}
358
- return out;
359
- }
360
- function extractText(data) {
361
- const content = data?.content;
362
- if (!Array.isArray(content)) return "";
363
- return content.filter((b) => b.type === "text" && b.text).map((b) => b.text).join("\n");
364
- }
365
- function truncate(s, n) {
366
- return s.length > n ? `${s.slice(0, n)}…` : s;
367
- }
368
- /**
369
- * 提问一轮:followup → 等 idle → 读答案 + 轨迹。
370
- * @param eventsStart 轨迹起点:显式传 0 = 全量轨迹(会话追问时页面重绘完整时间线,
371
- * v1.25.10 用户拍板);不传(undefined)= 本轮增量(followup 前 events 之后)
372
- * @param options.includeTrajectory 是否计算轨迹(v1.26.10:**3100 对外只提供问答**——
373
- * 公开问答页 / ACP JSON-RPC 不返回 trajectory,传 false 跳过计算;3099 调试页默认 true 保留)
374
- */
375
- async function askSkiff(ctx, agent, question, eventsStart, options) {
376
- const before = eventsStart === void 0 ? agent.session.events.length : eventsStart;
377
- agent.followup(createUserMessage({
378
- content: [{
379
- type: "text",
380
- text: question
381
- }],
382
- source: PLUGIN_SOURCE
383
- }));
384
- await waitIdle(ctx, agent);
385
- const answer = lastAssistantText(agent);
386
- const trajectory = options?.includeTrajectory === false ? [] : eventsToTrajectory(agent.session.events.slice(before));
387
- return {
388
- answer,
389
- sessionId: String(agent.session.id ?? ""),
390
- trajectory
391
- };
392
- }
393
- /**
394
- * Skiff 会话的轨迹纪律参与判定:非 skiff 会话恒 true(正常参与);
395
- * skiff 会话按角色 trajectory 子集(session/keeper/rebuild)决定;
396
- * 注册表缺失(进程重启遗留等)→ 保守旁路(false,完全独立)。
397
- */
398
- function skiffTrajectoryEnabled(root, sessionId, key) {
399
- if (!isSkiffSessionId(sessionId)) return true;
400
- const roleName = sessionId ? skiffRoleFor(sessionId) : null;
401
- if (!roleName) return false;
402
- const role = readSkiffRoles(root).get(roleName);
403
- return trajectorySubset(role)[key];
404
- }
405
- /**
406
- * acc_msm 的 Skiff 门控:非 skiff 会话恒放行;skiff 会话——
407
- * exec 非白名单 MSM 拒绝(不列名单)、register/deregister 必拒、
408
- * list 白名单过滤、check/guide/ccc-config 只读放行。
409
- */
410
- function skiffMsmGate(root, sessionId, action, name) {
411
- if (!isSkiffSessionId(sessionId)) return {};
412
- const roleName = sessionId ? skiffRoleFor(sessionId) : null;
413
- const role = roleName ? readSkiffRoles(root).get(roleName) : void 0;
414
- if (!roleName || !role) return { reject: "MSM not allowed in this skiff session" };
415
- if (action === "register" || action === "deregister") return { reject: "register/deregister is not allowed in skiff sessions" };
416
- if (action === "exec") {
417
- if (!name || !(role.msms ?? []).includes(name)) return { reject: "MSM not allowed" };
418
- return {};
419
- }
420
- if (action === "list") return { whitelist: roleMsmWhitelist(role) };
421
- return {};
422
- }
423
- //#endregion
424
- export { writeFailedStatus as _, skiffSessionInfo as a, unregisterSkiffSession as c, handymanProgressPaths as d, listActiveHandymen as f, splitModel as g, requireWhitelistedModel as h, skiffMsmGate as i, HANDYMAN_GUIDE as l, readProgress as m, createSkiffAgent as n, skiffSessionSnapshot as o, newStopToken as p, getSkiffAgent as r, skiffTrajectoryEnabled as s, askSkiff as t, buildRoundPrompt as u, writeProgress as v, skiffRoleFor$1 as y };