@shgroup/dsh-serenity-hooks 1.30.8 → 1.30.10
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/ccc.d.ts +13 -0
- package/lib/config-ops.d.ts +10 -0
- package/lib/index.js +662 -134
- 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 +54 -1
- package/lib/weixin-hook.d.ts +9 -0
- package/lib/{weixin-route-BJ4g4ZNq.js → weixin-route-BD_yYAZz.js} +1 -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
|
-
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-
|
|
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-BD_yYAZz.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,12 +1830,25 @@ 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 上传并自行补记。
|
|
1838
|
+
|
|
1839
|
+
▸ weixin.autoReplyWithLastMessage(v1.30.10,关闭桥的自动输出):
|
|
1840
|
+
缺省 true = 现状:桥在 agent 一轮结束后把**最终 assistant 文本**自动回发用户(source="reply")。
|
|
1841
|
+
显式 false = 关闭:桥**不再转发最终文本**,输出权交给 agent——每轮 question 前缀注入
|
|
1842
|
+
「Reply Output (manual mode)」纪律块(含可直接照抄的 weixin-send 命令,CCC 根/账号/用户 id
|
|
1843
|
+
已填好),agent 自己决定发什么、发几条(全部记 source="proactive")。
|
|
1844
|
+
语义边界(用户拍板):只关最终文本;系统类消息(新对话通知 / 语音无法解析提示)保留;
|
|
1845
|
+
**agent 一轮未输出 → 静默不兜底**(否则开关失去意义);typing 指示与 incoming hook 不受影响。
|
|
1805
1846
|
|
|
1806
1847
|
Config:
|
|
1807
1848
|
{ "weixin": {
|
|
1808
1849
|
"enabled": true,
|
|
1809
1850
|
"hook": "scripts/weixin-message-hook.ts",
|
|
1851
|
+
"autoReplyWithLastMessage": true,
|
|
1810
1852
|
"accounts": [{ "accountId": "wechat-1", "name": "家庭助手", "enabled": true }],
|
|
1811
1853
|
"routes": [{ "user": "*", "role": "zhaocai" }]
|
|
1812
1854
|
} }
|
|
@@ -1817,7 +1859,10 @@ CCC 级微信个人号接入(iLink 协议):dsh 一进程多 CCC,每 CCC
|
|
|
1817
1859
|
|
|
1818
1860
|
触发(双向):
|
|
1819
1861
|
incoming = 用户 → bot:路由命中后、媒体落盘后触发(文本含语音转写;媒体带落盘 relPath)
|
|
1820
|
-
outgoing = bot →
|
|
1862
|
+
outgoing = bot → 用户:发送成功后触发(reply = 用户实际收到的纯文本,已剥离 think)
|
|
1863
|
+
· source="reply"(缺省)= 回复用户消息;source="proactive"(v1.30.9)= bot 主动发起
|
|
1864
|
+
(CCC 经 weixin-send MSM → dsp 主动发送入口 → sendProactiveText)
|
|
1865
|
+
· file(仅 send-file 补记):{ "name": "报告.pdf", "size": 12345, "caption": "可选" }
|
|
1821
1866
|
|
|
1822
1867
|
脚本约定(事件 JSON 单行经 stdin 传入;bun 优先 node 兜底):
|
|
1823
1868
|
#!/usr/bin/env bun 或 node script.js —— 读 process.stdin 整段 JSON.parse
|
|
@@ -1831,7 +1876,7 @@ CCC 级微信个人号接入(iLink 协议):dsh 一进程多 CCC,每 CCC
|
|
|
1831
1876
|
"message": { "text": "你好", "media": [{ "kind": "image", "relPath": "_tmp/weixin-inbound/<hash>/img_x.jpg" }] } }
|
|
1832
1877
|
{ "event": "outgoing", "ts": ..., "cccRoot": ..., "accountId": ...,
|
|
1833
1878
|
"userId": ..., "sessionId": ..., "role": ...,
|
|
1834
|
-
"reply": "已记录(纯文本)" }
|
|
1879
|
+
"reply": "已记录(纯文本)", "source": "reply" | "proactive" }
|
|
1835
1880
|
|
|
1836
1881
|
示例脚本(追加到按日文件——持久化归 CCC 自选:文件/DB/远端均可):
|
|
1837
1882
|
const fs = require('node:fs'); const p = '/path/ccc/AGENT_SESSIONS/_weixin-log.jsonl';
|
|
@@ -5048,6 +5093,10 @@ function defaultAdvancedSettings() {
|
|
|
5048
5093
|
publicAsk: {
|
|
5049
5094
|
key: "",
|
|
5050
5095
|
allowed: []
|
|
5096
|
+
},
|
|
5097
|
+
weixinApi: {
|
|
5098
|
+
enabled: true,
|
|
5099
|
+
port: 3082
|
|
5051
5100
|
}
|
|
5052
5101
|
};
|
|
5053
5102
|
}
|
|
@@ -5098,6 +5147,7 @@ function mergeWithDefaults(raw) {
|
|
|
5098
5147
|
const gateway = o.gateway ?? {};
|
|
5099
5148
|
const persona = o.persona ?? {};
|
|
5100
5149
|
const publicAsk = o.publicAsk ?? {};
|
|
5150
|
+
const weixinApi = o.weixinApi ?? {};
|
|
5101
5151
|
return {
|
|
5102
5152
|
gateway: {
|
|
5103
5153
|
enabled: typeof gateway.enabled === "boolean" ? gateway.enabled : def.gateway.enabled,
|
|
@@ -5116,6 +5166,10 @@ function mergeWithDefaults(raw) {
|
|
|
5116
5166
|
publicAsk: {
|
|
5117
5167
|
key: typeof publicAsk.key === "string" ? publicAsk.key : def.publicAsk.key,
|
|
5118
5168
|
allowed: Array.isArray(publicAsk.allowed) ? publicAsk.allowed.filter((n) => typeof n === "string" && n !== "") : def.publicAsk.allowed
|
|
5169
|
+
},
|
|
5170
|
+
weixinApi: {
|
|
5171
|
+
enabled: typeof weixinApi.enabled === "boolean" ? weixinApi.enabled : def.weixinApi.enabled,
|
|
5172
|
+
port: typeof weixinApi.port === "number" && Number.isInteger(weixinApi.port) && weixinApi.port >= 0 ? weixinApi.port : def.weixinApi.port
|
|
5119
5173
|
}
|
|
5120
5174
|
};
|
|
5121
5175
|
}
|
|
@@ -5153,7 +5207,11 @@ function updateAdvancedSettings(patch) {
|
|
|
5153
5207
|
publicAsk: patch.publicAsk !== void 0 ? {
|
|
5154
5208
|
key: typeof patch.publicAsk.key === "string" ? patch.publicAsk.key : current.publicAsk.key,
|
|
5155
5209
|
allowed: Array.isArray(patch.publicAsk.allowed) ? patch.publicAsk.allowed.filter((n) => typeof n === "string" && n !== "") : current.publicAsk.allowed
|
|
5156
|
-
} : current.publicAsk
|
|
5210
|
+
} : current.publicAsk,
|
|
5211
|
+
weixinApi: patch.weixinApi !== void 0 ? {
|
|
5212
|
+
enabled: typeof patch.weixinApi.enabled === "boolean" ? patch.weixinApi.enabled : current.weixinApi.enabled,
|
|
5213
|
+
port: typeof patch.weixinApi.port === "number" && Number.isInteger(patch.weixinApi.port) && patch.weixinApi.port >= 0 ? patch.weixinApi.port : current.weixinApi.port
|
|
5214
|
+
} : current.weixinApi
|
|
5157
5215
|
};
|
|
5158
5216
|
writeAdvancedSettings(next);
|
|
5159
5217
|
return next;
|
|
@@ -6175,7 +6233,7 @@ function saveFileToTmp(root, fileName, data) {
|
|
|
6175
6233
|
writeFileSync(join(dir, filename), bytes);
|
|
6176
6234
|
return `${FILE_UPLOAD_DIR}/${filename}`;
|
|
6177
6235
|
}
|
|
6178
|
-
function readBody$
|
|
6236
|
+
function readBody$3(req, maxBytes) {
|
|
6179
6237
|
return new Promise((resolve, reject) => {
|
|
6180
6238
|
let data = "";
|
|
6181
6239
|
req.on("data", (chunk) => {
|
|
@@ -6189,7 +6247,7 @@ function readBody$2(req, maxBytes) {
|
|
|
6189
6247
|
req.on("error", reject);
|
|
6190
6248
|
});
|
|
6191
6249
|
}
|
|
6192
|
-
function sendJson$
|
|
6250
|
+
function sendJson$2(res, code, body) {
|
|
6193
6251
|
const payload = JSON.stringify(body);
|
|
6194
6252
|
res.writeHead(code, {
|
|
6195
6253
|
"content-type": "application/json; charset=utf-8",
|
|
@@ -6227,7 +6285,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
6227
6285
|
handler: async (req, res) => {
|
|
6228
6286
|
try {
|
|
6229
6287
|
if (req.method !== "GET") {
|
|
6230
|
-
sendJson$
|
|
6288
|
+
sendJson$2(res, 405, { error: "method not allowed" });
|
|
6231
6289
|
return;
|
|
6232
6290
|
}
|
|
6233
6291
|
const url = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
@@ -6237,12 +6295,12 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
6237
6295
|
});
|
|
6238
6296
|
const root = findSerenityRoot(workspace);
|
|
6239
6297
|
if (!root) {
|
|
6240
|
-
sendJson$
|
|
6298
|
+
sendJson$2(res, 200, { handymen: [] });
|
|
6241
6299
|
return;
|
|
6242
6300
|
}
|
|
6243
|
-
sendJson$
|
|
6301
|
+
sendJson$2(res, 200, { handymen: listActiveHandymen(root) });
|
|
6244
6302
|
} catch (err) {
|
|
6245
|
-
sendJson$
|
|
6303
|
+
sendJson$2(res, 400, { error: err.message ?? String(err) });
|
|
6246
6304
|
}
|
|
6247
6305
|
}
|
|
6248
6306
|
});
|
|
@@ -6252,14 +6310,14 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
6252
6310
|
handler: async (req, res) => {
|
|
6253
6311
|
try {
|
|
6254
6312
|
if (req.method !== "POST") {
|
|
6255
|
-
sendJson$
|
|
6313
|
+
sendJson$2(res, 405, { error: "method not allowed" });
|
|
6256
6314
|
return;
|
|
6257
6315
|
}
|
|
6258
6316
|
if (req.headers["x-serenity-ui"] !== "1") {
|
|
6259
|
-
sendJson$
|
|
6317
|
+
sendJson$2(res, 403, { error: "图片落盘仅限 WebUI(client 专用)" });
|
|
6260
6318
|
return;
|
|
6261
6319
|
}
|
|
6262
|
-
const raw = await readBody$
|
|
6320
|
+
const raw = await readBody$3(req, 20971520);
|
|
6263
6321
|
const body = JSON.parse(raw);
|
|
6264
6322
|
const workspace = resolveWorkspace(ctx, {
|
|
6265
6323
|
sessionId: body.sessionId,
|
|
@@ -6267,12 +6325,12 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
6267
6325
|
});
|
|
6268
6326
|
const root = findSerenityRoot(workspace);
|
|
6269
6327
|
if (!root) {
|
|
6270
|
-
sendJson$
|
|
6328
|
+
sendJson$2(res, 404, { error: `no CCC found from workspace: ${workspace}` });
|
|
6271
6329
|
return;
|
|
6272
6330
|
}
|
|
6273
|
-
sendJson$
|
|
6331
|
+
sendJson$2(res, 200, { path: saveImageToTmp(root, body.mediaType ?? "", body.data ?? "") });
|
|
6274
6332
|
} catch (err) {
|
|
6275
|
-
sendJson$
|
|
6333
|
+
sendJson$2(res, 400, { error: err.message ?? String(err) });
|
|
6276
6334
|
}
|
|
6277
6335
|
}
|
|
6278
6336
|
});
|
|
@@ -6282,14 +6340,14 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
6282
6340
|
handler: async (req, res) => {
|
|
6283
6341
|
try {
|
|
6284
6342
|
if (req.method !== "POST") {
|
|
6285
|
-
sendJson$
|
|
6343
|
+
sendJson$2(res, 405, { error: "method not allowed" });
|
|
6286
6344
|
return;
|
|
6287
6345
|
}
|
|
6288
6346
|
if (req.headers["x-serenity-ui"] !== "1") {
|
|
6289
|
-
sendJson$
|
|
6347
|
+
sendJson$2(res, 403, { error: "文件落盘仅限 WebUI(client 专用)" });
|
|
6290
6348
|
return;
|
|
6291
6349
|
}
|
|
6292
|
-
const raw = await readBody$
|
|
6350
|
+
const raw = await readBody$3(req, 20971520);
|
|
6293
6351
|
const body = JSON.parse(raw);
|
|
6294
6352
|
const workspace = resolveWorkspace(ctx, {
|
|
6295
6353
|
sessionId: body.sessionId,
|
|
@@ -6297,12 +6355,12 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
6297
6355
|
});
|
|
6298
6356
|
const root = findSerenityRoot(workspace);
|
|
6299
6357
|
if (!root) {
|
|
6300
|
-
sendJson$
|
|
6358
|
+
sendJson$2(res, 404, { error: `no CCC found from workspace: ${workspace}` });
|
|
6301
6359
|
return;
|
|
6302
6360
|
}
|
|
6303
|
-
sendJson$
|
|
6361
|
+
sendJson$2(res, 200, { path: saveFileToTmp(root, body.name ?? "", body.data ?? "") });
|
|
6304
6362
|
} catch (err) {
|
|
6305
|
-
sendJson$
|
|
6363
|
+
sendJson$2(res, 400, { error: err.message ?? String(err) });
|
|
6306
6364
|
}
|
|
6307
6365
|
}
|
|
6308
6366
|
});
|
|
@@ -6318,7 +6376,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
6318
6376
|
workspace: url.searchParams.get("workspace") ?? void 0
|
|
6319
6377
|
}), configPaths);
|
|
6320
6378
|
const runtime = ctx.get("codeRuntime");
|
|
6321
|
-
sendJson$
|
|
6379
|
+
sendJson$2(res, 200, {
|
|
6322
6380
|
...status,
|
|
6323
6381
|
...runtime === void 0 ? { codeRuntime: null } : { codeRuntime: {
|
|
6324
6382
|
language: runtime.language,
|
|
@@ -6329,10 +6387,10 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
6329
6387
|
}
|
|
6330
6388
|
if (req.method === "POST") {
|
|
6331
6389
|
if (req.headers["x-serenity-ui"] !== "1") {
|
|
6332
|
-
sendJson$
|
|
6390
|
+
sendJson$2(res, 403, { error: "safe-mode 切换仅限 WebUI(agent 不可自行开关)" });
|
|
6333
6391
|
return;
|
|
6334
6392
|
}
|
|
6335
|
-
const raw = await readBody$
|
|
6393
|
+
const raw = await readBody$3(req, 65536);
|
|
6336
6394
|
const body = JSON.parse(raw);
|
|
6337
6395
|
const workspace = resolveWorkspace(ctx, {
|
|
6338
6396
|
sessionId: body.sessionId,
|
|
@@ -6340,19 +6398,19 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
6340
6398
|
});
|
|
6341
6399
|
const root = findSerenityRoot(workspace);
|
|
6342
6400
|
if (!root) {
|
|
6343
|
-
sendJson$
|
|
6401
|
+
sendJson$2(res, 404, { error: `no CCC found from workspace: ${workspace}` });
|
|
6344
6402
|
return;
|
|
6345
6403
|
}
|
|
6346
6404
|
const result = setSafeMode(root, body.on === true);
|
|
6347
|
-
sendJson$
|
|
6405
|
+
sendJson$2(res, 200, {
|
|
6348
6406
|
...getStatus(workspace, configPaths),
|
|
6349
6407
|
...result
|
|
6350
6408
|
});
|
|
6351
6409
|
return;
|
|
6352
6410
|
}
|
|
6353
|
-
sendJson$
|
|
6411
|
+
sendJson$2(res, 405, { error: "method not allowed" });
|
|
6354
6412
|
} catch (err) {
|
|
6355
|
-
sendJson$
|
|
6413
|
+
sendJson$2(res, 400, { error: err.message ?? String(err) });
|
|
6356
6414
|
}
|
|
6357
6415
|
}
|
|
6358
6416
|
});
|
|
@@ -6362,7 +6420,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
6362
6420
|
handler: async (req, res) => {
|
|
6363
6421
|
try {
|
|
6364
6422
|
if (req.headers["x-serenity-ui"] !== "1") {
|
|
6365
|
-
sendJson$
|
|
6423
|
+
sendJson$2(res, 403, { error: "高级设定仅限 WebUI(client 专用)" });
|
|
6366
6424
|
return;
|
|
6367
6425
|
}
|
|
6368
6426
|
if (req.method === "GET") {
|
|
@@ -6371,24 +6429,24 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
6371
6429
|
try {
|
|
6372
6430
|
known = projectKnownWorkspaces(hostService(ctx, "workspaceRegistry")?.list?.() ?? [], settings.gateway.workspaces);
|
|
6373
6431
|
} catch {}
|
|
6374
|
-
sendJson$
|
|
6432
|
+
sendJson$2(res, 200, {
|
|
6375
6433
|
config: toWire(settings),
|
|
6376
6434
|
knownWorkspaces: known
|
|
6377
6435
|
});
|
|
6378
6436
|
return;
|
|
6379
6437
|
}
|
|
6380
6438
|
if (req.method === "PUT") {
|
|
6381
|
-
const raw = await readBody$
|
|
6439
|
+
const raw = await readBody$3(req, 131072);
|
|
6382
6440
|
const saved = applyWirePatch(JSON.parse(raw).config ?? {});
|
|
6383
6441
|
try {
|
|
6384
6442
|
ctx.emit?.("serenity/config-updated");
|
|
6385
6443
|
} catch {}
|
|
6386
|
-
sendJson$
|
|
6444
|
+
sendJson$2(res, 200, { config: toWire(saved) });
|
|
6387
6445
|
return;
|
|
6388
6446
|
}
|
|
6389
|
-
sendJson$
|
|
6447
|
+
sendJson$2(res, 405, { error: "method not allowed" });
|
|
6390
6448
|
} catch (err) {
|
|
6391
|
-
sendJson$
|
|
6449
|
+
sendJson$2(res, 400, { error: err.message ?? String(err) });
|
|
6392
6450
|
}
|
|
6393
6451
|
}
|
|
6394
6452
|
});
|
|
@@ -6398,7 +6456,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
6398
6456
|
handler: async (req, res) => {
|
|
6399
6457
|
try {
|
|
6400
6458
|
if (req.method !== "GET") {
|
|
6401
|
-
sendJson$
|
|
6459
|
+
sendJson$2(res, 405, { error: "method not allowed" });
|
|
6402
6460
|
return;
|
|
6403
6461
|
}
|
|
6404
6462
|
const url = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
@@ -6407,10 +6465,10 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
6407
6465
|
workspace: url.searchParams.get("workspace") ?? void 0
|
|
6408
6466
|
});
|
|
6409
6467
|
const root = findSerenityRoot(workspace) ?? "";
|
|
6410
|
-
const { discoverCccs } = await import("./skiff-debug-
|
|
6411
|
-
sendJson$
|
|
6468
|
+
const { discoverCccs } = await import("./skiff-debug-TONJlpgF.js").then((n) => n.i);
|
|
6469
|
+
sendJson$2(res, 200, { cccs: await discoverCccs(ctx, root) });
|
|
6412
6470
|
} catch (err) {
|
|
6413
|
-
sendJson$
|
|
6471
|
+
sendJson$2(res, 400, { error: err.message ?? String(err) });
|
|
6414
6472
|
}
|
|
6415
6473
|
}
|
|
6416
6474
|
});
|
|
@@ -6420,20 +6478,20 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
6420
6478
|
handler: async (req, res) => {
|
|
6421
6479
|
try {
|
|
6422
6480
|
if (req.headers["x-serenity-ui"] !== "1") {
|
|
6423
|
-
sendJson$
|
|
6481
|
+
sendJson$2(res, 403, { error: "仅限 WebUI(key 属敏感凭据)" });
|
|
6424
6482
|
return;
|
|
6425
6483
|
}
|
|
6426
6484
|
if (req.method === "PUT") {
|
|
6427
|
-
const raw = await readBody$
|
|
6485
|
+
const raw = await readBody$3(req, 16384);
|
|
6428
6486
|
if (JSON.parse(raw).action !== "rotate") {
|
|
6429
|
-
sendJson$
|
|
6487
|
+
sendJson$2(res, 400, { error: "unsupported action (expected \"rotate\")" });
|
|
6430
6488
|
return;
|
|
6431
6489
|
}
|
|
6432
|
-
sendJson$
|
|
6490
|
+
sendJson$2(res, 200, { key: rotatePublicAskKey() });
|
|
6433
6491
|
return;
|
|
6434
6492
|
}
|
|
6435
6493
|
if (req.method !== "GET") {
|
|
6436
|
-
sendJson$
|
|
6494
|
+
sendJson$2(res, 405, { error: "method not allowed" });
|
|
6437
6495
|
return;
|
|
6438
6496
|
}
|
|
6439
6497
|
const settings = readAdvancedSettings();
|
|
@@ -6446,7 +6504,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
6446
6504
|
name,
|
|
6447
6505
|
url: `${base}/c/${encodeURIComponent(name)}`
|
|
6448
6506
|
}));
|
|
6449
|
-
sendJson$
|
|
6507
|
+
sendJson$2(res, 200, {
|
|
6450
6508
|
enabled: simple.publicAskEnabled ?? false,
|
|
6451
6509
|
port,
|
|
6452
6510
|
key: ensurePublicAskKey(),
|
|
@@ -6455,7 +6513,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
6455
6513
|
listUrl: `${base}/`
|
|
6456
6514
|
});
|
|
6457
6515
|
} catch (err) {
|
|
6458
|
-
sendJson$
|
|
6516
|
+
sendJson$2(res, 400, { error: err.message ?? String(err) });
|
|
6459
6517
|
}
|
|
6460
6518
|
}
|
|
6461
6519
|
});
|
|
@@ -6466,13 +6524,13 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
6466
6524
|
try {
|
|
6467
6525
|
if (req.method === "POST") {
|
|
6468
6526
|
if (req.headers["x-serenity-ui"] !== "1") {
|
|
6469
|
-
sendJson$
|
|
6527
|
+
sendJson$2(res, 403, { error: "立即唤起仅限 WebUI(agent 不可自行唤起)" });
|
|
6470
6528
|
return;
|
|
6471
6529
|
}
|
|
6472
|
-
const raw = await readBody$
|
|
6530
|
+
const raw = await readBody$3(req, 16384);
|
|
6473
6531
|
const body = JSON.parse(raw);
|
|
6474
6532
|
if (body.action !== "wake") {
|
|
6475
|
-
sendJson$
|
|
6533
|
+
sendJson$2(res, 400, { error: "unsupported action (expected \"wake\")" });
|
|
6476
6534
|
return;
|
|
6477
6535
|
}
|
|
6478
6536
|
const workspace = resolveWorkspace(ctx, {
|
|
@@ -6482,21 +6540,21 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
6482
6540
|
const root = findSerenityRoot(workspace);
|
|
6483
6541
|
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
6542
|
if (!effectiveRoot) {
|
|
6485
|
-
sendJson$
|
|
6543
|
+
sendJson$2(res, 404, { error: `no CCC found from workspace: ${workspace}` });
|
|
6486
6544
|
return;
|
|
6487
6545
|
}
|
|
6488
6546
|
const { performAutopilotWake, readAutopilotSettings } = await Promise.resolve().then(() => autopilot_trajectory_exports);
|
|
6489
6547
|
const settings = readAutopilotSettings(effectiveRoot);
|
|
6490
6548
|
if (!settings) {
|
|
6491
|
-
sendJson$
|
|
6549
|
+
sendJson$2(res, 400, { error: "Autopilot Trajectory 未配置(.opencode/serenity.json 缺段)" });
|
|
6492
6550
|
return;
|
|
6493
6551
|
}
|
|
6494
6552
|
const result = await performAutopilotWake(ctx, effectiveRoot, settings, { force: true });
|
|
6495
|
-
sendJson$
|
|
6553
|
+
sendJson$2(res, result.ok ? 200 : 400, result);
|
|
6496
6554
|
return;
|
|
6497
6555
|
}
|
|
6498
6556
|
if (req.method !== "GET") {
|
|
6499
|
-
sendJson$
|
|
6557
|
+
sendJson$2(res, 405, { error: "method not allowed" });
|
|
6500
6558
|
return;
|
|
6501
6559
|
}
|
|
6502
6560
|
const url = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
@@ -6508,12 +6566,12 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
6508
6566
|
const { getAutopilotStatus, collectAutopilotCccs } = await Promise.resolve().then(() => autopilot_trajectory_exports);
|
|
6509
6567
|
const effectiveRoot = url.searchParams.get("ccc")?.trim() ?? (!url.searchParams.get("workspace") && !url.searchParams.get("sessionId") ? collectAutopilotCccs(ctx)[0] : null) ?? root;
|
|
6510
6568
|
if (!effectiveRoot) {
|
|
6511
|
-
sendJson$
|
|
6569
|
+
sendJson$2(res, 200, { status: null });
|
|
6512
6570
|
return;
|
|
6513
6571
|
}
|
|
6514
|
-
sendJson$
|
|
6572
|
+
sendJson$2(res, 200, { status: getAutopilotStatus(effectiveRoot) });
|
|
6515
6573
|
} catch (err) {
|
|
6516
|
-
sendJson$
|
|
6574
|
+
sendJson$2(res, 400, { error: err.message ?? String(err) });
|
|
6517
6575
|
}
|
|
6518
6576
|
}
|
|
6519
6577
|
});
|
|
@@ -6523,28 +6581,28 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
6523
6581
|
handler: async (req, res) => {
|
|
6524
6582
|
try {
|
|
6525
6583
|
if (req.headers["x-serenity-ui"] !== "1") {
|
|
6526
|
-
sendJson$
|
|
6584
|
+
sendJson$2(res, 403, { error: "微信桥配置仅限 WebUI(client 专用)" });
|
|
6527
6585
|
return;
|
|
6528
6586
|
}
|
|
6529
6587
|
const url = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
6530
6588
|
if (req.method === "GET") {
|
|
6531
6589
|
const chk = requireCcc(url.searchParams.get("ccc") ?? void 0);
|
|
6532
6590
|
if (!chk.ok) {
|
|
6533
|
-
sendJson$
|
|
6591
|
+
sendJson$2(res, 400, { error: chk.error });
|
|
6534
6592
|
return;
|
|
6535
6593
|
}
|
|
6536
|
-
const { readWeixinSettings } = await import("./weixin-route-
|
|
6594
|
+
const { readWeixinSettings } = await import("./weixin-route-BD_yYAZz.js").then((n) => n.u);
|
|
6537
6595
|
const { weixinBridgeStatus } = await Promise.resolve().then(() => weixin_bridge_exports);
|
|
6538
6596
|
const settings = readWeixinSettings(chk.root);
|
|
6539
6597
|
const bridge = weixinBridgeStatus().find((b) => b.ccc === chk.root);
|
|
6540
|
-
const { readWeixinCredential } = await import("./weixin-route-
|
|
6598
|
+
const { readWeixinCredential } = await import("./weixin-route-BD_yYAZz.js").then((n) => n.u);
|
|
6541
6599
|
const accounts = (settings.accounts ?? []).map((a) => ({
|
|
6542
6600
|
accountId: a.accountId,
|
|
6543
6601
|
name: a.name ?? void 0,
|
|
6544
6602
|
enabled: a.enabled !== false,
|
|
6545
6603
|
bound: readWeixinCredential(chk.root, a.accountId) !== null
|
|
6546
6604
|
}));
|
|
6547
|
-
sendJson$
|
|
6605
|
+
sendJson$2(res, 200, {
|
|
6548
6606
|
enabled: settings.enabled === true,
|
|
6549
6607
|
botType: settings.botType ?? void 0,
|
|
6550
6608
|
accounts,
|
|
@@ -6554,22 +6612,22 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
6554
6612
|
return;
|
|
6555
6613
|
}
|
|
6556
6614
|
if (req.method !== "POST") {
|
|
6557
|
-
sendJson$
|
|
6615
|
+
sendJson$2(res, 405, { error: "method not allowed" });
|
|
6558
6616
|
return;
|
|
6559
6617
|
}
|
|
6560
|
-
const raw = await readBody$
|
|
6618
|
+
const raw = await readBody$3(req, 131072);
|
|
6561
6619
|
const body = JSON.parse(raw);
|
|
6562
6620
|
const chk = requireCcc(body.ccc);
|
|
6563
6621
|
if (!chk.ok) {
|
|
6564
|
-
sendJson$
|
|
6622
|
+
sendJson$2(res, 400, { error: chk.error });
|
|
6565
6623
|
return;
|
|
6566
6624
|
}
|
|
6567
6625
|
const root = chk.root;
|
|
6568
6626
|
if (body.action === "login-start") {
|
|
6569
|
-
const { fetchQRCode } = await import("./weixin-api-
|
|
6627
|
+
const { fetchQRCode } = await import("./weixin-api-BMpljodH.js").then((n) => n.l);
|
|
6570
6628
|
const { randomUUID } = await import("node:crypto");
|
|
6571
6629
|
for (const [key, v] of weixinLogins) if (Date.now() - v.startedAt > WEIXIN_LOGIN_TTL_MS) weixinLogins.delete(key);
|
|
6572
|
-
const { readWeixinSettings } = await import("./weixin-route-
|
|
6630
|
+
const { readWeixinSettings } = await import("./weixin-route-BD_yYAZz.js").then((n) => n.u);
|
|
6573
6631
|
const qr = await fetchQRCode({ botType: readWeixinSettings(root).botType ?? void 0 });
|
|
6574
6632
|
const loginKey = randomUUID();
|
|
6575
6633
|
weixinLogins.set(loginKey, {
|
|
@@ -6578,7 +6636,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
6578
6636
|
startedAt: Date.now(),
|
|
6579
6637
|
polling: false
|
|
6580
6638
|
});
|
|
6581
|
-
sendJson$
|
|
6639
|
+
sendJson$2(res, 200, {
|
|
6582
6640
|
qrcode: qr.qrcode,
|
|
6583
6641
|
qrcode_img_content: qr.qrcode_img_content,
|
|
6584
6642
|
loginKey
|
|
@@ -6587,40 +6645,40 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
6587
6645
|
}
|
|
6588
6646
|
if (body.action === "remove-account") {
|
|
6589
6647
|
if (!body.accountId) {
|
|
6590
|
-
sendJson$
|
|
6648
|
+
sendJson$2(res, 400, { error: "missing accountId" });
|
|
6591
6649
|
return;
|
|
6592
6650
|
}
|
|
6593
|
-
const { removeWeixinAccount } = await import("./weixin-route-
|
|
6651
|
+
const { removeWeixinAccount } = await import("./weixin-route-BD_yYAZz.js").then((n) => n.u);
|
|
6594
6652
|
const { syncCccBridge } = await Promise.resolve().then(() => weixin_bridge_exports);
|
|
6595
6653
|
removeWeixinAccount(root, body.accountId);
|
|
6596
6654
|
syncCccBridge(ctx, root);
|
|
6597
|
-
sendJson$
|
|
6655
|
+
sendJson$2(res, 200, { removed: body.accountId });
|
|
6598
6656
|
return;
|
|
6599
6657
|
}
|
|
6600
6658
|
if (body.action === "save-routes") {
|
|
6601
6659
|
const routes = body.routes ?? [];
|
|
6602
6660
|
if (!Array.isArray(routes) || routes.some((r) => typeof r?.user !== "string" || typeof r?.role !== "string" || r.user === "" || r.role === "")) {
|
|
6603
|
-
sendJson$
|
|
6661
|
+
sendJson$2(res, 400, { error: "invalid routes (expected [{user, role}, ...])" });
|
|
6604
6662
|
return;
|
|
6605
6663
|
}
|
|
6606
|
-
const { saveWeixinRoutes } = await import("./weixin-route-
|
|
6664
|
+
const { saveWeixinRoutes } = await import("./weixin-route-BD_yYAZz.js").then((n) => n.u);
|
|
6607
6665
|
const { readSkiffRoles } = await import("./skiff-role-DlrbHPLD.js").then((n) => n.c);
|
|
6608
6666
|
const roles = readSkiffRoles(root);
|
|
6609
6667
|
for (const r of routes) if (!roles.has(r.role)) {
|
|
6610
|
-
sendJson$
|
|
6668
|
+
sendJson$2(res, 400, { error: `unknown role: ${r.role} (not in ${root} skiff.roles)` });
|
|
6611
6669
|
return;
|
|
6612
6670
|
}
|
|
6613
6671
|
saveWeixinRoutes(root, routes);
|
|
6614
|
-
sendJson$
|
|
6672
|
+
sendJson$2(res, 200, { saved: routes.length });
|
|
6615
6673
|
return;
|
|
6616
6674
|
}
|
|
6617
6675
|
if (body.action === "set-enabled") {
|
|
6618
6676
|
const enabled = body.enabled === true;
|
|
6619
|
-
const { readWeixinSettings, setWeixinEnabled } = await import("./weixin-route-
|
|
6677
|
+
const { readWeixinSettings, setWeixinEnabled } = await import("./weixin-route-BD_yYAZz.js").then((n) => n.u);
|
|
6620
6678
|
const { syncCccBridge } = await Promise.resolve().then(() => weixin_bridge_exports);
|
|
6621
6679
|
const settings = readWeixinSettings(root);
|
|
6622
6680
|
if (enabled && (settings.accounts ?? []).length === 0) {
|
|
6623
|
-
sendJson$
|
|
6681
|
+
sendJson$2(res, 400, { error: "启用前请先扫码绑定至少一个账号" });
|
|
6624
6682
|
return;
|
|
6625
6683
|
}
|
|
6626
6684
|
setWeixinEnabled(root, enabled);
|
|
@@ -6629,12 +6687,12 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
6629
6687
|
const { stopCccBridge } = await Promise.resolve().then(() => weixin_bridge_exports);
|
|
6630
6688
|
stopCccBridge(root);
|
|
6631
6689
|
}
|
|
6632
|
-
sendJson$
|
|
6690
|
+
sendJson$2(res, 200, { enabled });
|
|
6633
6691
|
return;
|
|
6634
6692
|
}
|
|
6635
|
-
sendJson$
|
|
6693
|
+
sendJson$2(res, 400, { error: `unsupported action: ${body.action ?? ""}` });
|
|
6636
6694
|
} catch (err) {
|
|
6637
|
-
sendJson$
|
|
6695
|
+
sendJson$2(res, 400, { error: err.message ?? String(err) });
|
|
6638
6696
|
}
|
|
6639
6697
|
}
|
|
6640
6698
|
});
|
|
@@ -6644,26 +6702,26 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
6644
6702
|
handler: async (req, res) => {
|
|
6645
6703
|
try {
|
|
6646
6704
|
if (req.headers["x-serenity-ui"] !== "1") {
|
|
6647
|
-
sendJson$
|
|
6705
|
+
sendJson$2(res, 403, { error: "微信桥配置仅限 WebUI(client 专用)" });
|
|
6648
6706
|
return;
|
|
6649
6707
|
}
|
|
6650
6708
|
if (req.method !== "GET") {
|
|
6651
|
-
sendJson$
|
|
6709
|
+
sendJson$2(res, 405, { error: "method not allowed" });
|
|
6652
6710
|
return;
|
|
6653
6711
|
}
|
|
6654
6712
|
const key = new URL(req.url ?? "/", "http://127.0.0.1").searchParams.get("key") ?? "";
|
|
6655
6713
|
const login = weixinLogins.get(key);
|
|
6656
6714
|
if (!login) {
|
|
6657
|
-
sendJson$
|
|
6715
|
+
sendJson$2(res, 404, { error: "login not found or expired" });
|
|
6658
6716
|
return;
|
|
6659
6717
|
}
|
|
6660
6718
|
if (Date.now() - login.startedAt > WEIXIN_LOGIN_TTL_MS) {
|
|
6661
6719
|
weixinLogins.delete(key);
|
|
6662
|
-
sendJson$
|
|
6720
|
+
sendJson$2(res, 200, { status: "expired" });
|
|
6663
6721
|
return;
|
|
6664
6722
|
}
|
|
6665
|
-
const { pollQRStatus } = await import("./weixin-api-
|
|
6666
|
-
const { readWeixinSettings } = await import("./weixin-route-
|
|
6723
|
+
const { pollQRStatus } = await import("./weixin-api-BMpljodH.js").then((n) => n.l);
|
|
6724
|
+
const { readWeixinSettings } = await import("./weixin-route-BD_yYAZz.js").then((n) => n.u);
|
|
6667
6725
|
const settings = readWeixinSettings(login.root);
|
|
6668
6726
|
const status = await pollQRStatus({
|
|
6669
6727
|
baseUrl: void 0,
|
|
@@ -6672,13 +6730,13 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
6672
6730
|
switch (status.status) {
|
|
6673
6731
|
case "confirmed": {
|
|
6674
6732
|
if (!status.bot_token) {
|
|
6675
|
-
sendJson$
|
|
6733
|
+
sendJson$2(res, 200, {
|
|
6676
6734
|
status: "error",
|
|
6677
6735
|
error: "confirmed but no bot_token"
|
|
6678
6736
|
});
|
|
6679
6737
|
return;
|
|
6680
6738
|
}
|
|
6681
|
-
const { upsertWeixinAccount, writeWeixinCredential, nextWeixinAccountId } = await import("./weixin-route-
|
|
6739
|
+
const { upsertWeixinAccount, writeWeixinCredential, nextWeixinAccountId } = await import("./weixin-route-BD_yYAZz.js").then((n) => n.u);
|
|
6682
6740
|
const { syncCccBridge } = await Promise.resolve().then(() => weixin_bridge_exports);
|
|
6683
6741
|
const accountId = nextWeixinAccountId(settings);
|
|
6684
6742
|
upsertWeixinAccount(login.root, {
|
|
@@ -6693,7 +6751,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
6693
6751
|
});
|
|
6694
6752
|
syncCccBridge(ctx, login.root);
|
|
6695
6753
|
weixinLogins.delete(key);
|
|
6696
|
-
sendJson$
|
|
6754
|
+
sendJson$2(res, 200, {
|
|
6697
6755
|
status: "confirmed",
|
|
6698
6756
|
accountId,
|
|
6699
6757
|
tokenSaved: true
|
|
@@ -6702,14 +6760,14 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
6702
6760
|
}
|
|
6703
6761
|
case "expired":
|
|
6704
6762
|
weixinLogins.delete(key);
|
|
6705
|
-
sendJson$
|
|
6763
|
+
sendJson$2(res, 200, { status: "expired" });
|
|
6706
6764
|
return;
|
|
6707
6765
|
default:
|
|
6708
|
-
sendJson$
|
|
6766
|
+
sendJson$2(res, 200, { status: status.status ?? "wait" });
|
|
6709
6767
|
return;
|
|
6710
6768
|
}
|
|
6711
6769
|
} catch (err) {
|
|
6712
|
-
sendJson$
|
|
6770
|
+
sendJson$2(res, 400, { error: err.message ?? String(err) });
|
|
6713
6771
|
}
|
|
6714
6772
|
}
|
|
6715
6773
|
});
|
|
@@ -6731,7 +6789,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
6731
6789
|
const root = sessionsRootDir();
|
|
6732
6790
|
if (req.method === "GET") {
|
|
6733
6791
|
const { candidates } = performCleanup(root, cutoffDaysAgo(days), liveIds, { dryRun: true });
|
|
6734
|
-
sendJson$
|
|
6792
|
+
sendJson$2(res, 200, {
|
|
6735
6793
|
root,
|
|
6736
6794
|
olderThanDays: days,
|
|
6737
6795
|
dryRun: true,
|
|
@@ -6745,15 +6803,15 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
6745
6803
|
return;
|
|
6746
6804
|
}
|
|
6747
6805
|
if (req.method !== "POST") {
|
|
6748
|
-
sendJson$
|
|
6806
|
+
sendJson$2(res, 405, { error: "method not allowed" });
|
|
6749
6807
|
return;
|
|
6750
6808
|
}
|
|
6751
6809
|
if (req.headers["x-serenity-ui"] !== "1") {
|
|
6752
|
-
sendJson$
|
|
6810
|
+
sendJson$2(res, 403, { error: "会话清理仅限 WebUI(client 专用)" });
|
|
6753
6811
|
return;
|
|
6754
6812
|
}
|
|
6755
6813
|
const { result } = performCleanup(root, cutoffDaysAgo(days), liveIds);
|
|
6756
|
-
sendJson$
|
|
6814
|
+
sendJson$2(res, 200, {
|
|
6757
6815
|
root,
|
|
6758
6816
|
olderThanDays: days,
|
|
6759
6817
|
dryRun: false,
|
|
@@ -6761,7 +6819,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
6761
6819
|
errors: result.errors
|
|
6762
6820
|
});
|
|
6763
6821
|
} catch (err) {
|
|
6764
|
-
sendJson$
|
|
6822
|
+
sendJson$2(res, 400, { error: err.message ?? String(err) });
|
|
6765
6823
|
}
|
|
6766
6824
|
}
|
|
6767
6825
|
});
|
|
@@ -7346,7 +7404,7 @@ function createDshCookieProvider(connection, authority) {
|
|
|
7346
7404
|
//#endregion
|
|
7347
7405
|
//#region src/gateway.ts
|
|
7348
7406
|
/** 读取请求体(≤ maxBytes;超限 reject) */
|
|
7349
|
-
function readBody$
|
|
7407
|
+
function readBody$2(req, maxBytes) {
|
|
7350
7408
|
return new Promise((resolve, reject) => {
|
|
7351
7409
|
let data = "";
|
|
7352
7410
|
req.on("data", (chunk) => {
|
|
@@ -7471,7 +7529,7 @@ function startGateway(config, getAccounts, getDshCookie) {
|
|
|
7471
7529
|
const session = authed(req);
|
|
7472
7530
|
if (session !== void 0) {
|
|
7473
7531
|
if (req.method === "POST" && isWorkspaceCreatePath(url.pathname)) {
|
|
7474
|
-
readBody$
|
|
7532
|
+
readBody$2(req, 131072).then((body) => {
|
|
7475
7533
|
const { rpcId, path } = parseWorkspaceCreateBody(body);
|
|
7476
7534
|
if (!allowWorkspaceCreate) {
|
|
7477
7535
|
res.writeHead(403, { "content-type": "application/json" });
|
|
@@ -8538,8 +8596,8 @@ function rpcError(id, code, message) {
|
|
|
8538
8596
|
* 实验性质:未开启时零资源占用(无监听、无 agent 创建)。
|
|
8539
8597
|
*/
|
|
8540
8598
|
/** 运行中的 ACP HTTP 服务(单实例;进程级) */
|
|
8541
|
-
let active = null;
|
|
8542
|
-
function readBody(req) {
|
|
8599
|
+
let active$1 = null;
|
|
8600
|
+
function readBody$1(req) {
|
|
8543
8601
|
return new Promise((resolve, reject) => {
|
|
8544
8602
|
const chunks = [];
|
|
8545
8603
|
req.on("data", (c) => chunks.push(c));
|
|
@@ -8547,7 +8605,7 @@ function readBody(req) {
|
|
|
8547
8605
|
req.on("error", reject);
|
|
8548
8606
|
});
|
|
8549
8607
|
}
|
|
8550
|
-
function sendJson(res, status, payload) {
|
|
8608
|
+
function sendJson$1(res, status, payload) {
|
|
8551
8609
|
const body = jscSafeJsonText(JSON.stringify(payload));
|
|
8552
8610
|
res.writeHead(status, {
|
|
8553
8611
|
"Content-Type": "application/json; charset=utf-8",
|
|
@@ -8569,7 +8627,7 @@ function sendHtml(res, html) {
|
|
|
8569
8627
|
* @param defaultRoot 默认 CCC 根(问答页候选发现兜底;resolveSkiffRoot 传入)
|
|
8570
8628
|
*/
|
|
8571
8629
|
async function startAcpHttpServer(ctx, port, defaultRoot) {
|
|
8572
|
-
if (active) return;
|
|
8630
|
+
if (active$1) return;
|
|
8573
8631
|
const server = createServer((req, res) => {
|
|
8574
8632
|
handle(ctx, defaultRoot ?? "", req, res);
|
|
8575
8633
|
});
|
|
@@ -8577,7 +8635,7 @@ async function startAcpHttpServer(ctx, port, defaultRoot) {
|
|
|
8577
8635
|
server.once("error", reject);
|
|
8578
8636
|
server.listen(port, "127.0.0.1", () => resolve());
|
|
8579
8637
|
});
|
|
8580
|
-
active = {
|
|
8638
|
+
active$1 = {
|
|
8581
8639
|
server,
|
|
8582
8640
|
port
|
|
8583
8641
|
};
|
|
@@ -8588,11 +8646,11 @@ async function startAcpHttpServer(ctx, port, defaultRoot) {
|
|
|
8588
8646
|
console.log(`[serenity-hooks] ✓ ACP HTTP: http://127.0.0.1:${port}(${faces.join(" + ") || "未开启任何面"};session/new 支持 {ccc, role, sessionId?})`);
|
|
8589
8647
|
}
|
|
8590
8648
|
function stopAcpHttpServer() {
|
|
8591
|
-
if (!active) return;
|
|
8649
|
+
if (!active$1) return;
|
|
8592
8650
|
try {
|
|
8593
|
-
active.server.close();
|
|
8651
|
+
active$1.server.close();
|
|
8594
8652
|
} catch {}
|
|
8595
|
-
active = null;
|
|
8653
|
+
active$1 = null;
|
|
8596
8654
|
}
|
|
8597
8655
|
async function handle(ctx, defaultRoot, req, res) {
|
|
8598
8656
|
try {
|
|
@@ -8601,14 +8659,14 @@ async function handle(ctx, defaultRoot, req, res) {
|
|
|
8601
8659
|
const ip = req.headers["x-forwarded-for"]?.split(",")[0]?.trim() || req.socket.remoteAddress || "unknown";
|
|
8602
8660
|
if (req.method === "POST" && url === "/") {
|
|
8603
8661
|
if (!s.acpEnabled) {
|
|
8604
|
-
sendJson(res, 403, { error: "ACP JSON-RPC disabled (enable in Serenity settings)" });
|
|
8662
|
+
sendJson$1(res, 403, { error: "ACP JSON-RPC disabled (enable in Serenity settings)" });
|
|
8605
8663
|
return;
|
|
8606
8664
|
}
|
|
8607
8665
|
let body;
|
|
8608
8666
|
try {
|
|
8609
|
-
body = JSON.parse(await readBody(req));
|
|
8667
|
+
body = JSON.parse(await readBody$1(req));
|
|
8610
8668
|
} catch {
|
|
8611
|
-
sendJson(res, 400, [{
|
|
8669
|
+
sendJson$1(res, 400, [{
|
|
8612
8670
|
jsonrpc: "2.0",
|
|
8613
8671
|
id: null,
|
|
8614
8672
|
error: {
|
|
@@ -8618,7 +8676,7 @@ async function handle(ctx, defaultRoot, req, res) {
|
|
|
8618
8676
|
}]);
|
|
8619
8677
|
return;
|
|
8620
8678
|
}
|
|
8621
|
-
sendJson(res, 200, await dispatchRpc(new AcpServer(ctx), body));
|
|
8679
|
+
sendJson$1(res, 200, await dispatchRpc(new AcpServer(ctx), body));
|
|
8622
8680
|
return;
|
|
8623
8681
|
}
|
|
8624
8682
|
if (!s.publicAskEnabled) {
|
|
@@ -8627,10 +8685,10 @@ async function handle(ctx, defaultRoot, req, res) {
|
|
|
8627
8685
|
return;
|
|
8628
8686
|
}
|
|
8629
8687
|
if (req.method === "POST" && (url === "/ask" || url.startsWith("/c/"))) {
|
|
8630
|
-
sendJson(res, 403, { error: "public ask disabled (enable in Serenity settings)" });
|
|
8688
|
+
sendJson$1(res, 403, { error: "public ask disabled (enable in Serenity settings)" });
|
|
8631
8689
|
return;
|
|
8632
8690
|
}
|
|
8633
|
-
sendJson(res, 404, { error: "not found" });
|
|
8691
|
+
sendJson$1(res, 404, { error: "not found" });
|
|
8634
8692
|
return;
|
|
8635
8693
|
}
|
|
8636
8694
|
const containerAllowed = (name) => {
|
|
@@ -8661,16 +8719,16 @@ async function handle(ctx, defaultRoot, req, res) {
|
|
|
8661
8719
|
const name = decodeURIComponent(aMatch[1] ?? "");
|
|
8662
8720
|
const ccc = (await discoverCccs(ctx, defaultRoot || (findSerenityRoot(process.cwd()) ?? ""))).find((c) => c.name === name);
|
|
8663
8721
|
if (!ccc || !containerAllowed(name)) {
|
|
8664
|
-
sendJson(res, 403, { error: `container "${name}" is not open for public ask` });
|
|
8722
|
+
sendJson$1(res, 403, { error: `container "${name}" is not open for public ask` });
|
|
8665
8723
|
return;
|
|
8666
8724
|
}
|
|
8667
8725
|
let body;
|
|
8668
8726
|
try {
|
|
8669
|
-
const parsed = JSON.parse(await readBody(req));
|
|
8727
|
+
const parsed = JSON.parse(await readBody$1(req));
|
|
8670
8728
|
if (parsed === null || typeof parsed !== "object") throw new Error("not an object");
|
|
8671
8729
|
body = parsed;
|
|
8672
8730
|
} catch {
|
|
8673
|
-
sendJson(res, 400, { error: "invalid JSON body" });
|
|
8731
|
+
sendJson$1(res, 400, { error: "invalid JSON body" });
|
|
8674
8732
|
return;
|
|
8675
8733
|
}
|
|
8676
8734
|
await handleAskParsed(ctx, ccc, body, res, ip);
|
|
@@ -8679,11 +8737,11 @@ async function handle(ctx, defaultRoot, req, res) {
|
|
|
8679
8737
|
if (req.method === "POST" && url === "/ask") {
|
|
8680
8738
|
let body;
|
|
8681
8739
|
try {
|
|
8682
|
-
const parsed = JSON.parse(await readBody(req));
|
|
8740
|
+
const parsed = JSON.parse(await readBody$1(req));
|
|
8683
8741
|
if (parsed === null || typeof parsed !== "object") throw new Error("not an object");
|
|
8684
8742
|
body = parsed;
|
|
8685
8743
|
} catch {
|
|
8686
|
-
sendJson(res, 400, { error: "invalid JSON body" });
|
|
8744
|
+
sendJson$1(res, 400, { error: "invalid JSON body" });
|
|
8687
8745
|
return;
|
|
8688
8746
|
}
|
|
8689
8747
|
const cccValue = typeof body.ccc === "string" && body.ccc !== "" ? body.ccc : void 0;
|
|
@@ -8699,15 +8757,15 @@ async function handle(ctx, defaultRoot, req, res) {
|
|
|
8699
8757
|
};
|
|
8700
8758
|
}
|
|
8701
8759
|
if (!ccc || !containerAllowed(ccc.name)) {
|
|
8702
|
-
sendJson(res, 403, { error: "container is not open for public ask" });
|
|
8760
|
+
sendJson$1(res, 403, { error: "container is not open for public ask" });
|
|
8703
8761
|
return;
|
|
8704
8762
|
}
|
|
8705
8763
|
await handleAskParsed(ctx, ccc, body, res, ip);
|
|
8706
8764
|
return;
|
|
8707
8765
|
}
|
|
8708
|
-
sendJson(res, 404, { error: "not found" });
|
|
8766
|
+
sendJson$1(res, 404, { error: "not found" });
|
|
8709
8767
|
} catch (err) {
|
|
8710
|
-
sendJson(res, 500, { error: err?.message ?? String(err) });
|
|
8768
|
+
sendJson$1(res, 500, { error: err?.message ?? String(err) });
|
|
8711
8769
|
}
|
|
8712
8770
|
}
|
|
8713
8771
|
/**
|
|
@@ -8719,12 +8777,12 @@ async function handle(ctx, defaultRoot, req, res) {
|
|
|
8719
8777
|
async function handleAskParsed(ctx, ccc, body, res, ip) {
|
|
8720
8778
|
const key = typeof body.key === "string" ? body.key : void 0;
|
|
8721
8779
|
if (isPublicAskIpLocked(ip)) {
|
|
8722
|
-
sendJson(res, 429, { error: "too many failed attempts — temporarily locked (retry later)" });
|
|
8780
|
+
sendJson$1(res, 429, { error: "too many failed attempts — temporarily locked (retry later)" });
|
|
8723
8781
|
return;
|
|
8724
8782
|
}
|
|
8725
8783
|
if (!verifyPublicAskKey(key)) {
|
|
8726
8784
|
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" });
|
|
8785
|
+
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
8786
|
return;
|
|
8729
8787
|
}
|
|
8730
8788
|
resetPublicAskIpFail(ip);
|
|
@@ -8733,7 +8791,7 @@ async function handleAskParsed(ctx, ccc, body, res, ip) {
|
|
|
8733
8791
|
const question = typeof body.question === "string" ? body.question : "";
|
|
8734
8792
|
const sessionId = typeof body.sessionId === "string" && body.sessionId !== "" ? body.sessionId : void 0;
|
|
8735
8793
|
if (!question.trim()) {
|
|
8736
|
-
sendJson(res, 400, { error: "empty question" });
|
|
8794
|
+
sendJson$1(res, 400, { error: "empty question" });
|
|
8737
8795
|
return;
|
|
8738
8796
|
}
|
|
8739
8797
|
const roles = readSkiffRoles(root);
|
|
@@ -8741,7 +8799,7 @@ async function handleAskParsed(ctx, ccc, body, res, ip) {
|
|
|
8741
8799
|
const effectiveRole = roleName && explicit ? roleName : roles.keys().next().value;
|
|
8742
8800
|
const effectiveCfg = (roleName && explicit ? explicit : effectiveRole ? roles.get(effectiveRole) : void 0) ?? void 0;
|
|
8743
8801
|
if (!effectiveRole || !effectiveCfg) {
|
|
8744
|
-
sendJson(res, 400, { error: `no skiff role available in ccc: ${root}` });
|
|
8802
|
+
sendJson$1(res, 400, { error: `no skiff role available in ccc: ${root}` });
|
|
8745
8803
|
return;
|
|
8746
8804
|
}
|
|
8747
8805
|
let agent;
|
|
@@ -8759,7 +8817,7 @@ async function handleAskParsed(ctx, ccc, body, res, ip) {
|
|
|
8759
8817
|
agent = (await createSkiffAgent(ctx, root, effectiveRole, effectiveCfg, hc?.defaultModel)).agent;
|
|
8760
8818
|
}
|
|
8761
8819
|
const result = await askSkiff(ctx, agent, question, void 0, { includeTrajectory: false });
|
|
8762
|
-
sendJson(res, 200, {
|
|
8820
|
+
sendJson$1(res, 200, {
|
|
8763
8821
|
answer: result.answer,
|
|
8764
8822
|
answer_html: renderSkiffMarkdown(result.answer, true),
|
|
8765
8823
|
sessionId: result.sessionId,
|
|
@@ -9140,7 +9198,8 @@ function buildOutgoingHookEvent(input) {
|
|
|
9140
9198
|
userId: input.userId,
|
|
9141
9199
|
sessionId: input.sessionId,
|
|
9142
9200
|
role: input.role,
|
|
9143
|
-
reply: input.reply
|
|
9201
|
+
reply: input.reply,
|
|
9202
|
+
...input.source === "proactive" ? { source: "proactive" } : {}
|
|
9144
9203
|
};
|
|
9145
9204
|
}
|
|
9146
9205
|
/**
|
|
@@ -9255,10 +9314,12 @@ function invokeWeixinHook(root, hookRel, event) {
|
|
|
9255
9314
|
var weixin_bridge_exports = /* @__PURE__ */ __exportAll({
|
|
9256
9315
|
handleIncoming: () => handleIncoming,
|
|
9257
9316
|
registerWeixinBridge: () => registerWeixinBridge,
|
|
9317
|
+
sendProactiveText: () => sendProactiveText,
|
|
9258
9318
|
stopAllBridges: () => stopAllBridges,
|
|
9259
9319
|
stopCccBridge: () => stopCccBridge,
|
|
9260
9320
|
syncCccBridge: () => syncCccBridge,
|
|
9261
|
-
weixinBridgeStatus: () => weixinBridgeStatus
|
|
9321
|
+
weixinBridgeStatus: () => weixinBridgeStatus,
|
|
9322
|
+
weixinManualOutputLine: () => weixinManualOutputLine
|
|
9262
9323
|
});
|
|
9263
9324
|
const bridges = /* @__PURE__ */ new Map();
|
|
9264
9325
|
/** 轮询间隔(getupdates 失败后重试延迟;成功 = 立即续轮询) */
|
|
@@ -9338,6 +9399,30 @@ async function runAccountLoop(ctx, root, accountId, cred, loop) {
|
|
|
9338
9399
|
}
|
|
9339
9400
|
}
|
|
9340
9401
|
/**
|
|
9402
|
+
* 手动输出模式的**输出纪律块**(v1.30.10,`weixin.autoReplyWithLastMessage: false` 时注入)。
|
|
9403
|
+
*
|
|
9404
|
+
* 为什么必须注入(R↓):关掉自动回发后,agent 若不知道"桥不会替你转发",用户就收不到任何回复
|
|
9405
|
+
* ——机制变更必须伴随可执行的行动指引。因此这里给的是**可直接照抄的完整命令**(CCC 根 + 账号 +
|
|
9406
|
+
* 用户 id 全部填好),而不是抽象提示(E↑)。
|
|
9407
|
+
*
|
|
9408
|
+
* 参数全部显式:`--ccc`(必填,无隐式当前 CCC)、`--account`(沿用收到消息的账号,避免多账号下
|
|
9409
|
+
* 用错 bot 身份回话)、`--user`(该用户 iLink id,免去别名解析)。
|
|
9410
|
+
*/
|
|
9411
|
+
function weixinManualOutputLine(root, accountId, userId) {
|
|
9412
|
+
return [
|
|
9413
|
+
"── Reply Output (manual mode) ──",
|
|
9414
|
+
"The bridge will NOT forward your final message to the user for this CCC.",
|
|
9415
|
+
"Send everything the user should see yourself, e.g.:",
|
|
9416
|
+
` msm("weixin-send", ["send", "--ccc", "${root}", "--account", "${accountId}", "--user", "${userId}", "<your reply>"])`,
|
|
9417
|
+
"Rules:",
|
|
9418
|
+
" 1. Send your reply before the turn ends — if you send nothing, the user sees nothing (no fallback).",
|
|
9419
|
+
" 2. You may send several messages (progress / final answer); split only when it helps the reader.",
|
|
9420
|
+
" 3. Plain text only — WeChat renders no markdown.",
|
|
9421
|
+
" 4. Each send is recorded automatically (source=proactive); do not repeat the same content.",
|
|
9422
|
+
"── ──"
|
|
9423
|
+
].join("\n");
|
|
9424
|
+
}
|
|
9425
|
+
/**
|
|
9341
9426
|
* 处理单条微信消息:路由 → skiff 会话(固定 id 创建/延续)→ 提问 → 回复回写。
|
|
9342
9427
|
*
|
|
9343
9428
|
* 会话语义:`weixinSessionIdFor(fromUserId)` 固定可重建——同用户长期同一会话;
|
|
@@ -9384,7 +9469,7 @@ async function handleIncoming(ctx, root, accountId, cred, msg) {
|
|
|
9384
9469
|
const existing = getSkiffAgent(sessionId);
|
|
9385
9470
|
const hc = readHandymanConfig(root);
|
|
9386
9471
|
if (existing) try {
|
|
9387
|
-
ensureSkiffSession(root, existing, roleName, role);
|
|
9472
|
+
ensureSkiffSession(root, existing, roleName, role, true);
|
|
9388
9473
|
} catch (err) {
|
|
9389
9474
|
console.warn(`[serenity-hooks] weixin-bridge ensure 失败(不影响处理): ${String(err?.message ?? err)}`);
|
|
9390
9475
|
}
|
|
@@ -9451,6 +9536,8 @@ async function handleIncoming(ctx, root, accountId, cred, msg) {
|
|
|
9451
9536
|
const parts = [];
|
|
9452
9537
|
const activeWorkspace = getActiveSessionInfo(sessionId);
|
|
9453
9538
|
if (activeWorkspace?.mdPath) parts.push(workspaceTrajectoryLine(activeWorkspace.mdPath));
|
|
9539
|
+
const manualOutput = settings.autoReplyWithLastMessage === false;
|
|
9540
|
+
if (manualOutput) parts.push(weixinManualOutputLine(root, accountId, fromUserId));
|
|
9454
9541
|
if (text) parts.push(text);
|
|
9455
9542
|
parts.push(...mediaNotes, ...degradedNotes);
|
|
9456
9543
|
const question = parts.join("\n");
|
|
@@ -9467,6 +9554,7 @@ async function handleIncoming(ctx, root, accountId, cred, msg) {
|
|
|
9467
9554
|
console.log(`[serenity-hooks] weixin hook incoming error: ${err instanceof Error ? err.message : String(err)}`);
|
|
9468
9555
|
});
|
|
9469
9556
|
const answer = (await askSkiff(ctx, ref.agent, question, void 0, { includeTrajectory: false })).answer ?? "";
|
|
9557
|
+
if (manualOutput) return;
|
|
9470
9558
|
if (answer === "") return;
|
|
9471
9559
|
const reply = markdownToPlainText(stripThink(answer));
|
|
9472
9560
|
await sendTextMessage({
|
|
@@ -9535,11 +9623,93 @@ function stopCccBridge(root) {
|
|
|
9535
9623
|
for (const loop of bridge.loops.values()) loop.stopped = true;
|
|
9536
9624
|
bridges.delete(root);
|
|
9537
9625
|
}
|
|
9538
|
-
/**
|
|
9626
|
+
/**
|
|
9627
|
+
* 停止全部桥(插件 dispose)
|
|
9628
|
+
*/
|
|
9539
9629
|
function stopAllBridges() {
|
|
9540
9630
|
for (const bridge of bridges.values()) for (const loop of bridge.loops.values()) loop.stopped = true;
|
|
9541
9631
|
bridges.clear();
|
|
9542
9632
|
}
|
|
9633
|
+
/**
|
|
9634
|
+
* 以 bot 身份主动给指定用户发文本(**不经用户消息触发**)。
|
|
9635
|
+
*
|
|
9636
|
+
* 为什么放在桥里而不是让 CCC 自己直连 iLink(用户拍板 A 方案):
|
|
9637
|
+
* ① **记录归桥**——发送成功后触发既有 outgoing hook,`_weixin-logs/*.jsonl` 与对话回复
|
|
9638
|
+
* 同源同格式(事件多一个 `source: 'proactive'` 标记);② 协议/账号/凭据解析单一真相源,
|
|
9639
|
+
* CCC 侧不必重复实现 sendmessage;③ 多账号可选。
|
|
9640
|
+
*
|
|
9641
|
+
* 记录一致性:`sessionId` 取 `weixinSessionIdFor(toUserId)`——与该用户平时对话**同一条轨迹**,
|
|
9642
|
+
* 记录里能直接按会话串联;`role` 取该 CCC 路由命中角色(未命中 → 空串)。
|
|
9643
|
+
*
|
|
9644
|
+
* 主动消息不带 context_token(iLink 接受 bot 主动发起;带 token 的路径见 handleIncoming 回复)。
|
|
9645
|
+
* @param input CCC 根 + 目标用户 + 文本 +(可选)账号
|
|
9646
|
+
* @returns 成功含 accountId/userId/sessionId/role;失败含稳定 code 与可行动提示(不抛错)
|
|
9647
|
+
*/
|
|
9648
|
+
async function sendProactiveText(input) {
|
|
9649
|
+
const { root, toUserId } = input;
|
|
9650
|
+
const settings = readWeixinSettings(root);
|
|
9651
|
+
if (!settings.enabled) return {
|
|
9652
|
+
ok: false,
|
|
9653
|
+
code: "BRIDGE_DISABLED",
|
|
9654
|
+
error: `微信桥未启用(ccc=${root})`,
|
|
9655
|
+
remediation: "在 CCC 的 .opencode/serenity.json weixin.enabled 打开"
|
|
9656
|
+
};
|
|
9657
|
+
const accounts = (settings.accounts ?? []).filter((a) => a.enabled !== false);
|
|
9658
|
+
if (accounts.length === 0) return {
|
|
9659
|
+
ok: false,
|
|
9660
|
+
code: "NO_ACCOUNT",
|
|
9661
|
+
error: "微信桥未配置任何启用账号",
|
|
9662
|
+
remediation: "在设置面板「微信桥」扫码绑定账号"
|
|
9663
|
+
};
|
|
9664
|
+
const accountId = input.accountId ?? accounts[0].accountId;
|
|
9665
|
+
if (!accounts.some((a) => a.accountId === accountId)) return {
|
|
9666
|
+
ok: false,
|
|
9667
|
+
code: "ACCOUNT_NOT_FOUND",
|
|
9668
|
+
error: `账号不存在或未启用: ${accountId}`,
|
|
9669
|
+
remediation: `可用账号: ${accounts.map((a) => a.accountId).join(", ")}`
|
|
9670
|
+
};
|
|
9671
|
+
const cred = readWeixinCredential(root, accountId);
|
|
9672
|
+
if (!cred) return {
|
|
9673
|
+
ok: false,
|
|
9674
|
+
code: "ACCOUNT_NOT_BOUND",
|
|
9675
|
+
error: `账号未绑定(无凭据): ${accountId}`,
|
|
9676
|
+
remediation: "在设置面板重新扫码绑定"
|
|
9677
|
+
};
|
|
9678
|
+
const sessionId = weixinSessionIdFor(toUserId);
|
|
9679
|
+
const role = matchWeixinRoute(settings.routes ?? [], toUserId) ?? "";
|
|
9680
|
+
try {
|
|
9681
|
+
await sendTextMessage({
|
|
9682
|
+
baseUrl: cred.baseUrl,
|
|
9683
|
+
token: cred.token,
|
|
9684
|
+
toUserId,
|
|
9685
|
+
text: input.text
|
|
9686
|
+
});
|
|
9687
|
+
} catch (err) {
|
|
9688
|
+
return {
|
|
9689
|
+
ok: false,
|
|
9690
|
+
code: "SEND_FAILED",
|
|
9691
|
+
error: `发送失败: ${err instanceof Error ? err.message : String(err)}`
|
|
9692
|
+
};
|
|
9693
|
+
}
|
|
9694
|
+
if (settings.hook) invokeWeixinHook(root, settings.hook, buildOutgoingHookEvent({
|
|
9695
|
+
cccRoot: root,
|
|
9696
|
+
accountId,
|
|
9697
|
+
userId: toUserId,
|
|
9698
|
+
sessionId,
|
|
9699
|
+
role,
|
|
9700
|
+
reply: markdownToPlainText(input.text),
|
|
9701
|
+
source: "proactive"
|
|
9702
|
+
})).catch((err) => {
|
|
9703
|
+
console.log(`[serenity-hooks] weixin hook outgoing(proactive) error: ${err instanceof Error ? err.message : String(err)}`);
|
|
9704
|
+
});
|
|
9705
|
+
return {
|
|
9706
|
+
ok: true,
|
|
9707
|
+
accountId,
|
|
9708
|
+
userId: toUserId,
|
|
9709
|
+
sessionId,
|
|
9710
|
+
role
|
|
9711
|
+
};
|
|
9712
|
+
}
|
|
9543
9713
|
/** 桥状态快照(面板数据源):每 CCC → 每账号 → 轮询健康 */
|
|
9544
9714
|
function weixinBridgeStatus() {
|
|
9545
9715
|
const out = [];
|
|
@@ -9571,6 +9741,363 @@ function registerWeixinBridge(ctx) {
|
|
|
9571
9741
|
} catch {}
|
|
9572
9742
|
}
|
|
9573
9743
|
//#endregion
|
|
9744
|
+
//#region src/weixin-send-api.ts
|
|
9745
|
+
/**
|
|
9746
|
+
* weixin-send-api.ts — 主动发送入口(S142 v1.30.9,用户需求"微信桥支持被调用发消息给指定用户")
|
|
9747
|
+
*
|
|
9748
|
+
* 形态(用户拍板 A2):**独立监听器,只绑 127.0.0.1**(默认 3082,plugin 全局配置 weixinApi)。
|
|
9749
|
+
* 为什么不挂在主 webServer(3080) 上:3080 会被 3081 公网网关原样反代(含全部请求头),
|
|
9750
|
+
* 而 3081 的登录态属于"家庭账号"——一旦挂上去,任何已登录的外部用户都能冒充 bot 发消息。
|
|
9751
|
+
* 独立 loopback 监听器则**不经网关、公网不可达**,因此不做共享密钥;仍校验来源地址。
|
|
9752
|
+
*
|
|
9753
|
+
* 调用者 = CCC 自己的 MSM(`msm weixin-send ...`):ACC **不新增工具**,只多一个本机 HTTP 面。
|
|
9754
|
+
* 发送与记录都经 `weixin-bridge.sendProactiveText` → 复用既有 outgoing hook(`source: 'proactive'`)。
|
|
9755
|
+
*
|
|
9756
|
+
* 契约:
|
|
9757
|
+
* POST /send { ccc: "<名称|绝对路径>", user: "<from_user_id>", text: "...", accountId?: "wechat-1" }
|
|
9758
|
+
* → 200 { ok: true, accountId, userId, sessionId, role }
|
|
9759
|
+
* → 4xx/5xx { ok: false, code, error, remediation? }
|
|
9760
|
+
* GET /health → 200 { ok: true, port }
|
|
9761
|
+
*
|
|
9762
|
+
* 职责切分(E↑):**别名 → id 的解析归 CCC**(MSM 读该 CCC localstore 的 WEIXIN_USER_*);
|
|
9763
|
+
* 本入口只接受 iLink 形式的 id。协议、账号选择、记录归桥。
|
|
9764
|
+
*/
|
|
9765
|
+
/** 请求体上限(文本消息远小于此;防滥用) */
|
|
9766
|
+
const MAX_BODY_BYTES = 65536;
|
|
9767
|
+
/** 文本长度上限(微信侧单条消息实践上限;超限拒绝而非静默截断) */
|
|
9768
|
+
const WEIXIN_SEND_MAX_TEXT = 4e3;
|
|
9769
|
+
/** 运行中的入口(null = 未启动) */
|
|
9770
|
+
let active = null;
|
|
9771
|
+
/** 当前入口地址(MSM 环境变量注入用;未启动 → null)——状态在叶模块(msm-ops 零依赖读取) */
|
|
9772
|
+
/** 测试辅助:读取当前监听端口(生产零调用) */
|
|
9773
|
+
function weixinSendPort() {
|
|
9774
|
+
return active?.port ?? null;
|
|
9775
|
+
}
|
|
9776
|
+
/** 来源必须是 loopback(127.0.0.1 / ::1 / IPv4-mapped) */
|
|
9777
|
+
function isLoopbackAddress(address) {
|
|
9778
|
+
if (typeof address !== "string" || address === "") return false;
|
|
9779
|
+
const a = address.startsWith("::ffff:") ? address.slice(7) : address;
|
|
9780
|
+
return a === "127.0.0.1" || a === "::1";
|
|
9781
|
+
}
|
|
9782
|
+
/**
|
|
9783
|
+
* 校验并归一 /send 请求体(纯函数,可测)。
|
|
9784
|
+
* 只做**结构**校验:ccc 必填(无隐式当前 CCC——用户拍板)、user 必须是 iLink id 形式、text 非空且不超限。
|
|
9785
|
+
*/
|
|
9786
|
+
function parseSendRequest(body) {
|
|
9787
|
+
if (body === null || typeof body !== "object") return {
|
|
9788
|
+
ok: false,
|
|
9789
|
+
error: "body must be a JSON object"
|
|
9790
|
+
};
|
|
9791
|
+
const o = body;
|
|
9792
|
+
const ccc = typeof o.ccc === "string" ? o.ccc.trim() : "";
|
|
9793
|
+
if (ccc === "") return {
|
|
9794
|
+
ok: false,
|
|
9795
|
+
error: "missing \"ccc\"",
|
|
9796
|
+
remediation: "ccc = CCC 名称或绝对路径(必填,无默认值)"
|
|
9797
|
+
};
|
|
9798
|
+
const user = typeof o.user === "string" ? o.user.trim() : "";
|
|
9799
|
+
if (user === "") return {
|
|
9800
|
+
ok: false,
|
|
9801
|
+
error: "missing \"user\"",
|
|
9802
|
+
remediation: "user = iLink from_user_id(形如 xxx@im.wechat);别名解析归调用方"
|
|
9803
|
+
};
|
|
9804
|
+
if (!user.includes("@")) return {
|
|
9805
|
+
ok: false,
|
|
9806
|
+
error: `invalid "user": ${user}`,
|
|
9807
|
+
remediation: "需为 iLink from_user_id(含 @);别名(yh/danica)请在调用方解析"
|
|
9808
|
+
};
|
|
9809
|
+
const text = typeof o.text === "string" ? o.text.trim() : "";
|
|
9810
|
+
if (text === "") return {
|
|
9811
|
+
ok: false,
|
|
9812
|
+
error: "missing \"text\"",
|
|
9813
|
+
remediation: "text = 要发送的文本(非空)"
|
|
9814
|
+
};
|
|
9815
|
+
if (text.length > 4e3) return {
|
|
9816
|
+
ok: false,
|
|
9817
|
+
error: `text too long: ${text.length} > ${WEIXIN_SEND_MAX_TEXT}`
|
|
9818
|
+
};
|
|
9819
|
+
const accountId = typeof o.accountId === "string" && o.accountId.trim() !== "" ? o.accountId.trim() : void 0;
|
|
9820
|
+
return {
|
|
9821
|
+
ok: true,
|
|
9822
|
+
ccc,
|
|
9823
|
+
user,
|
|
9824
|
+
text,
|
|
9825
|
+
...accountId ? { accountId } : {}
|
|
9826
|
+
};
|
|
9827
|
+
}
|
|
9828
|
+
/**
|
|
9829
|
+
* 按名称/路径解析 CCC 根(纯函数,可测)。
|
|
9830
|
+
* 匹配顺序:① 绝对路径精确/上溯 ② 目录名 ③ `.serenity` 首行名(大小写不敏感)。
|
|
9831
|
+
* 歧义(多个同名)→ 报错并列出候选(不猜——用户拍板"ccc 必须指定")。
|
|
9832
|
+
*/
|
|
9833
|
+
function matchCcc(input, candidates) {
|
|
9834
|
+
const trimmed = input.trim();
|
|
9835
|
+
if (trimmed === "") return {
|
|
9836
|
+
ok: false,
|
|
9837
|
+
error: "empty ccc"
|
|
9838
|
+
};
|
|
9839
|
+
if (trimmed.startsWith("/")) {
|
|
9840
|
+
const root = findSerenityRoot(trimmed);
|
|
9841
|
+
if (!root) return {
|
|
9842
|
+
ok: false,
|
|
9843
|
+
error: `no CCC found from path: ${trimmed}`,
|
|
9844
|
+
remediation: "确认该路径在某个 CCC 内(含 .serenity 标记)"
|
|
9845
|
+
};
|
|
9846
|
+
return {
|
|
9847
|
+
ok: true,
|
|
9848
|
+
root
|
|
9849
|
+
};
|
|
9850
|
+
}
|
|
9851
|
+
const lower = trimmed.toLowerCase();
|
|
9852
|
+
const exactPath = candidates.find((c) => c.root.toLowerCase() === lower);
|
|
9853
|
+
if (exactPath) return {
|
|
9854
|
+
ok: true,
|
|
9855
|
+
root: exactPath.root
|
|
9856
|
+
};
|
|
9857
|
+
const byName = candidates.filter((c) => c.dirName.toLowerCase() === lower || c.cccName !== null && c.cccName.toLowerCase() === lower);
|
|
9858
|
+
if (byName.length === 1) return {
|
|
9859
|
+
ok: true,
|
|
9860
|
+
root: byName[0].root
|
|
9861
|
+
};
|
|
9862
|
+
if (byName.length > 1) return {
|
|
9863
|
+
ok: false,
|
|
9864
|
+
error: `ambiguous ccc name: ${trimmed}`,
|
|
9865
|
+
remediation: `同名多个,请用绝对路径:${byName.map((c) => c.root).join(" | ")}`
|
|
9866
|
+
};
|
|
9867
|
+
return {
|
|
9868
|
+
ok: false,
|
|
9869
|
+
error: `unknown ccc: ${trimmed}`,
|
|
9870
|
+
remediation: candidates.length === 0 ? "当前进程未发现任何 CCC(打开一个 CCC 会话后重试)" : `已知 CCC: ${candidates.map((c) => c.dirName).join(", ")}`
|
|
9871
|
+
};
|
|
9872
|
+
}
|
|
9873
|
+
/** 组装候选列表(discoverCccs 投影 + `.serenity` 名;动态 import 保持本模块静态依赖轻量) */
|
|
9874
|
+
async function collectCandidates(ctx) {
|
|
9875
|
+
const { discoverCccs } = await import("./skiff-debug-TONJlpgF.js").then((n) => n.i);
|
|
9876
|
+
const entries = await discoverCccs(ctx, process.cwd());
|
|
9877
|
+
const seen = /* @__PURE__ */ new Set();
|
|
9878
|
+
const out = [];
|
|
9879
|
+
for (const e of entries) {
|
|
9880
|
+
if (!e?.root || seen.has(e.root)) continue;
|
|
9881
|
+
seen.add(e.root);
|
|
9882
|
+
out.push({
|
|
9883
|
+
root: e.root,
|
|
9884
|
+
dirName: e.name || basename(e.root),
|
|
9885
|
+
cccName: readCccName$2(e.root)
|
|
9886
|
+
});
|
|
9887
|
+
}
|
|
9888
|
+
return out;
|
|
9889
|
+
}
|
|
9890
|
+
function sendJson(res, status, payload) {
|
|
9891
|
+
const body = JSON.stringify(payload);
|
|
9892
|
+
res.writeHead(status, {
|
|
9893
|
+
"content-type": "application/json; charset=utf-8",
|
|
9894
|
+
"content-length": Buffer.byteLength(body)
|
|
9895
|
+
});
|
|
9896
|
+
res.end(body);
|
|
9897
|
+
}
|
|
9898
|
+
async function readBody(req) {
|
|
9899
|
+
const chunks = [];
|
|
9900
|
+
let total = 0;
|
|
9901
|
+
for await (const chunk of req) {
|
|
9902
|
+
const buf = chunk;
|
|
9903
|
+
total += buf.length;
|
|
9904
|
+
if (total > MAX_BODY_BYTES) throw new Error(`body too large (> ${MAX_BODY_BYTES} bytes)`);
|
|
9905
|
+
chunks.push(buf);
|
|
9906
|
+
}
|
|
9907
|
+
return Buffer.concat(chunks).toString("utf-8");
|
|
9908
|
+
}
|
|
9909
|
+
/** HTTP 状态映射(失败 code → 语义化状态;调用方按 code 判定,不看文案) */
|
|
9910
|
+
function statusForCode(code) {
|
|
9911
|
+
switch (code) {
|
|
9912
|
+
case "ACCOUNT_NOT_BOUND":
|
|
9913
|
+
case "BRIDGE_DISABLED":
|
|
9914
|
+
case "NO_ACCOUNT": return 409;
|
|
9915
|
+
case "ACCOUNT_NOT_FOUND": return 400;
|
|
9916
|
+
case "SEND_FAILED": return 502;
|
|
9917
|
+
default: return 400;
|
|
9918
|
+
}
|
|
9919
|
+
}
|
|
9920
|
+
/** 单请求处理(导出供测试直接驱动,不经真实端口) */
|
|
9921
|
+
async function handleWeixinSendRequest(ctx, req, res) {
|
|
9922
|
+
if (!isLoopbackAddress(req.socket?.remoteAddress)) {
|
|
9923
|
+
sendJson(res, 403, {
|
|
9924
|
+
ok: false,
|
|
9925
|
+
code: "FORBIDDEN",
|
|
9926
|
+
error: "only loopback clients allowed"
|
|
9927
|
+
});
|
|
9928
|
+
return;
|
|
9929
|
+
}
|
|
9930
|
+
const path = (req.url ?? "/").split("?")[0];
|
|
9931
|
+
if (req.method === "GET" && path === "/health") {
|
|
9932
|
+
sendJson(res, 200, {
|
|
9933
|
+
ok: true,
|
|
9934
|
+
port: weixinSendPort()
|
|
9935
|
+
});
|
|
9936
|
+
return;
|
|
9937
|
+
}
|
|
9938
|
+
if (path !== "/send") {
|
|
9939
|
+
sendJson(res, 404, {
|
|
9940
|
+
ok: false,
|
|
9941
|
+
code: "NOT_FOUND",
|
|
9942
|
+
error: `unknown path: ${path}`
|
|
9943
|
+
});
|
|
9944
|
+
return;
|
|
9945
|
+
}
|
|
9946
|
+
if (req.method !== "POST") {
|
|
9947
|
+
sendJson(res, 405, {
|
|
9948
|
+
ok: false,
|
|
9949
|
+
code: "METHOD_NOT_ALLOWED",
|
|
9950
|
+
error: "use POST /send"
|
|
9951
|
+
});
|
|
9952
|
+
return;
|
|
9953
|
+
}
|
|
9954
|
+
let raw;
|
|
9955
|
+
try {
|
|
9956
|
+
raw = await readBody(req);
|
|
9957
|
+
} catch (err) {
|
|
9958
|
+
sendJson(res, 413, {
|
|
9959
|
+
ok: false,
|
|
9960
|
+
code: "BODY_TOO_LARGE",
|
|
9961
|
+
error: String(err?.message ?? err)
|
|
9962
|
+
});
|
|
9963
|
+
return;
|
|
9964
|
+
}
|
|
9965
|
+
let body;
|
|
9966
|
+
try {
|
|
9967
|
+
body = JSON.parse(raw === "" ? "{}" : raw);
|
|
9968
|
+
} catch (err) {
|
|
9969
|
+
sendJson(res, 400, {
|
|
9970
|
+
ok: false,
|
|
9971
|
+
code: "BAD_JSON",
|
|
9972
|
+
error: `invalid JSON: ${String(err?.message ?? err)}`
|
|
9973
|
+
});
|
|
9974
|
+
return;
|
|
9975
|
+
}
|
|
9976
|
+
const parsed = parseSendRequest(body);
|
|
9977
|
+
if (!parsed.ok) {
|
|
9978
|
+
sendJson(res, 400, {
|
|
9979
|
+
ok: false,
|
|
9980
|
+
code: "BAD_REQUEST",
|
|
9981
|
+
error: parsed.error,
|
|
9982
|
+
remediation: parsed.remediation
|
|
9983
|
+
});
|
|
9984
|
+
return;
|
|
9985
|
+
}
|
|
9986
|
+
let root;
|
|
9987
|
+
try {
|
|
9988
|
+
const matched = matchCcc(parsed.ccc, await collectCandidates(ctx));
|
|
9989
|
+
if (!matched.ok) {
|
|
9990
|
+
sendJson(res, 404, {
|
|
9991
|
+
ok: false,
|
|
9992
|
+
code: "UNKNOWN_CCC",
|
|
9993
|
+
error: matched.error,
|
|
9994
|
+
remediation: matched.remediation
|
|
9995
|
+
});
|
|
9996
|
+
return;
|
|
9997
|
+
}
|
|
9998
|
+
root = matched.root;
|
|
9999
|
+
} catch (err) {
|
|
10000
|
+
sendJson(res, 500, {
|
|
10001
|
+
ok: false,
|
|
10002
|
+
code: "CCC_DISCOVERY_FAILED",
|
|
10003
|
+
error: String(err?.message ?? err)
|
|
10004
|
+
});
|
|
10005
|
+
return;
|
|
10006
|
+
}
|
|
10007
|
+
const result = await sendProactiveText({
|
|
10008
|
+
root,
|
|
10009
|
+
toUserId: parsed.user,
|
|
10010
|
+
text: parsed.text,
|
|
10011
|
+
...parsed.accountId ? { accountId: parsed.accountId } : {}
|
|
10012
|
+
});
|
|
10013
|
+
if (result.ok) {
|
|
10014
|
+
sendJson(res, 200, {
|
|
10015
|
+
ok: true,
|
|
10016
|
+
accountId: result.accountId,
|
|
10017
|
+
userId: result.userId,
|
|
10018
|
+
sessionId: result.sessionId,
|
|
10019
|
+
role: result.role
|
|
10020
|
+
});
|
|
10021
|
+
return;
|
|
10022
|
+
}
|
|
10023
|
+
sendJson(res, statusForCode(result.code), {
|
|
10024
|
+
ok: false,
|
|
10025
|
+
code: result.code,
|
|
10026
|
+
error: result.error,
|
|
10027
|
+
remediation: result.remediation
|
|
10028
|
+
});
|
|
10029
|
+
}
|
|
10030
|
+
/** 启动监听器(仅 127.0.0.1);已启动 → 幂等返回 */
|
|
10031
|
+
async function startWeixinSendApi(ctx, port) {
|
|
10032
|
+
if (active) return active.port;
|
|
10033
|
+
const server = createServer((req, res) => {
|
|
10034
|
+
handleWeixinSendRequest(ctx, req, res).catch((err) => {
|
|
10035
|
+
console.warn(`[serenity-hooks] ✗ weixin send api 处理失败: ${String(err?.message ?? err)}`);
|
|
10036
|
+
try {
|
|
10037
|
+
sendJson(res, 500, {
|
|
10038
|
+
ok: false,
|
|
10039
|
+
code: "INTERNAL",
|
|
10040
|
+
error: "internal error"
|
|
10041
|
+
});
|
|
10042
|
+
} catch {}
|
|
10043
|
+
});
|
|
10044
|
+
});
|
|
10045
|
+
await new Promise((resolve, reject) => {
|
|
10046
|
+
server.once("error", reject);
|
|
10047
|
+
server.listen(port, "127.0.0.1", () => resolve());
|
|
10048
|
+
});
|
|
10049
|
+
server.unref();
|
|
10050
|
+
const addr = server.address();
|
|
10051
|
+
const actualPort = typeof addr === "object" && addr !== null ? addr.port : port;
|
|
10052
|
+
active = {
|
|
10053
|
+
server,
|
|
10054
|
+
port: actualPort
|
|
10055
|
+
};
|
|
10056
|
+
setWeixinSendEndpoint(`http://127.0.0.1:${actualPort}`);
|
|
10057
|
+
console.log(`[serenity-hooks] ✓ 微信主动发送入口: http://127.0.0.1:${actualPort}/send(仅 loopback;CCC MSM 调用)`);
|
|
10058
|
+
return actualPort;
|
|
10059
|
+
}
|
|
10060
|
+
/** 停止监听器(幂等) */
|
|
10061
|
+
function stopWeixinSendApi() {
|
|
10062
|
+
if (!active) return;
|
|
10063
|
+
try {
|
|
10064
|
+
active.server.close();
|
|
10065
|
+
} catch {}
|
|
10066
|
+
active = null;
|
|
10067
|
+
setWeixinSendEndpoint(null);
|
|
10068
|
+
}
|
|
10069
|
+
/**
|
|
10070
|
+
* 装配(index.ts apply 调用):按 plugin 全局配置 weixinApi 启停,配置变化热同步。
|
|
10071
|
+
* 默认启用(仅 loopback,零公网暴露);`enabled:false` 或 `port:0` 关闭。
|
|
10072
|
+
*/
|
|
10073
|
+
function registerWeixinSendApi(ctx) {
|
|
10074
|
+
const sync = () => {
|
|
10075
|
+
let cfg;
|
|
10076
|
+
try {
|
|
10077
|
+
cfg = readAdvancedSettings().weixinApi;
|
|
10078
|
+
} catch (err) {
|
|
10079
|
+
console.warn(`[serenity-hooks] ✗ weixin send api 配置读取失败,跳过同步: ${String(err?.message ?? err)}`);
|
|
10080
|
+
return;
|
|
10081
|
+
}
|
|
10082
|
+
const want = cfg.enabled && cfg.port > 0;
|
|
10083
|
+
if (want && !active) {
|
|
10084
|
+
startWeixinSendApi(ctx, cfg.port).catch((err) => {
|
|
10085
|
+
console.warn(`[serenity-hooks] ✗ 微信主动发送入口启动失败(port=${cfg.port}): ${String(err?.message ?? err)}`);
|
|
10086
|
+
});
|
|
10087
|
+
return;
|
|
10088
|
+
}
|
|
10089
|
+
if (!want && active) stopWeixinSendApi();
|
|
10090
|
+
};
|
|
10091
|
+
try {
|
|
10092
|
+
ctx.on("serenity/config-updated", sync);
|
|
10093
|
+
} catch {}
|
|
10094
|
+
try {
|
|
10095
|
+
ctx.on("serenity/settings-changed", sync);
|
|
10096
|
+
} catch {}
|
|
10097
|
+
sync();
|
|
10098
|
+
registerDisposer(ctx, "weixin send api", () => stopWeixinSendApi());
|
|
10099
|
+
}
|
|
10100
|
+
//#endregion
|
|
9574
10101
|
//#region src/seams/lifecycle.ts
|
|
9575
10102
|
/** 从 Agent / Session / 事件负载中取会话 id(形状宽容:未知结构返回 null) */
|
|
9576
10103
|
function sessionIdOf(value) {
|
|
@@ -9703,6 +10230,7 @@ function apply(ctx, config) {
|
|
|
9703
10230
|
registerAcp(ctx);
|
|
9704
10231
|
registerAutopilot(ctx);
|
|
9705
10232
|
registerWeixinBridge(ctx);
|
|
10233
|
+
registerWeixinSendApi(ctx);
|
|
9706
10234
|
registerLifecycle(ctx);
|
|
9707
10235
|
}
|
|
9708
10236
|
/**
|