chatccc 0.2.33 → 0.2.35

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.
@@ -0,0 +1,134 @@
1
+ /**
2
+ * feishu-platform.ts — 可替换的飞书 API 实现层
3
+ *
4
+ * 默认情况下所有函数直接委托给 feishu-api.ts(真实飞书 API)。
5
+ * 在 --simulate 模式下通过 setPlatform() 整体替换为 SimulatedPlatform。
6
+ *
7
+ * 设计:每个导出函数都是一个"通过 _impl 代理"的包装器,
8
+ * 消费者不需要感知底层是真实飞书还是模拟实现。
9
+ */
10
+
11
+ import * as realApi from "./feishu-api.ts";
12
+
13
+ // ---------------------------------------------------------------------------
14
+ // 平台接口:覆盖 feishu-api.ts 的所有公开导出函数签名
15
+ // ---------------------------------------------------------------------------
16
+
17
+ export interface FeishuPlatform {
18
+ getTenantAccessToken: typeof realApi.getTenantAccessToken;
19
+ sendTextReply: typeof realApi.sendTextReply;
20
+ sendCardReply: typeof realApi.sendCardReply;
21
+ sendRawCard: typeof realApi.sendRawCard;
22
+ sendImageReply: typeof realApi.sendImageReply;
23
+ sendFileReply: typeof realApi.sendFileReply;
24
+ addReaction: typeof realApi.addReaction;
25
+ recallMessage: typeof realApi.recallMessage;
26
+ updateCardMessage: typeof realApi.updateCardMessage;
27
+ createGroupChat: typeof realApi.createGroupChat;
28
+ updateChatInfo: typeof realApi.updateChatInfo;
29
+ getChatInfo: typeof realApi.getChatInfo;
30
+ setChatAvatar: typeof realApi.setChatAvatar;
31
+ getOrDownloadImage: typeof realApi.getOrDownloadImage;
32
+ verifyAllPermissions: typeof realApi.verifyAllPermissions;
33
+ reportPermissionResults: typeof realApi.reportPermissionResults;
34
+ extractSessionInfo: typeof realApi.extractSessionInfo;
35
+ extractSessionId: typeof realApi.extractSessionId;
36
+ formatDelayNotice: typeof realApi.formatDelayNotice;
37
+ sendRestartCard: typeof realApi.sendRestartCard;
38
+ }
39
+
40
+ let _impl: FeishuPlatform = realApi;
41
+
42
+ /** 替换当前平台实现(模拟模式入口) */
43
+ export function setPlatform(impl: FeishuPlatform): void {
44
+ _impl = impl;
45
+ }
46
+
47
+ /** 获取当前平台实现(仅供诊断/测试) */
48
+ export function getPlatform(): FeishuPlatform {
49
+ return _impl;
50
+ }
51
+
52
+ // ---------------------------------------------------------------------------
53
+ // 包装器:每个函数直接委托到 _impl,签名与原函数完全一致
54
+ // ---------------------------------------------------------------------------
55
+
56
+ export function getTenantAccessToken(): ReturnType<typeof realApi.getTenantAccessToken> {
57
+ return _impl.getTenantAccessToken();
58
+ }
59
+
60
+ export function sendTextReply(...args: Parameters<typeof realApi.sendTextReply>): ReturnType<typeof realApi.sendTextReply> {
61
+ return _impl.sendTextReply(...args);
62
+ }
63
+
64
+ export function sendCardReply(...args: Parameters<typeof realApi.sendCardReply>): ReturnType<typeof realApi.sendCardReply> {
65
+ return _impl.sendCardReply(...args);
66
+ }
67
+
68
+ export function sendRawCard(...args: Parameters<typeof realApi.sendRawCard>): ReturnType<typeof realApi.sendRawCard> {
69
+ return _impl.sendRawCard(...args);
70
+ }
71
+
72
+ export function sendImageReply(...args: Parameters<typeof realApi.sendImageReply>): ReturnType<typeof realApi.sendImageReply> {
73
+ return _impl.sendImageReply(...args);
74
+ }
75
+
76
+ export function sendFileReply(...args: Parameters<typeof realApi.sendFileReply>): ReturnType<typeof realApi.sendFileReply> {
77
+ return _impl.sendFileReply(...args);
78
+ }
79
+
80
+ export function addReaction(...args: Parameters<typeof realApi.addReaction>): ReturnType<typeof realApi.addReaction> {
81
+ return _impl.addReaction(...args);
82
+ }
83
+
84
+ export function recallMessage(...args: Parameters<typeof realApi.recallMessage>): ReturnType<typeof realApi.recallMessage> {
85
+ return _impl.recallMessage(...args);
86
+ }
87
+
88
+ export function updateCardMessage(...args: Parameters<typeof realApi.updateCardMessage>): ReturnType<typeof realApi.updateCardMessage> {
89
+ return _impl.updateCardMessage(...args);
90
+ }
91
+
92
+ export function createGroupChat(...args: Parameters<typeof realApi.createGroupChat>): ReturnType<typeof realApi.createGroupChat> {
93
+ return _impl.createGroupChat(...args);
94
+ }
95
+
96
+ export function updateChatInfo(...args: Parameters<typeof realApi.updateChatInfo>): ReturnType<typeof realApi.updateChatInfo> {
97
+ return _impl.updateChatInfo(...args);
98
+ }
99
+
100
+ export function getChatInfo(...args: Parameters<typeof realApi.getChatInfo>): ReturnType<typeof realApi.getChatInfo> {
101
+ return _impl.getChatInfo(...args);
102
+ }
103
+
104
+ export function setChatAvatar(...args: Parameters<typeof realApi.setChatAvatar>): ReturnType<typeof realApi.setChatAvatar> {
105
+ return _impl.setChatAvatar(...args);
106
+ }
107
+
108
+ export function getOrDownloadImage(...args: Parameters<typeof realApi.getOrDownloadImage>): ReturnType<typeof realApi.getOrDownloadImage> {
109
+ return _impl.getOrDownloadImage(...args);
110
+ }
111
+
112
+ export function verifyAllPermissions(...args: Parameters<typeof realApi.verifyAllPermissions>): ReturnType<typeof realApi.verifyAllPermissions> {
113
+ return _impl.verifyAllPermissions(...args);
114
+ }
115
+
116
+ export function reportPermissionResults(...args: Parameters<typeof realApi.reportPermissionResults>): ReturnType<typeof realApi.reportPermissionResults> {
117
+ return _impl.reportPermissionResults(...args);
118
+ }
119
+
120
+ export function extractSessionInfo(...args: Parameters<typeof realApi.extractSessionInfo>): ReturnType<typeof realApi.extractSessionInfo> {
121
+ return _impl.extractSessionInfo(...args);
122
+ }
123
+
124
+ export function extractSessionId(...args: Parameters<typeof realApi.extractSessionId>): ReturnType<typeof realApi.extractSessionId> {
125
+ return _impl.extractSessionId(...args);
126
+ }
127
+
128
+ export function formatDelayNotice(...args: Parameters<typeof realApi.formatDelayNotice>): ReturnType<typeof realApi.formatDelayNotice> {
129
+ return _impl.formatDelayNotice(...args);
130
+ }
131
+
132
+ export function sendRestartCard(...args: Parameters<typeof realApi.sendRestartCard>): ReturnType<typeof realApi.sendRestartCard> {
133
+ return _impl.sendRestartCard(...args);
134
+ }
package/src/index.ts CHANGED
@@ -24,7 +24,7 @@
24
24
 
25
25
  import { spawn } from "node:child_process";
26
26
  import { readdir, stat } from "node:fs/promises";
27
- import { createServer, type Server } from "node:http";
27
+ import { createServer, type IncomingMessage, type Server, type ServerResponse } from "node:http";
28
28
  import { resolve, dirname } from "node:path";
29
29
 
30
30
  import { WSClient, EventDispatcher } from "@larksuiteoapi/node-sdk";
@@ -47,6 +47,7 @@ import {
47
47
  PID_FILE,
48
48
  PROJECT_ROOT,
49
49
  USE_LOCAL,
50
+ USE_SIMULATE,
50
51
  appendChatLog,
51
52
  explainMissingFeishuCredentialsAndExit,
52
53
  fileLog,
@@ -71,6 +72,7 @@ import {
71
72
  getTenantAccessToken,
72
73
  recallMessage,
73
74
  sendCardReply,
75
+ sendRawCard,
74
76
  sendTextReply,
75
77
  setChatAvatar,
76
78
  updateCardMessage,
@@ -79,12 +81,15 @@ import {
79
81
  sendRestartCard,
80
82
  verifyAllPermissions,
81
83
  reportPermissionResults,
82
- } from "./feishu-api.ts";
84
+ setPlatform,
85
+ } from "./feishu-platform.ts";
83
86
  import { buildHelpCard, buildStatusCard, buildProgressCard, buildCdContent, buildCdCard, buildSessionsCard } from "./cards.ts";
84
87
  import { updateCardKitCard } from "./cardkit.ts";
85
88
  import { handleAgentImageRequest } from "./agent-image-rpc.ts";
86
89
  import { handleAgentFileRequest } from "./agent-file-rpc.ts";
87
90
  import { handleAgentGrantsRequest } from "./agent-grants-rpc.ts";
91
+ import { SimulatedPlatform, SIM_DEFAULT_CHAT_ID } from "./sim-platform.ts";
92
+ import { setMessageHandler } from "./sim-store.ts";
88
93
  import { formatGitResult, gitResultHeaderTemplate, runGitCommand } from "./git-command.ts";
89
94
  import {
90
95
  MAX_PROCESSED,
@@ -251,6 +256,53 @@ function parseCardAction(data: unknown): CardActionResult | null {
251
256
 
252
257
  let broadcastToRelay: (data: unknown) => void = () => {};
253
258
 
259
+ // ---------------------------------------------------------------------------
260
+ // Simulate mode: inject message via HTTP
261
+ // ---------------------------------------------------------------------------
262
+
263
+ async function handleSimInjectMessage(req: IncomingMessage, res: ServerResponse): Promise<boolean> {
264
+ const url = new URL(req.url ?? "/", "http://127.0.0.1");
265
+ if (url.pathname !== "/api/sim/inject-message") return false;
266
+ if (req.method !== "POST") {
267
+ res.writeHead(405, { "Content-Type": "application/json; charset=utf-8" });
268
+ res.end(JSON.stringify({ ok: false, error: "Method not allowed, use POST" }));
269
+ return true;
270
+ }
271
+
272
+ const chunks: Buffer[] = [];
273
+ for await (const chunk of req) { chunks.push(Buffer.from(chunk)); }
274
+ const body = Buffer.concat(chunks).toString("utf-8");
275
+ let parsed: { text?: string; chat_id?: string; open_id?: string; chat_type?: string };
276
+ try { parsed = JSON.parse(body); } catch {
277
+ res.writeHead(400, { "Content-Type": "application/json; charset=utf-8" });
278
+ res.end(JSON.stringify({ ok: false, error: "Invalid JSON body" }));
279
+ return true;
280
+ }
281
+
282
+ const text = parsed.text;
283
+ if (!text) {
284
+ res.writeHead(400, { "Content-Type": "application/json; charset=utf-8" });
285
+ res.end(JSON.stringify({ ok: false, error: "Missing 'text' field" }));
286
+ return true;
287
+ }
288
+
289
+ const chatId = parsed.chat_id || SIM_DEFAULT_CHAT_ID;
290
+ const openId = parsed.open_id || "sim_user_001";
291
+ const chatType = parsed.chat_type || "group";
292
+
293
+ console.log(`[${ts()}] [SIM:INJECT] chat=${chatId} text="${text.slice(0, 80)}"`);
294
+ appendChatLog(chatId, openId, text);
295
+
296
+ // Fire and forget: process command, respond 202 immediately
297
+ handleCommand(text, chatId, openId, Date.now(), chatType).catch((err) =>
298
+ console.error(`[${ts()}] [SIM:INJECT] handleCommand error: ${(err as Error).message}`)
299
+ );
300
+
301
+ res.writeHead(202, { "Content-Type": "application/json; charset=utf-8" });
302
+ res.end(JSON.stringify({ ok: true, chat_id: chatId }));
303
+ return true;
304
+ }
305
+
254
306
  // ---------------------------------------------------------------------------
255
307
  // Command handler
256
308
  // ---------------------------------------------------------------------------
@@ -351,17 +403,9 @@ async function handleCommand(text: string, chatId: string, openId: string, msgTi
351
403
  // /cd 无参数:展示卡片(含最近使用路径按钮)
352
404
  const recentDirs = await getRecentDirs();
353
405
  const card = buildCdCard(targetDir, withStats, recentDirs, sessionCwd);
354
- const resp = await fetch(`${BASE_URL}/im/v1/messages?receive_id_type=chat_id`, {
355
- method: "POST",
356
- headers: {
357
- Authorization: `Bearer ${cdToken}`,
358
- "Content-Type": "application/json",
359
- },
360
- body: JSON.stringify({ receive_id: chatId, msg_type: "interactive", content: card }),
361
- });
362
- const respData: Record<string, any> = await resp.json().catch(() => ({}));
363
- console.log(`[${ts()}] [CD] card sent, code=${respData.code}, msgId=${respData.data?.message_id ?? "N/A"}, recentDirs=${recentDirs.length}`);
364
- logTrace(tid, "DONE", { outcome: "cd_card", code: respData.code, msgId: respData.data?.message_id });
406
+ const ok = await sendRawCard(cdToken, chatId, card);
407
+ console.log(`[${ts()}] [CD] card sent, ok=${ok}, recentDirs=${recentDirs.length}`);
408
+ logTrace(tid, "DONE", { outcome: "cd_card", ok });
365
409
  } else {
366
410
  // /cd <path>:切换目录,发送文本卡片
367
411
  const content = buildCdContent(targetDir, withStats, isUpdate, sessionCwd);
@@ -437,6 +481,9 @@ async function handleCommand(text: string, chatId: string, openId: string, msgTi
437
481
  return;
438
482
  }
439
483
 
484
+ // 让新群的默认工作目录继承当前会话的 cwd
485
+ await setDefaultCwd(cwd, newChatId);
486
+
440
487
  const adapter = getAdapterForTool(tool);
441
488
  await sendCardReply(
442
489
  freshToken, newChatId, `${toolLabel} Session Ready`,
@@ -537,17 +584,9 @@ async function handleCommand(text: string, chatId: string, openId: string, msgTi
537
584
  statusText.push(`**上下文 Token 数:** ~${status.lastContextTokens.toLocaleString()}`);
538
585
  }
539
586
  const card = buildStatusCard(statusText.join("\n"), isActive ? "blue" : "green");
540
- const statusResp = await fetch(`${BASE_URL}/im/v1/messages?receive_id_type=chat_id`, {
541
- method: "POST",
542
- headers: {
543
- Authorization: `Bearer ${freshToken}`,
544
- "Content-Type": "application/json",
545
- },
546
- body: JSON.stringify({ receive_id: chatId, msg_type: "interactive", content: card }),
547
- });
548
- const statusRespData: Record<string, any> = await statusResp.json().catch(() => ({}));
549
- console.log(`[${ts()}] [STATUS] card sent, code=${statusRespData.code}, msgId=${statusRespData.data?.message_id ?? "N/A"}`);
550
- logTrace(tid, "DONE", { outcome: "status", code: statusRespData.code });
587
+ const ok = await sendRawCard(freshToken, chatId, card);
588
+ console.log(`[${ts()}] [STATUS] card sent, ok=${ok}`);
589
+ logTrace(tid, "DONE", { outcome: "status", ok });
551
590
  return;
552
591
  }
553
592
 
@@ -564,17 +603,9 @@ async function handleCommand(text: string, chatId: string, openId: string, msgTi
564
603
  tool: s.tool,
565
604
  }));
566
605
  const card = buildSessionsCard(cardData);
567
- const sessionsResp = await fetch(`${BASE_URL}/im/v1/messages?receive_id_type=chat_id`, {
568
- method: "POST",
569
- headers: {
570
- Authorization: `Bearer ${freshToken}`,
571
- "Content-Type": "application/json",
572
- },
573
- body: JSON.stringify({ receive_id: chatId, msg_type: "interactive", content: card }),
574
- });
575
- const sessionsRespData: Record<string, any> = await sessionsResp.json().catch(() => ({}));
576
- console.log(`[${ts()}] [SESSIONS] card sent, code=${sessionsRespData.code}, count=${allSessions.length}`);
577
- logTrace(tid, "DONE", { outcome: "sessions", code: sessionsRespData.code, count: allSessions.length });
606
+ const ok = await sendRawCard(freshToken, chatId, card);
607
+ console.log(`[${ts()}] [SESSIONS] card sent, ok=${ok}, count=${allSessions.length}`);
608
+ logTrace(tid, "DONE", { outcome: "sessions", ok, count: allSessions.length });
578
609
  return;
579
610
  }
580
611
 
@@ -757,21 +788,13 @@ async function handleCommand(text: string, chatId: string, openId: string, msgTi
757
788
  logTrace(tid, "SEND", { method: "help_card", chatId });
758
789
  const replyToken = await getTenantAccessToken();
759
790
  const card = buildHelpCard(text);
760
- const helpResp = await fetch(`${BASE_URL}/im/v1/messages?receive_id_type=chat_id`, {
761
- method: "POST",
762
- headers: {
763
- Authorization: `Bearer ${replyToken}`,
764
- "Content-Type": "application/json",
765
- },
766
- body: JSON.stringify({ receive_id: chatId, msg_type: "interactive", content: card }),
767
- });
768
- const helpData = (await helpResp.json().catch(() => ({}))) as { code: number; msg?: string; data?: { message_id?: string } };
769
- if (helpData.code !== 0) {
770
- console.error(`[${ts()}] [SEND] help_card FAIL: chatId=${chatId} code=${helpData.code} msg="${helpData.msg ?? ""}"`);
771
- logTrace(tid, "DONE", { outcome: "help_card_fail", code: helpData.code, msg: helpData.msg });
791
+ const ok = await sendRawCard(replyToken, chatId, card);
792
+ if (!ok) {
793
+ console.error(`[${ts()}] [SEND] help_card FAIL: chatId=${chatId}`);
794
+ logTrace(tid, "DONE", { outcome: "help_card_fail" });
772
795
  } else {
773
- console.log(`[${ts()}] [SEND] help_card OK: chatId=${chatId} msgId=${helpData.data?.message_id ?? "N/A"}`);
774
- logTrace(tid, "DONE", { outcome: "help_card_sent", msgId: helpData.data?.message_id });
796
+ console.log(`[${ts()}] [SEND] help_card OK: chatId=${chatId}`);
797
+ logTrace(tid, "DONE", { outcome: "help_card_sent" });
775
798
  }
776
799
  }
777
800
 
@@ -1053,6 +1076,55 @@ async function main(): Promise<void> {
1053
1076
  // server.close)——只负责诊断与默认致命退出,不替代清理逻辑。
1054
1077
  installCrashLogging({ flush: () => fileLog.flush() });
1055
1078
 
1079
+ // 模拟模式:独立端口 18079,不与 SDK 实例冲突,不走飞书凭证/权限/WSClient
1080
+ if (USE_SIMULATE) {
1081
+ const SIM_PORT = 18079;
1082
+ console.log("\n[Simulate] 模拟飞书环境模式");
1083
+ setPlatform(SimulatedPlatform);
1084
+ console.log(" 已切换到 SimulatedPlatform(零飞书依赖)");
1085
+ appendStartupTrace("main: simulate mode", { port: SIM_PORT });
1086
+
1087
+ // 注册消息处理器,让 SimAgent.sendMessage() 能进程内触发 handleCommand
1088
+ setMessageHandler(
1089
+ (text, chatId, openId, _ts, chatType, traceId) =>
1090
+ handleCommand(text, chatId, openId, Date.now(), chatType, traceId),
1091
+ );
1092
+
1093
+ setExtraApiHandler(async (req, res) => {
1094
+ const injected = await handleSimInjectMessage(req, res);
1095
+ if (injected) return true;
1096
+ return (await handleAgentGrantsRequest(req, res)) || (await handleAgentImageRequest(req, res)) || (await handleAgentFileRequest(req, res));
1097
+ });
1098
+
1099
+ const simServer = createServer(createUiRouter());
1100
+ await new Promise<void>((resolveListen, rejectListen) => {
1101
+ const onError = (err: NodeJS.ErrnoException): void => {
1102
+ simServer.removeListener("listening", onListening);
1103
+ rejectListen(err);
1104
+ };
1105
+ const onListening = (): void => {
1106
+ simServer.removeListener("error", onError);
1107
+ resolveListen();
1108
+ };
1109
+ simServer.once("error", onError);
1110
+ simServer.once("listening", onListening);
1111
+ simServer.listen(SIM_PORT, "127.0.0.1");
1112
+ }).catch((err: NodeJS.ErrnoException) => {
1113
+ console.error(`\n[启动] 监听失败:端口 ${SIM_PORT}(${err.code ?? "?"} — ${err.message})`);
1114
+ process.exit(1);
1115
+ });
1116
+
1117
+ console.log(`\n${"=".repeat(60)}`);
1118
+ console.log(` ChatCCC — 模拟飞书环境模式`);
1119
+ console.log(`${"=".repeat(60)}`);
1120
+ console.log(` 发送消息: POST http://127.0.0.1:${SIM_PORT}/api/sim/inject-message`);
1121
+ console.log(` 消息日志: ~/.chatccc/sim/messages.jsonl`);
1122
+ console.log(`${"=".repeat(60)}\n`);
1123
+
1124
+ installShutdownHandlers(simServer);
1125
+ return;
1126
+ }
1127
+
1056
1128
  if (Number.isNaN(CHATCCC_PORT) || CHATCCC_PORT < 1 || CHATCCC_PORT > 65535) {
1057
1129
  console.error("\n[启动] 预检失败: config.json 的 port 字段不是有效端口号(1–65535)。");
1058
1130
  console.error(` 当前配置: ${CHATCCC_PORT}`);
package/src/session.ts CHANGED
@@ -25,7 +25,7 @@ import {
25
25
  sendCardKitMessage,
26
26
  updateCardKitCard,
27
27
  } from "./cardkit.ts";
28
- import { sendTextReply, setChatAvatar } from "./feishu-api.ts";
28
+ import { sendTextReply, setChatAvatar } from "./feishu-platform.ts";
29
29
  import { logTrace } from "./trace.ts";
30
30
  import type { UnifiedBlock } from "./adapters/adapter-interface.ts";
31
31
  import type { ToolAdapter } from "./adapters/adapter-interface.ts";
@@ -0,0 +1,156 @@
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
+ const agent: SimAgent = {
75
+ userId,
76
+ account,
77
+
78
+ async sendMessage(chatId, text) {
79
+ const chat = simStore.getChat(chatId);
80
+ if (!chat) throw new Error(`Chat "${chatId}" not found`);
81
+ if (!chat.memberIds.includes(userId)) {
82
+ throw new Error(`User "${userId}" is not a member of chat "${chatId}"`);
83
+ }
84
+ await dispatchMessage(text, chatId, userId, chat.type === "p2p" ? "p2p" : "group");
85
+ },
86
+
87
+ createP2pWithBot() {
88
+ const chat = simStore.createP2pChat(userId);
89
+ return chat.id;
90
+ },
91
+
92
+ getMessages(chatId) {
93
+ return simStore.getMessages(chatId, userId);
94
+ },
95
+
96
+ listChats() {
97
+ return simStore.getChatsForUser(userId).map((c) => ({
98
+ id: c.id,
99
+ name: c.name,
100
+ type: c.type,
101
+ }));
102
+ },
103
+
104
+ on(event, handler) {
105
+ if (event === "message") {
106
+ const filteredHandler = (payload: { chatId: string; message: SimMessage }) => {
107
+ // 只推送给该用户所在群的消息
108
+ const chat = simStore.getChat(payload.chatId);
109
+ if (chat && chat.memberIds.includes(userId)) {
110
+ handler(payload.chatId, payload.message);
111
+ }
112
+ };
113
+ // 保存映射以便 off 能移除
114
+ (handler as any).__filtered = filteredHandler;
115
+ simStore.on("message", filteredHandler);
116
+ } else if (event === "invited_to_group") {
117
+ const filteredHandler = (payload: { chatId: string; userId: string }) => {
118
+ if (payload.userId === userId) {
119
+ handler(payload.chatId, payload.userId);
120
+ }
121
+ };
122
+ (handler as any).__filtered = filteredHandler;
123
+ simStore.on("member_added", filteredHandler);
124
+ }
125
+ },
126
+
127
+ off(event, handler) {
128
+ const filtered = (handler as any).__filtered;
129
+ if (event === "message" && filtered) {
130
+ simStore.off("message", filtered);
131
+ } else if (event === "invited_to_group" && filtered) {
132
+ simStore.off("member_added", filtered);
133
+ }
134
+ },
135
+
136
+ waitForReply(chatId, timeoutMs = 30000) {
137
+ return new Promise((resolve) => {
138
+ const timer = setTimeout(() => {
139
+ agent.off("message", onMessage);
140
+ resolve(null);
141
+ }, timeoutMs);
142
+
143
+ const onMessage = (cid: string, msg: SimMessage) => {
144
+ if (cid === chatId && msg.senderId === "bot") {
145
+ clearTimeout(timer);
146
+ agent.off("message", onMessage);
147
+ resolve(msg);
148
+ }
149
+ };
150
+ agent.on("message", onMessage);
151
+ });
152
+ },
153
+ };
154
+
155
+ return agent;
156
+ }