@shgroup/dsh-serenity-hooks 1.25.11 → 1.26.1
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 +43 -0
- package/lib/index.js +603 -59
- 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.
|
|
3
|
+
"version": "1.26.1",
|
|
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
|
@@ -3895,6 +3895,9 @@ Instruction-following style:
|
|
|
3895
3895
|
const setSkiffPort = (v) => {
|
|
3896
3896
|
scope.set("skiffDebugPort", Math.min(65535, Math.max(1024, Math.round(v))));
|
|
3897
3897
|
};
|
|
3898
|
+
const setAcpPort = (v) => {
|
|
3899
|
+
scope.set("acpHttpPort", Math.min(65535, Math.max(1024, Math.round(v))));
|
|
3900
|
+
};
|
|
3898
3901
|
if (!ready) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3899
3902
|
className: "ss-section",
|
|
3900
3903
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
@@ -3908,6 +3911,9 @@ Instruction-following style:
|
|
|
3908
3911
|
const namingOn = value?.namingEnabled ?? true;
|
|
3909
3912
|
const skiffOn = value?.skiffEnabled ?? false;
|
|
3910
3913
|
const skiffPort = value?.skiffDebugPort ?? 3099;
|
|
3914
|
+
const acpOn = value?.acpEnabled ?? false;
|
|
3915
|
+
const acpPort = value?.acpHttpPort ?? 3100;
|
|
3916
|
+
const publicAskOn = value?.publicAskEnabled ?? false;
|
|
3911
3917
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3912
3918
|
className: "ss-section",
|
|
3913
3919
|
children: [
|
|
@@ -3996,6 +4002,43 @@ Instruction-following style:
|
|
|
3996
4002
|
})
|
|
3997
4003
|
}) })]
|
|
3998
4004
|
}),
|
|
4005
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Group, {
|
|
4006
|
+
title: "ACP / 建议问答",
|
|
4007
|
+
children: [
|
|
4008
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RowCard, {
|
|
4009
|
+
title: "ACP JSON-RPC 端点",
|
|
4010
|
+
desc: "程序化调用 Skiff 角色(指定 CCC+角色+会话;实验性,仅监听 127.0.0.1)",
|
|
4011
|
+
control: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Toggle, {
|
|
4012
|
+
checked: acpOn,
|
|
4013
|
+
onChange: (on) => toggle("acpEnabled", on)
|
|
4014
|
+
})
|
|
4015
|
+
}) }),
|
|
4016
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RowCard, {
|
|
4017
|
+
title: "建议问答页",
|
|
4018
|
+
desc: "按认知容器暴露问答页供他人验证(需 key;首次开启自动生成固定 key,见 ~/.dsh/serenity-hooks.json)",
|
|
4019
|
+
control: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Toggle, {
|
|
4020
|
+
checked: publicAskOn,
|
|
4021
|
+
onChange: (on) => toggle("publicAskEnabled", on)
|
|
4022
|
+
})
|
|
4023
|
+
}) }),
|
|
4024
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RowCard, {
|
|
4025
|
+
title: "HTTP 端口",
|
|
4026
|
+
desc: "ACP JSON-RPC + 问答页共用端口(1024 ~ 65535,默认 3100)",
|
|
4027
|
+
control: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
4028
|
+
className: "ss-threshold",
|
|
4029
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
4030
|
+
className: "ss-portInput",
|
|
4031
|
+
type: "number",
|
|
4032
|
+
min: 1024,
|
|
4033
|
+
max: 65535,
|
|
4034
|
+
value: acpPort,
|
|
4035
|
+
disabled: !acpOn && !publicAskOn,
|
|
4036
|
+
onChange: (e) => setAcpPort(Number(e.target.value))
|
|
4037
|
+
})
|
|
4038
|
+
})
|
|
4039
|
+
}) })
|
|
4040
|
+
]
|
|
4041
|
+
}),
|
|
3999
4042
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4000
4043
|
className: "ss-group",
|
|
4001
4044
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
|
package/lib/index.js
CHANGED
|
@@ -736,6 +736,13 @@ function registerSkiffSession$1(sessionId, role, ccc) {
|
|
|
736
736
|
ccc
|
|
737
737
|
});
|
|
738
738
|
}
|
|
739
|
+
function unregisterSkiffSession$1(sessionId) {
|
|
740
|
+
skiffSessions.delete(sessionId);
|
|
741
|
+
}
|
|
742
|
+
/** 测试/调试:注册表快照(sessionId → {role, ccc}) */
|
|
743
|
+
function skiffSessionSnapshot$1() {
|
|
744
|
+
return new Map(skiffSessions);
|
|
745
|
+
}
|
|
739
746
|
//#endregion
|
|
740
747
|
//#region src/seams/guards.ts
|
|
741
748
|
/**
|
|
@@ -2394,6 +2401,19 @@ function registerSkiffSession(sessionId, role, ccc, agent) {
|
|
|
2394
2401
|
skiffAgents.set(sessionId, agent);
|
|
2395
2402
|
registerSkiffSession$1(sessionId, role, ccc);
|
|
2396
2403
|
}
|
|
2404
|
+
function unregisterSkiffSession(sessionId) {
|
|
2405
|
+
skiffAgents.delete(sessionId);
|
|
2406
|
+
unregisterSkiffSession$1(sessionId);
|
|
2407
|
+
}
|
|
2408
|
+
/** 测试/调试:注册表快照(sessionId → {role} 兼容展示) */
|
|
2409
|
+
function skiffSessionSnapshot() {
|
|
2410
|
+
const out = /* @__PURE__ */ new Map();
|
|
2411
|
+
for (const [id, b] of skiffSessionSnapshot$1()) out.set(id, {
|
|
2412
|
+
role: b.role,
|
|
2413
|
+
ccc: b.ccc
|
|
2414
|
+
});
|
|
2415
|
+
return out;
|
|
2416
|
+
}
|
|
2397
2417
|
/** Skiff agent 挂载的 DSH preset(v1.25.3 修复:read/grep/glob 等平台工具由 preset 决定工具面;
|
|
2398
2418
|
* handyman 经 composeFrom 继承父、skiff 无父上下文——直接挂 DSH 默认 standard preset;
|
|
2399
2419
|
* guard 角色白名单再按角色过滤可见/可用面——白名单外工具仍 deny) */
|
|
@@ -5237,7 +5257,10 @@ const simpleSettingsSchema = z.object({
|
|
|
5237
5257
|
rebuildThreshold: z.number().min(.01).max(1).default(.9),
|
|
5238
5258
|
namingEnabled: z.boolean().default(true),
|
|
5239
5259
|
skiffEnabled: z.boolean().default(false),
|
|
5240
|
-
skiffDebugPort: z.number().min(1024).max(65535).default(3099)
|
|
5260
|
+
skiffDebugPort: z.number().min(1024).max(65535).default(3099),
|
|
5261
|
+
acpEnabled: z.boolean().default(false),
|
|
5262
|
+
acpHttpPort: z.number().min(1024).max(65535).default(3100),
|
|
5263
|
+
publicAskEnabled: z.boolean().default(false)
|
|
5241
5264
|
});
|
|
5242
5265
|
/** 从插件 Config 提取 entry 默认(settings base 层) */
|
|
5243
5266
|
function entryDefaults(config) {
|
|
@@ -5247,7 +5270,10 @@ function entryDefaults(config) {
|
|
|
5247
5270
|
rebuildThreshold: config.rebuild?.thresholdRatio ?? .9,
|
|
5248
5271
|
namingEnabled: config.naming?.enabled ?? true,
|
|
5249
5272
|
skiffEnabled: config.skiff?.enabled ?? false,
|
|
5250
|
-
skiffDebugPort: config.skiff?.debugPort ?? 3099
|
|
5273
|
+
skiffDebugPort: config.skiff?.debugPort ?? 3099,
|
|
5274
|
+
acpEnabled: config.acp?.enabled ?? false,
|
|
5275
|
+
acpHttpPort: config.acp?.httpPort ?? 3100,
|
|
5276
|
+
publicAskEnabled: config.publicAsk?.enabled ?? false
|
|
5251
5277
|
};
|
|
5252
5278
|
}
|
|
5253
5279
|
/** 运行时源(installSettingsSection 注入:settings scope 或 entry fallback) */
|
|
@@ -5260,7 +5286,10 @@ function defaultSimpleSettings() {
|
|
|
5260
5286
|
rebuildThreshold: .9,
|
|
5261
5287
|
namingEnabled: true,
|
|
5262
5288
|
skiffEnabled: false,
|
|
5263
|
-
skiffDebugPort: 3099
|
|
5289
|
+
skiffDebugPort: 3099,
|
|
5290
|
+
acpEnabled: false,
|
|
5291
|
+
acpHttpPort: 3100,
|
|
5292
|
+
publicAskEnabled: false
|
|
5264
5293
|
};
|
|
5265
5294
|
}
|
|
5266
5295
|
/**
|
|
@@ -6898,7 +6927,11 @@ dsp 只提供机制(双白名单强制 + 基础提示词 + 调试问答页)
|
|
|
6898
6927
|
|
|
6899
6928
|
── 运行 ──
|
|
6900
6929
|
调试:设置面板「Serenity」页 Skiff 区块开启 → http://127.0.0.1:<debugPort> 问答页实测
|
|
6901
|
-
|
|
6930
|
+
ACP 程序化面(v1.26.0):设置面板「ACP」区块开启 → HTTP JSON-RPC 端点(默认 3100)——
|
|
6931
|
+
POST / {jsonrpc, id, method, params}
|
|
6932
|
+
session/new {ccc, role, sessionId?} → 创建/延续会话(sessionId 可选=进程内追问)
|
|
6933
|
+
session/prompt {sessionId, question} → {answer, trajectory}
|
|
6934
|
+
session/cancel / session/close / session/list / request_permission(恒 allow)
|
|
6902
6935
|
启停 = 人工(设置面板开关,不随插件加载自动启动);未开启零资源占用
|
|
6903
6936
|
生效机制:角色配置**实时读取**(改 .opencode/serenity.json → 刷新调试页即生效);
|
|
6904
6937
|
改配置后用 skiff_admin apply 做显式校验 + 应用确认(绑定 CCC + 角色清单)
|
|
@@ -7339,7 +7372,8 @@ function defaultAdvancedSettings() {
|
|
|
7339
7372
|
persona: {
|
|
7340
7373
|
mode: "",
|
|
7341
7374
|
overrideText: ""
|
|
7342
|
-
}
|
|
7375
|
+
},
|
|
7376
|
+
publicAsk: { key: "" }
|
|
7343
7377
|
};
|
|
7344
7378
|
}
|
|
7345
7379
|
const SCRYPT_KEYLEN = 32;
|
|
@@ -7390,6 +7424,7 @@ function mergeWithDefaults(raw) {
|
|
|
7390
7424
|
const rebuild = o.rebuild ?? {};
|
|
7391
7425
|
const naming = o.naming ?? {};
|
|
7392
7426
|
const persona = o.persona ?? {};
|
|
7427
|
+
const publicAsk = o.publicAsk ?? {};
|
|
7393
7428
|
return {
|
|
7394
7429
|
gateway: {
|
|
7395
7430
|
enabled: typeof gateway.enabled === "boolean" ? gateway.enabled : def.gateway.enabled,
|
|
@@ -7409,7 +7444,8 @@ function mergeWithDefaults(raw) {
|
|
|
7409
7444
|
persona: {
|
|
7410
7445
|
mode: typeof persona.mode === "string" ? persona.mode : def.persona.mode,
|
|
7411
7446
|
overrideText: typeof persona.overrideText === "string" ? persona.overrideText : def.persona.overrideText
|
|
7412
|
-
}
|
|
7447
|
+
},
|
|
7448
|
+
publicAsk: { key: typeof publicAsk.key === "string" ? publicAsk.key : def.publicAsk.key }
|
|
7413
7449
|
};
|
|
7414
7450
|
}
|
|
7415
7451
|
/** 读取全局配置(文件缺失/坏 JSON → 默认值) */
|
|
@@ -7449,12 +7485,33 @@ function updateAdvancedSettings(patch) {
|
|
|
7449
7485
|
persona: ps !== void 0 ? {
|
|
7450
7486
|
mode: typeof ps.mode === "string" ? ps.mode : current.persona.mode,
|
|
7451
7487
|
overrideText: typeof ps.overrideText === "string" ? ps.overrideText : current.persona.overrideText
|
|
7452
|
-
} : current.persona
|
|
7488
|
+
} : current.persona,
|
|
7489
|
+
publicAsk: patch.publicAsk !== void 0 ? { key: typeof patch.publicAsk.key === "string" ? patch.publicAsk.key : current.publicAsk.key } : current.publicAsk
|
|
7453
7490
|
};
|
|
7454
7491
|
writeAdvancedSettings(next);
|
|
7455
7492
|
return next;
|
|
7456
7493
|
}
|
|
7457
7494
|
/**
|
|
7495
|
+
* 确保问答页访问 key 存在(首次启用自动生成 + 写回固定,用户拍板"配置时生成"):
|
|
7496
|
+
* 读取全局配置 publicAsk.key;为空 → 生成 32 字节随机 hex(64 字符)→ 写回持久化。
|
|
7497
|
+
* @returns 当前固定 key(已生成则返回既有值——**幂等**,不会覆盖用户手改的 key)
|
|
7498
|
+
*/
|
|
7499
|
+
function ensurePublicAskKey() {
|
|
7500
|
+
const current = readAdvancedSettings();
|
|
7501
|
+
if (current.publicAsk.key !== "") return current.publicAsk.key;
|
|
7502
|
+
const key = randomBytes(32).toString("hex");
|
|
7503
|
+
updateAdvancedSettings({ publicAsk: { key } });
|
|
7504
|
+
return key;
|
|
7505
|
+
}
|
|
7506
|
+
/** 校验问答页访问 key(timing-safe;key 未生成 → 恒 false——没有 key 不工作) */
|
|
7507
|
+
function verifyPublicAskKey(provided) {
|
|
7508
|
+
const expected = readAdvancedSettings().publicAsk.key;
|
|
7509
|
+
if (!expected || typeof provided !== "string" || provided === "") return false;
|
|
7510
|
+
const a = Buffer.from(provided);
|
|
7511
|
+
const b = Buffer.from(expected);
|
|
7512
|
+
return a.length === b.length && timingSafeEqual(a, b);
|
|
7513
|
+
}
|
|
7514
|
+
/**
|
|
7458
7515
|
* 一次性迁移(v1.21.x → v1.22):旧版把 `serenityAdvanced` 存在 CCC localstore.json;
|
|
7459
7516
|
* 新版归 plugin 全局文件。全局文件已存在 → 跳过(幂等);localstore 无旧节 → 跳过。
|
|
7460
7517
|
* @param root - CCC 根(localstore.json 所在目录)
|
|
@@ -8298,7 +8355,7 @@ function saveFileToTmp(root, fileName, data) {
|
|
|
8298
8355
|
writeFileSync(join(dir, filename), bytes);
|
|
8299
8356
|
return `${FILE_UPLOAD_DIR}/${filename}`;
|
|
8300
8357
|
}
|
|
8301
|
-
function readBody$
|
|
8358
|
+
function readBody$3(req, maxBytes) {
|
|
8302
8359
|
return new Promise((resolve, reject) => {
|
|
8303
8360
|
let data = "";
|
|
8304
8361
|
req.on("data", (chunk) => {
|
|
@@ -8312,7 +8369,7 @@ function readBody$2(req, maxBytes) {
|
|
|
8312
8369
|
req.on("error", reject);
|
|
8313
8370
|
});
|
|
8314
8371
|
}
|
|
8315
|
-
function sendJson$
|
|
8372
|
+
function sendJson$2(res, code, body) {
|
|
8316
8373
|
const payload = JSON.stringify(body);
|
|
8317
8374
|
res.writeHead(code, {
|
|
8318
8375
|
"content-type": "application/json; charset=utf-8",
|
|
@@ -8350,7 +8407,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
8350
8407
|
handler: async (req, res) => {
|
|
8351
8408
|
try {
|
|
8352
8409
|
if (req.method !== "GET") {
|
|
8353
|
-
sendJson$
|
|
8410
|
+
sendJson$2(res, 405, { error: "method not allowed" });
|
|
8354
8411
|
return;
|
|
8355
8412
|
}
|
|
8356
8413
|
const url = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
@@ -8359,12 +8416,12 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
8359
8416
|
workspace: url.searchParams.get("workspace") ?? void 0
|
|
8360
8417
|
}));
|
|
8361
8418
|
if (!root) {
|
|
8362
|
-
sendJson$
|
|
8419
|
+
sendJson$2(res, 200, { handymen: [] });
|
|
8363
8420
|
return;
|
|
8364
8421
|
}
|
|
8365
|
-
sendJson$
|
|
8422
|
+
sendJson$2(res, 200, { handymen: listActiveHandymen(root) });
|
|
8366
8423
|
} catch (err) {
|
|
8367
|
-
sendJson$
|
|
8424
|
+
sendJson$2(res, 400, { error: err.message ?? String(err) });
|
|
8368
8425
|
}
|
|
8369
8426
|
}
|
|
8370
8427
|
});
|
|
@@ -8374,14 +8431,14 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
8374
8431
|
handler: async (req, res) => {
|
|
8375
8432
|
try {
|
|
8376
8433
|
if (req.method !== "POST") {
|
|
8377
|
-
sendJson$
|
|
8434
|
+
sendJson$2(res, 405, { error: "method not allowed" });
|
|
8378
8435
|
return;
|
|
8379
8436
|
}
|
|
8380
8437
|
if (req.headers["x-serenity-ui"] !== "1") {
|
|
8381
|
-
sendJson$
|
|
8438
|
+
sendJson$2(res, 403, { error: "图片落盘仅限 WebUI(client 专用)" });
|
|
8382
8439
|
return;
|
|
8383
8440
|
}
|
|
8384
|
-
const raw = await readBody$
|
|
8441
|
+
const raw = await readBody$3(req, 20971520);
|
|
8385
8442
|
const body = JSON.parse(raw);
|
|
8386
8443
|
const workspace = resolveWorkspace(ctx, {
|
|
8387
8444
|
sessionId: body.sessionId,
|
|
@@ -8389,12 +8446,12 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
8389
8446
|
});
|
|
8390
8447
|
const root = findSerenityRoot(workspace);
|
|
8391
8448
|
if (!root) {
|
|
8392
|
-
sendJson$
|
|
8449
|
+
sendJson$2(res, 404, { error: `no CCC found from workspace: ${workspace}` });
|
|
8393
8450
|
return;
|
|
8394
8451
|
}
|
|
8395
|
-
sendJson$
|
|
8452
|
+
sendJson$2(res, 200, { path: saveImageToTmp(root, body.mediaType ?? "", body.data ?? "") });
|
|
8396
8453
|
} catch (err) {
|
|
8397
|
-
sendJson$
|
|
8454
|
+
sendJson$2(res, 400, { error: err.message ?? String(err) });
|
|
8398
8455
|
}
|
|
8399
8456
|
}
|
|
8400
8457
|
});
|
|
@@ -8404,14 +8461,14 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
8404
8461
|
handler: async (req, res) => {
|
|
8405
8462
|
try {
|
|
8406
8463
|
if (req.method !== "POST") {
|
|
8407
|
-
sendJson$
|
|
8464
|
+
sendJson$2(res, 405, { error: "method not allowed" });
|
|
8408
8465
|
return;
|
|
8409
8466
|
}
|
|
8410
8467
|
if (req.headers["x-serenity-ui"] !== "1") {
|
|
8411
|
-
sendJson$
|
|
8468
|
+
sendJson$2(res, 403, { error: "文件落盘仅限 WebUI(client 专用)" });
|
|
8412
8469
|
return;
|
|
8413
8470
|
}
|
|
8414
|
-
const raw = await readBody$
|
|
8471
|
+
const raw = await readBody$3(req, 20971520);
|
|
8415
8472
|
const body = JSON.parse(raw);
|
|
8416
8473
|
const workspace = resolveWorkspace(ctx, {
|
|
8417
8474
|
sessionId: body.sessionId,
|
|
@@ -8419,12 +8476,12 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
8419
8476
|
});
|
|
8420
8477
|
const root = findSerenityRoot(workspace);
|
|
8421
8478
|
if (!root) {
|
|
8422
|
-
sendJson$
|
|
8479
|
+
sendJson$2(res, 404, { error: `no CCC found from workspace: ${workspace}` });
|
|
8423
8480
|
return;
|
|
8424
8481
|
}
|
|
8425
|
-
sendJson$
|
|
8482
|
+
sendJson$2(res, 200, { path: saveFileToTmp(root, body.name ?? "", body.data ?? "") });
|
|
8426
8483
|
} catch (err) {
|
|
8427
|
-
sendJson$
|
|
8484
|
+
sendJson$2(res, 400, { error: err.message ?? String(err) });
|
|
8428
8485
|
}
|
|
8429
8486
|
}
|
|
8430
8487
|
});
|
|
@@ -8440,7 +8497,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
8440
8497
|
workspace: url.searchParams.get("workspace") ?? void 0
|
|
8441
8498
|
}), configPaths);
|
|
8442
8499
|
const runtime = ctx.get("codeRuntime");
|
|
8443
|
-
sendJson$
|
|
8500
|
+
sendJson$2(res, 200, {
|
|
8444
8501
|
...status,
|
|
8445
8502
|
...runtime === void 0 ? { codeRuntime: null } : { codeRuntime: {
|
|
8446
8503
|
language: runtime.language,
|
|
@@ -8451,10 +8508,10 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
8451
8508
|
}
|
|
8452
8509
|
if (req.method === "POST") {
|
|
8453
8510
|
if (req.headers["x-serenity-ui"] !== "1") {
|
|
8454
|
-
sendJson$
|
|
8511
|
+
sendJson$2(res, 403, { error: "safe-mode 切换仅限 WebUI(agent 不可自行开关)" });
|
|
8455
8512
|
return;
|
|
8456
8513
|
}
|
|
8457
|
-
const raw = await readBody$
|
|
8514
|
+
const raw = await readBody$3(req, 65536);
|
|
8458
8515
|
const body = JSON.parse(raw);
|
|
8459
8516
|
const workspace = resolveWorkspace(ctx, {
|
|
8460
8517
|
sessionId: body.sessionId,
|
|
@@ -8462,19 +8519,19 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
8462
8519
|
});
|
|
8463
8520
|
const root = findSerenityRoot(workspace);
|
|
8464
8521
|
if (!root) {
|
|
8465
|
-
sendJson$
|
|
8522
|
+
sendJson$2(res, 404, { error: `no CCC found from workspace: ${workspace}` });
|
|
8466
8523
|
return;
|
|
8467
8524
|
}
|
|
8468
8525
|
const result = setSafeMode(root, body.on === true);
|
|
8469
|
-
sendJson$
|
|
8526
|
+
sendJson$2(res, 200, {
|
|
8470
8527
|
...getStatus(workspace, configPaths),
|
|
8471
8528
|
...result
|
|
8472
8529
|
});
|
|
8473
8530
|
return;
|
|
8474
8531
|
}
|
|
8475
|
-
sendJson$
|
|
8532
|
+
sendJson$2(res, 405, { error: "method not allowed" });
|
|
8476
8533
|
} catch (err) {
|
|
8477
|
-
sendJson$
|
|
8534
|
+
sendJson$2(res, 400, { error: err.message ?? String(err) });
|
|
8478
8535
|
}
|
|
8479
8536
|
}
|
|
8480
8537
|
});
|
|
@@ -8484,25 +8541,25 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
8484
8541
|
handler: async (req, res) => {
|
|
8485
8542
|
try {
|
|
8486
8543
|
if (req.headers["x-serenity-ui"] !== "1") {
|
|
8487
|
-
sendJson$
|
|
8544
|
+
sendJson$2(res, 403, { error: "高级设定仅限 WebUI(client 专用)" });
|
|
8488
8545
|
return;
|
|
8489
8546
|
}
|
|
8490
8547
|
if (req.method === "GET") {
|
|
8491
|
-
sendJson$
|
|
8548
|
+
sendJson$2(res, 200, { config: toWire(readAdvancedSettings()) });
|
|
8492
8549
|
return;
|
|
8493
8550
|
}
|
|
8494
8551
|
if (req.method === "PUT") {
|
|
8495
|
-
const raw = await readBody$
|
|
8552
|
+
const raw = await readBody$3(req, 131072);
|
|
8496
8553
|
const saved = applyWirePatch(JSON.parse(raw).config ?? {});
|
|
8497
8554
|
try {
|
|
8498
8555
|
ctx.emit?.("serenity/config-updated");
|
|
8499
8556
|
} catch {}
|
|
8500
|
-
sendJson$
|
|
8557
|
+
sendJson$2(res, 200, { config: toWire(saved) });
|
|
8501
8558
|
return;
|
|
8502
8559
|
}
|
|
8503
|
-
sendJson$
|
|
8560
|
+
sendJson$2(res, 405, { error: "method not allowed" });
|
|
8504
8561
|
} catch (err) {
|
|
8505
|
-
sendJson$
|
|
8562
|
+
sendJson$2(res, 400, { error: err.message ?? String(err) });
|
|
8506
8563
|
}
|
|
8507
8564
|
}
|
|
8508
8565
|
});
|
|
@@ -8958,7 +9015,7 @@ function buildProxyHeaders(reqHeaders, mainPort, bodyOverride) {
|
|
|
8958
9015
|
//#endregion
|
|
8959
9016
|
//#region src/gateway.ts
|
|
8960
9017
|
/** 读取请求体(≤ maxBytes;超限 reject) */
|
|
8961
|
-
function readBody$
|
|
9018
|
+
function readBody$2(req, maxBytes) {
|
|
8962
9019
|
return new Promise((resolve, reject) => {
|
|
8963
9020
|
let data = "";
|
|
8964
9021
|
req.on("data", (chunk) => {
|
|
@@ -9091,7 +9148,7 @@ function startGateway(config, getAccounts) {
|
|
|
9091
9148
|
if (session !== void 0) {
|
|
9092
9149
|
if (req.method === "POST" && url.pathname === "/api/workspace.create") {
|
|
9093
9150
|
if (!allowWorkspaceCreate) {
|
|
9094
|
-
readBody$
|
|
9151
|
+
readBody$2(req, 131072).then((body) => {
|
|
9095
9152
|
let rpcId = "unknown";
|
|
9096
9153
|
try {
|
|
9097
9154
|
const parsed = JSON.parse(body);
|
|
@@ -9127,7 +9184,7 @@ function startGateway(config, getAccounts) {
|
|
|
9127
9184
|
});
|
|
9128
9185
|
return;
|
|
9129
9186
|
}
|
|
9130
|
-
readBody$
|
|
9187
|
+
readBody$2(req, 131072).then((body) => {
|
|
9131
9188
|
let rpcId = "unknown";
|
|
9132
9189
|
let path;
|
|
9133
9190
|
try {
|
|
@@ -11079,8 +11136,8 @@ x.lex;
|
|
|
11079
11136
|
* 实验性质:未开启时零资源占用(无监听、无 agent 创建)。
|
|
11080
11137
|
*/
|
|
11081
11138
|
/** 运行中的调试服务(单实例;进程级) */
|
|
11082
|
-
let active = null;
|
|
11083
|
-
function readBody(req) {
|
|
11139
|
+
let active$1 = null;
|
|
11140
|
+
function readBody$1(req) {
|
|
11084
11141
|
return new Promise((resolve, reject) => {
|
|
11085
11142
|
const chunks = [];
|
|
11086
11143
|
req.on("data", (c) => chunks.push(c));
|
|
@@ -11088,7 +11145,7 @@ function readBody(req) {
|
|
|
11088
11145
|
req.on("error", reject);
|
|
11089
11146
|
});
|
|
11090
11147
|
}
|
|
11091
|
-
function sendJson(res, status, payload) {
|
|
11148
|
+
function sendJson$1(res, status, payload) {
|
|
11092
11149
|
const body = JSON.stringify(payload);
|
|
11093
11150
|
res.writeHead(status, {
|
|
11094
11151
|
"Content-Type": "application/json; charset=utf-8",
|
|
@@ -11096,7 +11153,7 @@ function sendJson(res, status, payload) {
|
|
|
11096
11153
|
});
|
|
11097
11154
|
res.end(body);
|
|
11098
11155
|
}
|
|
11099
|
-
function sendHtml(res, html) {
|
|
11156
|
+
function sendHtml$1(res, html) {
|
|
11100
11157
|
res.writeHead(200, {
|
|
11101
11158
|
"Content-Type": "text/html; charset=utf-8",
|
|
11102
11159
|
"Cache-Control": "no-store"
|
|
@@ -11356,42 +11413,42 @@ function renderSkiffMarkdown(raw) {
|
|
|
11356
11413
|
* @param webPort 主 WebUI 端口(WebUI 链接)
|
|
11357
11414
|
*/
|
|
11358
11415
|
async function startSkiffDebugServer(ctx, root, port, webPort) {
|
|
11359
|
-
if (active) return;
|
|
11416
|
+
if (active$1) return;
|
|
11360
11417
|
const server = createServer((req, res) => {
|
|
11361
|
-
handle(ctx, root, webPort, req, res);
|
|
11418
|
+
handle$1(ctx, root, webPort, req, res);
|
|
11362
11419
|
});
|
|
11363
11420
|
await new Promise((resolve, reject) => {
|
|
11364
11421
|
server.once("error", reject);
|
|
11365
11422
|
server.listen(port, "127.0.0.1", () => resolve());
|
|
11366
11423
|
});
|
|
11367
|
-
active = {
|
|
11424
|
+
active$1 = {
|
|
11368
11425
|
server,
|
|
11369
11426
|
port
|
|
11370
11427
|
};
|
|
11371
11428
|
console.log(`[serenity-hooks] ✓ Skiff 调试问答页: http://127.0.0.1:${port}(默认 CCC: ${root},WebUI: ${webPort})`);
|
|
11372
11429
|
}
|
|
11373
11430
|
function stopSkiffDebugServer() {
|
|
11374
|
-
if (!active) return;
|
|
11431
|
+
if (!active$1) return;
|
|
11375
11432
|
try {
|
|
11376
|
-
active.server.close();
|
|
11433
|
+
active$1.server.close();
|
|
11377
11434
|
} catch {}
|
|
11378
|
-
active = null;
|
|
11435
|
+
active$1 = null;
|
|
11379
11436
|
}
|
|
11380
|
-
async function handle(ctx, defaultRoot, webPort, req, res) {
|
|
11437
|
+
async function handle$1(ctx, defaultRoot, webPort, req, res) {
|
|
11381
11438
|
try {
|
|
11382
11439
|
const url = (req.url ?? "/").split("?")[0] ?? "/";
|
|
11383
11440
|
if (req.method === "GET" && url === "/") {
|
|
11384
|
-
sendHtml(res, skiffDebugPage(await discoverCccs(ctx, defaultRoot), defaultRoot, webPort));
|
|
11441
|
+
sendHtml$1(res, skiffDebugPage(await discoverCccs(ctx, defaultRoot), defaultRoot, webPort));
|
|
11385
11442
|
return;
|
|
11386
11443
|
}
|
|
11387
11444
|
if (req.method === "POST" && url === "/ask") {
|
|
11388
11445
|
let body;
|
|
11389
11446
|
try {
|
|
11390
|
-
const parsed = JSON.parse(await readBody(req));
|
|
11447
|
+
const parsed = JSON.parse(await readBody$1(req));
|
|
11391
11448
|
if (parsed === null || typeof parsed !== "object") throw new Error("not an object");
|
|
11392
11449
|
body = parsed;
|
|
11393
11450
|
} catch {
|
|
11394
|
-
sendJson(res, 400, { error: "invalid JSON body" });
|
|
11451
|
+
sendJson$1(res, 400, { error: "invalid JSON body" });
|
|
11395
11452
|
return;
|
|
11396
11453
|
}
|
|
11397
11454
|
const ccc = typeof body.ccc === "string" && body.ccc !== "" ? body.ccc : defaultRoot;
|
|
@@ -11400,11 +11457,11 @@ async function handle(ctx, defaultRoot, webPort, req, res) {
|
|
|
11400
11457
|
const sessionId = typeof body.sessionId === "string" && body.sessionId !== "" ? body.sessionId : void 0;
|
|
11401
11458
|
const role = readSkiffRoles(ccc).get(roleName);
|
|
11402
11459
|
if (!roleName || !role) {
|
|
11403
|
-
sendJson(res, 400, { error: `unknown role: ${roleName} (ccc: ${ccc})` });
|
|
11460
|
+
sendJson$1(res, 400, { error: `unknown role: ${roleName} (ccc: ${ccc})` });
|
|
11404
11461
|
return;
|
|
11405
11462
|
}
|
|
11406
11463
|
if (!question.trim()) {
|
|
11407
|
-
sendJson(res, 400, { error: "empty question" });
|
|
11464
|
+
sendJson$1(res, 400, { error: "empty question" });
|
|
11408
11465
|
return;
|
|
11409
11466
|
}
|
|
11410
11467
|
let agent;
|
|
@@ -11413,11 +11470,11 @@ async function handle(ctx, defaultRoot, webPort, req, res) {
|
|
|
11413
11470
|
const info = skiffSessionInfo(sessionId);
|
|
11414
11471
|
const live = getSkiffAgent(sessionId);
|
|
11415
11472
|
if (!info || !live) {
|
|
11416
|
-
sendJson(res, 400, { error: "session is not recoverable (process restarted or session unknown) — start a new conversation" });
|
|
11473
|
+
sendJson$1(res, 400, { error: "session is not recoverable (process restarted or session unknown) — start a new conversation" });
|
|
11417
11474
|
return;
|
|
11418
11475
|
}
|
|
11419
11476
|
if (info.role !== roleName || info.ccc !== ccc) {
|
|
11420
|
-
sendJson(res, 400, { error: `session "${sessionId}" belongs to role "${info.role}" in another CCC — start a new conversation` });
|
|
11477
|
+
sendJson$1(res, 400, { error: `session "${sessionId}" belongs to role "${info.role}" in another CCC — start a new conversation` });
|
|
11421
11478
|
return;
|
|
11422
11479
|
}
|
|
11423
11480
|
agent = live;
|
|
@@ -11426,6 +11483,361 @@ async function handle(ctx, defaultRoot, webPort, req, res) {
|
|
|
11426
11483
|
const hc = readHandymanConfig(ccc);
|
|
11427
11484
|
if (!agent) agent = (await createSkiffAgent(ctx, ccc, roleName, role, hc?.defaultModel)).agent;
|
|
11428
11485
|
const result = await askSkiff(ctx, agent, question, 0);
|
|
11486
|
+
sendJson$1(res, 200, {
|
|
11487
|
+
answer: result.answer,
|
|
11488
|
+
answer_html: renderSkiffMarkdown(result.answer),
|
|
11489
|
+
sessionId: result.sessionId,
|
|
11490
|
+
continued,
|
|
11491
|
+
trajectory: result.trajectory
|
|
11492
|
+
});
|
|
11493
|
+
return;
|
|
11494
|
+
}
|
|
11495
|
+
sendJson$1(res, 404, { error: "not found" });
|
|
11496
|
+
} catch (err) {
|
|
11497
|
+
sendJson$1(res, 500, { error: err?.message ?? String(err) });
|
|
11498
|
+
}
|
|
11499
|
+
}
|
|
11500
|
+
//#endregion
|
|
11501
|
+
//#region src/acp-core.ts
|
|
11502
|
+
/** JSON-RPC 错误码(对齐标准) */
|
|
11503
|
+
const RPC_ERROR = {
|
|
11504
|
+
PARSE: -32700,
|
|
11505
|
+
INVALID_REQUEST: -32600,
|
|
11506
|
+
METHOD_NOT_FOUND: -32601,
|
|
11507
|
+
INVALID_PARAMS: -32602,
|
|
11508
|
+
INTERNAL: -32603
|
|
11509
|
+
};
|
|
11510
|
+
/**
|
|
11511
|
+
* ACP 会话管理器:持有 skiff agent 会话(与 skiff-core 注册表同源——
|
|
11512
|
+
* 创建走 createSkiffAgent(自动注册),关闭走 unregisterSkiffSession)。
|
|
11513
|
+
* 传输无关:HTTP 端点/企业微信桥/未来 stdio 都调 handle(method, params)。
|
|
11514
|
+
*/
|
|
11515
|
+
var AcpServer = class {
|
|
11516
|
+
ctx;
|
|
11517
|
+
constructor(ctx) {
|
|
11518
|
+
this.ctx = ctx;
|
|
11519
|
+
}
|
|
11520
|
+
/** 实现级方法调度(返回 result 或抛 JSON-RPC 语义错误) */
|
|
11521
|
+
async handle(method, params) {
|
|
11522
|
+
switch (method) {
|
|
11523
|
+
case "initialize": return this.initialize();
|
|
11524
|
+
case "authenticate": return {};
|
|
11525
|
+
case "session/new": return this.newSession(params);
|
|
11526
|
+
case "session/list": return this.listSessions();
|
|
11527
|
+
case "session/prompt": return this.prompt(params);
|
|
11528
|
+
case "session/cancel": return this.cancel(params);
|
|
11529
|
+
case "session/close": return this.closeSession(params);
|
|
11530
|
+
case "session/request_permission": return { allow: true };
|
|
11531
|
+
default: throw new RpcMethodError(`method not found: ${method}`);
|
|
11532
|
+
}
|
|
11533
|
+
}
|
|
11534
|
+
initialize() {
|
|
11535
|
+
return {
|
|
11536
|
+
protocolVersion: 1,
|
|
11537
|
+
capabilities: {
|
|
11538
|
+
sessionId: true,
|
|
11539
|
+
ccc: true,
|
|
11540
|
+
role: true
|
|
11541
|
+
},
|
|
11542
|
+
serverInfo: {
|
|
11543
|
+
name: "dsh-serenity-hooks-acp",
|
|
11544
|
+
version: "1.26.0"
|
|
11545
|
+
}
|
|
11546
|
+
};
|
|
11547
|
+
}
|
|
11548
|
+
/**
|
|
11549
|
+
* session/new:{ ccc, role, sessionId? } → 创建或延续 skiff 会话。
|
|
11550
|
+
* 无 sessionId → createSkiffAgent(新会话,continued:false);
|
|
11551
|
+
* 有 → 进程内延续(getSkiffAgent 命中 + role/ccc 绑定校验;未命中 → 错误)。
|
|
11552
|
+
*/
|
|
11553
|
+
async newSession(params) {
|
|
11554
|
+
const ccc = typeof params?.ccc === "string" && params.ccc !== "" ? params.ccc : void 0;
|
|
11555
|
+
const roleName = typeof params?.role === "string" ? params.role : void 0;
|
|
11556
|
+
const sessionId = typeof params?.sessionId === "string" && params.sessionId !== "" ? params.sessionId : void 0;
|
|
11557
|
+
if (!ccc) throw new RpcInvalidParams("session/new requires ccc (CCC root path)");
|
|
11558
|
+
if (!roleName) throw new RpcInvalidParams("session/new requires role");
|
|
11559
|
+
const role = readSkiffRoles(ccc).get(roleName);
|
|
11560
|
+
if (!role) throw new RpcInvalidParams(`unknown role: ${roleName} (ccc: ${ccc})`);
|
|
11561
|
+
if (sessionId) {
|
|
11562
|
+
const info = skiffSessionInfo(sessionId);
|
|
11563
|
+
const live = getSkiffAgent(sessionId);
|
|
11564
|
+
if (!info || !live) throw new RpcInvalidParams("session is not recoverable (process restarted or session unknown) — start a new conversation");
|
|
11565
|
+
if (info.role !== roleName || info.ccc !== ccc) throw new RpcInvalidParams(`session "${sessionId}" belongs to role "${info.role}" in another CCC`);
|
|
11566
|
+
return {
|
|
11567
|
+
sessionId,
|
|
11568
|
+
role: roleName,
|
|
11569
|
+
ccc,
|
|
11570
|
+
continued: true
|
|
11571
|
+
};
|
|
11572
|
+
}
|
|
11573
|
+
const hc = readHandymanConfig(ccc);
|
|
11574
|
+
return {
|
|
11575
|
+
sessionId: (await createSkiffAgent(this.ctx, ccc, roleName, role, hc?.defaultModel)).sessionId,
|
|
11576
|
+
role: roleName,
|
|
11577
|
+
ccc,
|
|
11578
|
+
continued: false
|
|
11579
|
+
};
|
|
11580
|
+
}
|
|
11581
|
+
listSessions() {
|
|
11582
|
+
const sessions = [];
|
|
11583
|
+
for (const [id, b] of skiffSessionSnapshot()) sessions.push({
|
|
11584
|
+
sessionId: id,
|
|
11585
|
+
role: b.role,
|
|
11586
|
+
ccc: b.ccc
|
|
11587
|
+
});
|
|
11588
|
+
return { sessions };
|
|
11589
|
+
}
|
|
11590
|
+
/** session/prompt:{ sessionId, question } → 答案 + 全量轨迹 */
|
|
11591
|
+
async prompt(params) {
|
|
11592
|
+
const sessionId = typeof params?.sessionId === "string" ? params.sessionId : void 0;
|
|
11593
|
+
const question = typeof params?.question === "string" ? params.question : void 0;
|
|
11594
|
+
if (!sessionId) throw new RpcInvalidParams("session/prompt requires sessionId");
|
|
11595
|
+
if (!question?.trim()) throw new RpcInvalidParams("session/prompt requires question");
|
|
11596
|
+
const agent = getSkiffAgent(sessionId);
|
|
11597
|
+
if (!agent) throw new RpcInvalidParams(`unknown session: ${sessionId} (not recoverable — process restarted?)`);
|
|
11598
|
+
const result = await askSkiff(this.ctx, agent, question, 0);
|
|
11599
|
+
return {
|
|
11600
|
+
answer: result.answer,
|
|
11601
|
+
sessionId: result.sessionId,
|
|
11602
|
+
trajectory: result.trajectory
|
|
11603
|
+
};
|
|
11604
|
+
}
|
|
11605
|
+
/** session/cancel:{ sessionId } → 中断当前 prompt(DSH interrupt) */
|
|
11606
|
+
cancel(params) {
|
|
11607
|
+
const sessionId = typeof params?.sessionId === "string" ? params.sessionId : void 0;
|
|
11608
|
+
if (!sessionId) return { cancelled: false };
|
|
11609
|
+
const agent = getSkiffAgent(sessionId);
|
|
11610
|
+
if (!agent) return { cancelled: false };
|
|
11611
|
+
try {
|
|
11612
|
+
agent.interrupt?.();
|
|
11613
|
+
} catch {}
|
|
11614
|
+
return { cancelled: true };
|
|
11615
|
+
}
|
|
11616
|
+
/** session/close:{ sessionId } → 释放 agent + 注册表清理(进程内) */
|
|
11617
|
+
closeSession(params) {
|
|
11618
|
+
const sessionId = typeof params?.sessionId === "string" ? params.sessionId : void 0;
|
|
11619
|
+
if (!sessionId) return { closed: false };
|
|
11620
|
+
if (!getSkiffAgent(sessionId)) return { closed: false };
|
|
11621
|
+
unregisterSkiffSession(sessionId);
|
|
11622
|
+
return { closed: true };
|
|
11623
|
+
}
|
|
11624
|
+
};
|
|
11625
|
+
/** 方法不存在错误(JSON-RPC -32601) */
|
|
11626
|
+
var RpcMethodError = class extends Error {
|
|
11627
|
+
constructor(message) {
|
|
11628
|
+
super(message);
|
|
11629
|
+
this.name = "RpcMethodError";
|
|
11630
|
+
}
|
|
11631
|
+
};
|
|
11632
|
+
/** 参数错误(JSON-RPC -32602) */
|
|
11633
|
+
var RpcInvalidParams = class extends Error {
|
|
11634
|
+
constructor(message) {
|
|
11635
|
+
super(message);
|
|
11636
|
+
this.name = "RpcInvalidParams";
|
|
11637
|
+
}
|
|
11638
|
+
};
|
|
11639
|
+
/**
|
|
11640
|
+
* JSON-RPC 2.0 批处理(单帧):解析请求/通知 → 调 AcpServer.handle → 响应。
|
|
11641
|
+
* 传输无关:acp-http 端点 / 未来 stdio 共用。
|
|
11642
|
+
* @returns 响应帧数组(请求有响应;通知无;解析失败 → 单错误响应)
|
|
11643
|
+
*/
|
|
11644
|
+
async function dispatchRpc(server, frame) {
|
|
11645
|
+
if (frame === null || typeof frame !== "object") return [rpcError(null, RPC_ERROR.INVALID_REQUEST, "invalid request")];
|
|
11646
|
+
const req = frame;
|
|
11647
|
+
if (req.jsonrpc !== "2.0" || typeof req.method !== "string") return [rpcError(req.id ?? null, RPC_ERROR.INVALID_REQUEST, "invalid request")];
|
|
11648
|
+
if (req.id === void 0) {
|
|
11649
|
+
try {
|
|
11650
|
+
await server.handle(req.method, req.params);
|
|
11651
|
+
} catch {}
|
|
11652
|
+
return [];
|
|
11653
|
+
}
|
|
11654
|
+
try {
|
|
11655
|
+
const result = await server.handle(req.method, req.params);
|
|
11656
|
+
return [{
|
|
11657
|
+
jsonrpc: "2.0",
|
|
11658
|
+
id: req.id,
|
|
11659
|
+
result
|
|
11660
|
+
}];
|
|
11661
|
+
} catch (error) {
|
|
11662
|
+
return [rpcError(req.id, rpcCodeOf(error), error instanceof Error ? error.message : String(error))];
|
|
11663
|
+
}
|
|
11664
|
+
}
|
|
11665
|
+
function rpcCodeOf(error) {
|
|
11666
|
+
if (error instanceof RpcMethodError) return RPC_ERROR.METHOD_NOT_FOUND;
|
|
11667
|
+
if (error instanceof RpcInvalidParams) return RPC_ERROR.INVALID_PARAMS;
|
|
11668
|
+
return RPC_ERROR.INTERNAL;
|
|
11669
|
+
}
|
|
11670
|
+
function rpcError(id, code, message) {
|
|
11671
|
+
return {
|
|
11672
|
+
jsonrpc: "2.0",
|
|
11673
|
+
id,
|
|
11674
|
+
error: {
|
|
11675
|
+
code,
|
|
11676
|
+
message
|
|
11677
|
+
}
|
|
11678
|
+
};
|
|
11679
|
+
}
|
|
11680
|
+
//#endregion
|
|
11681
|
+
//#region src/acp-http.ts
|
|
11682
|
+
/**
|
|
11683
|
+
* acp-http.ts — ACP HTTP JSON-RPC + 建议问答页(F4c/F4d,v1.26.x 实验性)
|
|
11684
|
+
*
|
|
11685
|
+
* node:http 服务(默认关,仅监听 127.0.0.1;启停 = 人工——设置面板「Serenity」
|
|
11686
|
+
* 页 ACP 区块开关 / 问答页开关,不随插件加载自动启动;仿 skiff 调试服务模式)。
|
|
11687
|
+
*
|
|
11688
|
+
* - POST / → JSON-RPC 2.0 单帧/批处理(acpEnabled 门控;ACP 程序化面)
|
|
11689
|
+
* - GET / → 建议问答页 HTML(publicAskEnabled 门控;F4d,S142 用户:
|
|
11690
|
+
* 按认知容器暴露问答页供他人验证——key 认证,无 key 不工作)
|
|
11691
|
+
* - POST /ask → { key, ccc, role?, question, sessionId? } → key 校验(timing-safe)
|
|
11692
|
+
* → acp-core 会话 → { answer, answer_html, sessionId, trajectory }
|
|
11693
|
+
*
|
|
11694
|
+
* 与 skiff 调试页(人工测试面)共享同一会话核心(acp-core → skiff-core);
|
|
11695
|
+
* 企业微信桥(F4c-2)同进程直调 acp-core 处理器函数,不经本端点。
|
|
11696
|
+
* stdio 形态(独立进程部署)后续复用同一 acp-core(官方 NDJSON 帧格式)。
|
|
11697
|
+
*
|
|
11698
|
+
* 实验性质:未开启时零资源占用(无监听、无 agent 创建)。
|
|
11699
|
+
*/
|
|
11700
|
+
/** 运行中的 ACP HTTP 服务(单实例;进程级) */
|
|
11701
|
+
let active = null;
|
|
11702
|
+
function readBody(req) {
|
|
11703
|
+
return new Promise((resolve, reject) => {
|
|
11704
|
+
const chunks = [];
|
|
11705
|
+
req.on("data", (c) => chunks.push(c));
|
|
11706
|
+
req.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
|
|
11707
|
+
req.on("error", reject);
|
|
11708
|
+
});
|
|
11709
|
+
}
|
|
11710
|
+
function sendJson(res, status, payload) {
|
|
11711
|
+
const body = JSON.stringify(payload);
|
|
11712
|
+
res.writeHead(status, {
|
|
11713
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
11714
|
+
"Cache-Control": "no-store"
|
|
11715
|
+
});
|
|
11716
|
+
res.end(body);
|
|
11717
|
+
}
|
|
11718
|
+
function sendHtml(res, html) {
|
|
11719
|
+
res.writeHead(200, {
|
|
11720
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
11721
|
+
"Cache-Control": "no-store"
|
|
11722
|
+
});
|
|
11723
|
+
res.end(html);
|
|
11724
|
+
}
|
|
11725
|
+
/**
|
|
11726
|
+
* 启动 ACP HTTP JSON-RPC + 建议问答页服务(单实例;重复启动幂等返回既有实例)。
|
|
11727
|
+
* @param ctx 插件上下文(AcpServer 内部经 skiff-core 创建 agent)
|
|
11728
|
+
* @param port 监听端口(仅 127.0.0.1)
|
|
11729
|
+
* @param defaultRoot 默认 CCC 根(问答页候选发现兜底;resolveSkiffRoot 传入)
|
|
11730
|
+
*/
|
|
11731
|
+
async function startAcpHttpServer(ctx, port, defaultRoot) {
|
|
11732
|
+
if (active) return;
|
|
11733
|
+
const server = createServer((req, res) => {
|
|
11734
|
+
handle(ctx, defaultRoot ?? "", req, res);
|
|
11735
|
+
});
|
|
11736
|
+
await new Promise((resolve, reject) => {
|
|
11737
|
+
server.once("error", reject);
|
|
11738
|
+
server.listen(port, "127.0.0.1", () => resolve());
|
|
11739
|
+
});
|
|
11740
|
+
active = {
|
|
11741
|
+
server,
|
|
11742
|
+
port
|
|
11743
|
+
};
|
|
11744
|
+
const s = readSimpleSettings();
|
|
11745
|
+
const faces = [];
|
|
11746
|
+
if (s.acpEnabled) faces.push("JSON-RPC");
|
|
11747
|
+
if (s.publicAskEnabled) faces.push("问答页(key 认证)");
|
|
11748
|
+
console.log(`[serenity-hooks] ✓ ACP HTTP: http://127.0.0.1:${port}(${faces.join(" + ") || "未开启任何面"};session/new 支持 {ccc, role, sessionId?})`);
|
|
11749
|
+
}
|
|
11750
|
+
function stopAcpHttpServer() {
|
|
11751
|
+
if (!active) return;
|
|
11752
|
+
try {
|
|
11753
|
+
active.server.close();
|
|
11754
|
+
} catch {}
|
|
11755
|
+
active = null;
|
|
11756
|
+
}
|
|
11757
|
+
async function handle(ctx, defaultRoot, req, res) {
|
|
11758
|
+
try {
|
|
11759
|
+
const url = (req.url ?? "/").split("?")[0] ?? "/";
|
|
11760
|
+
const s = readSimpleSettings();
|
|
11761
|
+
if (req.method === "POST" && url === "/") {
|
|
11762
|
+
if (!s.acpEnabled) {
|
|
11763
|
+
sendJson(res, 403, { error: "ACP JSON-RPC disabled (enable in Serenity settings)" });
|
|
11764
|
+
return;
|
|
11765
|
+
}
|
|
11766
|
+
let body;
|
|
11767
|
+
try {
|
|
11768
|
+
body = JSON.parse(await readBody(req));
|
|
11769
|
+
} catch {
|
|
11770
|
+
sendJson(res, 400, [{
|
|
11771
|
+
jsonrpc: "2.0",
|
|
11772
|
+
id: null,
|
|
11773
|
+
error: {
|
|
11774
|
+
code: -32700,
|
|
11775
|
+
message: "parse error"
|
|
11776
|
+
}
|
|
11777
|
+
}]);
|
|
11778
|
+
return;
|
|
11779
|
+
}
|
|
11780
|
+
sendJson(res, 200, await dispatchRpc(new AcpServer(ctx), body));
|
|
11781
|
+
return;
|
|
11782
|
+
}
|
|
11783
|
+
if (req.method === "GET" && url === "/") {
|
|
11784
|
+
if (!s.publicAskEnabled) {
|
|
11785
|
+
sendHtml(res, publicAskDisabledHtml());
|
|
11786
|
+
return;
|
|
11787
|
+
}
|
|
11788
|
+
sendHtml(res, publicAskPage(await discoverCccs(ctx, defaultRoot || (findSerenityRoot(process.cwd()) ?? "")), readSimpleSettings() && hasPublicAskKey()));
|
|
11789
|
+
return;
|
|
11790
|
+
}
|
|
11791
|
+
if (req.method === "POST" && url === "/ask") {
|
|
11792
|
+
if (!s.publicAskEnabled) {
|
|
11793
|
+
sendJson(res, 403, { error: "public ask disabled (enable in Serenity settings)" });
|
|
11794
|
+
return;
|
|
11795
|
+
}
|
|
11796
|
+
let body;
|
|
11797
|
+
try {
|
|
11798
|
+
const parsed = JSON.parse(await readBody(req));
|
|
11799
|
+
if (parsed === null || typeof parsed !== "object") throw new Error("not an object");
|
|
11800
|
+
body = parsed;
|
|
11801
|
+
} catch {
|
|
11802
|
+
sendJson(res, 400, { error: "invalid JSON body" });
|
|
11803
|
+
return;
|
|
11804
|
+
}
|
|
11805
|
+
if (!verifyPublicAskKey(typeof body.key === "string" ? body.key : void 0)) {
|
|
11806
|
+
sendJson(res, 401, { error: "invalid or missing key" });
|
|
11807
|
+
return;
|
|
11808
|
+
}
|
|
11809
|
+
const ccc = typeof body.ccc === "string" && body.ccc !== "" ? body.ccc : defaultRoot || "";
|
|
11810
|
+
const roleName = typeof body.role === "string" && body.role !== "" ? body.role : void 0;
|
|
11811
|
+
const question = typeof body.question === "string" ? body.question : "";
|
|
11812
|
+
const sessionId = typeof body.sessionId === "string" && body.sessionId !== "" ? body.sessionId : void 0;
|
|
11813
|
+
if (!ccc) {
|
|
11814
|
+
sendJson(res, 400, { error: "ccc required" });
|
|
11815
|
+
return;
|
|
11816
|
+
}
|
|
11817
|
+
if (!question.trim()) {
|
|
11818
|
+
sendJson(res, 400, { error: "empty question" });
|
|
11819
|
+
return;
|
|
11820
|
+
}
|
|
11821
|
+
const roles = readSkiffRoles(ccc);
|
|
11822
|
+
const explicit = roleName ? roles.get(roleName) : void 0;
|
|
11823
|
+
const effectiveRole = roleName && explicit ? roleName : roles.keys().next().value;
|
|
11824
|
+
const effectiveCfg = (roleName && explicit ? explicit : effectiveRole ? roles.get(effectiveRole) : void 0) ?? void 0;
|
|
11825
|
+
if (!effectiveRole || !effectiveCfg) {
|
|
11826
|
+
sendJson(res, 400, { error: `no skiff role available in ccc: ${ccc}` });
|
|
11827
|
+
return;
|
|
11828
|
+
}
|
|
11829
|
+
let agent;
|
|
11830
|
+
let continued = false;
|
|
11831
|
+
if (sessionId) {
|
|
11832
|
+
const info = skiffSessionInfo(sessionId);
|
|
11833
|
+
const live = getSkiffAgent(sessionId);
|
|
11834
|
+
if (info && live && info.role === effectiveRole && info.ccc === ccc) {
|
|
11835
|
+
agent = live;
|
|
11836
|
+
continued = true;
|
|
11837
|
+
}
|
|
11838
|
+
}
|
|
11839
|
+
if (!agent) agent = (await createSkiffAgent(ctx, ccc, effectiveRole, effectiveCfg, readHandymanConfig(ccc)?.defaultModel)).agent;
|
|
11840
|
+
const result = await askSkiff(ctx, agent, question, 0);
|
|
11429
11841
|
sendJson(res, 200, {
|
|
11430
11842
|
answer: result.answer,
|
|
11431
11843
|
answer_html: renderSkiffMarkdown(result.answer),
|
|
@@ -11440,6 +11852,105 @@ async function handle(ctx, defaultRoot, webPort, req, res) {
|
|
|
11440
11852
|
sendJson(res, 500, { error: err?.message ?? String(err) });
|
|
11441
11853
|
}
|
|
11442
11854
|
}
|
|
11855
|
+
function hasPublicAskKey() {
|
|
11856
|
+
try {
|
|
11857
|
+
return ensurePublicAskKey() !== "";
|
|
11858
|
+
} catch {
|
|
11859
|
+
return false;
|
|
11860
|
+
}
|
|
11861
|
+
}
|
|
11862
|
+
/** 问答页未启用提示页 */
|
|
11863
|
+
function publicAskDisabledHtml() {
|
|
11864
|
+
return `<!DOCTYPE html>
|
|
11865
|
+
<html lang="zh"><head><meta charset="utf-8"><title>Serenity Public Ask</title></head>
|
|
11866
|
+
<body style="font-family:system-ui;padding:48px;text-align:center;color:#57606a">
|
|
11867
|
+
<h2>Serenity Public Ask</h2>
|
|
11868
|
+
<p>问答页未启用(请在 dsh 设置面板「Serenity」页开启「建议问答」)。</p>
|
|
11869
|
+
</body></html>`;
|
|
11870
|
+
}
|
|
11871
|
+
/** 建议问答页(F4d):key 输入 + CCC 选择 + 问题 + 答案(marked 服务端渲染) */
|
|
11872
|
+
function publicAskPage(cccs, _hasKey) {
|
|
11873
|
+
return `<!DOCTYPE html>
|
|
11874
|
+
<html lang="zh">
|
|
11875
|
+
<head>
|
|
11876
|
+
<meta charset="utf-8">
|
|
11877
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
11878
|
+
<title>Serenity Public Ask</title>
|
|
11879
|
+
<style>
|
|
11880
|
+
:root { color-scheme: light dark; }
|
|
11881
|
+
body { font-family: system-ui, -apple-system, sans-serif; margin: 0; padding: 24px; background: #f6f7f9; color: #1f2328; }
|
|
11882
|
+
@media (prefers-color-scheme: dark) { body { background: #1a1b1e; color: #e6e6e6; } }
|
|
11883
|
+
main { max-width: 720px; margin: 0 auto; }
|
|
11884
|
+
h1 { font-size: 18px; margin: 0 0 4px; }
|
|
11885
|
+
.sub { opacity: .65; font-size: 13px; margin-bottom: 16px; }
|
|
11886
|
+
label { font-size: 13px; font-weight: 600; display: block; margin: 12px 0 4px; }
|
|
11887
|
+
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; }
|
|
11888
|
+
@media (prefers-color-scheme: dark) { input, select, textarea { background: #26282c; border-color: #3a3d42; } }
|
|
11889
|
+
textarea { min-height: 72px; resize: vertical; }
|
|
11890
|
+
button { margin-top: 12px; padding: 9px 18px; border-radius: 8px; border: 0; background: #0ba875; color: #fff; font-size: 14px; font-weight: 600; cursor: pointer; }
|
|
11891
|
+
button:disabled { opacity: .55; cursor: wait; }
|
|
11892
|
+
#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; }
|
|
11893
|
+
@media (prefers-color-scheme: dark) { #answer { background: #26282c; border-color: #3a3d42; } }
|
|
11894
|
+
#answer p { margin: 6px 0; }
|
|
11895
|
+
#answer h1, #answer h2, #answer h3 { margin: 12px 0 6px; font-size: 15px; }
|
|
11896
|
+
#answer code { background: rgba(127,127,127,.14); border-radius: 4px; padding: 1px 5px; font-size: 13px; font-family: ui-monospace, monospace; }
|
|
11897
|
+
#answer pre { background: rgba(127,127,127,.1); border-radius: 8px; padding: 10px 12px; overflow-x: auto; }
|
|
11898
|
+
#answer ul, #answer ol { margin: 6px 0; padding-left: 22px; }
|
|
11899
|
+
.err { color: #cf222e; white-space: pre-wrap; }
|
|
11900
|
+
.muted { opacity: .6; font-size: 13px; }
|
|
11901
|
+
details.think { margin: 8px 0; border: 1px solid rgba(210,153,34,.4); border-radius: 8px; background: rgba(210,153,34,.06); }
|
|
11902
|
+
details.think summary { cursor: pointer; padding: 6px 10px; font-size: 12px; color: #d29922; font-weight: 600; }
|
|
11903
|
+
</style>
|
|
11904
|
+
</head>
|
|
11905
|
+
<body>
|
|
11906
|
+
<main>
|
|
11907
|
+
<h1>Serenity Public Ask</h1>
|
|
11908
|
+
<div class="sub">认知容器问答验证页 — 选择认知容器后提问(访问需 key)</div>
|
|
11909
|
+
<label for="key">访问 Key</label>
|
|
11910
|
+
<input id="key" type="password" placeholder="请输入访问 key" autocomplete="off">
|
|
11911
|
+
<label for="ccc">认知容器</label>
|
|
11912
|
+
<select id="ccc"></select>
|
|
11913
|
+
<label for="q">问题</label>
|
|
11914
|
+
<textarea id="q" placeholder="向该认知容器提问…"></textarea>
|
|
11915
|
+
<button id="ask">提问</button>
|
|
11916
|
+
<div id="answer" class="muted">等待提问…</div>
|
|
11917
|
+
</main>
|
|
11918
|
+
<script id="ask-data" type="application/json">${JSON.stringify(cccs).replace(/</g, "\\u003c")}<\/script>
|
|
11919
|
+
<script>
|
|
11920
|
+
const CCCS = JSON.parse(document.getElementById('ask-data').textContent)
|
|
11921
|
+
const keyInput = document.getElementById('key')
|
|
11922
|
+
const cccSel = document.getElementById('ccc')
|
|
11923
|
+
const btn = document.getElementById('ask')
|
|
11924
|
+
const answer = document.getElementById('answer')
|
|
11925
|
+
let sessionId = ''
|
|
11926
|
+
cccSel.innerHTML = CCCS.map((c) => '<option value="' + c.root + '">' + c.name + (c.roles.length ? ' (' + c.roles.length + ' 角色)' : '') + '</option>').join('') || '<option value="">(未发现认知容器)</option>'
|
|
11927
|
+
btn.addEventListener('click', async () => {
|
|
11928
|
+
const ccc = cccSel.value
|
|
11929
|
+
const q = document.getElementById('q').value.trim()
|
|
11930
|
+
const key = keyInput.value.trim()
|
|
11931
|
+
if (!ccc || !q) { answer.className = 'err'; answer.textContent = '请选择认知容器并输入问题'; return }
|
|
11932
|
+
if (!key) { answer.className = 'err'; answer.textContent = '请输入访问 key(没有 key 无法使用)'; return }
|
|
11933
|
+
btn.disabled = true
|
|
11934
|
+
answer.className = 'muted'
|
|
11935
|
+
answer.textContent = '运行中…'
|
|
11936
|
+
try {
|
|
11937
|
+
const res = await fetch('/ask', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ key, ccc, question: q, sessionId: sessionId || undefined }) })
|
|
11938
|
+
const data = await res.json()
|
|
11939
|
+
if (!res.ok) { sessionId = ''; throw new Error(data.error || ('HTTP ' + res.status)) }
|
|
11940
|
+
answer.className = ''
|
|
11941
|
+
answer.innerHTML = data.answer_html || data.answer || '(空回答)'
|
|
11942
|
+
sessionId = data.sessionId || ''
|
|
11943
|
+
} catch (err) {
|
|
11944
|
+
answer.className = 'err'
|
|
11945
|
+
answer.textContent = String(err.message || err)
|
|
11946
|
+
} finally {
|
|
11947
|
+
btn.disabled = false
|
|
11948
|
+
}
|
|
11949
|
+
})
|
|
11950
|
+
<\/script>
|
|
11951
|
+
</body>
|
|
11952
|
+
</html>`;
|
|
11953
|
+
}
|
|
11443
11954
|
//#endregion
|
|
11444
11955
|
//#region src/index.ts
|
|
11445
11956
|
const name = "dsh-serenity-hooks";
|
|
@@ -11476,6 +11987,10 @@ const Config = z.object({
|
|
|
11476
11987
|
skiff: z.object({
|
|
11477
11988
|
enabled: z.boolean().default(false),
|
|
11478
11989
|
debugPort: z.number().min(1024).max(65535).default(3099)
|
|
11990
|
+
}),
|
|
11991
|
+
acp: z.object({
|
|
11992
|
+
enabled: z.boolean().default(false),
|
|
11993
|
+
httpPort: z.number().min(1024).max(65535).default(3100)
|
|
11479
11994
|
})
|
|
11480
11995
|
});
|
|
11481
11996
|
function apply(ctx, config) {
|
|
@@ -11515,6 +12030,7 @@ function apply(ctx, config) {
|
|
|
11515
12030
|
if (config.opencodeSkills) registerOpencodeSkills(ctx);
|
|
11516
12031
|
registerBootstrap(ctx);
|
|
11517
12032
|
registerSkiff(ctx);
|
|
12033
|
+
registerAcp(ctx);
|
|
11518
12034
|
}
|
|
11519
12035
|
/**
|
|
11520
12036
|
* F4 Skiff 调试服务装配:启停 = 人工(设置面板 Skiff 区块开关,settings 持久化)。
|
|
@@ -11579,5 +12095,33 @@ function readWebPort(ctx) {
|
|
|
11579
12095
|
return 3080;
|
|
11580
12096
|
}
|
|
11581
12097
|
}
|
|
12098
|
+
/**
|
|
12099
|
+
* F4c ACP HTTP + F4d 建议问答页装配(v1.26.x 实验性):启停 = 人工(设置面板
|
|
12100
|
+
* 「Serenity」页 ACP / 问答页开关,settings 持久化)。任一面开启即启动服务
|
|
12101
|
+
* (同一端口:POST / = JSON-RPC 需 acpEnabled;GET / + /ask = 问答页需 publicAskEnabled)。
|
|
12102
|
+
* 会话创建/延续走 acp-core → skiff-core;仅监听 127.0.0.1;未开启零资源占用。
|
|
12103
|
+
*/
|
|
12104
|
+
function registerAcp(ctx) {
|
|
12105
|
+
let started = false;
|
|
12106
|
+
const sync = () => {
|
|
12107
|
+
const s = readSimpleSettings();
|
|
12108
|
+
const anyFace = s.acpEnabled || s.publicAskEnabled;
|
|
12109
|
+
if (anyFace && !started) {
|
|
12110
|
+
const root = resolveSkiffRoot(ctx);
|
|
12111
|
+
startAcpHttpServer(ctx, s.acpHttpPort, root ?? void 0).then(() => {
|
|
12112
|
+
started = true;
|
|
12113
|
+
}).catch((err) => {
|
|
12114
|
+
console.error(`[serenity-hooks] ✗ ACP HTTP 服务启动失败: ${String(err?.message ?? err)}`);
|
|
12115
|
+
});
|
|
12116
|
+
} else if (!anyFace && started) {
|
|
12117
|
+
stopAcpHttpServer();
|
|
12118
|
+
started = false;
|
|
12119
|
+
}
|
|
12120
|
+
};
|
|
12121
|
+
try {
|
|
12122
|
+
ctx.on("serenity/settings-changed", sync);
|
|
12123
|
+
} catch {}
|
|
12124
|
+
sync();
|
|
12125
|
+
}
|
|
11582
12126
|
//#endregion
|
|
11583
12127
|
export { Config, apply, inject, name };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shgroup/dsh-serenity-hooks",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.26.1",
|
|
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": {
|