chatccc 0.2.52 → 0.2.54
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/README.md +95 -256
- package/bin/chatccc.mjs +23 -23
- package/demo/ilink_echo_probe.ts +222 -222
- package/im-skills/feishu-skill/download-video.mjs +162 -162
- package/im-skills/feishu-skill/receive-send-file.md +66 -66
- package/im-skills/feishu-skill/receive-send-image.md +27 -27
- package/im-skills/feishu-skill/send-file.mjs +50 -50
- package/im-skills/feishu-skill/send-image.mjs +50 -50
- package/im-skills/feishu-skill/skill.md +10 -10
- package/package.json +59 -59
- package/src/__tests__/agent-image-rpc.test.ts +33 -33
- package/src/__tests__/agent-rpc-body.test.ts +41 -41
- package/src/__tests__/card-plain-text.test.ts +42 -42
- package/src/__tests__/codex-adapter.test.ts +304 -304
- package/src/__tests__/config-reload.test.ts +26 -26
- package/src/__tests__/config-sample.test.ts +19 -19
- package/src/__tests__/crash-logging.test.ts +62 -62
- package/src/__tests__/fixtures/codex_simple_text.jsonl +4 -4
- package/src/__tests__/fixtures/codex_with_tool.jsonl +6 -6
- package/src/__tests__/im-skills.test.ts +46 -46
- package/src/__tests__/privacy.test.ts +143 -0
- package/src/__tests__/session.test.ts +57 -2
- package/src/__tests__/sim-agent.test.ts +173 -173
- package/src/__tests__/sim-platform.test.ts +76 -76
- package/src/__tests__/sim-store.test.ts +213 -213
- package/src/__tests__/stream-state.test.ts +134 -134
- package/src/__tests__/wechat-platform.test.ts +57 -32
- package/src/adapters/codex-adapter.ts +294 -294
- package/src/adapters/codex-session-meta-store.ts +130 -130
- package/src/agent-file-rpc.ts +156 -156
- package/src/agent-image-rpc.ts +152 -152
- package/src/agent-rpc-body.ts +48 -48
- package/src/card-plain-text.ts +108 -108
- package/src/feishu-api.ts +7 -3
- package/src/im-skills.ts +109 -109
- package/src/platform-adapter.ts +60 -60
- package/src/privacy.ts +68 -0
- package/src/session-chat-binding.ts +6 -1
- package/src/session.ts +40 -35
- package/src/sim-agent.ts +167 -167
- package/src/trace.ts +50 -50
- package/src/wechat-platform.ts +4 -1
package/src/sim-agent.ts
CHANGED
|
@@ -1,167 +1,167 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* sim-agent.ts — SimAgent: 模拟飞书用户的编程接口
|
|
3
|
-
*
|
|
4
|
-
* SimAgent 代表一个模拟飞书用户,提供纯代码 API:
|
|
5
|
-
* - sendMessage(): 发送消息给 bot
|
|
6
|
-
* - on("message"): 订阅 bot 和其他人的回复
|
|
7
|
-
* - on("invited_to_group"): 当被拉入群时收到通知
|
|
8
|
-
* - waitForReply(): Promise 模式等待 bot 回复
|
|
9
|
-
* - getMessages(): 查看会话历史消息
|
|
10
|
-
*
|
|
11
|
-
* 不依赖 UI,进程内直接调用,适合用于自动化测试和 Agent 编程。
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import { simStore, dispatchMessage, SIM_DEFAULT_CHAT_ID } from "./sim-store.ts";
|
|
15
|
-
import type { SimAccount, SimMessage } from "./sim-store.ts";
|
|
16
|
-
|
|
17
|
-
// ---------------------------------------------------------------------------
|
|
18
|
-
// 类型
|
|
19
|
-
// ---------------------------------------------------------------------------
|
|
20
|
-
|
|
21
|
-
export type MessageEventCallback = (chatId: string, msg: SimMessage) => void;
|
|
22
|
-
export type InvitedToGroupCallback = (chatId: string, userId: string) => void;
|
|
23
|
-
|
|
24
|
-
export interface SimAgent {
|
|
25
|
-
/** 绑定的用户 ID */
|
|
26
|
-
userId: string;
|
|
27
|
-
|
|
28
|
-
/** 绑定的账户信息 */
|
|
29
|
-
account: SimAccount;
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* 发送文本消息。
|
|
33
|
-
* chatId 默认 sim_default;p2p 私聊需指定。
|
|
34
|
-
*/
|
|
35
|
-
sendMessage(chatId: string, text: string): Promise<void>;
|
|
36
|
-
|
|
37
|
-
/** 创建与 bot 的私聊,返回 p2p chatId */
|
|
38
|
-
createP2pWithBot(): string;
|
|
39
|
-
|
|
40
|
-
/** 获取用户在指定会话中的消息历史 */
|
|
41
|
-
getMessages(chatId: string): SimMessage[];
|
|
42
|
-
|
|
43
|
-
/** 获取用户所属的所有会话 */
|
|
44
|
-
listChats(): { id: string; name: string; type: "p2p" | "group" }[];
|
|
45
|
-
|
|
46
|
-
// ---- 事件订阅 ----
|
|
47
|
-
/** 订阅消息事件(所有该用户所在群的消息) */
|
|
48
|
-
on(event: "message", handler: MessageEventCallback): void;
|
|
49
|
-
/** 订阅被拉入群事件 */
|
|
50
|
-
on(event: "invited_to_group", handler: InvitedToGroupCallback): void;
|
|
51
|
-
/** 取消订阅 */
|
|
52
|
-
off(event: string, handler: (...args: any[]) => void): void;
|
|
53
|
-
|
|
54
|
-
// ---- Promise 模式 ----
|
|
55
|
-
/**
|
|
56
|
-
* 等待指定会话的下一条 bot 回复。
|
|
57
|
-
* 超时返回 null(默认 30 秒)。
|
|
58
|
-
*/
|
|
59
|
-
waitForReply(chatId: string, timeoutMs?: number): Promise<SimMessage | null>;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// ---------------------------------------------------------------------------
|
|
63
|
-
// 实现
|
|
64
|
-
// ---------------------------------------------------------------------------
|
|
65
|
-
|
|
66
|
-
export function createSimAgent(userId: string): SimAgent {
|
|
67
|
-
const account = simStore.getAccount(userId);
|
|
68
|
-
if (!account) throw new Error(`Account "${userId}" not found. Register it in simStore first.`);
|
|
69
|
-
if (account.kind !== "user") throw new Error(`Account "${userId}" is not a user account.`);
|
|
70
|
-
|
|
71
|
-
// 确保用户有至少一个会话(bot 私聊自动创建)
|
|
72
|
-
simStore.createP2pChat(userId);
|
|
73
|
-
|
|
74
|
-
function onEvent(event: "message", handler: MessageEventCallback): void;
|
|
75
|
-
function onEvent(event: "invited_to_group", handler: InvitedToGroupCallback): void;
|
|
76
|
-
function onEvent(
|
|
77
|
-
event: "message" | "invited_to_group",
|
|
78
|
-
handler: MessageEventCallback | InvitedToGroupCallback,
|
|
79
|
-
): void {
|
|
80
|
-
if (event === "message") {
|
|
81
|
-
const messageHandler = handler as MessageEventCallback;
|
|
82
|
-
const filteredHandler = (payload: { chatId: string; message: SimMessage }) => {
|
|
83
|
-
// 只推送给该用户所在群的消息
|
|
84
|
-
const chat = simStore.getChat(payload.chatId);
|
|
85
|
-
if (chat && chat.memberIds.includes(userId)) {
|
|
86
|
-
messageHandler(payload.chatId, payload.message);
|
|
87
|
-
}
|
|
88
|
-
};
|
|
89
|
-
// 保存映射以便 off 能移除
|
|
90
|
-
(handler as any).__filtered = filteredHandler;
|
|
91
|
-
simStore.on("message", filteredHandler);
|
|
92
|
-
} else {
|
|
93
|
-
const invitedHandler = handler as InvitedToGroupCallback;
|
|
94
|
-
const filteredHandler = (payload: { chatId: string; userId: string }) => {
|
|
95
|
-
if (payload.userId === userId) {
|
|
96
|
-
invitedHandler(payload.chatId, payload.userId);
|
|
97
|
-
}
|
|
98
|
-
};
|
|
99
|
-
(handler as any).__filtered = filteredHandler;
|
|
100
|
-
simStore.on("member_added", filteredHandler);
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
function offEvent(event: string, handler: (...args: any[]) => void): void {
|
|
105
|
-
const filtered = (handler as any).__filtered;
|
|
106
|
-
if (event === "message" && filtered) {
|
|
107
|
-
simStore.off("message", filtered);
|
|
108
|
-
} else if (event === "invited_to_group" && filtered) {
|
|
109
|
-
simStore.off("member_added", filtered);
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
const agent: SimAgent = {
|
|
114
|
-
userId,
|
|
115
|
-
account,
|
|
116
|
-
|
|
117
|
-
async sendMessage(chatId, text) {
|
|
118
|
-
const chat = simStore.getChat(chatId);
|
|
119
|
-
if (!chat) throw new Error(`Chat "${chatId}" not found`);
|
|
120
|
-
if (!chat.memberIds.includes(userId)) {
|
|
121
|
-
throw new Error(`User "${userId}" is not a member of chat "${chatId}"`);
|
|
122
|
-
}
|
|
123
|
-
await dispatchMessage(text, chatId, userId, chat.type === "p2p" ? "p2p" : "group");
|
|
124
|
-
},
|
|
125
|
-
|
|
126
|
-
createP2pWithBot() {
|
|
127
|
-
const chat = simStore.createP2pChat(userId);
|
|
128
|
-
return chat.id;
|
|
129
|
-
},
|
|
130
|
-
|
|
131
|
-
getMessages(chatId) {
|
|
132
|
-
return simStore.getMessages(chatId, userId);
|
|
133
|
-
},
|
|
134
|
-
|
|
135
|
-
listChats() {
|
|
136
|
-
return simStore.getChatsForUser(userId).map((c) => ({
|
|
137
|
-
id: c.id,
|
|
138
|
-
name: c.name,
|
|
139
|
-
type: c.type,
|
|
140
|
-
}));
|
|
141
|
-
},
|
|
142
|
-
|
|
143
|
-
on: onEvent,
|
|
144
|
-
|
|
145
|
-
off: offEvent,
|
|
146
|
-
|
|
147
|
-
waitForReply(chatId, timeoutMs = 30000) {
|
|
148
|
-
return new Promise((resolve) => {
|
|
149
|
-
const timer = setTimeout(() => {
|
|
150
|
-
agent.off("message", onMessage);
|
|
151
|
-
resolve(null);
|
|
152
|
-
}, timeoutMs);
|
|
153
|
-
|
|
154
|
-
const onMessage = (cid: string, msg: SimMessage) => {
|
|
155
|
-
if (cid === chatId && msg.senderId === "bot") {
|
|
156
|
-
clearTimeout(timer);
|
|
157
|
-
agent.off("message", onMessage);
|
|
158
|
-
resolve(msg);
|
|
159
|
-
}
|
|
160
|
-
};
|
|
161
|
-
agent.on("message", onMessage);
|
|
162
|
-
});
|
|
163
|
-
},
|
|
164
|
-
};
|
|
165
|
-
|
|
166
|
-
return agent;
|
|
167
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* sim-agent.ts — SimAgent: 模拟飞书用户的编程接口
|
|
3
|
+
*
|
|
4
|
+
* SimAgent 代表一个模拟飞书用户,提供纯代码 API:
|
|
5
|
+
* - sendMessage(): 发送消息给 bot
|
|
6
|
+
* - on("message"): 订阅 bot 和其他人的回复
|
|
7
|
+
* - on("invited_to_group"): 当被拉入群时收到通知
|
|
8
|
+
* - waitForReply(): Promise 模式等待 bot 回复
|
|
9
|
+
* - getMessages(): 查看会话历史消息
|
|
10
|
+
*
|
|
11
|
+
* 不依赖 UI,进程内直接调用,适合用于自动化测试和 Agent 编程。
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { simStore, dispatchMessage, SIM_DEFAULT_CHAT_ID } from "./sim-store.ts";
|
|
15
|
+
import type { SimAccount, SimMessage } from "./sim-store.ts";
|
|
16
|
+
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
// 类型
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
export type MessageEventCallback = (chatId: string, msg: SimMessage) => void;
|
|
22
|
+
export type InvitedToGroupCallback = (chatId: string, userId: string) => void;
|
|
23
|
+
|
|
24
|
+
export interface SimAgent {
|
|
25
|
+
/** 绑定的用户 ID */
|
|
26
|
+
userId: string;
|
|
27
|
+
|
|
28
|
+
/** 绑定的账户信息 */
|
|
29
|
+
account: SimAccount;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* 发送文本消息。
|
|
33
|
+
* chatId 默认 sim_default;p2p 私聊需指定。
|
|
34
|
+
*/
|
|
35
|
+
sendMessage(chatId: string, text: string): Promise<void>;
|
|
36
|
+
|
|
37
|
+
/** 创建与 bot 的私聊,返回 p2p chatId */
|
|
38
|
+
createP2pWithBot(): string;
|
|
39
|
+
|
|
40
|
+
/** 获取用户在指定会话中的消息历史 */
|
|
41
|
+
getMessages(chatId: string): SimMessage[];
|
|
42
|
+
|
|
43
|
+
/** 获取用户所属的所有会话 */
|
|
44
|
+
listChats(): { id: string; name: string; type: "p2p" | "group" }[];
|
|
45
|
+
|
|
46
|
+
// ---- 事件订阅 ----
|
|
47
|
+
/** 订阅消息事件(所有该用户所在群的消息) */
|
|
48
|
+
on(event: "message", handler: MessageEventCallback): void;
|
|
49
|
+
/** 订阅被拉入群事件 */
|
|
50
|
+
on(event: "invited_to_group", handler: InvitedToGroupCallback): void;
|
|
51
|
+
/** 取消订阅 */
|
|
52
|
+
off(event: string, handler: (...args: any[]) => void): void;
|
|
53
|
+
|
|
54
|
+
// ---- Promise 模式 ----
|
|
55
|
+
/**
|
|
56
|
+
* 等待指定会话的下一条 bot 回复。
|
|
57
|
+
* 超时返回 null(默认 30 秒)。
|
|
58
|
+
*/
|
|
59
|
+
waitForReply(chatId: string, timeoutMs?: number): Promise<SimMessage | null>;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
// 实现
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
|
|
66
|
+
export function createSimAgent(userId: string): SimAgent {
|
|
67
|
+
const account = simStore.getAccount(userId);
|
|
68
|
+
if (!account) throw new Error(`Account "${userId}" not found. Register it in simStore first.`);
|
|
69
|
+
if (account.kind !== "user") throw new Error(`Account "${userId}" is not a user account.`);
|
|
70
|
+
|
|
71
|
+
// 确保用户有至少一个会话(bot 私聊自动创建)
|
|
72
|
+
simStore.createP2pChat(userId);
|
|
73
|
+
|
|
74
|
+
function onEvent(event: "message", handler: MessageEventCallback): void;
|
|
75
|
+
function onEvent(event: "invited_to_group", handler: InvitedToGroupCallback): void;
|
|
76
|
+
function onEvent(
|
|
77
|
+
event: "message" | "invited_to_group",
|
|
78
|
+
handler: MessageEventCallback | InvitedToGroupCallback,
|
|
79
|
+
): void {
|
|
80
|
+
if (event === "message") {
|
|
81
|
+
const messageHandler = handler as MessageEventCallback;
|
|
82
|
+
const filteredHandler = (payload: { chatId: string; message: SimMessage }) => {
|
|
83
|
+
// 只推送给该用户所在群的消息
|
|
84
|
+
const chat = simStore.getChat(payload.chatId);
|
|
85
|
+
if (chat && chat.memberIds.includes(userId)) {
|
|
86
|
+
messageHandler(payload.chatId, payload.message);
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
// 保存映射以便 off 能移除
|
|
90
|
+
(handler as any).__filtered = filteredHandler;
|
|
91
|
+
simStore.on("message", filteredHandler);
|
|
92
|
+
} else {
|
|
93
|
+
const invitedHandler = handler as InvitedToGroupCallback;
|
|
94
|
+
const filteredHandler = (payload: { chatId: string; userId: string }) => {
|
|
95
|
+
if (payload.userId === userId) {
|
|
96
|
+
invitedHandler(payload.chatId, payload.userId);
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
(handler as any).__filtered = filteredHandler;
|
|
100
|
+
simStore.on("member_added", filteredHandler);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function offEvent(event: string, handler: (...args: any[]) => void): void {
|
|
105
|
+
const filtered = (handler as any).__filtered;
|
|
106
|
+
if (event === "message" && filtered) {
|
|
107
|
+
simStore.off("message", filtered);
|
|
108
|
+
} else if (event === "invited_to_group" && filtered) {
|
|
109
|
+
simStore.off("member_added", filtered);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const agent: SimAgent = {
|
|
114
|
+
userId,
|
|
115
|
+
account,
|
|
116
|
+
|
|
117
|
+
async sendMessage(chatId, text) {
|
|
118
|
+
const chat = simStore.getChat(chatId);
|
|
119
|
+
if (!chat) throw new Error(`Chat "${chatId}" not found`);
|
|
120
|
+
if (!chat.memberIds.includes(userId)) {
|
|
121
|
+
throw new Error(`User "${userId}" is not a member of chat "${chatId}"`);
|
|
122
|
+
}
|
|
123
|
+
await dispatchMessage(text, chatId, userId, chat.type === "p2p" ? "p2p" : "group");
|
|
124
|
+
},
|
|
125
|
+
|
|
126
|
+
createP2pWithBot() {
|
|
127
|
+
const chat = simStore.createP2pChat(userId);
|
|
128
|
+
return chat.id;
|
|
129
|
+
},
|
|
130
|
+
|
|
131
|
+
getMessages(chatId) {
|
|
132
|
+
return simStore.getMessages(chatId, userId);
|
|
133
|
+
},
|
|
134
|
+
|
|
135
|
+
listChats() {
|
|
136
|
+
return simStore.getChatsForUser(userId).map((c) => ({
|
|
137
|
+
id: c.id,
|
|
138
|
+
name: c.name,
|
|
139
|
+
type: c.type,
|
|
140
|
+
}));
|
|
141
|
+
},
|
|
142
|
+
|
|
143
|
+
on: onEvent,
|
|
144
|
+
|
|
145
|
+
off: offEvent,
|
|
146
|
+
|
|
147
|
+
waitForReply(chatId, timeoutMs = 30000) {
|
|
148
|
+
return new Promise((resolve) => {
|
|
149
|
+
const timer = setTimeout(() => {
|
|
150
|
+
agent.off("message", onMessage);
|
|
151
|
+
resolve(null);
|
|
152
|
+
}, timeoutMs);
|
|
153
|
+
|
|
154
|
+
const onMessage = (cid: string, msg: SimMessage) => {
|
|
155
|
+
if (cid === chatId && msg.senderId === "bot") {
|
|
156
|
+
clearTimeout(timer);
|
|
157
|
+
agent.off("message", onMessage);
|
|
158
|
+
resolve(msg);
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
agent.on("message", onMessage);
|
|
162
|
+
});
|
|
163
|
+
},
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
return agent;
|
|
167
|
+
}
|
package/src/trace.ts
CHANGED
|
@@ -1,51 +1,51 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 轻量级消息全链路 trace 工具。
|
|
3
|
-
*
|
|
4
|
-
* 每条消息生成唯一 traceId,在关键决策点、API 调用、消息发送时输出结构化日志。
|
|
5
|
-
* grep traceId 即可看到该消息的完整 RECV → BRANCH → SEND → DONE 链路。
|
|
6
|
-
*
|
|
7
|
-
* 设计原则:trace 自身绝不能抛异常导致主流程中断——所有函数内都有 try/catch 兜底。
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
let _traceSeq = 0;
|
|
11
|
-
|
|
12
|
-
/** 基于时间戳 + 递增序号生成短 trace ID(约 10 字符) */
|
|
13
|
-
export function makeTraceId(): string {
|
|
14
|
-
try {
|
|
15
|
-
_traceSeq++;
|
|
16
|
-
const ts = Date.now().toString(36).slice(-6);
|
|
17
|
-
const seq = _traceSeq.toString(36).padStart(3, "0");
|
|
18
|
-
return `${ts}${seq}`;
|
|
19
|
-
} catch {
|
|
20
|
-
// 极端情况(计数器溢出等)降级为纯时间戳
|
|
21
|
-
return Date.now().toString(36);
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* 输出一条 trace 日志。
|
|
27
|
-
* detail 中的值做浅层截断(字符串 > 200 字符会截断),避免日志膨胀。
|
|
28
|
-
*/
|
|
29
|
-
export function logTrace(traceId: string, step: string, detail?: Record<string, unknown>): void {
|
|
30
|
-
try {
|
|
31
|
-
const safe: Record<string, unknown> = {};
|
|
32
|
-
if (detail) {
|
|
33
|
-
for (const [k, v] of Object.entries(detail)) {
|
|
34
|
-
if (typeof v === "string" && v.length > 200) {
|
|
35
|
-
safe[k] = v.slice(0, 200) + "...(truncated)";
|
|
36
|
-
} else {
|
|
37
|
-
safe[k] = v;
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
const extra = Object.keys(safe).length > 0 ? " " + JSON.stringify(safe) : "";
|
|
42
|
-
console.log(`[TRACE ${traceId}] ${step}${extra}`);
|
|
43
|
-
} catch {
|
|
44
|
-
// trace 自身绝不能抛异常
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/** 重置序号(仅单测使用,确保 trace ID 可预测) */
|
|
49
|
-
export function _resetTraceSeqForTest(): void {
|
|
50
|
-
_traceSeq = 0;
|
|
1
|
+
/**
|
|
2
|
+
* 轻量级消息全链路 trace 工具。
|
|
3
|
+
*
|
|
4
|
+
* 每条消息生成唯一 traceId,在关键决策点、API 调用、消息发送时输出结构化日志。
|
|
5
|
+
* grep traceId 即可看到该消息的完整 RECV → BRANCH → SEND → DONE 链路。
|
|
6
|
+
*
|
|
7
|
+
* 设计原则:trace 自身绝不能抛异常导致主流程中断——所有函数内都有 try/catch 兜底。
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
let _traceSeq = 0;
|
|
11
|
+
|
|
12
|
+
/** 基于时间戳 + 递增序号生成短 trace ID(约 10 字符) */
|
|
13
|
+
export function makeTraceId(): string {
|
|
14
|
+
try {
|
|
15
|
+
_traceSeq++;
|
|
16
|
+
const ts = Date.now().toString(36).slice(-6);
|
|
17
|
+
const seq = _traceSeq.toString(36).padStart(3, "0");
|
|
18
|
+
return `${ts}${seq}`;
|
|
19
|
+
} catch {
|
|
20
|
+
// 极端情况(计数器溢出等)降级为纯时间戳
|
|
21
|
+
return Date.now().toString(36);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* 输出一条 trace 日志。
|
|
27
|
+
* detail 中的值做浅层截断(字符串 > 200 字符会截断),避免日志膨胀。
|
|
28
|
+
*/
|
|
29
|
+
export function logTrace(traceId: string, step: string, detail?: Record<string, unknown>): void {
|
|
30
|
+
try {
|
|
31
|
+
const safe: Record<string, unknown> = {};
|
|
32
|
+
if (detail) {
|
|
33
|
+
for (const [k, v] of Object.entries(detail)) {
|
|
34
|
+
if (typeof v === "string" && v.length > 200) {
|
|
35
|
+
safe[k] = v.slice(0, 200) + "...(truncated)";
|
|
36
|
+
} else {
|
|
37
|
+
safe[k] = v;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
const extra = Object.keys(safe).length > 0 ? " " + JSON.stringify(safe) : "";
|
|
42
|
+
console.log(`[TRACE ${traceId}] ${step}${extra}`);
|
|
43
|
+
} catch {
|
|
44
|
+
// trace 自身绝不能抛异常
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** 重置序号(仅单测使用,确保 trace ID 可预测) */
|
|
49
|
+
export function _resetTraceSeqForTest(): void {
|
|
50
|
+
_traceSeq = 0;
|
|
51
51
|
}
|
package/src/wechat-platform.ts
CHANGED
|
@@ -24,6 +24,7 @@ import type { PlatformAdapter } from "./platform-adapter.ts";
|
|
|
24
24
|
import { setupFileLogging } from "./shared.ts";
|
|
25
25
|
import { appendChatLog } from "./config.ts";
|
|
26
26
|
import { cardJsonToPlainText } from "./card-plain-text.ts";
|
|
27
|
+
import { applyPrivacy } from "./privacy.ts";
|
|
27
28
|
|
|
28
29
|
interface TerminalQrRenderer {
|
|
29
30
|
generate(
|
|
@@ -146,6 +147,8 @@ export function createWechatAdapter(
|
|
|
146
147
|
const wire = getWire();
|
|
147
148
|
if (!wire) return false;
|
|
148
149
|
|
|
150
|
+
text = applyPrivacy(text);
|
|
151
|
+
|
|
149
152
|
// 微信 claw 连发限制:统计用户未回复时已连发条数
|
|
150
153
|
const count = (consecutiveSendCount.get(chatId) ?? 0) + 1;
|
|
151
154
|
consecutiveSendCount.set(chatId, count);
|
|
@@ -160,7 +163,7 @@ export function createWechatAdapter(
|
|
|
160
163
|
// 超过10条后非最终消息不再发送(claw 限制)
|
|
161
164
|
if (count > 10 && !isFinal) {
|
|
162
165
|
log(`[WECHAT] sendText skipped (claw limit): chatId=${chatId} count=${count}`);
|
|
163
|
-
return
|
|
166
|
+
return false;
|
|
164
167
|
}
|
|
165
168
|
|
|
166
169
|
try {
|