@shgroup/dsh-serenity-hooks 1.27.2 → 1.27.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dsh.plugin.json +1 -1
- package/experiments/{autotrajectory → autopilot-trajectory}/SKILL.md +24 -23
- package/experiments/{autotrajectory/scripts/autotrajectory-exp.ts → autopilot-trajectory/scripts/autopilot-trajectory.ts} +46 -46
- package/lib/autopilot-trajectory.d.ts +226 -0
- package/lib/ccc.d.ts +15 -10
- package/lib/client.js +273 -195
- package/lib/index.js +310 -146
- package/lib/{skiff-debug-vLsdk7wV.js → skiff-debug-BRlc17GU.js} +27 -0
- package/lib/tools/{autotrajectory-exp.d.ts → autopilot-trajectory.d.ts} +9 -8
- package/lib/{weixin-api-b8HLmcPE.js → weixin-api-DAugVJ-f.js} +126 -3
- package/lib/weixin-api.d.ts +91 -4
- package/lib/weixin-bridge.d.ts +13 -10
- package/lib/{weixin-route-CvAWARN3.js → weixin-route-EVu56Ytp.js} +51 -6
- package/lib/weixin-route.d.ts +28 -2
- package/package.json +3 -3
- package/lib/autotrajectory.d.ts +0 -193
|
@@ -295,9 +295,32 @@ async function createSkiffAgent(ctx, root, roleName, role, defaultModel, session
|
|
|
295
295
|
resumed: handle.resumed
|
|
296
296
|
};
|
|
297
297
|
}
|
|
298
|
+
/** 取 live agent(DSH 文档:`ctx.agents.get(id)` 返回 bare Agent;会话 live 时 resume/create 均不可用) */
|
|
299
|
+
function getLiveAgent(ctx, id) {
|
|
300
|
+
try {
|
|
301
|
+
return ctx.agents?.get?.(id);
|
|
302
|
+
} catch {
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
/** live 复用返回(resumed:true 历史延续;dispose 空操作——live agent 不归本模块所有,不能拆) */
|
|
307
|
+
function liveReuseRef(live, setup) {
|
|
308
|
+
return setup(live.ctx).catch(() => {}).then(() => ({
|
|
309
|
+
agent: live,
|
|
310
|
+
dispose: async () => {},
|
|
311
|
+
resumed: true
|
|
312
|
+
}));
|
|
313
|
+
}
|
|
298
314
|
/**
|
|
299
315
|
* resume-or-create 分派:固定 id → 优先 resume(持久化历史延续),失败降级 create;
|
|
300
316
|
* 随机 id(无固定 sessionId)→ 恒 create。返回 handle + resumed 标志。
|
|
317
|
+
*
|
|
318
|
+
* **v1.27.3 修复(微信桥"重启后不响应"根因):live 会话优先复用**——重启后 DSH 把
|
|
319
|
+
* 关闭时仍 live 的持久化会话恢复为 live(crash/restart 恢复语义)。此时 resume 报
|
|
320
|
+
* "cannot prepare session X while it is live"(session-persistence coordinator 显式
|
|
321
|
+
* 拒绝 live 会话)、create 报 "session X already exists"(磁盘有持久化 log 必拒)→
|
|
322
|
+
* 微信桥 catch 吞错 → 用户消息无回复。唯一可行路径 = `ctx.agents.get(id)` 取 live
|
|
323
|
+
* agent 直接续用(resumed:true;角色提示词重挂与内存注册表登记由 createSkiffAgent 统一做)。
|
|
301
324
|
*/
|
|
302
325
|
async function createOrResumeAgent(ctx, id, root, model, setup, fixedId) {
|
|
303
326
|
if (!fixedId) return {
|
|
@@ -312,6 +335,8 @@ async function createOrResumeAgent(ctx, id, root, model, setup, fixedId) {
|
|
|
312
335
|
}),
|
|
313
336
|
resumed: false
|
|
314
337
|
};
|
|
338
|
+
const liveAgent = getLiveAgent(ctx, id);
|
|
339
|
+
if (liveAgent) return liveReuseRef(liveAgent, setup);
|
|
315
340
|
const agentsWithResume = ctx.agents;
|
|
316
341
|
if (typeof agentsWithResume.resume !== "function") return {
|
|
317
342
|
...await ctx.agents.create({
|
|
@@ -335,6 +360,8 @@ async function createOrResumeAgent(ctx, id, root, model, setup, fixedId) {
|
|
|
335
360
|
resumed: true
|
|
336
361
|
};
|
|
337
362
|
} catch (err) {
|
|
363
|
+
const liveNow = getLiveAgent(ctx, id);
|
|
364
|
+
if (liveNow) return liveReuseRef(liveNow, setup);
|
|
338
365
|
const msg = err instanceof Error ? err.message : String(err);
|
|
339
366
|
const stack = err instanceof Error ? err.stack ?? "" : "";
|
|
340
367
|
console.log(`[serenity-hooks] skiff resume 失败降级 create (id=${id}): ${msg}`);
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* autopilot-trajectory.ts — Autopilot Trajectory 一站式管理工具(v1.26.12 实验
|
|
3
|
+
* autotrajectory-exp → v1.27.4 正式化改名)
|
|
3
4
|
*
|
|
4
|
-
* 定位:dsp **只提供工具与知识**,不向 CCC
|
|
5
|
-
*
|
|
5
|
+
* 定位:dsp **只提供工具与知识**,不向 CCC 自动安装任何东西(机制是 CCC 的自选动作)——
|
|
6
|
+
* agent 调本工具(doc/全报告)即懂机制,init/random/check 辅助,
|
|
6
7
|
* 实际执行(写配置/写偏见脚本/标记会话)由 CCC 自己决定、自己用现有工具完成。
|
|
7
8
|
*
|
|
8
9
|
* 实现:薄封装——exec 包内静态脚本(npm files 含 experiments/),脚本是单一真相源。
|
|
@@ -13,12 +14,12 @@
|
|
|
13
14
|
import { defineTool } from '@deepseek-ai/dsh-tools';
|
|
14
15
|
import type { Context } from 'cordis';
|
|
15
16
|
/**
|
|
16
|
-
*
|
|
17
|
+
* 定位包内脚本(npm files 分发 experiments/autopilot-trajectory/)。
|
|
17
18
|
* 布局差异:tsdown bundle 后 import.meta.url 指向 lib/index.js(lib → 包根 1 层);
|
|
18
19
|
* vitest 源码直跑时指向 src/tools/x.ts(src/tools → 包根 2 层)——逐级上溯查找,
|
|
19
|
-
* 两种布局都稳(找到 experiments/
|
|
20
|
+
* 两种布局都稳(找到 experiments/autopilot-trajectory/scripts/autopilot-trajectory.ts 即止)。
|
|
20
21
|
*/
|
|
21
22
|
export declare function findExpScript(startDir: string): string | null;
|
|
22
|
-
export declare const
|
|
23
|
-
/** 创建
|
|
24
|
-
export declare function
|
|
23
|
+
export declare const AUTOPILOT_ACTIONS: readonly ["all", "init", "random", "diag", "doc", "check", "status", "guide", "diag-live"];
|
|
24
|
+
/** 创建 autopilot-trajectory 工具(闭包捕获 ctx → diag-live 进程内诊断;v1.26.14 + v1.27.4 改名) */
|
|
25
|
+
export declare function createAutopilotTool(ctx: Context): ReturnType<typeof defineTool>;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
|
|
2
|
-
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { createDecipheriv, randomBytes } from "node:crypto";
|
|
3
3
|
//#region src/weixin-api.ts
|
|
4
4
|
/**
|
|
5
5
|
* weixin-api.ts — 微信个人号 iLink Bot API 客户端(F4c-3,v1.27.0 实验性)
|
|
@@ -23,15 +23,25 @@ var weixin_api_exports = /* @__PURE__ */ __exportAll({
|
|
|
23
23
|
MessageItemType: () => MessageItemType,
|
|
24
24
|
MessageState: () => MessageState,
|
|
25
25
|
MessageType: () => MessageType,
|
|
26
|
+
TypingStatus: () => TypingStatus,
|
|
27
|
+
aes128EcbDecrypt: () => aes128EcbDecrypt,
|
|
26
28
|
buildClientVersion: () => buildClientVersion,
|
|
29
|
+
buildMediaDownloadUrl: () => buildMediaDownloadUrl,
|
|
30
|
+
downloadMedia: () => downloadMedia,
|
|
27
31
|
fetchQRCode: () => fetchQRCode,
|
|
32
|
+
getConfig: () => getConfig,
|
|
28
33
|
getUpdates: () => getUpdates,
|
|
29
34
|
markdownToPlainText: () => markdownToPlainText,
|
|
35
|
+
parseMediaAesKey: () => parseMediaAesKey,
|
|
30
36
|
pollQRStatus: () => pollQRStatus,
|
|
31
|
-
sendTextMessage: () => sendTextMessage
|
|
37
|
+
sendTextMessage: () => sendTextMessage,
|
|
38
|
+
sendTyping: () => sendTyping,
|
|
39
|
+
sniffImageExt: () => sniffImageExt
|
|
32
40
|
});
|
|
33
41
|
/** iLink API Base URL(腾讯官方) */
|
|
34
42
|
const ILINK_DEFAULT_BASE_URL = "https://ilinkai.weixin.qq.com";
|
|
43
|
+
/** CDN Base(媒体上传/下载;P3 媒体期用) */
|
|
44
|
+
const ILINK_CDN_BASE_URL = "https://novac2c.cdn.weixin.qq.com/c2c";
|
|
35
45
|
/** iLink-App-Id(openclaw-weixin 同款) */
|
|
36
46
|
const ILINK_APP_ID = "bot";
|
|
37
47
|
/** 通道版本(对齐 openclaw-weixin 2.1.1;buildClientVersion 编码) */
|
|
@@ -193,6 +203,119 @@ async function sendTextMessage(params) {
|
|
|
193
203
|
timeoutMs: params.timeoutMs ?? 15e3
|
|
194
204
|
});
|
|
195
205
|
}
|
|
206
|
+
/** sendtyping status:1=TYPING(开始)0=CANCEL(结束)。
|
|
207
|
+
* ⚠️ 对齐**官方 openclaw-weixin 参考实现**(index.ts onReplyStart → status 1 /
|
|
208
|
+
* onCleanup → status 0)——早期协议注释"2=CANCEL"为误记,以参考实现为准。 */
|
|
209
|
+
const TypingStatus = {
|
|
210
|
+
TYPING: 1,
|
|
211
|
+
CANCEL: 0
|
|
212
|
+
};
|
|
213
|
+
/** 取对话配置(typing_ticket:sendtyping 的前置——先 getconfig 拿 ticket 再发状态) */
|
|
214
|
+
async function getConfig(params) {
|
|
215
|
+
const rawText = await apiPost({
|
|
216
|
+
baseUrl: params.baseUrl,
|
|
217
|
+
endpoint: "ilink/bot/getconfig",
|
|
218
|
+
body: JSON.stringify({
|
|
219
|
+
ilink_user_id: params.ilinkUserId,
|
|
220
|
+
context_token: params.contextToken
|
|
221
|
+
}),
|
|
222
|
+
token: params.token,
|
|
223
|
+
timeoutMs: params.timeoutMs ?? 1e4
|
|
224
|
+
});
|
|
225
|
+
return JSON.parse(rawText);
|
|
226
|
+
}
|
|
227
|
+
/** 发送"正在输入"状态(status: 1=开始 0=结束;ticket 来自 getConfig) */
|
|
228
|
+
async function sendTyping(params) {
|
|
229
|
+
await apiPost({
|
|
230
|
+
baseUrl: params.baseUrl,
|
|
231
|
+
endpoint: "ilink/bot/sendtyping",
|
|
232
|
+
body: JSON.stringify({
|
|
233
|
+
ilink_user_id: params.ilinkUserId,
|
|
234
|
+
typing_ticket: params.typingTicket,
|
|
235
|
+
status: params.status
|
|
236
|
+
}),
|
|
237
|
+
token: params.token,
|
|
238
|
+
timeoutMs: params.timeoutMs ?? 1e4
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
/** 构造 CDN 下载 URL(full_url 优先;否则 encrypted_query_param 拼 download 端点;都无 → null) */
|
|
242
|
+
function buildMediaDownloadUrl(media) {
|
|
243
|
+
if (media.full_url) return media.full_url;
|
|
244
|
+
if (media.encrypt_query_param) return `${ILINK_CDN_BASE_URL}/download?encrypted_query_param=${encodeURIComponent(media.encrypt_query_param)}`;
|
|
245
|
+
return null;
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* 解析媒体 AES key(兼容三种编码,对齐参考实现):
|
|
249
|
+
* ① 32+ hex 字符 → hex Buffer(16) ② base64 解码 = 16 字节直接用
|
|
250
|
+
* ③ base64 解码 = 32 字节 → ascii 是 hex 串 → 再 hex → Buffer(16);其他 → null
|
|
251
|
+
*/
|
|
252
|
+
function parseMediaAesKey(keyText) {
|
|
253
|
+
if (!keyText) return null;
|
|
254
|
+
if (/^[0-9a-fA-F]{32,}$/.test(keyText)) {
|
|
255
|
+
const buf = Buffer.from(keyText, "hex");
|
|
256
|
+
return buf.length === 16 ? buf : null;
|
|
257
|
+
}
|
|
258
|
+
try {
|
|
259
|
+
const decoded = Buffer.from(keyText, "base64");
|
|
260
|
+
if (decoded.length === 16) return decoded;
|
|
261
|
+
if (decoded.length === 32) {
|
|
262
|
+
const hexStr = decoded.toString("ascii");
|
|
263
|
+
if (/^[0-9a-fA-F]{32}$/.test(hexStr)) return Buffer.from(hexStr, "hex");
|
|
264
|
+
}
|
|
265
|
+
} catch {}
|
|
266
|
+
return null;
|
|
267
|
+
}
|
|
268
|
+
/** AES-128-ECB 解密(无 IV;node:crypto 内置) */
|
|
269
|
+
function aes128EcbDecrypt(data, key) {
|
|
270
|
+
const decipher = createDecipheriv("aes-128-ecb", key, null);
|
|
271
|
+
return Buffer.concat([decipher.update(data), decipher.final()]);
|
|
272
|
+
}
|
|
273
|
+
/** 图片 magic-byte 嗅探(vlm-describe 支持 jpg/png/gif/webp/bmp——比固定 .jpg 可靠) */
|
|
274
|
+
function sniffImageExt(data) {
|
|
275
|
+
if (data.length >= 3 && data[0] === 255 && data[1] === 216 && data[2] === 255) return "jpg";
|
|
276
|
+
if (data.length >= 8 && data[0] === 137 && data[1] === 80 && data[2] === 78 && data[3] === 71) return "png";
|
|
277
|
+
if (data.length >= 3 && data[0] === 71 && data[1] === 73 && data[2] === 70) return "gif";
|
|
278
|
+
if (data.length >= 12 && data.subarray(0, 4).toString("ascii") === "RIFF" && data.subarray(8, 12).toString("ascii") === "WEBP") return "webp";
|
|
279
|
+
if (data.length >= 2 && data[0] === 66 && data[1] === 77) return "bmp";
|
|
280
|
+
return "bin";
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* 下载 + 解密一条媒体(图片/文件)。失败/超时/无 URL → null(不抛——调用方降级)。
|
|
284
|
+
* key 提取:item.aeskey || item.media.aes_key(对齐参考实现)。
|
|
285
|
+
*/
|
|
286
|
+
async function downloadMedia(params) {
|
|
287
|
+
const mediaItem = params.item[params.mediaType];
|
|
288
|
+
if (!mediaItem) return null;
|
|
289
|
+
const media = mediaItem.media;
|
|
290
|
+
const url = buildMediaDownloadUrl(media ?? {});
|
|
291
|
+
if (!url) return null;
|
|
292
|
+
const itemKey = mediaItem.aeskey;
|
|
293
|
+
const fileName = mediaItem.file_name;
|
|
294
|
+
const key = parseMediaAesKey(itemKey ?? media?.aes_key);
|
|
295
|
+
const controller = new AbortController();
|
|
296
|
+
const timer = setTimeout(() => controller.abort(), params.timeoutMs ?? 3e4);
|
|
297
|
+
try {
|
|
298
|
+
const res = await currentFetch(url, { signal: controller.signal });
|
|
299
|
+
if (!res.ok) return null;
|
|
300
|
+
const buf = Buffer.from(await res.arrayBuffer());
|
|
301
|
+
if (key) try {
|
|
302
|
+
return {
|
|
303
|
+
data: aes128EcbDecrypt(buf, key),
|
|
304
|
+
fileName
|
|
305
|
+
};
|
|
306
|
+
} catch {
|
|
307
|
+
return null;
|
|
308
|
+
}
|
|
309
|
+
return {
|
|
310
|
+
data: buf,
|
|
311
|
+
fileName
|
|
312
|
+
};
|
|
313
|
+
} catch {
|
|
314
|
+
return null;
|
|
315
|
+
} finally {
|
|
316
|
+
clearTimeout(timer);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
196
319
|
/** Markdown → 纯文本(代码块/图片/链接/表格/标题/粗斜体/删除线 7 类) */
|
|
197
320
|
function markdownToPlainText(text) {
|
|
198
321
|
let result = text;
|
|
@@ -208,4 +331,4 @@ function markdownToPlainText(text) {
|
|
|
208
331
|
return result.trim();
|
|
209
332
|
}
|
|
210
333
|
//#endregion
|
|
211
|
-
export { sendTextMessage as
|
|
334
|
+
export { sendTextMessage as a, weixin_api_exports as c, getUpdates as i, downloadMedia as n, sendTyping as o, getConfig as r, sniffImageExt as s, TypingStatus as t };
|
package/lib/weixin-api.d.ts
CHANGED
|
@@ -47,16 +47,50 @@ export declare function pollQRStatus(params: {
|
|
|
47
47
|
qrcode: string;
|
|
48
48
|
timeoutMs?: number;
|
|
49
49
|
}): Promise<QrStatusResult>;
|
|
50
|
+
/** CDN 媒体元数据(下载侧;协议见 docs/weixin-bot-api.md §6) */
|
|
51
|
+
export interface CDNMedia {
|
|
52
|
+
encrypt_query_param?: string;
|
|
53
|
+
aes_key?: string;
|
|
54
|
+
encrypt_type?: number;
|
|
55
|
+
full_url?: string;
|
|
56
|
+
}
|
|
57
|
+
/** 媒体项引用(extractWeixinMedia 产物:kind + **完整消息项**——downloadMedia 用 item[mediaType] 提取) */
|
|
58
|
+
export interface WeixinMediaRef {
|
|
59
|
+
kind: 'image' | 'file';
|
|
60
|
+
fileName?: string;
|
|
61
|
+
item: WeixinMessageItem;
|
|
62
|
+
}
|
|
50
63
|
export interface WeixinMessageItem {
|
|
51
64
|
type?: number;
|
|
52
65
|
msg_id?: string;
|
|
53
66
|
text_item?: {
|
|
54
67
|
text?: string;
|
|
55
68
|
};
|
|
56
|
-
image_item?:
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
69
|
+
image_item?: {
|
|
70
|
+
media?: CDNMedia;
|
|
71
|
+
thumb_media?: CDNMedia;
|
|
72
|
+
aeskey?: string;
|
|
73
|
+
url?: string;
|
|
74
|
+
mid_size?: number;
|
|
75
|
+
thumb_size?: number;
|
|
76
|
+
};
|
|
77
|
+
/** 语音项:**text = 微信服务端自带语音转写**(官方 openclaw-weixin 直接读该字段,无需下载/ASR) */
|
|
78
|
+
voice_item?: {
|
|
79
|
+
text?: string;
|
|
80
|
+
media?: CDNMedia;
|
|
81
|
+
playtime?: number;
|
|
82
|
+
};
|
|
83
|
+
file_item?: {
|
|
84
|
+
media?: CDNMedia;
|
|
85
|
+
file_name?: string;
|
|
86
|
+
md5?: string;
|
|
87
|
+
len?: string;
|
|
88
|
+
};
|
|
89
|
+
video_item?: {
|
|
90
|
+
media?: CDNMedia;
|
|
91
|
+
video_size?: number;
|
|
92
|
+
thumb_media?: CDNMedia;
|
|
93
|
+
};
|
|
60
94
|
}
|
|
61
95
|
export interface WeixinMessage {
|
|
62
96
|
seq?: number;
|
|
@@ -115,5 +149,58 @@ export declare function sendTextMessage(params: {
|
|
|
115
149
|
clientId?: string;
|
|
116
150
|
timeoutMs?: number;
|
|
117
151
|
}): Promise<void>;
|
|
152
|
+
/** sendtyping status:1=TYPING(开始)0=CANCEL(结束)。
|
|
153
|
+
* ⚠️ 对齐**官方 openclaw-weixin 参考实现**(index.ts onReplyStart → status 1 /
|
|
154
|
+
* onCleanup → status 0)——早期协议注释"2=CANCEL"为误记,以参考实现为准。 */
|
|
155
|
+
export declare const TypingStatus: {
|
|
156
|
+
readonly TYPING: 1;
|
|
157
|
+
readonly CANCEL: 0;
|
|
158
|
+
};
|
|
159
|
+
export interface GetConfigResp {
|
|
160
|
+
ret?: number;
|
|
161
|
+
errmsg?: string;
|
|
162
|
+
typing_ticket?: string;
|
|
163
|
+
}
|
|
164
|
+
/** 取对话配置(typing_ticket:sendtyping 的前置——先 getconfig 拿 ticket 再发状态) */
|
|
165
|
+
export declare function getConfig(params: {
|
|
166
|
+
baseUrl: string;
|
|
167
|
+
token: string;
|
|
168
|
+
ilinkUserId: string;
|
|
169
|
+
contextToken: string;
|
|
170
|
+
timeoutMs?: number;
|
|
171
|
+
}): Promise<GetConfigResp>;
|
|
172
|
+
/** 发送"正在输入"状态(status: 1=开始 0=结束;ticket 来自 getConfig) */
|
|
173
|
+
export declare function sendTyping(params: {
|
|
174
|
+
baseUrl: string;
|
|
175
|
+
token: string;
|
|
176
|
+
ilinkUserId: string;
|
|
177
|
+
typingTicket: string;
|
|
178
|
+
status: number;
|
|
179
|
+
timeoutMs?: number;
|
|
180
|
+
}): Promise<void>;
|
|
181
|
+
/** 构造 CDN 下载 URL(full_url 优先;否则 encrypted_query_param 拼 download 端点;都无 → null) */
|
|
182
|
+
export declare function buildMediaDownloadUrl(media: CDNMedia): string | null;
|
|
183
|
+
/**
|
|
184
|
+
* 解析媒体 AES key(兼容三种编码,对齐参考实现):
|
|
185
|
+
* ① 32+ hex 字符 → hex Buffer(16) ② base64 解码 = 16 字节直接用
|
|
186
|
+
* ③ base64 解码 = 32 字节 → ascii 是 hex 串 → 再 hex → Buffer(16);其他 → null
|
|
187
|
+
*/
|
|
188
|
+
export declare function parseMediaAesKey(keyText?: string): Buffer | null;
|
|
189
|
+
/** AES-128-ECB 解密(无 IV;node:crypto 内置) */
|
|
190
|
+
export declare function aes128EcbDecrypt(data: Buffer, key: Buffer): Buffer;
|
|
191
|
+
/** 图片 magic-byte 嗅探(vlm-describe 支持 jpg/png/gif/webp/bmp——比固定 .jpg 可靠) */
|
|
192
|
+
export declare function sniffImageExt(data: Buffer): 'jpg' | 'png' | 'gif' | 'webp' | 'bmp' | 'bin';
|
|
193
|
+
/**
|
|
194
|
+
* 下载 + 解密一条媒体(图片/文件)。失败/超时/无 URL → null(不抛——调用方降级)。
|
|
195
|
+
* key 提取:item.aeskey || item.media.aes_key(对齐参考实现)。
|
|
196
|
+
*/
|
|
197
|
+
export declare function downloadMedia(params: {
|
|
198
|
+
item: WeixinMessageItem;
|
|
199
|
+
mediaType: 'image_item' | 'file_item';
|
|
200
|
+
timeoutMs?: number;
|
|
201
|
+
}): Promise<{
|
|
202
|
+
data: Buffer;
|
|
203
|
+
fileName?: string;
|
|
204
|
+
} | null>;
|
|
118
205
|
/** Markdown → 纯文本(代码块/图片/链接/表格/标题/粗斜体/删除线 7 类) */
|
|
119
206
|
export declare function markdownToPlainText(text: string): string;
|
package/lib/weixin-bridge.d.ts
CHANGED
|
@@ -14,6 +14,9 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import type { Context } from 'cordis';
|
|
16
16
|
import { type WeixinAccountCredential } from './weixin-route.js';
|
|
17
|
+
import { type WeixinMessage } from './weixin-api.js';
|
|
18
|
+
/** 测试辅助:清空 typing_ticket 缓存(生产零调用) */
|
|
19
|
+
export declare function resetWeixinTypingCache(): void;
|
|
17
20
|
/**
|
|
18
21
|
* 处理单条微信消息:路由 → skiff 会话(固定 id 创建/延续)→ 提问 → 回复回写。
|
|
19
22
|
*
|
|
@@ -22,17 +25,17 @@ import { type WeixinAccountCredential } from './weixin-route.js';
|
|
|
22
25
|
* 固定 id resume-or-create(v1.27.2):磁盘已有持久化 log → resume(历史延续,
|
|
23
26
|
* 重启后记忆保留——真正的"同用户长期延续");无 log(首次)→ create。
|
|
24
27
|
* "新的对话已开始"通知仅真正首次(create)时发送;resume/进程内延续不发。
|
|
28
|
+
*
|
|
29
|
+
* 完善(v1.27.3):
|
|
30
|
+
* - **语音支持**:`voice_item.text` = 微信服务端自带语音转写 → 与文本同路径进对话
|
|
31
|
+
* (无需下载/ASR);语音无转写 → 降级提示"暂时无法解析"
|
|
32
|
+
* - **正在输入**:处理前 sendtyping 1(微信显示"正在输入..."),处理后(含异常)0
|
|
33
|
+
* - **媒体接收(图片/文件)**:桥侧 CDN 下载 + AES 解密 → 落盘 CCC 根
|
|
34
|
+
* `_tmp/weixin-inbound/<userhash>/` → question 注入「存在性 + 路径」(ACC 层只保证
|
|
35
|
+
* 可达性——"让会话知道文件的存在并可以拿到";识别/解析归角色 LLM 决策,不编排)。
|
|
36
|
+
* 降级不静默:下载失败 / 超 20MB → 注入说明进对话;typing 窗口覆盖下载(M7)。
|
|
25
37
|
*/
|
|
26
|
-
export declare function handleIncoming(ctx: Context, root: string,
|
|
27
|
-
from_user_id?: string;
|
|
28
|
-
context_token?: string;
|
|
29
|
-
item_list?: Array<{
|
|
30
|
-
type?: number;
|
|
31
|
-
text_item?: {
|
|
32
|
-
text?: string;
|
|
33
|
-
};
|
|
34
|
-
}>;
|
|
35
|
-
}): Promise<void>;
|
|
38
|
+
export declare function handleIncoming(ctx: Context, root: string, accountId: string, cred: WeixinAccountCredential, msg: Pick<WeixinMessage, 'from_user_id' | 'context_token' | 'item_list'>): Promise<void>;
|
|
36
39
|
/**
|
|
37
40
|
* 启动/重建某 CCC 的桥:读配置 → 对每个 enabled + 有凭据的账号启动轮询循环。
|
|
38
41
|
* 已存在(配置变化热重建)→ 先停旧循环再启动新的。
|
|
@@ -292,15 +292,19 @@ function runLocalStore(root, args) {
|
|
|
292
292
|
var weixin_route_exports = /* @__PURE__ */ __exportAll({
|
|
293
293
|
WEIXIN_SESSION_PREFIX: () => WEIXIN_SESSION_PREFIX,
|
|
294
294
|
clearWeixinCredential: () => clearWeixinCredential,
|
|
295
|
+
extractWeixinMedia: () => extractWeixinMedia,
|
|
295
296
|
extractWeixinText: () => extractWeixinText,
|
|
297
|
+
hasVoiceItem: () => hasVoiceItem,
|
|
296
298
|
matchWeixinRoute: () => matchWeixinRoute,
|
|
297
299
|
nextWeixinAccountId: () => nextWeixinAccountId,
|
|
298
300
|
readWeixinCredential: () => readWeixinCredential,
|
|
299
301
|
readWeixinSettings: () => readWeixinSettings,
|
|
300
302
|
removeWeixinAccount: () => removeWeixinAccount,
|
|
303
|
+
sanitizeFileName: () => sanitizeFileName,
|
|
301
304
|
saveWeixinRoutes: () => saveWeixinRoutes,
|
|
302
305
|
setWeixinEnabled: () => setWeixinEnabled,
|
|
303
306
|
upsertWeixinAccount: () => upsertWeixinAccount,
|
|
307
|
+
weixinInboundDir: () => weixinInboundDir,
|
|
304
308
|
weixinSessionIdFor: () => weixinSessionIdFor,
|
|
305
309
|
writeWeixinCredential: () => writeWeixinCredential
|
|
306
310
|
});
|
|
@@ -314,7 +318,7 @@ function accountKeyPart(accountId) {
|
|
|
314
318
|
/** 微信桥 skiff 会话 id 前缀(seams 旁路判定;对外面纯净——守卫识别外部面) */
|
|
315
319
|
const WEIXIN_SESSION_PREFIX = "skiff-weixin-";
|
|
316
320
|
/** 微信用户 → 固定会话 id(同用户长期同一会话,记忆延续;多用户天然隔离)。
|
|
317
|
-
* **v1.27.
|
|
321
|
+
* **v1.27.2 错位一位(用户拍板)**:`.slice(0, 16)` → `.slice(1, 17)`——旧规则生成的
|
|
318
322
|
* 固定 id 已与磁盘损坏的持久化 log 绑定(dsh 不可硬删,create 同 id 必撞)→
|
|
319
323
|
* 新规则下同用户生成**全新 id**,避开损坏 log;固定可重建语义不变(同用户恒同 id)。 */
|
|
320
324
|
function weixinSessionIdFor(fromUserId) {
|
|
@@ -390,18 +394,59 @@ function matchWeixinRoute(routes, fromUserId) {
|
|
|
390
394
|
return null;
|
|
391
395
|
}
|
|
392
396
|
/**
|
|
393
|
-
*
|
|
397
|
+
* 提取消息文本(文本项 + **语音项的服务端转写**):
|
|
398
|
+
* - type 1 TEXT → `text_item.text`
|
|
399
|
+
* - type 3 VOICE → `voice_item.text`——**微信服务端自带语音转写**(官方 openclaw-weixin
|
|
400
|
+
* 直接读该字段并入对话文本,无需下载/ASR;对齐参考实现 index.ts voiceTexts)。
|
|
394
401
|
* @returns 纯文本;无文本 → null
|
|
395
402
|
*/
|
|
396
403
|
function extractWeixinText(msg) {
|
|
397
404
|
const items = msg.item_list;
|
|
398
405
|
if (!Array.isArray(items) || items.length === 0) return null;
|
|
399
|
-
for (const item of items)
|
|
400
|
-
|
|
401
|
-
|
|
406
|
+
for (const item of items) {
|
|
407
|
+
if (item.type === 1 && typeof item.text_item?.text === "string") {
|
|
408
|
+
const text = item.text_item.text.trim();
|
|
409
|
+
if (text !== "") return text;
|
|
410
|
+
}
|
|
411
|
+
if (item.type === 3 && typeof item.voice_item?.text === "string") {
|
|
412
|
+
const text = item.voice_item.text.trim();
|
|
413
|
+
if (text !== "") return text;
|
|
414
|
+
}
|
|
402
415
|
}
|
|
403
416
|
return null;
|
|
404
417
|
}
|
|
418
|
+
/** 是否包含语音项(无转写文本时的降级提示判定——bridge 用) */
|
|
419
|
+
function hasVoiceItem(msg) {
|
|
420
|
+
return (msg.item_list ?? []).some((item) => item.type === 3 && item.voice_item != null);
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* 提取消息媒体项(图片 type 2 / 文件 type 4):
|
|
424
|
+
* 只收集**带 media 元数据**的项(下载前置条件);返回 kind + **完整消息项**(downloadMedia 用
|
|
425
|
+
* `item[mediaType]` 取 image_item/file_item)。纯 ACC 层可达性——不做内容处理(v0.2 用户拍板)。
|
|
426
|
+
*/
|
|
427
|
+
function extractWeixinMedia(msg) {
|
|
428
|
+
const out = [];
|
|
429
|
+
for (const item of msg.item_list ?? []) if (item.type === 2 && item.image_item?.media) out.push({
|
|
430
|
+
kind: "image",
|
|
431
|
+
fileName: void 0,
|
|
432
|
+
item
|
|
433
|
+
});
|
|
434
|
+
else if (item.type === 4 && item.file_item?.media) out.push({
|
|
435
|
+
kind: "file",
|
|
436
|
+
fileName: item.file_item.file_name,
|
|
437
|
+
item
|
|
438
|
+
});
|
|
439
|
+
return out;
|
|
440
|
+
}
|
|
441
|
+
/** 文件名净化:basename + 去控制字符 + 截断 128(防路径穿越——落盘安全) */
|
|
442
|
+
function sanitizeFileName(name) {
|
|
443
|
+
return (name.split(/[\\/]/).pop() ?? "").replace(/[\x00-\x1f\x7f]/g, "").trim().slice(0, 128);
|
|
444
|
+
}
|
|
445
|
+
/** 落盘目录:`<CCC 根>/_tmp/weixin-inbound/<userhash>/`(gitignored ✓;agent read 边界内 ✓;按用户分目录) */
|
|
446
|
+
function weixinInboundDir(root, fromUserId) {
|
|
447
|
+
const userHash = createHash("sha256").update(fromUserId).digest("hex").slice(0, 12);
|
|
448
|
+
return join(root, "_tmp", "weixin-inbound", userHash);
|
|
449
|
+
}
|
|
405
450
|
/**
|
|
406
451
|
* 读取 serenity.json 文件(原始;不存在 → 空对象)。
|
|
407
452
|
* 注意:loadSerenityConfig 返回解析对象但无法写回——此处直接文件读写。
|
|
@@ -486,4 +531,4 @@ function setWeixinEnabled(root, enabled) {
|
|
|
486
531
|
return next;
|
|
487
532
|
}
|
|
488
533
|
//#endregion
|
|
489
|
-
export {
|
|
534
|
+
export { readWeixinCredential as a, weixinInboundDir as c, LOCALSTORE_SCOPES as d, checkLocalstoreGitCompliance as f, runLocalStore as h, matchWeixinRoute as i, weixinSessionIdFor as l, readGitTrack as m, extractWeixinText as n, readWeixinSettings as o, localstorePath as p, hasVoiceItem as r, sanitizeFileName as s, extractWeixinMedia as t, weixin_route_exports as u };
|
package/lib/weixin-route.d.ts
CHANGED
|
@@ -12,12 +12,13 @@
|
|
|
12
12
|
* 不绑定具体 role(用户自选路由目标)。
|
|
13
13
|
*/
|
|
14
14
|
import { type WeixinSettings, type WeixinRouteConfig, type WeixinAccountConfig } from './ccc.js';
|
|
15
|
+
import type { WeixinMediaRef, WeixinMessageItem } from './weixin-api.js';
|
|
15
16
|
/** 微信桥 skiff 会话 id 前缀(seams 旁路判定;对外面纯净——守卫识别外部面) */
|
|
16
17
|
export declare const WEIXIN_SESSION_PREFIX = "skiff-weixin-";
|
|
17
18
|
/** 判定 sessionId 是否为微信桥会话(外部面——输出守卫/轨迹隐藏生效) */
|
|
18
19
|
export declare function isWeixinSessionId(sessionId: string | undefined): boolean;
|
|
19
20
|
/** 微信用户 → 固定会话 id(同用户长期同一会话,记忆延续;多用户天然隔离)。
|
|
20
|
-
* **v1.27.
|
|
21
|
+
* **v1.27.2 错位一位(用户拍板)**:`.slice(0, 16)` → `.slice(1, 17)`——旧规则生成的
|
|
21
22
|
* 固定 id 已与磁盘损坏的持久化 log 绑定(dsh 不可硬删,create 同 id 必撞)→
|
|
22
23
|
* 新规则下同用户生成**全新 id**,避开损坏 log;固定可重建语义不变(同用户恒同 id)。 */
|
|
23
24
|
export declare function weixinSessionIdFor(fromUserId: string): string;
|
|
@@ -44,7 +45,10 @@ export declare function clearWeixinCredential(root: string, accountId: string):
|
|
|
44
45
|
*/
|
|
45
46
|
export declare function matchWeixinRoute(routes: WeixinRouteConfig[], fromUserId: string): string | null;
|
|
46
47
|
/**
|
|
47
|
-
*
|
|
48
|
+
* 提取消息文本(文本项 + **语音项的服务端转写**):
|
|
49
|
+
* - type 1 TEXT → `text_item.text`
|
|
50
|
+
* - type 3 VOICE → `voice_item.text`——**微信服务端自带语音转写**(官方 openclaw-weixin
|
|
51
|
+
* 直接读该字段并入对话文本,无需下载/ASR;对齐参考实现 index.ts voiceTexts)。
|
|
48
52
|
* @returns 纯文本;无文本 → null
|
|
49
53
|
*/
|
|
50
54
|
export declare function extractWeixinText(msg: {
|
|
@@ -53,8 +57,30 @@ export declare function extractWeixinText(msg: {
|
|
|
53
57
|
text_item?: {
|
|
54
58
|
text?: string;
|
|
55
59
|
};
|
|
60
|
+
voice_item?: {
|
|
61
|
+
text?: string;
|
|
62
|
+
};
|
|
56
63
|
}>;
|
|
57
64
|
}): string | null;
|
|
65
|
+
/** 是否包含语音项(无转写文本时的降级提示判定——bridge 用) */
|
|
66
|
+
export declare function hasVoiceItem(msg: {
|
|
67
|
+
item_list?: Array<{
|
|
68
|
+
type?: number;
|
|
69
|
+
voice_item?: unknown;
|
|
70
|
+
}>;
|
|
71
|
+
}): boolean;
|
|
72
|
+
/**
|
|
73
|
+
* 提取消息媒体项(图片 type 2 / 文件 type 4):
|
|
74
|
+
* 只收集**带 media 元数据**的项(下载前置条件);返回 kind + **完整消息项**(downloadMedia 用
|
|
75
|
+
* `item[mediaType]` 取 image_item/file_item)。纯 ACC 层可达性——不做内容处理(v0.2 用户拍板)。
|
|
76
|
+
*/
|
|
77
|
+
export declare function extractWeixinMedia(msg: {
|
|
78
|
+
item_list?: WeixinMessageItem[];
|
|
79
|
+
}): WeixinMediaRef[];
|
|
80
|
+
/** 文件名净化:basename + 去控制字符 + 截断 128(防路径穿越——落盘安全) */
|
|
81
|
+
export declare function sanitizeFileName(name: string): string;
|
|
82
|
+
/** 落盘目录:`<CCC 根>/_tmp/weixin-inbound/<userhash>/`(gitignored ✓;agent read 边界内 ✓;按用户分目录) */
|
|
83
|
+
export declare function weixinInboundDir(root: string, fromUserId: string): string;
|
|
58
84
|
/** 添加/更新账号(serenity.json 结构部分;凭据单独写 localstore) */
|
|
59
85
|
export declare function upsertWeixinAccount(root: string, account: WeixinAccountConfig): WeixinSettings;
|
|
60
86
|
/** 移除账号(serenity.json 结构 + localstore 凭据;同时停桥由调用方 syncCccBridge) */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shgroup/dsh-serenity-hooks",
|
|
3
|
-
"version": "1.27.
|
|
3
|
+
"version": "1.27.4",
|
|
4
4
|
"description": "宁静号 ACC harness — Native Cordis 插件(DeepSeek Harness 运行时)。真实 DSH 工具注册(cc_fs/session/acc_msm 等 9 工具)+ 拦截缝机械约束(safe-mode/路径守卫/会话落盘)+ 系统提示词注入(ACC/CCE/Constraints/SKILL/Session 五块)。适配 DSH 公开版(deepseek-ai/deepseek-harness 0.1.0-rc)。",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -31,8 +31,8 @@
|
|
|
31
31
|
"cordis.patch.yml",
|
|
32
32
|
"dsh.plugin.json",
|
|
33
33
|
"README.md",
|
|
34
|
-
"experiments/
|
|
35
|
-
"experiments/
|
|
34
|
+
"experiments/autopilot-trajectory/SKILL.md",
|
|
35
|
+
"experiments/autopilot-trajectory/scripts/autopilot-trajectory.ts"
|
|
36
36
|
],
|
|
37
37
|
"scripts": {
|
|
38
38
|
"typecheck": "tsc --noEmit",
|