@shgroup/dsh-serenity-hooks 1.30.8 → 1.30.9
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/config-ops.d.ts +10 -0
- package/lib/index.js +615 -124
- package/lib/msm-ops.d.ts +1 -1
- package/lib/skiff-core.d.ts +10 -2
- package/lib/{skiff-debug-8pF60JBO.js → skiff-debug-TONJlpgF.js} +23 -4
- package/lib/{weixin-api-OUCuw0Ws.js → weixin-api-BMpljodH.js} +25 -3
- package/lib/weixin-api.d.ts +12 -1
- package/lib/weixin-bridge.d.ts +43 -1
- package/lib/weixin-hook.d.ts +9 -0
- package/lib/weixin-send-api.d.ts +87 -0
- package/lib/weixin-send-endpoint.d.ts +17 -0
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
|
|
2
2
|
import { a as findSerenityRoot, c as matchBlacklist, d as readCccName$2, f as readHandymanConfig, i as findGitRoot, l as pathInside, m as resolveInside, n as SAFE_MODE_MARKER, o as isSafeModeOn, r as classifyPath, s as loadSerenityConfig, t as DEFAULT_SERENITY_CONFIG_PATHS, u as readBlacklist } from "./ccc-DAsSHsub.js";
|
|
3
3
|
import { a as resolveRoleSystemPrompt, i as readSkiffRoles, l as systemPromptSource, r as isSkiffSessionId, s as roleToolWhitelist } from "./skiff-role-DlrbHPLD.js";
|
|
4
|
-
import { C as newStopToken, D as writeFailedStatus, E as splitModel, O as writeProgress, S as listActiveHandymen, T as requireWhitelistedModel, _ as workspaceTrajectoryLine, a as startSkiffDebugServer, b as buildRoundPrompt, c as askSkiff, d as getSkiffAgent, f as skiffMsmGate, g as unregisterSkiffSession, h as skiffTrajectoryEnabled, k as skiffRoleFor, l as createSkiffAgent, m as skiffSessionSnapshot, n as jscSafeJsonText, o as stopSkiffDebugServer, p as skiffSessionInfo, r as renderSkiffMarkdown, s as stripThink, t as discoverCccs, u as ensureSkiffSession, v as waitAgentIdle, w as readProgress, x as handymanProgressPaths, y as HANDYMAN_GUIDE } from "./skiff-debug-
|
|
4
|
+
import { C as newStopToken, D as writeFailedStatus, E as splitModel, O as writeProgress, S as listActiveHandymen, T as requireWhitelistedModel, _ as workspaceTrajectoryLine, a as startSkiffDebugServer, b as buildRoundPrompt, c as askSkiff, d as getSkiffAgent, f as skiffMsmGate, g as unregisterSkiffSession, h as skiffTrajectoryEnabled, k as skiffRoleFor, l as createSkiffAgent, m as skiffSessionSnapshot, n as jscSafeJsonText, o as stopSkiffDebugServer, p as skiffSessionInfo, r as renderSkiffMarkdown, s as stripThink, t as discoverCccs, u as ensureSkiffSession, v as waitAgentIdle, w as readProgress, x as handymanProgressPaths, y as HANDYMAN_GUIDE } from "./skiff-debug-TONJlpgF.js";
|
|
5
5
|
import { i as hostSessions, n as hostInjected, o as hostWebServer, r as hostService, t as hostAgents } from "./access-CdL6BAYj.js";
|
|
6
6
|
import { a as readWeixinCredential, c as weixinInboundDir, d as LOCALSTORE_SCOPES, f as checkLocalstoreGitCompliance, h as runLocalStore, i as matchWeixinRoute, l as weixinSessionIdFor, m as readGitTrack, n as extractWeixinText, o as readWeixinSettings, p as localstorePath, r as hasVoiceItem, s as sanitizeFileName$1, t as extractWeixinMedia } from "./weixin-route-BJ4g4ZNq.js";
|
|
7
7
|
import { C as summarize, S as showSession, _ as readActiveSessionMd, a as archiveSessions, b as sessionsRoot, c as createSession, d as findSession, f as getActiveSessionInfo, g as qaCheck, h as parseSessionContextFromEvents, i as SESSION_ACTIONS, l as extractSessionMdPathFromText, m as listSessions, n as readLastBound, o as clearActiveSessionInfo, p as healthCheck, r as DEFAULT_SESSION_SCOPE, s as closeSession, t as appendBound, u as findLatestActiveSessionMd, v as resolveSessionByTitle, w as useSession, x as setActiveSessionInfo, y as sessionEvents } from "./session-bound-D2ANqVn-.js";
|
|
8
8
|
import { n as registerSettingsSection, t as readSimpleSettings } from "./settings-section-BPk0e1du.js";
|
|
9
|
-
import { a as markdownToPlainText, c as sniffImageExt, i as getUpdates, n as downloadMedia, o as sendTextMessage, r as getConfig, s as sendTyping, t as TypingStatus } from "./weixin-api-
|
|
9
|
+
import { a as markdownToPlainText, c as sniffImageExt, i as getUpdates, n as downloadMedia, o as sendTextMessage, r as getConfig, s as sendTyping, t as TypingStatus } from "./weixin-api-BMpljodH.js";
|
|
10
10
|
import z from "@deepseek-ai/schemastery";
|
|
11
11
|
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
12
12
|
import { appendFileSync, chmodSync, cpSync, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, rmdirSync, statSync, unlinkSync, utimesSync, writeFileSync } from "node:fs";
|
|
@@ -1577,6 +1577,28 @@ const gitTool = defineTool({
|
|
|
1577
1577
|
}
|
|
1578
1578
|
});
|
|
1579
1579
|
//#endregion
|
|
1580
|
+
//#region src/weixin-send-endpoint.ts
|
|
1581
|
+
/**
|
|
1582
|
+
* weixin-send-endpoint.ts — 主动发送入口地址的**零依赖叶模块**(v1.30.9,S142)
|
|
1583
|
+
*
|
|
1584
|
+
* 为什么单独一个模块(R↓):地址的读者是 `msm-ops.buildMsmEnv`(把地址注入 MSM 子进程 env),
|
|
1585
|
+
* 写者才是 `weixin-send-api`(监听器启停)。若 msm-ops 直接 import weixin-send-api,
|
|
1586
|
+
* 静态依赖链会拖进整条微信桥栈(weixin-bridge → skiff-core → `@deepseek-ai/dsh-llm`)——
|
|
1587
|
+
* 后果实测:`acc-extras` / `ops` / `skiff-admin` 三个测试套件因解析不到宿主包而**整文件加载失败**
|
|
1588
|
+
* (它们只用到 MSM 注册表逻辑)。叶模块把「读」与「写」解耦:读者零依赖,写者自己承担重量。
|
|
1589
|
+
*
|
|
1590
|
+
* 同款先例:`host/effect.ts`(拆卸登记)、`agent-idle.ts`(等待结算)。
|
|
1591
|
+
*/
|
|
1592
|
+
let endpoint = null;
|
|
1593
|
+
/** 设置当前入口地址(weixin-send-api 启停时调用;null = 未启动) */
|
|
1594
|
+
function setWeixinSendEndpoint(value) {
|
|
1595
|
+
endpoint = value;
|
|
1596
|
+
}
|
|
1597
|
+
/** 当前入口地址(形如 http://127.0.0.1:3082;未启动 → null) */
|
|
1598
|
+
function weixinSendEndpoint() {
|
|
1599
|
+
return endpoint;
|
|
1600
|
+
}
|
|
1601
|
+
//#endregion
|
|
1580
1602
|
//#region src/msm-ops.ts
|
|
1581
1603
|
/**
|
|
1582
1604
|
* msm-ops.ts — msm 纯操作层(acc_msm → msm 执行 + container_admin 管理面,v1.30;零 DSH 依赖)
|
|
@@ -1625,14 +1647,21 @@ function assertPathInsideRoot(root, value, flagName) {
|
|
|
1625
1647
|
if (e instanceof Error && e.message.includes("symlink")) throw e;
|
|
1626
1648
|
}
|
|
1627
1649
|
}
|
|
1628
|
-
/**
|
|
1650
|
+
/**
|
|
1651
|
+
* 业务子进程 env:注入 SERENITY_ROOT / SERENITY_CCC / SERENITY_VERSION(对齐 osp)
|
|
1652
|
+
* + SERENITY_WEIXIN_API(v1.30.9:微信主动发送入口地址——MSM 脚本零硬编码端口;
|
|
1653
|
+
* 未启动则不注入,脚本回退默认值)。
|
|
1654
|
+
*/
|
|
1629
1655
|
function buildMsmEnv(root) {
|
|
1630
|
-
|
|
1656
|
+
const env = {
|
|
1631
1657
|
...process.env,
|
|
1632
1658
|
SERENITY_ROOT: root,
|
|
1633
1659
|
SERENITY_CCC: readCccName(root) ?? "",
|
|
1634
1660
|
SERENITY_VERSION: ACC_VERSION
|
|
1635
1661
|
};
|
|
1662
|
+
const weixinApi = weixinSendEndpoint();
|
|
1663
|
+
if (weixinApi) env.SERENITY_WEIXIN_API = weixinApi;
|
|
1664
|
+
return env;
|
|
1636
1665
|
}
|
|
1637
1666
|
/**
|
|
1638
1667
|
* Windows 兼容(审计观察点 A):`.cmd` 不能直接被 CreateProcess 解析——
|
|
@@ -1801,7 +1830,11 @@ CCC 级微信个人号接入(iLink 协议):dsh 一进程多 CCC,每 CCC
|
|
|
1801
1830
|
账号/路由/开关在此文件;**bot_token 凭据在 CCC localstore credential scope**
|
|
1802
1831
|
(扫码绑定后自动写入,永不进 git 明文面)。
|
|
1803
1832
|
路由 user → role:exact 优先,* 通配兜底;role 必须 ∈ 该 CCC skiff.roles。
|
|
1804
|
-
面板(WebUI 设置 → 微信桥)可扫码绑定/移除账号/编辑路由;msm("weixin-doctor", ["status"|"diag"|"verify"])
|
|
1833
|
+
面板(WebUI 设置 → 微信桥)可扫码绑定/移除账号/编辑路由;msm("weixin-doctor", ["status"|"diag"|"verify"|"guide"]) 排查 + 大而全指南。凭据主动查看:localstore get WEIXIN_<ACCOUNT>_TOKEN。
|
|
1834
|
+
▸ 主动发送(v1.30.9):CCC 用 msm("weixin-send", ["send", "--ccc", "<CCC>", "--user", "<别名|id>", "<文本>"])
|
|
1835
|
+
给用户发消息——经 dsp 主动发送入口(**只绑 127.0.0.1**,plugin 全局配置 ~/.dsh/serenity-hooks.json
|
|
1836
|
+
的 weixinApi.port,默认 3082;公网不可达故无密钥)→ weixin-bridge.sendProactiveText →
|
|
1837
|
+
成功后触发 outgoing hook(source="proactive")。send-file 仍由 MSM 直连 iLink 上传并自行补记。
|
|
1805
1838
|
|
|
1806
1839
|
Config:
|
|
1807
1840
|
{ "weixin": {
|
|
@@ -1817,7 +1850,10 @@ CCC 级微信个人号接入(iLink 协议):dsh 一进程多 CCC,每 CCC
|
|
|
1817
1850
|
|
|
1818
1851
|
触发(双向):
|
|
1819
1852
|
incoming = 用户 → bot:路由命中后、媒体落盘后触发(文本含语音转写;媒体带落盘 relPath)
|
|
1820
|
-
outgoing = bot →
|
|
1853
|
+
outgoing = bot → 用户:发送成功后触发(reply = 用户实际收到的纯文本,已剥离 think)
|
|
1854
|
+
· source="reply"(缺省)= 回复用户消息;source="proactive"(v1.30.9)= bot 主动发起
|
|
1855
|
+
(CCC 经 weixin-send MSM → dsp 主动发送入口 → sendProactiveText)
|
|
1856
|
+
· file(仅 send-file 补记):{ "name": "报告.pdf", "size": 12345, "caption": "可选" }
|
|
1821
1857
|
|
|
1822
1858
|
脚本约定(事件 JSON 单行经 stdin 传入;bun 优先 node 兜底):
|
|
1823
1859
|
#!/usr/bin/env bun 或 node script.js —— 读 process.stdin 整段 JSON.parse
|
|
@@ -1831,7 +1867,7 @@ CCC 级微信个人号接入(iLink 协议):dsh 一进程多 CCC,每 CCC
|
|
|
1831
1867
|
"message": { "text": "你好", "media": [{ "kind": "image", "relPath": "_tmp/weixin-inbound/<hash>/img_x.jpg" }] } }
|
|
1832
1868
|
{ "event": "outgoing", "ts": ..., "cccRoot": ..., "accountId": ...,
|
|
1833
1869
|
"userId": ..., "sessionId": ..., "role": ...,
|
|
1834
|
-
"reply": "已记录(纯文本)" }
|
|
1870
|
+
"reply": "已记录(纯文本)", "source": "reply" | "proactive" }
|
|
1835
1871
|
|
|
1836
1872
|
示例脚本(追加到按日文件——持久化归 CCC 自选:文件/DB/远端均可):
|
|
1837
1873
|
const fs = require('node:fs'); const p = '/path/ccc/AGENT_SESSIONS/_weixin-log.jsonl';
|
|
@@ -5048,6 +5084,10 @@ function defaultAdvancedSettings() {
|
|
|
5048
5084
|
publicAsk: {
|
|
5049
5085
|
key: "",
|
|
5050
5086
|
allowed: []
|
|
5087
|
+
},
|
|
5088
|
+
weixinApi: {
|
|
5089
|
+
enabled: true,
|
|
5090
|
+
port: 3082
|
|
5051
5091
|
}
|
|
5052
5092
|
};
|
|
5053
5093
|
}
|
|
@@ -5098,6 +5138,7 @@ function mergeWithDefaults(raw) {
|
|
|
5098
5138
|
const gateway = o.gateway ?? {};
|
|
5099
5139
|
const persona = o.persona ?? {};
|
|
5100
5140
|
const publicAsk = o.publicAsk ?? {};
|
|
5141
|
+
const weixinApi = o.weixinApi ?? {};
|
|
5101
5142
|
return {
|
|
5102
5143
|
gateway: {
|
|
5103
5144
|
enabled: typeof gateway.enabled === "boolean" ? gateway.enabled : def.gateway.enabled,
|
|
@@ -5116,6 +5157,10 @@ function mergeWithDefaults(raw) {
|
|
|
5116
5157
|
publicAsk: {
|
|
5117
5158
|
key: typeof publicAsk.key === "string" ? publicAsk.key : def.publicAsk.key,
|
|
5118
5159
|
allowed: Array.isArray(publicAsk.allowed) ? publicAsk.allowed.filter((n) => typeof n === "string" && n !== "") : def.publicAsk.allowed
|
|
5160
|
+
},
|
|
5161
|
+
weixinApi: {
|
|
5162
|
+
enabled: typeof weixinApi.enabled === "boolean" ? weixinApi.enabled : def.weixinApi.enabled,
|
|
5163
|
+
port: typeof weixinApi.port === "number" && Number.isInteger(weixinApi.port) && weixinApi.port >= 0 ? weixinApi.port : def.weixinApi.port
|
|
5119
5164
|
}
|
|
5120
5165
|
};
|
|
5121
5166
|
}
|
|
@@ -5153,7 +5198,11 @@ function updateAdvancedSettings(patch) {
|
|
|
5153
5198
|
publicAsk: patch.publicAsk !== void 0 ? {
|
|
5154
5199
|
key: typeof patch.publicAsk.key === "string" ? patch.publicAsk.key : current.publicAsk.key,
|
|
5155
5200
|
allowed: Array.isArray(patch.publicAsk.allowed) ? patch.publicAsk.allowed.filter((n) => typeof n === "string" && n !== "") : current.publicAsk.allowed
|
|
5156
|
-
} : current.publicAsk
|
|
5201
|
+
} : current.publicAsk,
|
|
5202
|
+
weixinApi: patch.weixinApi !== void 0 ? {
|
|
5203
|
+
enabled: typeof patch.weixinApi.enabled === "boolean" ? patch.weixinApi.enabled : current.weixinApi.enabled,
|
|
5204
|
+
port: typeof patch.weixinApi.port === "number" && Number.isInteger(patch.weixinApi.port) && patch.weixinApi.port >= 0 ? patch.weixinApi.port : current.weixinApi.port
|
|
5205
|
+
} : current.weixinApi
|
|
5157
5206
|
};
|
|
5158
5207
|
writeAdvancedSettings(next);
|
|
5159
5208
|
return next;
|
|
@@ -6175,7 +6224,7 @@ function saveFileToTmp(root, fileName, data) {
|
|
|
6175
6224
|
writeFileSync(join(dir, filename), bytes);
|
|
6176
6225
|
return `${FILE_UPLOAD_DIR}/${filename}`;
|
|
6177
6226
|
}
|
|
6178
|
-
function readBody$
|
|
6227
|
+
function readBody$3(req, maxBytes) {
|
|
6179
6228
|
return new Promise((resolve, reject) => {
|
|
6180
6229
|
let data = "";
|
|
6181
6230
|
req.on("data", (chunk) => {
|
|
@@ -6189,7 +6238,7 @@ function readBody$2(req, maxBytes) {
|
|
|
6189
6238
|
req.on("error", reject);
|
|
6190
6239
|
});
|
|
6191
6240
|
}
|
|
6192
|
-
function sendJson$
|
|
6241
|
+
function sendJson$2(res, code, body) {
|
|
6193
6242
|
const payload = JSON.stringify(body);
|
|
6194
6243
|
res.writeHead(code, {
|
|
6195
6244
|
"content-type": "application/json; charset=utf-8",
|
|
@@ -6227,7 +6276,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
6227
6276
|
handler: async (req, res) => {
|
|
6228
6277
|
try {
|
|
6229
6278
|
if (req.method !== "GET") {
|
|
6230
|
-
sendJson$
|
|
6279
|
+
sendJson$2(res, 405, { error: "method not allowed" });
|
|
6231
6280
|
return;
|
|
6232
6281
|
}
|
|
6233
6282
|
const url = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
@@ -6237,12 +6286,12 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
6237
6286
|
});
|
|
6238
6287
|
const root = findSerenityRoot(workspace);
|
|
6239
6288
|
if (!root) {
|
|
6240
|
-
sendJson$
|
|
6289
|
+
sendJson$2(res, 200, { handymen: [] });
|
|
6241
6290
|
return;
|
|
6242
6291
|
}
|
|
6243
|
-
sendJson$
|
|
6292
|
+
sendJson$2(res, 200, { handymen: listActiveHandymen(root) });
|
|
6244
6293
|
} catch (err) {
|
|
6245
|
-
sendJson$
|
|
6294
|
+
sendJson$2(res, 400, { error: err.message ?? String(err) });
|
|
6246
6295
|
}
|
|
6247
6296
|
}
|
|
6248
6297
|
});
|
|
@@ -6252,14 +6301,14 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
6252
6301
|
handler: async (req, res) => {
|
|
6253
6302
|
try {
|
|
6254
6303
|
if (req.method !== "POST") {
|
|
6255
|
-
sendJson$
|
|
6304
|
+
sendJson$2(res, 405, { error: "method not allowed" });
|
|
6256
6305
|
return;
|
|
6257
6306
|
}
|
|
6258
6307
|
if (req.headers["x-serenity-ui"] !== "1") {
|
|
6259
|
-
sendJson$
|
|
6308
|
+
sendJson$2(res, 403, { error: "图片落盘仅限 WebUI(client 专用)" });
|
|
6260
6309
|
return;
|
|
6261
6310
|
}
|
|
6262
|
-
const raw = await readBody$
|
|
6311
|
+
const raw = await readBody$3(req, 20971520);
|
|
6263
6312
|
const body = JSON.parse(raw);
|
|
6264
6313
|
const workspace = resolveWorkspace(ctx, {
|
|
6265
6314
|
sessionId: body.sessionId,
|
|
@@ -6267,12 +6316,12 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
6267
6316
|
});
|
|
6268
6317
|
const root = findSerenityRoot(workspace);
|
|
6269
6318
|
if (!root) {
|
|
6270
|
-
sendJson$
|
|
6319
|
+
sendJson$2(res, 404, { error: `no CCC found from workspace: ${workspace}` });
|
|
6271
6320
|
return;
|
|
6272
6321
|
}
|
|
6273
|
-
sendJson$
|
|
6322
|
+
sendJson$2(res, 200, { path: saveImageToTmp(root, body.mediaType ?? "", body.data ?? "") });
|
|
6274
6323
|
} catch (err) {
|
|
6275
|
-
sendJson$
|
|
6324
|
+
sendJson$2(res, 400, { error: err.message ?? String(err) });
|
|
6276
6325
|
}
|
|
6277
6326
|
}
|
|
6278
6327
|
});
|
|
@@ -6282,14 +6331,14 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
6282
6331
|
handler: async (req, res) => {
|
|
6283
6332
|
try {
|
|
6284
6333
|
if (req.method !== "POST") {
|
|
6285
|
-
sendJson$
|
|
6334
|
+
sendJson$2(res, 405, { error: "method not allowed" });
|
|
6286
6335
|
return;
|
|
6287
6336
|
}
|
|
6288
6337
|
if (req.headers["x-serenity-ui"] !== "1") {
|
|
6289
|
-
sendJson$
|
|
6338
|
+
sendJson$2(res, 403, { error: "文件落盘仅限 WebUI(client 专用)" });
|
|
6290
6339
|
return;
|
|
6291
6340
|
}
|
|
6292
|
-
const raw = await readBody$
|
|
6341
|
+
const raw = await readBody$3(req, 20971520);
|
|
6293
6342
|
const body = JSON.parse(raw);
|
|
6294
6343
|
const workspace = resolveWorkspace(ctx, {
|
|
6295
6344
|
sessionId: body.sessionId,
|
|
@@ -6297,12 +6346,12 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
6297
6346
|
});
|
|
6298
6347
|
const root = findSerenityRoot(workspace);
|
|
6299
6348
|
if (!root) {
|
|
6300
|
-
sendJson$
|
|
6349
|
+
sendJson$2(res, 404, { error: `no CCC found from workspace: ${workspace}` });
|
|
6301
6350
|
return;
|
|
6302
6351
|
}
|
|
6303
|
-
sendJson$
|
|
6352
|
+
sendJson$2(res, 200, { path: saveFileToTmp(root, body.name ?? "", body.data ?? "") });
|
|
6304
6353
|
} catch (err) {
|
|
6305
|
-
sendJson$
|
|
6354
|
+
sendJson$2(res, 400, { error: err.message ?? String(err) });
|
|
6306
6355
|
}
|
|
6307
6356
|
}
|
|
6308
6357
|
});
|
|
@@ -6318,7 +6367,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
6318
6367
|
workspace: url.searchParams.get("workspace") ?? void 0
|
|
6319
6368
|
}), configPaths);
|
|
6320
6369
|
const runtime = ctx.get("codeRuntime");
|
|
6321
|
-
sendJson$
|
|
6370
|
+
sendJson$2(res, 200, {
|
|
6322
6371
|
...status,
|
|
6323
6372
|
...runtime === void 0 ? { codeRuntime: null } : { codeRuntime: {
|
|
6324
6373
|
language: runtime.language,
|
|
@@ -6329,10 +6378,10 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
6329
6378
|
}
|
|
6330
6379
|
if (req.method === "POST") {
|
|
6331
6380
|
if (req.headers["x-serenity-ui"] !== "1") {
|
|
6332
|
-
sendJson$
|
|
6381
|
+
sendJson$2(res, 403, { error: "safe-mode 切换仅限 WebUI(agent 不可自行开关)" });
|
|
6333
6382
|
return;
|
|
6334
6383
|
}
|
|
6335
|
-
const raw = await readBody$
|
|
6384
|
+
const raw = await readBody$3(req, 65536);
|
|
6336
6385
|
const body = JSON.parse(raw);
|
|
6337
6386
|
const workspace = resolveWorkspace(ctx, {
|
|
6338
6387
|
sessionId: body.sessionId,
|
|
@@ -6340,19 +6389,19 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
6340
6389
|
});
|
|
6341
6390
|
const root = findSerenityRoot(workspace);
|
|
6342
6391
|
if (!root) {
|
|
6343
|
-
sendJson$
|
|
6392
|
+
sendJson$2(res, 404, { error: `no CCC found from workspace: ${workspace}` });
|
|
6344
6393
|
return;
|
|
6345
6394
|
}
|
|
6346
6395
|
const result = setSafeMode(root, body.on === true);
|
|
6347
|
-
sendJson$
|
|
6396
|
+
sendJson$2(res, 200, {
|
|
6348
6397
|
...getStatus(workspace, configPaths),
|
|
6349
6398
|
...result
|
|
6350
6399
|
});
|
|
6351
6400
|
return;
|
|
6352
6401
|
}
|
|
6353
|
-
sendJson$
|
|
6402
|
+
sendJson$2(res, 405, { error: "method not allowed" });
|
|
6354
6403
|
} catch (err) {
|
|
6355
|
-
sendJson$
|
|
6404
|
+
sendJson$2(res, 400, { error: err.message ?? String(err) });
|
|
6356
6405
|
}
|
|
6357
6406
|
}
|
|
6358
6407
|
});
|
|
@@ -6362,7 +6411,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
6362
6411
|
handler: async (req, res) => {
|
|
6363
6412
|
try {
|
|
6364
6413
|
if (req.headers["x-serenity-ui"] !== "1") {
|
|
6365
|
-
sendJson$
|
|
6414
|
+
sendJson$2(res, 403, { error: "高级设定仅限 WebUI(client 专用)" });
|
|
6366
6415
|
return;
|
|
6367
6416
|
}
|
|
6368
6417
|
if (req.method === "GET") {
|
|
@@ -6371,24 +6420,24 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
6371
6420
|
try {
|
|
6372
6421
|
known = projectKnownWorkspaces(hostService(ctx, "workspaceRegistry")?.list?.() ?? [], settings.gateway.workspaces);
|
|
6373
6422
|
} catch {}
|
|
6374
|
-
sendJson$
|
|
6423
|
+
sendJson$2(res, 200, {
|
|
6375
6424
|
config: toWire(settings),
|
|
6376
6425
|
knownWorkspaces: known
|
|
6377
6426
|
});
|
|
6378
6427
|
return;
|
|
6379
6428
|
}
|
|
6380
6429
|
if (req.method === "PUT") {
|
|
6381
|
-
const raw = await readBody$
|
|
6430
|
+
const raw = await readBody$3(req, 131072);
|
|
6382
6431
|
const saved = applyWirePatch(JSON.parse(raw).config ?? {});
|
|
6383
6432
|
try {
|
|
6384
6433
|
ctx.emit?.("serenity/config-updated");
|
|
6385
6434
|
} catch {}
|
|
6386
|
-
sendJson$
|
|
6435
|
+
sendJson$2(res, 200, { config: toWire(saved) });
|
|
6387
6436
|
return;
|
|
6388
6437
|
}
|
|
6389
|
-
sendJson$
|
|
6438
|
+
sendJson$2(res, 405, { error: "method not allowed" });
|
|
6390
6439
|
} catch (err) {
|
|
6391
|
-
sendJson$
|
|
6440
|
+
sendJson$2(res, 400, { error: err.message ?? String(err) });
|
|
6392
6441
|
}
|
|
6393
6442
|
}
|
|
6394
6443
|
});
|
|
@@ -6398,7 +6447,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
6398
6447
|
handler: async (req, res) => {
|
|
6399
6448
|
try {
|
|
6400
6449
|
if (req.method !== "GET") {
|
|
6401
|
-
sendJson$
|
|
6450
|
+
sendJson$2(res, 405, { error: "method not allowed" });
|
|
6402
6451
|
return;
|
|
6403
6452
|
}
|
|
6404
6453
|
const url = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
@@ -6407,10 +6456,10 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
6407
6456
|
workspace: url.searchParams.get("workspace") ?? void 0
|
|
6408
6457
|
});
|
|
6409
6458
|
const root = findSerenityRoot(workspace) ?? "";
|
|
6410
|
-
const { discoverCccs } = await import("./skiff-debug-
|
|
6411
|
-
sendJson$
|
|
6459
|
+
const { discoverCccs } = await import("./skiff-debug-TONJlpgF.js").then((n) => n.i);
|
|
6460
|
+
sendJson$2(res, 200, { cccs: await discoverCccs(ctx, root) });
|
|
6412
6461
|
} catch (err) {
|
|
6413
|
-
sendJson$
|
|
6462
|
+
sendJson$2(res, 400, { error: err.message ?? String(err) });
|
|
6414
6463
|
}
|
|
6415
6464
|
}
|
|
6416
6465
|
});
|
|
@@ -6420,20 +6469,20 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
6420
6469
|
handler: async (req, res) => {
|
|
6421
6470
|
try {
|
|
6422
6471
|
if (req.headers["x-serenity-ui"] !== "1") {
|
|
6423
|
-
sendJson$
|
|
6472
|
+
sendJson$2(res, 403, { error: "仅限 WebUI(key 属敏感凭据)" });
|
|
6424
6473
|
return;
|
|
6425
6474
|
}
|
|
6426
6475
|
if (req.method === "PUT") {
|
|
6427
|
-
const raw = await readBody$
|
|
6476
|
+
const raw = await readBody$3(req, 16384);
|
|
6428
6477
|
if (JSON.parse(raw).action !== "rotate") {
|
|
6429
|
-
sendJson$
|
|
6478
|
+
sendJson$2(res, 400, { error: "unsupported action (expected \"rotate\")" });
|
|
6430
6479
|
return;
|
|
6431
6480
|
}
|
|
6432
|
-
sendJson$
|
|
6481
|
+
sendJson$2(res, 200, { key: rotatePublicAskKey() });
|
|
6433
6482
|
return;
|
|
6434
6483
|
}
|
|
6435
6484
|
if (req.method !== "GET") {
|
|
6436
|
-
sendJson$
|
|
6485
|
+
sendJson$2(res, 405, { error: "method not allowed" });
|
|
6437
6486
|
return;
|
|
6438
6487
|
}
|
|
6439
6488
|
const settings = readAdvancedSettings();
|
|
@@ -6446,7 +6495,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
6446
6495
|
name,
|
|
6447
6496
|
url: `${base}/c/${encodeURIComponent(name)}`
|
|
6448
6497
|
}));
|
|
6449
|
-
sendJson$
|
|
6498
|
+
sendJson$2(res, 200, {
|
|
6450
6499
|
enabled: simple.publicAskEnabled ?? false,
|
|
6451
6500
|
port,
|
|
6452
6501
|
key: ensurePublicAskKey(),
|
|
@@ -6455,7 +6504,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
6455
6504
|
listUrl: `${base}/`
|
|
6456
6505
|
});
|
|
6457
6506
|
} catch (err) {
|
|
6458
|
-
sendJson$
|
|
6507
|
+
sendJson$2(res, 400, { error: err.message ?? String(err) });
|
|
6459
6508
|
}
|
|
6460
6509
|
}
|
|
6461
6510
|
});
|
|
@@ -6466,13 +6515,13 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
6466
6515
|
try {
|
|
6467
6516
|
if (req.method === "POST") {
|
|
6468
6517
|
if (req.headers["x-serenity-ui"] !== "1") {
|
|
6469
|
-
sendJson$
|
|
6518
|
+
sendJson$2(res, 403, { error: "立即唤起仅限 WebUI(agent 不可自行唤起)" });
|
|
6470
6519
|
return;
|
|
6471
6520
|
}
|
|
6472
|
-
const raw = await readBody$
|
|
6521
|
+
const raw = await readBody$3(req, 16384);
|
|
6473
6522
|
const body = JSON.parse(raw);
|
|
6474
6523
|
if (body.action !== "wake") {
|
|
6475
|
-
sendJson$
|
|
6524
|
+
sendJson$2(res, 400, { error: "unsupported action (expected \"wake\")" });
|
|
6476
6525
|
return;
|
|
6477
6526
|
}
|
|
6478
6527
|
const workspace = resolveWorkspace(ctx, {
|
|
@@ -6482,21 +6531,21 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
6482
6531
|
const root = findSerenityRoot(workspace);
|
|
6483
6532
|
const effectiveRoot = body.ccc?.trim() || !body.workspace && !body.sessionId ? body.ccc?.trim() ?? (await Promise.resolve().then(() => autopilot_trajectory_exports)).collectAutopilotCccs(ctx)[0] ?? root : root;
|
|
6484
6533
|
if (!effectiveRoot) {
|
|
6485
|
-
sendJson$
|
|
6534
|
+
sendJson$2(res, 404, { error: `no CCC found from workspace: ${workspace}` });
|
|
6486
6535
|
return;
|
|
6487
6536
|
}
|
|
6488
6537
|
const { performAutopilotWake, readAutopilotSettings } = await Promise.resolve().then(() => autopilot_trajectory_exports);
|
|
6489
6538
|
const settings = readAutopilotSettings(effectiveRoot);
|
|
6490
6539
|
if (!settings) {
|
|
6491
|
-
sendJson$
|
|
6540
|
+
sendJson$2(res, 400, { error: "Autopilot Trajectory 未配置(.opencode/serenity.json 缺段)" });
|
|
6492
6541
|
return;
|
|
6493
6542
|
}
|
|
6494
6543
|
const result = await performAutopilotWake(ctx, effectiveRoot, settings, { force: true });
|
|
6495
|
-
sendJson$
|
|
6544
|
+
sendJson$2(res, result.ok ? 200 : 400, result);
|
|
6496
6545
|
return;
|
|
6497
6546
|
}
|
|
6498
6547
|
if (req.method !== "GET") {
|
|
6499
|
-
sendJson$
|
|
6548
|
+
sendJson$2(res, 405, { error: "method not allowed" });
|
|
6500
6549
|
return;
|
|
6501
6550
|
}
|
|
6502
6551
|
const url = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
@@ -6508,12 +6557,12 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
6508
6557
|
const { getAutopilotStatus, collectAutopilotCccs } = await Promise.resolve().then(() => autopilot_trajectory_exports);
|
|
6509
6558
|
const effectiveRoot = url.searchParams.get("ccc")?.trim() ?? (!url.searchParams.get("workspace") && !url.searchParams.get("sessionId") ? collectAutopilotCccs(ctx)[0] : null) ?? root;
|
|
6510
6559
|
if (!effectiveRoot) {
|
|
6511
|
-
sendJson$
|
|
6560
|
+
sendJson$2(res, 200, { status: null });
|
|
6512
6561
|
return;
|
|
6513
6562
|
}
|
|
6514
|
-
sendJson$
|
|
6563
|
+
sendJson$2(res, 200, { status: getAutopilotStatus(effectiveRoot) });
|
|
6515
6564
|
} catch (err) {
|
|
6516
|
-
sendJson$
|
|
6565
|
+
sendJson$2(res, 400, { error: err.message ?? String(err) });
|
|
6517
6566
|
}
|
|
6518
6567
|
}
|
|
6519
6568
|
});
|
|
@@ -6523,14 +6572,14 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
6523
6572
|
handler: async (req, res) => {
|
|
6524
6573
|
try {
|
|
6525
6574
|
if (req.headers["x-serenity-ui"] !== "1") {
|
|
6526
|
-
sendJson$
|
|
6575
|
+
sendJson$2(res, 403, { error: "微信桥配置仅限 WebUI(client 专用)" });
|
|
6527
6576
|
return;
|
|
6528
6577
|
}
|
|
6529
6578
|
const url = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
6530
6579
|
if (req.method === "GET") {
|
|
6531
6580
|
const chk = requireCcc(url.searchParams.get("ccc") ?? void 0);
|
|
6532
6581
|
if (!chk.ok) {
|
|
6533
|
-
sendJson$
|
|
6582
|
+
sendJson$2(res, 400, { error: chk.error });
|
|
6534
6583
|
return;
|
|
6535
6584
|
}
|
|
6536
6585
|
const { readWeixinSettings } = await import("./weixin-route-BJ4g4ZNq.js").then((n) => n.u);
|
|
@@ -6544,7 +6593,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
6544
6593
|
enabled: a.enabled !== false,
|
|
6545
6594
|
bound: readWeixinCredential(chk.root, a.accountId) !== null
|
|
6546
6595
|
}));
|
|
6547
|
-
sendJson$
|
|
6596
|
+
sendJson$2(res, 200, {
|
|
6548
6597
|
enabled: settings.enabled === true,
|
|
6549
6598
|
botType: settings.botType ?? void 0,
|
|
6550
6599
|
accounts,
|
|
@@ -6554,19 +6603,19 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
6554
6603
|
return;
|
|
6555
6604
|
}
|
|
6556
6605
|
if (req.method !== "POST") {
|
|
6557
|
-
sendJson$
|
|
6606
|
+
sendJson$2(res, 405, { error: "method not allowed" });
|
|
6558
6607
|
return;
|
|
6559
6608
|
}
|
|
6560
|
-
const raw = await readBody$
|
|
6609
|
+
const raw = await readBody$3(req, 131072);
|
|
6561
6610
|
const body = JSON.parse(raw);
|
|
6562
6611
|
const chk = requireCcc(body.ccc);
|
|
6563
6612
|
if (!chk.ok) {
|
|
6564
|
-
sendJson$
|
|
6613
|
+
sendJson$2(res, 400, { error: chk.error });
|
|
6565
6614
|
return;
|
|
6566
6615
|
}
|
|
6567
6616
|
const root = chk.root;
|
|
6568
6617
|
if (body.action === "login-start") {
|
|
6569
|
-
const { fetchQRCode } = await import("./weixin-api-
|
|
6618
|
+
const { fetchQRCode } = await import("./weixin-api-BMpljodH.js").then((n) => n.l);
|
|
6570
6619
|
const { randomUUID } = await import("node:crypto");
|
|
6571
6620
|
for (const [key, v] of weixinLogins) if (Date.now() - v.startedAt > WEIXIN_LOGIN_TTL_MS) weixinLogins.delete(key);
|
|
6572
6621
|
const { readWeixinSettings } = await import("./weixin-route-BJ4g4ZNq.js").then((n) => n.u);
|
|
@@ -6578,7 +6627,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
6578
6627
|
startedAt: Date.now(),
|
|
6579
6628
|
polling: false
|
|
6580
6629
|
});
|
|
6581
|
-
sendJson$
|
|
6630
|
+
sendJson$2(res, 200, {
|
|
6582
6631
|
qrcode: qr.qrcode,
|
|
6583
6632
|
qrcode_img_content: qr.qrcode_img_content,
|
|
6584
6633
|
loginKey
|
|
@@ -6587,31 +6636,31 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
6587
6636
|
}
|
|
6588
6637
|
if (body.action === "remove-account") {
|
|
6589
6638
|
if (!body.accountId) {
|
|
6590
|
-
sendJson$
|
|
6639
|
+
sendJson$2(res, 400, { error: "missing accountId" });
|
|
6591
6640
|
return;
|
|
6592
6641
|
}
|
|
6593
6642
|
const { removeWeixinAccount } = await import("./weixin-route-BJ4g4ZNq.js").then((n) => n.u);
|
|
6594
6643
|
const { syncCccBridge } = await Promise.resolve().then(() => weixin_bridge_exports);
|
|
6595
6644
|
removeWeixinAccount(root, body.accountId);
|
|
6596
6645
|
syncCccBridge(ctx, root);
|
|
6597
|
-
sendJson$
|
|
6646
|
+
sendJson$2(res, 200, { removed: body.accountId });
|
|
6598
6647
|
return;
|
|
6599
6648
|
}
|
|
6600
6649
|
if (body.action === "save-routes") {
|
|
6601
6650
|
const routes = body.routes ?? [];
|
|
6602
6651
|
if (!Array.isArray(routes) || routes.some((r) => typeof r?.user !== "string" || typeof r?.role !== "string" || r.user === "" || r.role === "")) {
|
|
6603
|
-
sendJson$
|
|
6652
|
+
sendJson$2(res, 400, { error: "invalid routes (expected [{user, role}, ...])" });
|
|
6604
6653
|
return;
|
|
6605
6654
|
}
|
|
6606
6655
|
const { saveWeixinRoutes } = await import("./weixin-route-BJ4g4ZNq.js").then((n) => n.u);
|
|
6607
6656
|
const { readSkiffRoles } = await import("./skiff-role-DlrbHPLD.js").then((n) => n.c);
|
|
6608
6657
|
const roles = readSkiffRoles(root);
|
|
6609
6658
|
for (const r of routes) if (!roles.has(r.role)) {
|
|
6610
|
-
sendJson$
|
|
6659
|
+
sendJson$2(res, 400, { error: `unknown role: ${r.role} (not in ${root} skiff.roles)` });
|
|
6611
6660
|
return;
|
|
6612
6661
|
}
|
|
6613
6662
|
saveWeixinRoutes(root, routes);
|
|
6614
|
-
sendJson$
|
|
6663
|
+
sendJson$2(res, 200, { saved: routes.length });
|
|
6615
6664
|
return;
|
|
6616
6665
|
}
|
|
6617
6666
|
if (body.action === "set-enabled") {
|
|
@@ -6620,7 +6669,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
6620
6669
|
const { syncCccBridge } = await Promise.resolve().then(() => weixin_bridge_exports);
|
|
6621
6670
|
const settings = readWeixinSettings(root);
|
|
6622
6671
|
if (enabled && (settings.accounts ?? []).length === 0) {
|
|
6623
|
-
sendJson$
|
|
6672
|
+
sendJson$2(res, 400, { error: "启用前请先扫码绑定至少一个账号" });
|
|
6624
6673
|
return;
|
|
6625
6674
|
}
|
|
6626
6675
|
setWeixinEnabled(root, enabled);
|
|
@@ -6629,12 +6678,12 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
6629
6678
|
const { stopCccBridge } = await Promise.resolve().then(() => weixin_bridge_exports);
|
|
6630
6679
|
stopCccBridge(root);
|
|
6631
6680
|
}
|
|
6632
|
-
sendJson$
|
|
6681
|
+
sendJson$2(res, 200, { enabled });
|
|
6633
6682
|
return;
|
|
6634
6683
|
}
|
|
6635
|
-
sendJson$
|
|
6684
|
+
sendJson$2(res, 400, { error: `unsupported action: ${body.action ?? ""}` });
|
|
6636
6685
|
} catch (err) {
|
|
6637
|
-
sendJson$
|
|
6686
|
+
sendJson$2(res, 400, { error: err.message ?? String(err) });
|
|
6638
6687
|
}
|
|
6639
6688
|
}
|
|
6640
6689
|
});
|
|
@@ -6644,25 +6693,25 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
6644
6693
|
handler: async (req, res) => {
|
|
6645
6694
|
try {
|
|
6646
6695
|
if (req.headers["x-serenity-ui"] !== "1") {
|
|
6647
|
-
sendJson$
|
|
6696
|
+
sendJson$2(res, 403, { error: "微信桥配置仅限 WebUI(client 专用)" });
|
|
6648
6697
|
return;
|
|
6649
6698
|
}
|
|
6650
6699
|
if (req.method !== "GET") {
|
|
6651
|
-
sendJson$
|
|
6700
|
+
sendJson$2(res, 405, { error: "method not allowed" });
|
|
6652
6701
|
return;
|
|
6653
6702
|
}
|
|
6654
6703
|
const key = new URL(req.url ?? "/", "http://127.0.0.1").searchParams.get("key") ?? "";
|
|
6655
6704
|
const login = weixinLogins.get(key);
|
|
6656
6705
|
if (!login) {
|
|
6657
|
-
sendJson$
|
|
6706
|
+
sendJson$2(res, 404, { error: "login not found or expired" });
|
|
6658
6707
|
return;
|
|
6659
6708
|
}
|
|
6660
6709
|
if (Date.now() - login.startedAt > WEIXIN_LOGIN_TTL_MS) {
|
|
6661
6710
|
weixinLogins.delete(key);
|
|
6662
|
-
sendJson$
|
|
6711
|
+
sendJson$2(res, 200, { status: "expired" });
|
|
6663
6712
|
return;
|
|
6664
6713
|
}
|
|
6665
|
-
const { pollQRStatus } = await import("./weixin-api-
|
|
6714
|
+
const { pollQRStatus } = await import("./weixin-api-BMpljodH.js").then((n) => n.l);
|
|
6666
6715
|
const { readWeixinSettings } = await import("./weixin-route-BJ4g4ZNq.js").then((n) => n.u);
|
|
6667
6716
|
const settings = readWeixinSettings(login.root);
|
|
6668
6717
|
const status = await pollQRStatus({
|
|
@@ -6672,7 +6721,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
6672
6721
|
switch (status.status) {
|
|
6673
6722
|
case "confirmed": {
|
|
6674
6723
|
if (!status.bot_token) {
|
|
6675
|
-
sendJson$
|
|
6724
|
+
sendJson$2(res, 200, {
|
|
6676
6725
|
status: "error",
|
|
6677
6726
|
error: "confirmed but no bot_token"
|
|
6678
6727
|
});
|
|
@@ -6693,7 +6742,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
6693
6742
|
});
|
|
6694
6743
|
syncCccBridge(ctx, login.root);
|
|
6695
6744
|
weixinLogins.delete(key);
|
|
6696
|
-
sendJson$
|
|
6745
|
+
sendJson$2(res, 200, {
|
|
6697
6746
|
status: "confirmed",
|
|
6698
6747
|
accountId,
|
|
6699
6748
|
tokenSaved: true
|
|
@@ -6702,14 +6751,14 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
6702
6751
|
}
|
|
6703
6752
|
case "expired":
|
|
6704
6753
|
weixinLogins.delete(key);
|
|
6705
|
-
sendJson$
|
|
6754
|
+
sendJson$2(res, 200, { status: "expired" });
|
|
6706
6755
|
return;
|
|
6707
6756
|
default:
|
|
6708
|
-
sendJson$
|
|
6757
|
+
sendJson$2(res, 200, { status: status.status ?? "wait" });
|
|
6709
6758
|
return;
|
|
6710
6759
|
}
|
|
6711
6760
|
} catch (err) {
|
|
6712
|
-
sendJson$
|
|
6761
|
+
sendJson$2(res, 400, { error: err.message ?? String(err) });
|
|
6713
6762
|
}
|
|
6714
6763
|
}
|
|
6715
6764
|
});
|
|
@@ -6731,7 +6780,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
6731
6780
|
const root = sessionsRootDir();
|
|
6732
6781
|
if (req.method === "GET") {
|
|
6733
6782
|
const { candidates } = performCleanup(root, cutoffDaysAgo(days), liveIds, { dryRun: true });
|
|
6734
|
-
sendJson$
|
|
6783
|
+
sendJson$2(res, 200, {
|
|
6735
6784
|
root,
|
|
6736
6785
|
olderThanDays: days,
|
|
6737
6786
|
dryRun: true,
|
|
@@ -6745,15 +6794,15 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
6745
6794
|
return;
|
|
6746
6795
|
}
|
|
6747
6796
|
if (req.method !== "POST") {
|
|
6748
|
-
sendJson$
|
|
6797
|
+
sendJson$2(res, 405, { error: "method not allowed" });
|
|
6749
6798
|
return;
|
|
6750
6799
|
}
|
|
6751
6800
|
if (req.headers["x-serenity-ui"] !== "1") {
|
|
6752
|
-
sendJson$
|
|
6801
|
+
sendJson$2(res, 403, { error: "会话清理仅限 WebUI(client 专用)" });
|
|
6753
6802
|
return;
|
|
6754
6803
|
}
|
|
6755
6804
|
const { result } = performCleanup(root, cutoffDaysAgo(days), liveIds);
|
|
6756
|
-
sendJson$
|
|
6805
|
+
sendJson$2(res, 200, {
|
|
6757
6806
|
root,
|
|
6758
6807
|
olderThanDays: days,
|
|
6759
6808
|
dryRun: false,
|
|
@@ -6761,7 +6810,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
6761
6810
|
errors: result.errors
|
|
6762
6811
|
});
|
|
6763
6812
|
} catch (err) {
|
|
6764
|
-
sendJson$
|
|
6813
|
+
sendJson$2(res, 400, { error: err.message ?? String(err) });
|
|
6765
6814
|
}
|
|
6766
6815
|
}
|
|
6767
6816
|
});
|
|
@@ -7346,7 +7395,7 @@ function createDshCookieProvider(connection, authority) {
|
|
|
7346
7395
|
//#endregion
|
|
7347
7396
|
//#region src/gateway.ts
|
|
7348
7397
|
/** 读取请求体(≤ maxBytes;超限 reject) */
|
|
7349
|
-
function readBody$
|
|
7398
|
+
function readBody$2(req, maxBytes) {
|
|
7350
7399
|
return new Promise((resolve, reject) => {
|
|
7351
7400
|
let data = "";
|
|
7352
7401
|
req.on("data", (chunk) => {
|
|
@@ -7471,7 +7520,7 @@ function startGateway(config, getAccounts, getDshCookie) {
|
|
|
7471
7520
|
const session = authed(req);
|
|
7472
7521
|
if (session !== void 0) {
|
|
7473
7522
|
if (req.method === "POST" && isWorkspaceCreatePath(url.pathname)) {
|
|
7474
|
-
readBody$
|
|
7523
|
+
readBody$2(req, 131072).then((body) => {
|
|
7475
7524
|
const { rpcId, path } = parseWorkspaceCreateBody(body);
|
|
7476
7525
|
if (!allowWorkspaceCreate) {
|
|
7477
7526
|
res.writeHead(403, { "content-type": "application/json" });
|
|
@@ -8538,8 +8587,8 @@ function rpcError(id, code, message) {
|
|
|
8538
8587
|
* 实验性质:未开启时零资源占用(无监听、无 agent 创建)。
|
|
8539
8588
|
*/
|
|
8540
8589
|
/** 运行中的 ACP HTTP 服务(单实例;进程级) */
|
|
8541
|
-
let active = null;
|
|
8542
|
-
function readBody(req) {
|
|
8590
|
+
let active$1 = null;
|
|
8591
|
+
function readBody$1(req) {
|
|
8543
8592
|
return new Promise((resolve, reject) => {
|
|
8544
8593
|
const chunks = [];
|
|
8545
8594
|
req.on("data", (c) => chunks.push(c));
|
|
@@ -8547,7 +8596,7 @@ function readBody(req) {
|
|
|
8547
8596
|
req.on("error", reject);
|
|
8548
8597
|
});
|
|
8549
8598
|
}
|
|
8550
|
-
function sendJson(res, status, payload) {
|
|
8599
|
+
function sendJson$1(res, status, payload) {
|
|
8551
8600
|
const body = jscSafeJsonText(JSON.stringify(payload));
|
|
8552
8601
|
res.writeHead(status, {
|
|
8553
8602
|
"Content-Type": "application/json; charset=utf-8",
|
|
@@ -8569,7 +8618,7 @@ function sendHtml(res, html) {
|
|
|
8569
8618
|
* @param defaultRoot 默认 CCC 根(问答页候选发现兜底;resolveSkiffRoot 传入)
|
|
8570
8619
|
*/
|
|
8571
8620
|
async function startAcpHttpServer(ctx, port, defaultRoot) {
|
|
8572
|
-
if (active) return;
|
|
8621
|
+
if (active$1) return;
|
|
8573
8622
|
const server = createServer((req, res) => {
|
|
8574
8623
|
handle(ctx, defaultRoot ?? "", req, res);
|
|
8575
8624
|
});
|
|
@@ -8577,7 +8626,7 @@ async function startAcpHttpServer(ctx, port, defaultRoot) {
|
|
|
8577
8626
|
server.once("error", reject);
|
|
8578
8627
|
server.listen(port, "127.0.0.1", () => resolve());
|
|
8579
8628
|
});
|
|
8580
|
-
active = {
|
|
8629
|
+
active$1 = {
|
|
8581
8630
|
server,
|
|
8582
8631
|
port
|
|
8583
8632
|
};
|
|
@@ -8588,11 +8637,11 @@ async function startAcpHttpServer(ctx, port, defaultRoot) {
|
|
|
8588
8637
|
console.log(`[serenity-hooks] ✓ ACP HTTP: http://127.0.0.1:${port}(${faces.join(" + ") || "未开启任何面"};session/new 支持 {ccc, role, sessionId?})`);
|
|
8589
8638
|
}
|
|
8590
8639
|
function stopAcpHttpServer() {
|
|
8591
|
-
if (!active) return;
|
|
8640
|
+
if (!active$1) return;
|
|
8592
8641
|
try {
|
|
8593
|
-
active.server.close();
|
|
8642
|
+
active$1.server.close();
|
|
8594
8643
|
} catch {}
|
|
8595
|
-
active = null;
|
|
8644
|
+
active$1 = null;
|
|
8596
8645
|
}
|
|
8597
8646
|
async function handle(ctx, defaultRoot, req, res) {
|
|
8598
8647
|
try {
|
|
@@ -8601,14 +8650,14 @@ async function handle(ctx, defaultRoot, req, res) {
|
|
|
8601
8650
|
const ip = req.headers["x-forwarded-for"]?.split(",")[0]?.trim() || req.socket.remoteAddress || "unknown";
|
|
8602
8651
|
if (req.method === "POST" && url === "/") {
|
|
8603
8652
|
if (!s.acpEnabled) {
|
|
8604
|
-
sendJson(res, 403, { error: "ACP JSON-RPC disabled (enable in Serenity settings)" });
|
|
8653
|
+
sendJson$1(res, 403, { error: "ACP JSON-RPC disabled (enable in Serenity settings)" });
|
|
8605
8654
|
return;
|
|
8606
8655
|
}
|
|
8607
8656
|
let body;
|
|
8608
8657
|
try {
|
|
8609
|
-
body = JSON.parse(await readBody(req));
|
|
8658
|
+
body = JSON.parse(await readBody$1(req));
|
|
8610
8659
|
} catch {
|
|
8611
|
-
sendJson(res, 400, [{
|
|
8660
|
+
sendJson$1(res, 400, [{
|
|
8612
8661
|
jsonrpc: "2.0",
|
|
8613
8662
|
id: null,
|
|
8614
8663
|
error: {
|
|
@@ -8618,7 +8667,7 @@ async function handle(ctx, defaultRoot, req, res) {
|
|
|
8618
8667
|
}]);
|
|
8619
8668
|
return;
|
|
8620
8669
|
}
|
|
8621
|
-
sendJson(res, 200, await dispatchRpc(new AcpServer(ctx), body));
|
|
8670
|
+
sendJson$1(res, 200, await dispatchRpc(new AcpServer(ctx), body));
|
|
8622
8671
|
return;
|
|
8623
8672
|
}
|
|
8624
8673
|
if (!s.publicAskEnabled) {
|
|
@@ -8627,10 +8676,10 @@ async function handle(ctx, defaultRoot, req, res) {
|
|
|
8627
8676
|
return;
|
|
8628
8677
|
}
|
|
8629
8678
|
if (req.method === "POST" && (url === "/ask" || url.startsWith("/c/"))) {
|
|
8630
|
-
sendJson(res, 403, { error: "public ask disabled (enable in Serenity settings)" });
|
|
8679
|
+
sendJson$1(res, 403, { error: "public ask disabled (enable in Serenity settings)" });
|
|
8631
8680
|
return;
|
|
8632
8681
|
}
|
|
8633
|
-
sendJson(res, 404, { error: "not found" });
|
|
8682
|
+
sendJson$1(res, 404, { error: "not found" });
|
|
8634
8683
|
return;
|
|
8635
8684
|
}
|
|
8636
8685
|
const containerAllowed = (name) => {
|
|
@@ -8661,16 +8710,16 @@ async function handle(ctx, defaultRoot, req, res) {
|
|
|
8661
8710
|
const name = decodeURIComponent(aMatch[1] ?? "");
|
|
8662
8711
|
const ccc = (await discoverCccs(ctx, defaultRoot || (findSerenityRoot(process.cwd()) ?? ""))).find((c) => c.name === name);
|
|
8663
8712
|
if (!ccc || !containerAllowed(name)) {
|
|
8664
|
-
sendJson(res, 403, { error: `container "${name}" is not open for public ask` });
|
|
8713
|
+
sendJson$1(res, 403, { error: `container "${name}" is not open for public ask` });
|
|
8665
8714
|
return;
|
|
8666
8715
|
}
|
|
8667
8716
|
let body;
|
|
8668
8717
|
try {
|
|
8669
|
-
const parsed = JSON.parse(await readBody(req));
|
|
8718
|
+
const parsed = JSON.parse(await readBody$1(req));
|
|
8670
8719
|
if (parsed === null || typeof parsed !== "object") throw new Error("not an object");
|
|
8671
8720
|
body = parsed;
|
|
8672
8721
|
} catch {
|
|
8673
|
-
sendJson(res, 400, { error: "invalid JSON body" });
|
|
8722
|
+
sendJson$1(res, 400, { error: "invalid JSON body" });
|
|
8674
8723
|
return;
|
|
8675
8724
|
}
|
|
8676
8725
|
await handleAskParsed(ctx, ccc, body, res, ip);
|
|
@@ -8679,11 +8728,11 @@ async function handle(ctx, defaultRoot, req, res) {
|
|
|
8679
8728
|
if (req.method === "POST" && url === "/ask") {
|
|
8680
8729
|
let body;
|
|
8681
8730
|
try {
|
|
8682
|
-
const parsed = JSON.parse(await readBody(req));
|
|
8731
|
+
const parsed = JSON.parse(await readBody$1(req));
|
|
8683
8732
|
if (parsed === null || typeof parsed !== "object") throw new Error("not an object");
|
|
8684
8733
|
body = parsed;
|
|
8685
8734
|
} catch {
|
|
8686
|
-
sendJson(res, 400, { error: "invalid JSON body" });
|
|
8735
|
+
sendJson$1(res, 400, { error: "invalid JSON body" });
|
|
8687
8736
|
return;
|
|
8688
8737
|
}
|
|
8689
8738
|
const cccValue = typeof body.ccc === "string" && body.ccc !== "" ? body.ccc : void 0;
|
|
@@ -8699,15 +8748,15 @@ async function handle(ctx, defaultRoot, req, res) {
|
|
|
8699
8748
|
};
|
|
8700
8749
|
}
|
|
8701
8750
|
if (!ccc || !containerAllowed(ccc.name)) {
|
|
8702
|
-
sendJson(res, 403, { error: "container is not open for public ask" });
|
|
8751
|
+
sendJson$1(res, 403, { error: "container is not open for public ask" });
|
|
8703
8752
|
return;
|
|
8704
8753
|
}
|
|
8705
8754
|
await handleAskParsed(ctx, ccc, body, res, ip);
|
|
8706
8755
|
return;
|
|
8707
8756
|
}
|
|
8708
|
-
sendJson(res, 404, { error: "not found" });
|
|
8757
|
+
sendJson$1(res, 404, { error: "not found" });
|
|
8709
8758
|
} catch (err) {
|
|
8710
|
-
sendJson(res, 500, { error: err?.message ?? String(err) });
|
|
8759
|
+
sendJson$1(res, 500, { error: err?.message ?? String(err) });
|
|
8711
8760
|
}
|
|
8712
8761
|
}
|
|
8713
8762
|
/**
|
|
@@ -8719,12 +8768,12 @@ async function handle(ctx, defaultRoot, req, res) {
|
|
|
8719
8768
|
async function handleAskParsed(ctx, ccc, body, res, ip) {
|
|
8720
8769
|
const key = typeof body.key === "string" ? body.key : void 0;
|
|
8721
8770
|
if (isPublicAskIpLocked(ip)) {
|
|
8722
|
-
sendJson(res, 429, { error: "too many failed attempts — temporarily locked (retry later)" });
|
|
8771
|
+
sendJson$1(res, 429, { error: "too many failed attempts — temporarily locked (retry later)" });
|
|
8723
8772
|
return;
|
|
8724
8773
|
}
|
|
8725
8774
|
if (!verifyPublicAskKey(key)) {
|
|
8726
8775
|
const remaining = recordPublicAskFail(ip);
|
|
8727
|
-
sendJson(res, remaining > 0 ? 429 : 401, { error: remaining > 0 ? `too many failed attempts — locked for ${Math.ceil(remaining / 6e4)} min` : "invalid or missing key" });
|
|
8776
|
+
sendJson$1(res, remaining > 0 ? 429 : 401, { error: remaining > 0 ? `too many failed attempts — locked for ${Math.ceil(remaining / 6e4)} min` : "invalid or missing key" });
|
|
8728
8777
|
return;
|
|
8729
8778
|
}
|
|
8730
8779
|
resetPublicAskIpFail(ip);
|
|
@@ -8733,7 +8782,7 @@ async function handleAskParsed(ctx, ccc, body, res, ip) {
|
|
|
8733
8782
|
const question = typeof body.question === "string" ? body.question : "";
|
|
8734
8783
|
const sessionId = typeof body.sessionId === "string" && body.sessionId !== "" ? body.sessionId : void 0;
|
|
8735
8784
|
if (!question.trim()) {
|
|
8736
|
-
sendJson(res, 400, { error: "empty question" });
|
|
8785
|
+
sendJson$1(res, 400, { error: "empty question" });
|
|
8737
8786
|
return;
|
|
8738
8787
|
}
|
|
8739
8788
|
const roles = readSkiffRoles(root);
|
|
@@ -8741,7 +8790,7 @@ async function handleAskParsed(ctx, ccc, body, res, ip) {
|
|
|
8741
8790
|
const effectiveRole = roleName && explicit ? roleName : roles.keys().next().value;
|
|
8742
8791
|
const effectiveCfg = (roleName && explicit ? explicit : effectiveRole ? roles.get(effectiveRole) : void 0) ?? void 0;
|
|
8743
8792
|
if (!effectiveRole || !effectiveCfg) {
|
|
8744
|
-
sendJson(res, 400, { error: `no skiff role available in ccc: ${root}` });
|
|
8793
|
+
sendJson$1(res, 400, { error: `no skiff role available in ccc: ${root}` });
|
|
8745
8794
|
return;
|
|
8746
8795
|
}
|
|
8747
8796
|
let agent;
|
|
@@ -8759,7 +8808,7 @@ async function handleAskParsed(ctx, ccc, body, res, ip) {
|
|
|
8759
8808
|
agent = (await createSkiffAgent(ctx, root, effectiveRole, effectiveCfg, hc?.defaultModel)).agent;
|
|
8760
8809
|
}
|
|
8761
8810
|
const result = await askSkiff(ctx, agent, question, void 0, { includeTrajectory: false });
|
|
8762
|
-
sendJson(res, 200, {
|
|
8811
|
+
sendJson$1(res, 200, {
|
|
8763
8812
|
answer: result.answer,
|
|
8764
8813
|
answer_html: renderSkiffMarkdown(result.answer, true),
|
|
8765
8814
|
sessionId: result.sessionId,
|
|
@@ -9140,7 +9189,8 @@ function buildOutgoingHookEvent(input) {
|
|
|
9140
9189
|
userId: input.userId,
|
|
9141
9190
|
sessionId: input.sessionId,
|
|
9142
9191
|
role: input.role,
|
|
9143
|
-
reply: input.reply
|
|
9192
|
+
reply: input.reply,
|
|
9193
|
+
...input.source === "proactive" ? { source: "proactive" } : {}
|
|
9144
9194
|
};
|
|
9145
9195
|
}
|
|
9146
9196
|
/**
|
|
@@ -9255,6 +9305,7 @@ function invokeWeixinHook(root, hookRel, event) {
|
|
|
9255
9305
|
var weixin_bridge_exports = /* @__PURE__ */ __exportAll({
|
|
9256
9306
|
handleIncoming: () => handleIncoming,
|
|
9257
9307
|
registerWeixinBridge: () => registerWeixinBridge,
|
|
9308
|
+
sendProactiveText: () => sendProactiveText,
|
|
9258
9309
|
stopAllBridges: () => stopAllBridges,
|
|
9259
9310
|
stopCccBridge: () => stopCccBridge,
|
|
9260
9311
|
syncCccBridge: () => syncCccBridge,
|
|
@@ -9384,7 +9435,7 @@ async function handleIncoming(ctx, root, accountId, cred, msg) {
|
|
|
9384
9435
|
const existing = getSkiffAgent(sessionId);
|
|
9385
9436
|
const hc = readHandymanConfig(root);
|
|
9386
9437
|
if (existing) try {
|
|
9387
|
-
ensureSkiffSession(root, existing, roleName, role);
|
|
9438
|
+
ensureSkiffSession(root, existing, roleName, role, true);
|
|
9388
9439
|
} catch (err) {
|
|
9389
9440
|
console.warn(`[serenity-hooks] weixin-bridge ensure 失败(不影响处理): ${String(err?.message ?? err)}`);
|
|
9390
9441
|
}
|
|
@@ -9535,11 +9586,93 @@ function stopCccBridge(root) {
|
|
|
9535
9586
|
for (const loop of bridge.loops.values()) loop.stopped = true;
|
|
9536
9587
|
bridges.delete(root);
|
|
9537
9588
|
}
|
|
9538
|
-
/**
|
|
9589
|
+
/**
|
|
9590
|
+
* 停止全部桥(插件 dispose)
|
|
9591
|
+
*/
|
|
9539
9592
|
function stopAllBridges() {
|
|
9540
9593
|
for (const bridge of bridges.values()) for (const loop of bridge.loops.values()) loop.stopped = true;
|
|
9541
9594
|
bridges.clear();
|
|
9542
9595
|
}
|
|
9596
|
+
/**
|
|
9597
|
+
* 以 bot 身份主动给指定用户发文本(**不经用户消息触发**)。
|
|
9598
|
+
*
|
|
9599
|
+
* 为什么放在桥里而不是让 CCC 自己直连 iLink(用户拍板 A 方案):
|
|
9600
|
+
* ① **记录归桥**——发送成功后触发既有 outgoing hook,`_weixin-logs/*.jsonl` 与对话回复
|
|
9601
|
+
* 同源同格式(事件多一个 `source: 'proactive'` 标记);② 协议/账号/凭据解析单一真相源,
|
|
9602
|
+
* CCC 侧不必重复实现 sendmessage;③ 多账号可选。
|
|
9603
|
+
*
|
|
9604
|
+
* 记录一致性:`sessionId` 取 `weixinSessionIdFor(toUserId)`——与该用户平时对话**同一条轨迹**,
|
|
9605
|
+
* 记录里能直接按会话串联;`role` 取该 CCC 路由命中角色(未命中 → 空串)。
|
|
9606
|
+
*
|
|
9607
|
+
* 主动消息不带 context_token(iLink 接受 bot 主动发起;带 token 的路径见 handleIncoming 回复)。
|
|
9608
|
+
* @param input CCC 根 + 目标用户 + 文本 +(可选)账号
|
|
9609
|
+
* @returns 成功含 accountId/userId/sessionId/role;失败含稳定 code 与可行动提示(不抛错)
|
|
9610
|
+
*/
|
|
9611
|
+
async function sendProactiveText(input) {
|
|
9612
|
+
const { root, toUserId } = input;
|
|
9613
|
+
const settings = readWeixinSettings(root);
|
|
9614
|
+
if (!settings.enabled) return {
|
|
9615
|
+
ok: false,
|
|
9616
|
+
code: "BRIDGE_DISABLED",
|
|
9617
|
+
error: `微信桥未启用(ccc=${root})`,
|
|
9618
|
+
remediation: "在 CCC 的 .opencode/serenity.json weixin.enabled 打开"
|
|
9619
|
+
};
|
|
9620
|
+
const accounts = (settings.accounts ?? []).filter((a) => a.enabled !== false);
|
|
9621
|
+
if (accounts.length === 0) return {
|
|
9622
|
+
ok: false,
|
|
9623
|
+
code: "NO_ACCOUNT",
|
|
9624
|
+
error: "微信桥未配置任何启用账号",
|
|
9625
|
+
remediation: "在设置面板「微信桥」扫码绑定账号"
|
|
9626
|
+
};
|
|
9627
|
+
const accountId = input.accountId ?? accounts[0].accountId;
|
|
9628
|
+
if (!accounts.some((a) => a.accountId === accountId)) return {
|
|
9629
|
+
ok: false,
|
|
9630
|
+
code: "ACCOUNT_NOT_FOUND",
|
|
9631
|
+
error: `账号不存在或未启用: ${accountId}`,
|
|
9632
|
+
remediation: `可用账号: ${accounts.map((a) => a.accountId).join(", ")}`
|
|
9633
|
+
};
|
|
9634
|
+
const cred = readWeixinCredential(root, accountId);
|
|
9635
|
+
if (!cred) return {
|
|
9636
|
+
ok: false,
|
|
9637
|
+
code: "ACCOUNT_NOT_BOUND",
|
|
9638
|
+
error: `账号未绑定(无凭据): ${accountId}`,
|
|
9639
|
+
remediation: "在设置面板重新扫码绑定"
|
|
9640
|
+
};
|
|
9641
|
+
const sessionId = weixinSessionIdFor(toUserId);
|
|
9642
|
+
const role = matchWeixinRoute(settings.routes ?? [], toUserId) ?? "";
|
|
9643
|
+
try {
|
|
9644
|
+
await sendTextMessage({
|
|
9645
|
+
baseUrl: cred.baseUrl,
|
|
9646
|
+
token: cred.token,
|
|
9647
|
+
toUserId,
|
|
9648
|
+
text: input.text
|
|
9649
|
+
});
|
|
9650
|
+
} catch (err) {
|
|
9651
|
+
return {
|
|
9652
|
+
ok: false,
|
|
9653
|
+
code: "SEND_FAILED",
|
|
9654
|
+
error: `发送失败: ${err instanceof Error ? err.message : String(err)}`
|
|
9655
|
+
};
|
|
9656
|
+
}
|
|
9657
|
+
if (settings.hook) invokeWeixinHook(root, settings.hook, buildOutgoingHookEvent({
|
|
9658
|
+
cccRoot: root,
|
|
9659
|
+
accountId,
|
|
9660
|
+
userId: toUserId,
|
|
9661
|
+
sessionId,
|
|
9662
|
+
role,
|
|
9663
|
+
reply: markdownToPlainText(input.text),
|
|
9664
|
+
source: "proactive"
|
|
9665
|
+
})).catch((err) => {
|
|
9666
|
+
console.log(`[serenity-hooks] weixin hook outgoing(proactive) error: ${err instanceof Error ? err.message : String(err)}`);
|
|
9667
|
+
});
|
|
9668
|
+
return {
|
|
9669
|
+
ok: true,
|
|
9670
|
+
accountId,
|
|
9671
|
+
userId: toUserId,
|
|
9672
|
+
sessionId,
|
|
9673
|
+
role
|
|
9674
|
+
};
|
|
9675
|
+
}
|
|
9543
9676
|
/** 桥状态快照(面板数据源):每 CCC → 每账号 → 轮询健康 */
|
|
9544
9677
|
function weixinBridgeStatus() {
|
|
9545
9678
|
const out = [];
|
|
@@ -9571,6 +9704,363 @@ function registerWeixinBridge(ctx) {
|
|
|
9571
9704
|
} catch {}
|
|
9572
9705
|
}
|
|
9573
9706
|
//#endregion
|
|
9707
|
+
//#region src/weixin-send-api.ts
|
|
9708
|
+
/**
|
|
9709
|
+
* weixin-send-api.ts — 主动发送入口(S142 v1.30.9,用户需求"微信桥支持被调用发消息给指定用户")
|
|
9710
|
+
*
|
|
9711
|
+
* 形态(用户拍板 A2):**独立监听器,只绑 127.0.0.1**(默认 3082,plugin 全局配置 weixinApi)。
|
|
9712
|
+
* 为什么不挂在主 webServer(3080) 上:3080 会被 3081 公网网关原样反代(含全部请求头),
|
|
9713
|
+
* 而 3081 的登录态属于"家庭账号"——一旦挂上去,任何已登录的外部用户都能冒充 bot 发消息。
|
|
9714
|
+
* 独立 loopback 监听器则**不经网关、公网不可达**,因此不做共享密钥;仍校验来源地址。
|
|
9715
|
+
*
|
|
9716
|
+
* 调用者 = CCC 自己的 MSM(`msm weixin-send ...`):ACC **不新增工具**,只多一个本机 HTTP 面。
|
|
9717
|
+
* 发送与记录都经 `weixin-bridge.sendProactiveText` → 复用既有 outgoing hook(`source: 'proactive'`)。
|
|
9718
|
+
*
|
|
9719
|
+
* 契约:
|
|
9720
|
+
* POST /send { ccc: "<名称|绝对路径>", user: "<from_user_id>", text: "...", accountId?: "wechat-1" }
|
|
9721
|
+
* → 200 { ok: true, accountId, userId, sessionId, role }
|
|
9722
|
+
* → 4xx/5xx { ok: false, code, error, remediation? }
|
|
9723
|
+
* GET /health → 200 { ok: true, port }
|
|
9724
|
+
*
|
|
9725
|
+
* 职责切分(E↑):**别名 → id 的解析归 CCC**(MSM 读该 CCC localstore 的 WEIXIN_USER_*);
|
|
9726
|
+
* 本入口只接受 iLink 形式的 id。协议、账号选择、记录归桥。
|
|
9727
|
+
*/
|
|
9728
|
+
/** 请求体上限(文本消息远小于此;防滥用) */
|
|
9729
|
+
const MAX_BODY_BYTES = 65536;
|
|
9730
|
+
/** 文本长度上限(微信侧单条消息实践上限;超限拒绝而非静默截断) */
|
|
9731
|
+
const WEIXIN_SEND_MAX_TEXT = 4e3;
|
|
9732
|
+
/** 运行中的入口(null = 未启动) */
|
|
9733
|
+
let active = null;
|
|
9734
|
+
/** 当前入口地址(MSM 环境变量注入用;未启动 → null)——状态在叶模块(msm-ops 零依赖读取) */
|
|
9735
|
+
/** 测试辅助:读取当前监听端口(生产零调用) */
|
|
9736
|
+
function weixinSendPort() {
|
|
9737
|
+
return active?.port ?? null;
|
|
9738
|
+
}
|
|
9739
|
+
/** 来源必须是 loopback(127.0.0.1 / ::1 / IPv4-mapped) */
|
|
9740
|
+
function isLoopbackAddress(address) {
|
|
9741
|
+
if (typeof address !== "string" || address === "") return false;
|
|
9742
|
+
const a = address.startsWith("::ffff:") ? address.slice(7) : address;
|
|
9743
|
+
return a === "127.0.0.1" || a === "::1";
|
|
9744
|
+
}
|
|
9745
|
+
/**
|
|
9746
|
+
* 校验并归一 /send 请求体(纯函数,可测)。
|
|
9747
|
+
* 只做**结构**校验:ccc 必填(无隐式当前 CCC——用户拍板)、user 必须是 iLink id 形式、text 非空且不超限。
|
|
9748
|
+
*/
|
|
9749
|
+
function parseSendRequest(body) {
|
|
9750
|
+
if (body === null || typeof body !== "object") return {
|
|
9751
|
+
ok: false,
|
|
9752
|
+
error: "body must be a JSON object"
|
|
9753
|
+
};
|
|
9754
|
+
const o = body;
|
|
9755
|
+
const ccc = typeof o.ccc === "string" ? o.ccc.trim() : "";
|
|
9756
|
+
if (ccc === "") return {
|
|
9757
|
+
ok: false,
|
|
9758
|
+
error: "missing \"ccc\"",
|
|
9759
|
+
remediation: "ccc = CCC 名称或绝对路径(必填,无默认值)"
|
|
9760
|
+
};
|
|
9761
|
+
const user = typeof o.user === "string" ? o.user.trim() : "";
|
|
9762
|
+
if (user === "") return {
|
|
9763
|
+
ok: false,
|
|
9764
|
+
error: "missing \"user\"",
|
|
9765
|
+
remediation: "user = iLink from_user_id(形如 xxx@im.wechat);别名解析归调用方"
|
|
9766
|
+
};
|
|
9767
|
+
if (!user.includes("@")) return {
|
|
9768
|
+
ok: false,
|
|
9769
|
+
error: `invalid "user": ${user}`,
|
|
9770
|
+
remediation: "需为 iLink from_user_id(含 @);别名(yh/danica)请在调用方解析"
|
|
9771
|
+
};
|
|
9772
|
+
const text = typeof o.text === "string" ? o.text.trim() : "";
|
|
9773
|
+
if (text === "") return {
|
|
9774
|
+
ok: false,
|
|
9775
|
+
error: "missing \"text\"",
|
|
9776
|
+
remediation: "text = 要发送的文本(非空)"
|
|
9777
|
+
};
|
|
9778
|
+
if (text.length > 4e3) return {
|
|
9779
|
+
ok: false,
|
|
9780
|
+
error: `text too long: ${text.length} > ${WEIXIN_SEND_MAX_TEXT}`
|
|
9781
|
+
};
|
|
9782
|
+
const accountId = typeof o.accountId === "string" && o.accountId.trim() !== "" ? o.accountId.trim() : void 0;
|
|
9783
|
+
return {
|
|
9784
|
+
ok: true,
|
|
9785
|
+
ccc,
|
|
9786
|
+
user,
|
|
9787
|
+
text,
|
|
9788
|
+
...accountId ? { accountId } : {}
|
|
9789
|
+
};
|
|
9790
|
+
}
|
|
9791
|
+
/**
|
|
9792
|
+
* 按名称/路径解析 CCC 根(纯函数,可测)。
|
|
9793
|
+
* 匹配顺序:① 绝对路径精确/上溯 ② 目录名 ③ `.serenity` 首行名(大小写不敏感)。
|
|
9794
|
+
* 歧义(多个同名)→ 报错并列出候选(不猜——用户拍板"ccc 必须指定")。
|
|
9795
|
+
*/
|
|
9796
|
+
function matchCcc(input, candidates) {
|
|
9797
|
+
const trimmed = input.trim();
|
|
9798
|
+
if (trimmed === "") return {
|
|
9799
|
+
ok: false,
|
|
9800
|
+
error: "empty ccc"
|
|
9801
|
+
};
|
|
9802
|
+
if (trimmed.startsWith("/")) {
|
|
9803
|
+
const root = findSerenityRoot(trimmed);
|
|
9804
|
+
if (!root) return {
|
|
9805
|
+
ok: false,
|
|
9806
|
+
error: `no CCC found from path: ${trimmed}`,
|
|
9807
|
+
remediation: "确认该路径在某个 CCC 内(含 .serenity 标记)"
|
|
9808
|
+
};
|
|
9809
|
+
return {
|
|
9810
|
+
ok: true,
|
|
9811
|
+
root
|
|
9812
|
+
};
|
|
9813
|
+
}
|
|
9814
|
+
const lower = trimmed.toLowerCase();
|
|
9815
|
+
const exactPath = candidates.find((c) => c.root.toLowerCase() === lower);
|
|
9816
|
+
if (exactPath) return {
|
|
9817
|
+
ok: true,
|
|
9818
|
+
root: exactPath.root
|
|
9819
|
+
};
|
|
9820
|
+
const byName = candidates.filter((c) => c.dirName.toLowerCase() === lower || c.cccName !== null && c.cccName.toLowerCase() === lower);
|
|
9821
|
+
if (byName.length === 1) return {
|
|
9822
|
+
ok: true,
|
|
9823
|
+
root: byName[0].root
|
|
9824
|
+
};
|
|
9825
|
+
if (byName.length > 1) return {
|
|
9826
|
+
ok: false,
|
|
9827
|
+
error: `ambiguous ccc name: ${trimmed}`,
|
|
9828
|
+
remediation: `同名多个,请用绝对路径:${byName.map((c) => c.root).join(" | ")}`
|
|
9829
|
+
};
|
|
9830
|
+
return {
|
|
9831
|
+
ok: false,
|
|
9832
|
+
error: `unknown ccc: ${trimmed}`,
|
|
9833
|
+
remediation: candidates.length === 0 ? "当前进程未发现任何 CCC(打开一个 CCC 会话后重试)" : `已知 CCC: ${candidates.map((c) => c.dirName).join(", ")}`
|
|
9834
|
+
};
|
|
9835
|
+
}
|
|
9836
|
+
/** 组装候选列表(discoverCccs 投影 + `.serenity` 名;动态 import 保持本模块静态依赖轻量) */
|
|
9837
|
+
async function collectCandidates(ctx) {
|
|
9838
|
+
const { discoverCccs } = await import("./skiff-debug-TONJlpgF.js").then((n) => n.i);
|
|
9839
|
+
const entries = await discoverCccs(ctx, process.cwd());
|
|
9840
|
+
const seen = /* @__PURE__ */ new Set();
|
|
9841
|
+
const out = [];
|
|
9842
|
+
for (const e of entries) {
|
|
9843
|
+
if (!e?.root || seen.has(e.root)) continue;
|
|
9844
|
+
seen.add(e.root);
|
|
9845
|
+
out.push({
|
|
9846
|
+
root: e.root,
|
|
9847
|
+
dirName: e.name || basename(e.root),
|
|
9848
|
+
cccName: readCccName$2(e.root)
|
|
9849
|
+
});
|
|
9850
|
+
}
|
|
9851
|
+
return out;
|
|
9852
|
+
}
|
|
9853
|
+
function sendJson(res, status, payload) {
|
|
9854
|
+
const body = JSON.stringify(payload);
|
|
9855
|
+
res.writeHead(status, {
|
|
9856
|
+
"content-type": "application/json; charset=utf-8",
|
|
9857
|
+
"content-length": Buffer.byteLength(body)
|
|
9858
|
+
});
|
|
9859
|
+
res.end(body);
|
|
9860
|
+
}
|
|
9861
|
+
async function readBody(req) {
|
|
9862
|
+
const chunks = [];
|
|
9863
|
+
let total = 0;
|
|
9864
|
+
for await (const chunk of req) {
|
|
9865
|
+
const buf = chunk;
|
|
9866
|
+
total += buf.length;
|
|
9867
|
+
if (total > MAX_BODY_BYTES) throw new Error(`body too large (> ${MAX_BODY_BYTES} bytes)`);
|
|
9868
|
+
chunks.push(buf);
|
|
9869
|
+
}
|
|
9870
|
+
return Buffer.concat(chunks).toString("utf-8");
|
|
9871
|
+
}
|
|
9872
|
+
/** HTTP 状态映射(失败 code → 语义化状态;调用方按 code 判定,不看文案) */
|
|
9873
|
+
function statusForCode(code) {
|
|
9874
|
+
switch (code) {
|
|
9875
|
+
case "ACCOUNT_NOT_BOUND":
|
|
9876
|
+
case "BRIDGE_DISABLED":
|
|
9877
|
+
case "NO_ACCOUNT": return 409;
|
|
9878
|
+
case "ACCOUNT_NOT_FOUND": return 400;
|
|
9879
|
+
case "SEND_FAILED": return 502;
|
|
9880
|
+
default: return 400;
|
|
9881
|
+
}
|
|
9882
|
+
}
|
|
9883
|
+
/** 单请求处理(导出供测试直接驱动,不经真实端口) */
|
|
9884
|
+
async function handleWeixinSendRequest(ctx, req, res) {
|
|
9885
|
+
if (!isLoopbackAddress(req.socket?.remoteAddress)) {
|
|
9886
|
+
sendJson(res, 403, {
|
|
9887
|
+
ok: false,
|
|
9888
|
+
code: "FORBIDDEN",
|
|
9889
|
+
error: "only loopback clients allowed"
|
|
9890
|
+
});
|
|
9891
|
+
return;
|
|
9892
|
+
}
|
|
9893
|
+
const path = (req.url ?? "/").split("?")[0];
|
|
9894
|
+
if (req.method === "GET" && path === "/health") {
|
|
9895
|
+
sendJson(res, 200, {
|
|
9896
|
+
ok: true,
|
|
9897
|
+
port: weixinSendPort()
|
|
9898
|
+
});
|
|
9899
|
+
return;
|
|
9900
|
+
}
|
|
9901
|
+
if (path !== "/send") {
|
|
9902
|
+
sendJson(res, 404, {
|
|
9903
|
+
ok: false,
|
|
9904
|
+
code: "NOT_FOUND",
|
|
9905
|
+
error: `unknown path: ${path}`
|
|
9906
|
+
});
|
|
9907
|
+
return;
|
|
9908
|
+
}
|
|
9909
|
+
if (req.method !== "POST") {
|
|
9910
|
+
sendJson(res, 405, {
|
|
9911
|
+
ok: false,
|
|
9912
|
+
code: "METHOD_NOT_ALLOWED",
|
|
9913
|
+
error: "use POST /send"
|
|
9914
|
+
});
|
|
9915
|
+
return;
|
|
9916
|
+
}
|
|
9917
|
+
let raw;
|
|
9918
|
+
try {
|
|
9919
|
+
raw = await readBody(req);
|
|
9920
|
+
} catch (err) {
|
|
9921
|
+
sendJson(res, 413, {
|
|
9922
|
+
ok: false,
|
|
9923
|
+
code: "BODY_TOO_LARGE",
|
|
9924
|
+
error: String(err?.message ?? err)
|
|
9925
|
+
});
|
|
9926
|
+
return;
|
|
9927
|
+
}
|
|
9928
|
+
let body;
|
|
9929
|
+
try {
|
|
9930
|
+
body = JSON.parse(raw === "" ? "{}" : raw);
|
|
9931
|
+
} catch (err) {
|
|
9932
|
+
sendJson(res, 400, {
|
|
9933
|
+
ok: false,
|
|
9934
|
+
code: "BAD_JSON",
|
|
9935
|
+
error: `invalid JSON: ${String(err?.message ?? err)}`
|
|
9936
|
+
});
|
|
9937
|
+
return;
|
|
9938
|
+
}
|
|
9939
|
+
const parsed = parseSendRequest(body);
|
|
9940
|
+
if (!parsed.ok) {
|
|
9941
|
+
sendJson(res, 400, {
|
|
9942
|
+
ok: false,
|
|
9943
|
+
code: "BAD_REQUEST",
|
|
9944
|
+
error: parsed.error,
|
|
9945
|
+
remediation: parsed.remediation
|
|
9946
|
+
});
|
|
9947
|
+
return;
|
|
9948
|
+
}
|
|
9949
|
+
let root;
|
|
9950
|
+
try {
|
|
9951
|
+
const matched = matchCcc(parsed.ccc, await collectCandidates(ctx));
|
|
9952
|
+
if (!matched.ok) {
|
|
9953
|
+
sendJson(res, 404, {
|
|
9954
|
+
ok: false,
|
|
9955
|
+
code: "UNKNOWN_CCC",
|
|
9956
|
+
error: matched.error,
|
|
9957
|
+
remediation: matched.remediation
|
|
9958
|
+
});
|
|
9959
|
+
return;
|
|
9960
|
+
}
|
|
9961
|
+
root = matched.root;
|
|
9962
|
+
} catch (err) {
|
|
9963
|
+
sendJson(res, 500, {
|
|
9964
|
+
ok: false,
|
|
9965
|
+
code: "CCC_DISCOVERY_FAILED",
|
|
9966
|
+
error: String(err?.message ?? err)
|
|
9967
|
+
});
|
|
9968
|
+
return;
|
|
9969
|
+
}
|
|
9970
|
+
const result = await sendProactiveText({
|
|
9971
|
+
root,
|
|
9972
|
+
toUserId: parsed.user,
|
|
9973
|
+
text: parsed.text,
|
|
9974
|
+
...parsed.accountId ? { accountId: parsed.accountId } : {}
|
|
9975
|
+
});
|
|
9976
|
+
if (result.ok) {
|
|
9977
|
+
sendJson(res, 200, {
|
|
9978
|
+
ok: true,
|
|
9979
|
+
accountId: result.accountId,
|
|
9980
|
+
userId: result.userId,
|
|
9981
|
+
sessionId: result.sessionId,
|
|
9982
|
+
role: result.role
|
|
9983
|
+
});
|
|
9984
|
+
return;
|
|
9985
|
+
}
|
|
9986
|
+
sendJson(res, statusForCode(result.code), {
|
|
9987
|
+
ok: false,
|
|
9988
|
+
code: result.code,
|
|
9989
|
+
error: result.error,
|
|
9990
|
+
remediation: result.remediation
|
|
9991
|
+
});
|
|
9992
|
+
}
|
|
9993
|
+
/** 启动监听器(仅 127.0.0.1);已启动 → 幂等返回 */
|
|
9994
|
+
async function startWeixinSendApi(ctx, port) {
|
|
9995
|
+
if (active) return active.port;
|
|
9996
|
+
const server = createServer((req, res) => {
|
|
9997
|
+
handleWeixinSendRequest(ctx, req, res).catch((err) => {
|
|
9998
|
+
console.warn(`[serenity-hooks] ✗ weixin send api 处理失败: ${String(err?.message ?? err)}`);
|
|
9999
|
+
try {
|
|
10000
|
+
sendJson(res, 500, {
|
|
10001
|
+
ok: false,
|
|
10002
|
+
code: "INTERNAL",
|
|
10003
|
+
error: "internal error"
|
|
10004
|
+
});
|
|
10005
|
+
} catch {}
|
|
10006
|
+
});
|
|
10007
|
+
});
|
|
10008
|
+
await new Promise((resolve, reject) => {
|
|
10009
|
+
server.once("error", reject);
|
|
10010
|
+
server.listen(port, "127.0.0.1", () => resolve());
|
|
10011
|
+
});
|
|
10012
|
+
server.unref();
|
|
10013
|
+
const addr = server.address();
|
|
10014
|
+
const actualPort = typeof addr === "object" && addr !== null ? addr.port : port;
|
|
10015
|
+
active = {
|
|
10016
|
+
server,
|
|
10017
|
+
port: actualPort
|
|
10018
|
+
};
|
|
10019
|
+
setWeixinSendEndpoint(`http://127.0.0.1:${actualPort}`);
|
|
10020
|
+
console.log(`[serenity-hooks] ✓ 微信主动发送入口: http://127.0.0.1:${actualPort}/send(仅 loopback;CCC MSM 调用)`);
|
|
10021
|
+
return actualPort;
|
|
10022
|
+
}
|
|
10023
|
+
/** 停止监听器(幂等) */
|
|
10024
|
+
function stopWeixinSendApi() {
|
|
10025
|
+
if (!active) return;
|
|
10026
|
+
try {
|
|
10027
|
+
active.server.close();
|
|
10028
|
+
} catch {}
|
|
10029
|
+
active = null;
|
|
10030
|
+
setWeixinSendEndpoint(null);
|
|
10031
|
+
}
|
|
10032
|
+
/**
|
|
10033
|
+
* 装配(index.ts apply 调用):按 plugin 全局配置 weixinApi 启停,配置变化热同步。
|
|
10034
|
+
* 默认启用(仅 loopback,零公网暴露);`enabled:false` 或 `port:0` 关闭。
|
|
10035
|
+
*/
|
|
10036
|
+
function registerWeixinSendApi(ctx) {
|
|
10037
|
+
const sync = () => {
|
|
10038
|
+
let cfg;
|
|
10039
|
+
try {
|
|
10040
|
+
cfg = readAdvancedSettings().weixinApi;
|
|
10041
|
+
} catch (err) {
|
|
10042
|
+
console.warn(`[serenity-hooks] ✗ weixin send api 配置读取失败,跳过同步: ${String(err?.message ?? err)}`);
|
|
10043
|
+
return;
|
|
10044
|
+
}
|
|
10045
|
+
const want = cfg.enabled && cfg.port > 0;
|
|
10046
|
+
if (want && !active) {
|
|
10047
|
+
startWeixinSendApi(ctx, cfg.port).catch((err) => {
|
|
10048
|
+
console.warn(`[serenity-hooks] ✗ 微信主动发送入口启动失败(port=${cfg.port}): ${String(err?.message ?? err)}`);
|
|
10049
|
+
});
|
|
10050
|
+
return;
|
|
10051
|
+
}
|
|
10052
|
+
if (!want && active) stopWeixinSendApi();
|
|
10053
|
+
};
|
|
10054
|
+
try {
|
|
10055
|
+
ctx.on("serenity/config-updated", sync);
|
|
10056
|
+
} catch {}
|
|
10057
|
+
try {
|
|
10058
|
+
ctx.on("serenity/settings-changed", sync);
|
|
10059
|
+
} catch {}
|
|
10060
|
+
sync();
|
|
10061
|
+
registerDisposer(ctx, "weixin send api", () => stopWeixinSendApi());
|
|
10062
|
+
}
|
|
10063
|
+
//#endregion
|
|
9574
10064
|
//#region src/seams/lifecycle.ts
|
|
9575
10065
|
/** 从 Agent / Session / 事件负载中取会话 id(形状宽容:未知结构返回 null) */
|
|
9576
10066
|
function sessionIdOf(value) {
|
|
@@ -9703,6 +10193,7 @@ function apply(ctx, config) {
|
|
|
9703
10193
|
registerAcp(ctx);
|
|
9704
10194
|
registerAutopilot(ctx);
|
|
9705
10195
|
registerWeixinBridge(ctx);
|
|
10196
|
+
registerWeixinSendApi(ctx);
|
|
9706
10197
|
registerLifecycle(ctx);
|
|
9707
10198
|
}
|
|
9708
10199
|
/**
|