chatccc 0.2.34 → 0.2.36
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/package.json +1 -1
- package/src/__tests__/sim-agent.test.ts +174 -0
- package/src/__tests__/sim-store.test.ts +214 -0
- package/src/index.ts +10 -0
- package/src/session.ts +1 -1
- package/src/sim-agent.ts +156 -0
- package/src/sim-platform.ts +21 -97
- package/src/sim-store.ts +313 -0
package/package.json
CHANGED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { describe, it, expect, afterAll, beforeEach } from "vitest";
|
|
2
|
+
import { unlink } from "node:fs/promises";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
|
|
6
|
+
import { createSimAgent } from "../sim-agent.ts";
|
|
7
|
+
import { simStore, setMessageHandler } from "../sim-store.ts";
|
|
8
|
+
|
|
9
|
+
const MESSAGES_FILE = join(homedir(), ".chatccc", "sim", "messages.jsonl");
|
|
10
|
+
|
|
11
|
+
describe("SimAgent", () => {
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
simStore.reset();
|
|
14
|
+
setMessageHandler(async () => {});
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
afterAll(async () => {
|
|
18
|
+
try { await unlink(MESSAGES_FILE); } catch { /* ok */ }
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("createSimAgent 绑定已有用户账户", () => {
|
|
22
|
+
const a = createSimAgent("sim_user_001");
|
|
23
|
+
expect(a.userId).toBe("sim_user_001");
|
|
24
|
+
expect(a.account.name).toBe("Developer");
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("createSimAgent 不存在用户抛出错误", () => {
|
|
28
|
+
expect(() => createSimAgent("nonexistent")).toThrow('Account "nonexistent" not found');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("createSimAgent bot 账户抛出错误", () => {
|
|
32
|
+
expect(() => createSimAgent("bot")).toThrow('is not a user account');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("createP2pWithBot 创建后用户能看到", () => {
|
|
36
|
+
const a = createSimAgent("sim_user_001");
|
|
37
|
+
const chatId = a.createP2pWithBot();
|
|
38
|
+
expect(chatId).toBe("p2p_sim_user_001_bot");
|
|
39
|
+
const chats = a.listChats();
|
|
40
|
+
expect(chats.some((c) => c.type === "p2p")).toBe(true);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("sendMessage 将消息发送到指定群", async () => {
|
|
44
|
+
let receivedText = "";
|
|
45
|
+
let receivedChatId = "";
|
|
46
|
+
setMessageHandler(async (text, chatId) => {
|
|
47
|
+
receivedText = text;
|
|
48
|
+
receivedChatId = chatId;
|
|
49
|
+
simStore.sendReply(chatId, "text", `echo: ${text}`);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const a = createSimAgent("sim_user_001");
|
|
53
|
+
await a.sendMessage("sim_default", "你好");
|
|
54
|
+
|
|
55
|
+
expect(receivedText).toBe("你好");
|
|
56
|
+
expect(receivedChatId).toBe("sim_default");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("sendMessage 非成员发送消息抛出错误", async () => {
|
|
60
|
+
simStore.registerAccount({ id: "alice_test", kind: "user", name: "Alice" });
|
|
61
|
+
const a = createSimAgent("alice_test");
|
|
62
|
+
await expect(a.sendMessage("sim_default", "hello"))
|
|
63
|
+
.rejects.toThrow('is not a member');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("on message 订阅收到 bot 回复", async () => {
|
|
67
|
+
setMessageHandler(async (text, chatId) => {
|
|
68
|
+
simStore.sendReply(chatId, "text", `reply to: ${text}`);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const a = createSimAgent("sim_user_001");
|
|
72
|
+
let received: { chatId: string; content: string } | null = null;
|
|
73
|
+
a.on("message", (chatId, msg) => {
|
|
74
|
+
received = { chatId, content: msg.content };
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
await a.sendMessage("sim_default", "ping");
|
|
78
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
79
|
+
|
|
80
|
+
expect(received).not.toBeNull();
|
|
81
|
+
expect(received!.content).toBe("reply to: ping");
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("waitForReply 等待 bot 回复", async () => {
|
|
85
|
+
setMessageHandler(async (text, chatId) => {
|
|
86
|
+
setTimeout(() => {
|
|
87
|
+
simStore.sendReply(chatId, "text", `async reply: ${text}`);
|
|
88
|
+
}, 10);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const a = createSimAgent("sim_user_001");
|
|
92
|
+
const replyPromise = a.waitForReply("sim_default", 5000);
|
|
93
|
+
await a.sendMessage("sim_default", "hello");
|
|
94
|
+
|
|
95
|
+
const reply = await replyPromise;
|
|
96
|
+
expect(reply).not.toBeNull();
|
|
97
|
+
expect(reply!.content).toBe("async reply: hello");
|
|
98
|
+
expect(reply!.senderId).toBe("bot");
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("waitForReply 超时返回 null", async () => {
|
|
102
|
+
setMessageHandler(async () => {});
|
|
103
|
+
|
|
104
|
+
const a = createSimAgent("sim_user_001");
|
|
105
|
+
const reply = await a.waitForReply("sim_default", 100);
|
|
106
|
+
expect(reply).toBeNull();
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("getMessages 获取消息历史", async () => {
|
|
110
|
+
setMessageHandler(async (text, chatId) => {
|
|
111
|
+
simStore.sendReply(chatId, "text", `echo: ${text}`);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
const a = createSimAgent("sim_user_001");
|
|
115
|
+
await a.sendMessage("sim_default", "msg1");
|
|
116
|
+
await a.sendMessage("sim_default", "msg2");
|
|
117
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
118
|
+
|
|
119
|
+
const msgs = a.getMessages("sim_default");
|
|
120
|
+
expect(msgs.length).toBe(4); // 2 user + 2 bot
|
|
121
|
+
expect(msgs[0].senderId).toBe("sim_user_001");
|
|
122
|
+
expect(msgs[0].content).toBe("msg1");
|
|
123
|
+
expect(msgs[1].senderId).toBe("bot");
|
|
124
|
+
expect(msgs[1].content).toBe("echo: msg1");
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("on invited_to_group 收到拉群通知", () => {
|
|
128
|
+
simStore.registerAccount({ id: "new_user", kind: "user", name: "New" });
|
|
129
|
+
const a = createSimAgent("new_user");
|
|
130
|
+
let invitedChatId = "";
|
|
131
|
+
let invitedUserId = "";
|
|
132
|
+
a.on("invited_to_group", (chatId, userId) => {
|
|
133
|
+
invitedChatId = chatId;
|
|
134
|
+
invitedUserId = userId;
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
simStore.createGroupChat("新群", ["new_user"]);
|
|
138
|
+
expect(invitedChatId).toMatch(/^sim_/);
|
|
139
|
+
expect(invitedUserId).toBe("new_user");
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("off 取消消息订阅", async () => {
|
|
143
|
+
setMessageHandler(async (text, chatId) => {
|
|
144
|
+
simStore.sendReply(chatId, "text", `echo: ${text}`);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
const a = createSimAgent("sim_user_001");
|
|
148
|
+
let count = 0;
|
|
149
|
+
const handler = () => { count++; };
|
|
150
|
+
a.on("message", handler);
|
|
151
|
+
|
|
152
|
+
await a.sendMessage("sim_default", "test1");
|
|
153
|
+
await new Promise((r) => setTimeout(r, 30));
|
|
154
|
+
// sendMessage 产生 2 条消息(recordMessage + sendReply)
|
|
155
|
+
const countBeforeOff = count;
|
|
156
|
+
expect(countBeforeOff).toBe(2);
|
|
157
|
+
|
|
158
|
+
a.off("message", handler);
|
|
159
|
+
|
|
160
|
+
await a.sendMessage("sim_default", "test2");
|
|
161
|
+
await new Promise((r) => setTimeout(r, 30));
|
|
162
|
+
|
|
163
|
+
// off 后不应再增加
|
|
164
|
+
expect(count).toBe(countBeforeOff);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("listChats 返回用户会话列表", () => {
|
|
168
|
+
const a = createSimAgent("sim_user_001");
|
|
169
|
+
const chats = a.listChats();
|
|
170
|
+
expect(chats.length).toBeGreaterThanOrEqual(1);
|
|
171
|
+
expect(chats.some((c) => c.id === "sim_default")).toBe(true);
|
|
172
|
+
expect(chats.some((c) => c.type === "p2p")).toBe(true);
|
|
173
|
+
});
|
|
174
|
+
});
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { describe, it, expect, afterAll } from "vitest";
|
|
2
|
+
import { unlink } from "node:fs/promises";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
|
|
6
|
+
import { SimStore, simStore, SIM_DEFAULT_CHAT_ID } from "../sim-store.ts";
|
|
7
|
+
import type { SimAccount, SimChat, SimMessage } from "../sim-store.ts";
|
|
8
|
+
|
|
9
|
+
const MESSAGES_FILE = join(homedir(), ".chatccc", "sim", "messages.jsonl");
|
|
10
|
+
|
|
11
|
+
describe("SimStore", () => {
|
|
12
|
+
afterAll(async () => {
|
|
13
|
+
try { await unlink(MESSAGES_FILE); } catch { /* ok */ }
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
// ---- 初始化 ----
|
|
17
|
+
|
|
18
|
+
it("默认初始化 bot + 默认用户 + 默认群", () => {
|
|
19
|
+
const store = new SimStore();
|
|
20
|
+
expect(store.getAccount("bot")).toBeDefined();
|
|
21
|
+
expect(store.getAccount("sim_user_001")).toBeDefined();
|
|
22
|
+
expect(store.getChat("sim_default")).toBeDefined();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("默认群成员包含 bot 和 sim_user_001", () => {
|
|
26
|
+
const store = new SimStore();
|
|
27
|
+
const chat = store.getChat("sim_default")!;
|
|
28
|
+
expect(chat.memberIds).toContain("bot");
|
|
29
|
+
expect(chat.memberIds).toContain("sim_user_001");
|
|
30
|
+
expect(chat.type).toBe("group");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// ---- 账户管理 ----
|
|
34
|
+
|
|
35
|
+
it("registerAccount 注册新用户", () => {
|
|
36
|
+
const store = new SimStore();
|
|
37
|
+
store.registerAccount({ id: "alice", kind: "user", name: "Alice" });
|
|
38
|
+
const a = store.getAccount("alice");
|
|
39
|
+
expect(a).toBeDefined();
|
|
40
|
+
expect(a!.name).toBe("Alice");
|
|
41
|
+
expect(a!.kind).toBe("user");
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("registerAccount 重复 id 抛出错误", () => {
|
|
45
|
+
const store = new SimStore();
|
|
46
|
+
store.registerAccount({ id: "bob", kind: "user", name: "Bob" });
|
|
47
|
+
expect(() => store.registerAccount({ id: "bob", kind: "user", name: "Bob2" }))
|
|
48
|
+
.toThrow('Account "bob" already exists');
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// ---- 群聊管理 ----
|
|
52
|
+
|
|
53
|
+
it("createGroupChat 创建群,bot 自动加入", () => {
|
|
54
|
+
const store = new SimStore();
|
|
55
|
+
store.registerAccount({ id: "alice", kind: "user", name: "Alice" });
|
|
56
|
+
const chat = store.createGroupChat("测试群", ["alice"]);
|
|
57
|
+
expect(chat.id).toMatch(/^sim_[0-9a-f]{8}$/);
|
|
58
|
+
expect(chat.name).toBe("测试群");
|
|
59
|
+
expect(chat.type).toBe("group");
|
|
60
|
+
expect(chat.memberIds).toContain("bot");
|
|
61
|
+
expect(chat.memberIds).toContain("alice");
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("createGroupChat emit chat_created 和 member_added 事件", () => {
|
|
65
|
+
const store = new SimStore();
|
|
66
|
+
store.registerAccount({ id: "alice", kind: "user", name: "Alice" });
|
|
67
|
+
|
|
68
|
+
let createdChat: SimChat | null = null;
|
|
69
|
+
let addedUserId = "";
|
|
70
|
+
store.on("chat_created", ({ chat }) => { createdChat = chat; });
|
|
71
|
+
store.on("member_added", ({ userId }) => { addedUserId = userId; });
|
|
72
|
+
|
|
73
|
+
store.createGroupChat("测试群", ["alice"]);
|
|
74
|
+
expect(createdChat).not.toBeNull();
|
|
75
|
+
expect(createdChat!.name).toBe("测试群");
|
|
76
|
+
expect(addedUserId).toBe("alice");
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("createP2pChat 创建私聊", () => {
|
|
80
|
+
const store = new SimStore();
|
|
81
|
+
store.registerAccount({ id: "alice", kind: "user", name: "Alice" });
|
|
82
|
+
const chat = store.createP2pChat("alice");
|
|
83
|
+
expect(chat.id).toBe("p2p_alice_bot");
|
|
84
|
+
expect(chat.type).toBe("p2p");
|
|
85
|
+
expect(chat.memberIds).toContain("bot");
|
|
86
|
+
expect(chat.memberIds).toContain("alice");
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("createP2pChat 重复调用返回同一个 chat", () => {
|
|
90
|
+
const store = new SimStore();
|
|
91
|
+
store.registerAccount({ id: "alice", kind: "user", name: "Alice" });
|
|
92
|
+
const c1 = store.createP2pChat("alice");
|
|
93
|
+
const c2 = store.createP2pChat("alice");
|
|
94
|
+
expect(c1.id).toBe(c2.id);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("getChatsForUser 返回用户所在的所有会话", () => {
|
|
98
|
+
const store = new SimStore();
|
|
99
|
+
store.registerAccount({ id: "alice", kind: "user", name: "Alice" });
|
|
100
|
+
store.createP2pChat("alice");
|
|
101
|
+
store.createGroupChat("群1", ["alice"]);
|
|
102
|
+
store.createGroupChat("群2", ["alice"]);
|
|
103
|
+
|
|
104
|
+
const chats = store.getChatsForUser("alice");
|
|
105
|
+
expect(chats.length).toBeGreaterThanOrEqual(2);
|
|
106
|
+
expect(chats.some((c) => c.type === "p2p")).toBe(true);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("addMember 添加成员并 emit 事件", () => {
|
|
110
|
+
const store = new SimStore();
|
|
111
|
+
store.registerAccount({ id: "alice", kind: "user", name: "Alice" });
|
|
112
|
+
const chat = store.createGroupChat("测试群", []);
|
|
113
|
+
|
|
114
|
+
let addedUserId = "";
|
|
115
|
+
store.on("member_added", ({ userId }) => { addedUserId = userId; });
|
|
116
|
+
|
|
117
|
+
store.addMember(chat.id, "alice");
|
|
118
|
+
expect(chat.memberIds).toContain("alice");
|
|
119
|
+
expect(addedUserId).toBe("alice");
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("updateChatInfo 更新已有群信息", () => {
|
|
123
|
+
const store = new SimStore();
|
|
124
|
+
store.registerAccount({ id: "alice", kind: "user", name: "Alice" });
|
|
125
|
+
const chat = store.createGroupChat("原群名", ["alice"]);
|
|
126
|
+
store.updateChatInfo(chat.id, "新群名", "Claude Code Session: abc123");
|
|
127
|
+
expect(chat.name).toBe("新群名");
|
|
128
|
+
expect(chat.description).toBe("Claude Code Session: abc123");
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("updateChatInfo 对不存在的群自动创建", () => {
|
|
132
|
+
const store = new SimStore();
|
|
133
|
+
store.updateChatInfo("sim_fake", "新群", "desc");
|
|
134
|
+
const chat = store.getChat("sim_fake");
|
|
135
|
+
expect(chat).toBeDefined();
|
|
136
|
+
expect(chat!.name).toBe("新群");
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// ---- 消息管理 ----
|
|
140
|
+
|
|
141
|
+
it("recordMessage 记录用户消息到 chat 历史", () => {
|
|
142
|
+
const store = new SimStore();
|
|
143
|
+
store.registerAccount({ id: "alice", kind: "user", name: "Alice" });
|
|
144
|
+
const chat = store.createGroupChat("测试群", ["alice"]);
|
|
145
|
+
store.recordMessage(chat.id, "alice", "text", "你好");
|
|
146
|
+
expect(chat.messages.length).toBe(1);
|
|
147
|
+
expect(chat.messages[0].content).toBe("你好");
|
|
148
|
+
expect(chat.messages[0].senderId).toBe("alice");
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("recordMessage emit message 事件", () => {
|
|
152
|
+
const store = new SimStore();
|
|
153
|
+
store.registerAccount({ id: "alice", kind: "user", name: "Alice" });
|
|
154
|
+
const chat = store.createGroupChat("测试群", ["alice"]);
|
|
155
|
+
|
|
156
|
+
let emittedMsg: SimMessage | null = null;
|
|
157
|
+
let emittedChatId = "";
|
|
158
|
+
store.on("message", ({ chatId, message }) => {
|
|
159
|
+
emittedChatId = chatId;
|
|
160
|
+
emittedMsg = message;
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
store.recordMessage(chat.id, "alice", "text", "hello");
|
|
164
|
+
expect(emittedChatId).toBe(chat.id);
|
|
165
|
+
expect(emittedMsg!.content).toBe("hello");
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("recordMessage 非成员不能看到消息", () => {
|
|
169
|
+
const store = new SimStore();
|
|
170
|
+
store.registerAccount({ id: "alice", kind: "user", name: "Alice" });
|
|
171
|
+
store.registerAccount({ id: "bob", kind: "user", name: "Bob" });
|
|
172
|
+
const chat = store.createGroupChat("测试群", ["alice"]); // bob 不在群内
|
|
173
|
+
|
|
174
|
+
store.recordMessage(chat.id, "alice", "text", "hello");
|
|
175
|
+
const msgs = store.getMessages(chat.id, "bob"); // bob 请求消息
|
|
176
|
+
expect(msgs.length).toBe(0);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it("recordMessage 成员可以看到消息", () => {
|
|
180
|
+
const store = new SimStore();
|
|
181
|
+
store.registerAccount({ id: "alice", kind: "user", name: "Alice" });
|
|
182
|
+
const chat = store.createGroupChat("测试群", ["alice"]);
|
|
183
|
+
|
|
184
|
+
store.recordMessage(chat.id, "alice", "text", "hello");
|
|
185
|
+
const msgs = store.getMessages(chat.id, "alice");
|
|
186
|
+
expect(msgs.length).toBe(1);
|
|
187
|
+
expect(msgs[0].content).toBe("hello");
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it("sendReply 即使 chat 不存在也能记录并 emit", () => {
|
|
191
|
+
const store = new SimStore();
|
|
192
|
+
let emitted = false;
|
|
193
|
+
store.on("message", () => { emitted = true; });
|
|
194
|
+
|
|
195
|
+
const msg = store.sendReply("unknown_chat", "text", "测试");
|
|
196
|
+
expect(msg.senderId).toBe("bot");
|
|
197
|
+
expect(emitted).toBe(true);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it("sendReply 追加到已有 chat 的消息历史", () => {
|
|
201
|
+
const store = new SimStore();
|
|
202
|
+
store.registerAccount({ id: "alice", kind: "user", name: "Alice" });
|
|
203
|
+
const chat = store.createGroupChat("测试群", ["alice"]);
|
|
204
|
+
|
|
205
|
+
store.sendReply(chat.id, "text", "bot reply");
|
|
206
|
+
expect(chat.messages.length).toBe(1);
|
|
207
|
+
expect(chat.messages[0].senderId).toBe("bot");
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it("getMessages 对不存在的 chat 返回空数组", () => {
|
|
211
|
+
const store = new SimStore();
|
|
212
|
+
expect(store.getMessages("nonexistent", "alice")).toEqual([]);
|
|
213
|
+
});
|
|
214
|
+
});
|
package/src/index.ts
CHANGED
|
@@ -89,6 +89,7 @@ import { handleAgentImageRequest } from "./agent-image-rpc.ts";
|
|
|
89
89
|
import { handleAgentFileRequest } from "./agent-file-rpc.ts";
|
|
90
90
|
import { handleAgentGrantsRequest } from "./agent-grants-rpc.ts";
|
|
91
91
|
import { SimulatedPlatform, SIM_DEFAULT_CHAT_ID } from "./sim-platform.ts";
|
|
92
|
+
import { setMessageHandler } from "./sim-store.ts";
|
|
92
93
|
import { formatGitResult, gitResultHeaderTemplate, runGitCommand } from "./git-command.ts";
|
|
93
94
|
import {
|
|
94
95
|
MAX_PROCESSED,
|
|
@@ -480,6 +481,9 @@ async function handleCommand(text: string, chatId: string, openId: string, msgTi
|
|
|
480
481
|
return;
|
|
481
482
|
}
|
|
482
483
|
|
|
484
|
+
// 让新群的默认工作目录继承当前会话的 cwd
|
|
485
|
+
await setDefaultCwd(cwd, newChatId);
|
|
486
|
+
|
|
483
487
|
const adapter = getAdapterForTool(tool);
|
|
484
488
|
await sendCardReply(
|
|
485
489
|
freshToken, newChatId, `${toolLabel} Session Ready`,
|
|
@@ -1080,6 +1084,12 @@ async function main(): Promise<void> {
|
|
|
1080
1084
|
console.log(" 已切换到 SimulatedPlatform(零飞书依赖)");
|
|
1081
1085
|
appendStartupTrace("main: simulate mode", { port: SIM_PORT });
|
|
1082
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
|
+
|
|
1083
1093
|
setExtraApiHandler(async (req, res) => {
|
|
1084
1094
|
const injected = await handleSimInjectMessage(req, res);
|
|
1085
1095
|
if (injected) return true;
|
package/src/session.ts
CHANGED
|
@@ -521,11 +521,11 @@ export async function resumeAndPrompt(
|
|
|
521
521
|
if (sendInterval) clearInterval(sendInterval);
|
|
522
522
|
revokeAgentImageGrant(imageGrant.token);
|
|
523
523
|
revokeAgentFileGrant(fileGrant.token);
|
|
524
|
-
clearSessionGrants(sessionId);
|
|
525
524
|
}
|
|
526
525
|
|
|
527
526
|
const cEntry = chatSessionMap.get(chatId);
|
|
528
527
|
if (!cEntry || cEntry.gen !== myGen) return;
|
|
528
|
+
clearSessionGrants(sessionId);
|
|
529
529
|
const wasStopped = cEntry.stopped;
|
|
530
530
|
const prevTs = lastMsgTimestamps.get(chatId);
|
|
531
531
|
if (prevTs === undefined || cEntry.msgTimestamp > prevTs) {
|
package/src/sim-agent.ts
ADDED
|
@@ -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
|
+
}
|
package/src/sim-platform.ts
CHANGED
|
@@ -1,18 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* sim-platform.ts — SimulatedPlatform: 零飞书依赖的本地模拟实现
|
|
3
3
|
*
|
|
4
|
-
* 所有飞书 API
|
|
5
|
-
* -
|
|
6
|
-
* -
|
|
4
|
+
* 所有飞书 API 调用都由本地 SimStore 替代:
|
|
5
|
+
* - 消息通过 SimStore 写入内存 + JSONL + 事件推送
|
|
6
|
+
* - 群聊/账户信息由 SimStore 统一管理
|
|
7
7
|
* - createGroupChat 生成 "sim_<uuid>" 格式的 chat_id
|
|
8
8
|
*
|
|
9
9
|
* 纯函数(extractSessionInfo / formatDelayNotice / reportPermissionResults)
|
|
10
10
|
* 直接从 feishu-api.ts re-export,不重新实现。
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import { randomUUID } from "node:crypto";
|
|
14
|
-
import { appendFile, mkdir } from "node:fs/promises";
|
|
15
|
-
import { homedir } from "node:os";
|
|
16
13
|
import { join } from "node:path";
|
|
17
14
|
|
|
18
15
|
import {
|
|
@@ -22,33 +19,14 @@ import {
|
|
|
22
19
|
reportPermissionResults as realReportPermissionResults,
|
|
23
20
|
} from "./feishu-api.ts";
|
|
24
21
|
import type { FeishuPlatform } from "./feishu-platform.ts";
|
|
22
|
+
import { simStore } from "./sim-store.ts";
|
|
25
23
|
|
|
26
24
|
// ---------------------------------------------------------------------------
|
|
27
25
|
// 持久化路径
|
|
28
26
|
// ---------------------------------------------------------------------------
|
|
29
27
|
|
|
28
|
+
import { homedir } from "node:os";
|
|
30
29
|
const SIM_DIR = join(homedir(), ".chatccc", "sim");
|
|
31
|
-
const MESSAGES_FILE = join(SIM_DIR, "messages.jsonl");
|
|
32
|
-
|
|
33
|
-
// ---------------------------------------------------------------------------
|
|
34
|
-
// 内存状态
|
|
35
|
-
// ---------------------------------------------------------------------------
|
|
36
|
-
|
|
37
|
-
interface SimChat {
|
|
38
|
-
name: string;
|
|
39
|
-
description: string;
|
|
40
|
-
members: string[];
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const chats = new Map<string, SimChat>();
|
|
44
|
-
|
|
45
|
-
// 默认模拟群:用户注入消息时如果不指定 chat_id,就用这个
|
|
46
|
-
const DEFAULT_CHAT_ID = "sim_default";
|
|
47
|
-
chats.set(DEFAULT_CHAT_ID, {
|
|
48
|
-
name: "默认模拟会话",
|
|
49
|
-
description: "",
|
|
50
|
-
members: ["sim_user_001"],
|
|
51
|
-
});
|
|
52
30
|
|
|
53
31
|
// ---------------------------------------------------------------------------
|
|
54
32
|
// 工具函数
|
|
@@ -58,16 +36,6 @@ function ts(): string {
|
|
|
58
36
|
return new Date().toISOString().replace("T", " ").slice(0, 19);
|
|
59
37
|
}
|
|
60
38
|
|
|
61
|
-
async function appendJsonl(line: Record<string, unknown>): Promise<void> {
|
|
62
|
-
await mkdir(SIM_DIR, { recursive: true });
|
|
63
|
-
await appendFile(MESSAGES_FILE, JSON.stringify(line) + "\n", "utf-8");
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/** 模拟 user_id 转 open_id 的映射 */
|
|
67
|
-
function resolveOpenId(userIds: string[]): string {
|
|
68
|
-
return userIds[0] ?? "sim_user_001";
|
|
69
|
-
}
|
|
70
|
-
|
|
71
39
|
// ---------------------------------------------------------------------------
|
|
72
40
|
// SimulatedPlatform
|
|
73
41
|
// ---------------------------------------------------------------------------
|
|
@@ -78,66 +46,35 @@ export const SimulatedPlatform: FeishuPlatform = {
|
|
|
78
46
|
return "sim_token";
|
|
79
47
|
},
|
|
80
48
|
|
|
81
|
-
// ----
|
|
49
|
+
// ---- 消息发送:委托给 simStore ----
|
|
82
50
|
async sendTextReply(_token, chatId, text) {
|
|
83
51
|
console.log(`[${ts()}] [SIM:SEND] text → ${chatId}: ${text.slice(0, 80)}`);
|
|
84
|
-
|
|
85
|
-
direction: "send",
|
|
86
|
-
chat_id: chatId,
|
|
87
|
-
msg_type: "text",
|
|
88
|
-
content: text,
|
|
89
|
-
timestamp: Date.now(),
|
|
90
|
-
});
|
|
52
|
+
simStore.sendReply(chatId, "text", text);
|
|
91
53
|
return true;
|
|
92
54
|
},
|
|
93
55
|
|
|
94
56
|
async sendCardReply(_token, chatId, title, content, _template) {
|
|
95
57
|
console.log(`[${ts()}] [SIM:SEND] card → ${chatId}: [${title}]`);
|
|
96
58
|
const text = `**[${title}]**\n${content}`;
|
|
97
|
-
|
|
98
|
-
direction: "send",
|
|
99
|
-
chat_id: chatId,
|
|
100
|
-
msg_type: "card",
|
|
101
|
-
header: title,
|
|
102
|
-
content: content,
|
|
103
|
-
timestamp: Date.now(),
|
|
104
|
-
});
|
|
59
|
+
simStore.sendReply(chatId, "card", text);
|
|
105
60
|
return true;
|
|
106
61
|
},
|
|
107
62
|
|
|
108
63
|
async sendImageReply(_token, chatId, imagePath) {
|
|
109
64
|
console.log(`[${ts()}] [SIM:SEND] image → ${chatId}: ${imagePath}`);
|
|
110
|
-
|
|
111
|
-
direction: "send",
|
|
112
|
-
chat_id: chatId,
|
|
113
|
-
msg_type: "image",
|
|
114
|
-
image_path: imagePath,
|
|
115
|
-
timestamp: Date.now(),
|
|
116
|
-
});
|
|
65
|
+
simStore.sendReply(chatId, "image", imagePath);
|
|
117
66
|
return true;
|
|
118
67
|
},
|
|
119
68
|
|
|
120
69
|
async sendFileReply(_token, chatId, filePath) {
|
|
121
70
|
console.log(`[${ts()}] [SIM:SEND] file → ${chatId}: ${filePath}`);
|
|
122
|
-
|
|
123
|
-
direction: "send",
|
|
124
|
-
chat_id: chatId,
|
|
125
|
-
msg_type: "file",
|
|
126
|
-
file_path: filePath,
|
|
127
|
-
timestamp: Date.now(),
|
|
128
|
-
});
|
|
71
|
+
simStore.sendReply(chatId, "file", filePath);
|
|
129
72
|
return true;
|
|
130
73
|
},
|
|
131
74
|
|
|
132
75
|
async sendRawCard(_token, chatId, cardJson) {
|
|
133
76
|
console.log(`[${ts()}] [SIM:SEND] raw_card → ${chatId}: ${cardJson.slice(0, 80)}...`);
|
|
134
|
-
|
|
135
|
-
direction: "send",
|
|
136
|
-
chat_id: chatId,
|
|
137
|
-
msg_type: "raw_card",
|
|
138
|
-
card_json_preview: cardJson.slice(0, 500),
|
|
139
|
-
timestamp: Date.now(),
|
|
140
|
-
});
|
|
77
|
+
simStore.sendReply(chatId, "raw_card", cardJson.slice(0, 500));
|
|
141
78
|
return true;
|
|
142
79
|
},
|
|
143
80
|
|
|
@@ -147,41 +84,29 @@ export const SimulatedPlatform: FeishuPlatform = {
|
|
|
147
84
|
},
|
|
148
85
|
|
|
149
86
|
async recallMessage(_token, _messageId) {
|
|
150
|
-
return true;
|
|
87
|
+
return true;
|
|
151
88
|
},
|
|
152
89
|
|
|
153
90
|
async updateCardMessage(_token, _messageId, content) {
|
|
154
|
-
//
|
|
155
|
-
|
|
156
|
-
type: "card_update",
|
|
157
|
-
message_id: _messageId,
|
|
158
|
-
content_preview: content.slice(0, 200),
|
|
159
|
-
timestamp: Date.now(),
|
|
160
|
-
});
|
|
91
|
+
// 模拟模式:记录卡片更新到控制台(无 chatId,不写入消息历史)
|
|
92
|
+
console.log(`[${ts()}] [SIM:CARD] update: msgId=${_messageId} preview=${content.slice(0, 80)}`);
|
|
161
93
|
return true;
|
|
162
94
|
},
|
|
163
95
|
|
|
164
96
|
// ---- 群聊管理 ----
|
|
165
97
|
async createGroupChat(_token, name, userIds) {
|
|
166
|
-
const
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
return chatId;
|
|
98
|
+
const chat = simStore.createGroupChat(name, userIds);
|
|
99
|
+
console.log(`[${ts()}] [SIM:CHAT] created: ${chat.id} name="${name}" members=${userIds.length}`);
|
|
100
|
+
return chat.id;
|
|
170
101
|
},
|
|
171
102
|
|
|
172
103
|
async updateChatInfo(_token, chatId, name, description) {
|
|
173
|
-
|
|
174
|
-
if (existing) {
|
|
175
|
-
existing.name = name;
|
|
176
|
-
existing.description = description;
|
|
177
|
-
} else {
|
|
178
|
-
chats.set(chatId, { name, description, members: ["sim_user_001"] });
|
|
179
|
-
}
|
|
104
|
+
simStore.updateChatInfo(chatId, name, description);
|
|
180
105
|
console.log(`[${ts()}] [SIM:CHAT] updated: ${chatId} name="${name}"`);
|
|
181
106
|
},
|
|
182
107
|
|
|
183
108
|
async getChatInfo(_token, chatId) {
|
|
184
|
-
const chat =
|
|
109
|
+
const chat = simStore.getChat(chatId);
|
|
185
110
|
if (!chat) throw new Error(`[99999] chat not found: ${chatId}`);
|
|
186
111
|
return { name: chat.name, description: chat.description };
|
|
187
112
|
},
|
|
@@ -193,7 +118,6 @@ export const SimulatedPlatform: FeishuPlatform = {
|
|
|
193
118
|
|
|
194
119
|
// ---- 图片下载 ----
|
|
195
120
|
async getOrDownloadImage(_token, _messageId, fileKey) {
|
|
196
|
-
// 模拟模式下图片不需要从飞书下载,返回占位路径
|
|
197
121
|
return join(SIM_DIR, "images", fileKey);
|
|
198
122
|
},
|
|
199
123
|
|
|
@@ -220,5 +144,5 @@ export const SimulatedPlatform: FeishuPlatform = {
|
|
|
220
144
|
},
|
|
221
145
|
};
|
|
222
146
|
|
|
223
|
-
/** 模拟模式下的默认 chat_id */
|
|
224
|
-
export
|
|
147
|
+
/** 模拟模式下的默认 chat_id(重新导出以保持向后兼容) */
|
|
148
|
+
export { SIM_DEFAULT_CHAT_ID } from "./sim-store.ts";
|
package/src/sim-store.ts
ADDED
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sim-store.ts — 模拟飞书环境的状态管理核心
|
|
3
|
+
*
|
|
4
|
+
* SimStore 是一个单例的 EventEmitter,管理所有模拟状态:
|
|
5
|
+
* - 账户(bot + 多个用户)
|
|
6
|
+
* - 会话(群聊 + 私聊)
|
|
7
|
+
* - 消息历史(内存 + JSONL 持久化)
|
|
8
|
+
*
|
|
9
|
+
* 同时暴露 setMessageHandler / dispatchMessage,让 SimAgent 进程内触发
|
|
10
|
+
* handleCommand 而无需通过 HTTP。
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { randomUUID } from "node:crypto";
|
|
14
|
+
import { appendFile, mkdir } from "node:fs/promises";
|
|
15
|
+
import { homedir } from "node:os";
|
|
16
|
+
import { join } from "node:path";
|
|
17
|
+
import { EventEmitter } from "node:events";
|
|
18
|
+
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// 类型定义
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
export interface SimAccount {
|
|
24
|
+
id: string; // "bot" | "sim_user_001" | "alice"
|
|
25
|
+
kind: "bot" | "user";
|
|
26
|
+
name: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface SimMessage {
|
|
30
|
+
id: string; // UUID
|
|
31
|
+
chatId: string;
|
|
32
|
+
senderId: string; // SimAccount.id
|
|
33
|
+
type: "text" | "card" | "image" | "file" | "raw_card" | "system";
|
|
34
|
+
content: string; // 文本内容或 JSON 字符串
|
|
35
|
+
timestamp: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface SimChat {
|
|
39
|
+
id: string; // "sim_default" | "sim_<uuid8>" | "p2p_alice_bot"
|
|
40
|
+
type: "group" | "p2p";
|
|
41
|
+
name: string;
|
|
42
|
+
description: string;
|
|
43
|
+
memberIds: string[]; // 所有成员,第一个是创建者
|
|
44
|
+
messages: SimMessage[]; // 消息历史(按时间排序)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** handleCommand 的回调签名 */
|
|
48
|
+
export type MessageHandler = (
|
|
49
|
+
text: string,
|
|
50
|
+
chatId: string,
|
|
51
|
+
openId: string,
|
|
52
|
+
timestamp: number,
|
|
53
|
+
chatType: string,
|
|
54
|
+
traceId?: string,
|
|
55
|
+
) => Promise<void>;
|
|
56
|
+
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
// 持久化路径
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
|
|
61
|
+
const SIM_DIR = join(homedir(), ".chatccc", "sim");
|
|
62
|
+
const MESSAGES_FILE = join(SIM_DIR, "messages.jsonl");
|
|
63
|
+
|
|
64
|
+
async function ensureDir(): Promise<void> {
|
|
65
|
+
await mkdir(SIM_DIR, { recursive: true });
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function appendJsonl(line: Record<string, unknown>): Promise<void> {
|
|
69
|
+
await ensureDir();
|
|
70
|
+
await appendFile(MESSAGES_FILE, JSON.stringify(line) + "\n", "utf-8");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
// SimStore
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
76
|
+
|
|
77
|
+
export class SimStore extends EventEmitter {
|
|
78
|
+
accounts = new Map<string, SimAccount>();
|
|
79
|
+
chats = new Map<string, SimChat>();
|
|
80
|
+
|
|
81
|
+
// ---- 初始化 ----
|
|
82
|
+
|
|
83
|
+
constructor() {
|
|
84
|
+
super();
|
|
85
|
+
this._initDefaults();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
private _initDefaults(): void {
|
|
89
|
+
// 注册 bot 账户
|
|
90
|
+
this.accounts.set("bot", { id: "bot", kind: "bot", name: "ChatCCC" });
|
|
91
|
+
|
|
92
|
+
// 注册默认用户
|
|
93
|
+
this.accounts.set("sim_user_001", { id: "sim_user_001", kind: "user", name: "Developer" });
|
|
94
|
+
|
|
95
|
+
// 创建默认群聊
|
|
96
|
+
const defaultChatId = "sim_default";
|
|
97
|
+
this.chats.set(defaultChatId, {
|
|
98
|
+
id: defaultChatId,
|
|
99
|
+
type: "group",
|
|
100
|
+
name: "默认模拟会话",
|
|
101
|
+
description: "",
|
|
102
|
+
memberIds: ["bot", "sim_user_001"],
|
|
103
|
+
messages: [],
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ---- 账户管理 ----
|
|
108
|
+
|
|
109
|
+
registerAccount(account: SimAccount): void {
|
|
110
|
+
if (this.accounts.has(account.id)) {
|
|
111
|
+
throw new Error(`Account "${account.id}" already exists`);
|
|
112
|
+
}
|
|
113
|
+
this.accounts.set(account.id, account);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
getAccount(id: string): SimAccount | undefined {
|
|
117
|
+
return this.accounts.get(id);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ---- 会话管理 ----
|
|
121
|
+
|
|
122
|
+
createGroupChat(name: string, memberIds: string[]): SimChat {
|
|
123
|
+
const chatId = `sim_${randomUUID().slice(0, 8)}`;
|
|
124
|
+
// bot 始终是成员
|
|
125
|
+
const allMembers = ["bot", ...memberIds.filter((id) => id !== "bot")];
|
|
126
|
+
const chat: SimChat = {
|
|
127
|
+
id: chatId,
|
|
128
|
+
type: "group",
|
|
129
|
+
name,
|
|
130
|
+
description: "",
|
|
131
|
+
memberIds: allMembers,
|
|
132
|
+
messages: [],
|
|
133
|
+
};
|
|
134
|
+
this.chats.set(chatId, chat);
|
|
135
|
+
|
|
136
|
+
// 通知被拉入群的用户
|
|
137
|
+
for (const uid of memberIds) {
|
|
138
|
+
this.emit("member_added", { chatId, userId: uid });
|
|
139
|
+
}
|
|
140
|
+
this.emit("chat_created", { chat });
|
|
141
|
+
|
|
142
|
+
return chat;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
createP2pChat(userId: string): SimChat {
|
|
146
|
+
// p2p 命名规则:p2p_<userId>_bot
|
|
147
|
+
const chatId = `p2p_${userId}_bot`;
|
|
148
|
+
const existing = this.chats.get(chatId);
|
|
149
|
+
if (existing) return existing;
|
|
150
|
+
|
|
151
|
+
const user = this.accounts.get(userId);
|
|
152
|
+
const chat: SimChat = {
|
|
153
|
+
id: chatId,
|
|
154
|
+
type: "p2p",
|
|
155
|
+
name: user ? `${user.name} 的私聊` : `私聊`,
|
|
156
|
+
description: "",
|
|
157
|
+
memberIds: ["bot", userId],
|
|
158
|
+
messages: [],
|
|
159
|
+
};
|
|
160
|
+
this.chats.set(chatId, chat);
|
|
161
|
+
this.emit("chat_created", { chat });
|
|
162
|
+
return chat;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
getChat(chatId: string): SimChat | undefined {
|
|
166
|
+
return this.chats.get(chatId);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
getChatsForUser(userId: string): SimChat[] {
|
|
170
|
+
return [...this.chats.values()].filter((c) => c.memberIds.includes(userId));
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
addMember(chatId: string, userId: string): void {
|
|
174
|
+
const chat = this.chats.get(chatId);
|
|
175
|
+
if (!chat) throw new Error(`Chat not found: ${chatId}`);
|
|
176
|
+
if (!chat.memberIds.includes(userId)) {
|
|
177
|
+
chat.memberIds.push(userId);
|
|
178
|
+
this.emit("member_added", { chatId, userId });
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
updateChatInfo(chatId: string, name: string, description: string): void {
|
|
183
|
+
const chat = this.chats.get(chatId);
|
|
184
|
+
if (chat) {
|
|
185
|
+
chat.name = name;
|
|
186
|
+
chat.description = description;
|
|
187
|
+
} else {
|
|
188
|
+
// 模拟模式下机器人可能尝试更新不存在的群(比如从旧状态恢复),兼容处理
|
|
189
|
+
this.chats.set(chatId, {
|
|
190
|
+
id: chatId,
|
|
191
|
+
type: "group",
|
|
192
|
+
name,
|
|
193
|
+
description,
|
|
194
|
+
memberIds: ["bot", "sim_user_001"],
|
|
195
|
+
messages: [],
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// ---- 消息管理 ----
|
|
201
|
+
|
|
202
|
+
/** 任何参与者发送消息 */
|
|
203
|
+
recordMessage(
|
|
204
|
+
chatId: string,
|
|
205
|
+
senderId: string,
|
|
206
|
+
type: SimMessage["type"],
|
|
207
|
+
content: string,
|
|
208
|
+
): SimMessage {
|
|
209
|
+
const chat = this.chats.get(chatId);
|
|
210
|
+
if (!chat) throw new Error(`Chat not found: ${chatId}`);
|
|
211
|
+
|
|
212
|
+
const msg: SimMessage = {
|
|
213
|
+
id: randomUUID(),
|
|
214
|
+
chatId,
|
|
215
|
+
senderId,
|
|
216
|
+
type,
|
|
217
|
+
content,
|
|
218
|
+
timestamp: Date.now(),
|
|
219
|
+
};
|
|
220
|
+
chat.messages.push(msg);
|
|
221
|
+
|
|
222
|
+
// 写 JSONL
|
|
223
|
+
appendJsonl({
|
|
224
|
+
direction: senderId === "bot" ? "send" : "recv",
|
|
225
|
+
chat_id: chatId,
|
|
226
|
+
sender_id: senderId,
|
|
227
|
+
msg_type: type,
|
|
228
|
+
content,
|
|
229
|
+
timestamp: msg.timestamp,
|
|
230
|
+
}).catch(() => {});
|
|
231
|
+
|
|
232
|
+
// 通知所有订阅者
|
|
233
|
+
this.emit("message", { chatId, message: msg });
|
|
234
|
+
|
|
235
|
+
return msg;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** bot 发送回复——即使 chat 不存在也记录(兼容未知 chat) */
|
|
239
|
+
sendReply(chatId: string, type: SimMessage["type"], content: string): SimMessage {
|
|
240
|
+
const msg: SimMessage = {
|
|
241
|
+
id: randomUUID(),
|
|
242
|
+
chatId,
|
|
243
|
+
senderId: "bot",
|
|
244
|
+
type,
|
|
245
|
+
content,
|
|
246
|
+
timestamp: Date.now(),
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
// 如果 chat 存在,追加到消息历史
|
|
250
|
+
const chat = this.chats.get(chatId);
|
|
251
|
+
if (chat) {
|
|
252
|
+
chat.messages.push(msg);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// 写 JSONL
|
|
256
|
+
appendJsonl({
|
|
257
|
+
direction: "send",
|
|
258
|
+
chat_id: chatId,
|
|
259
|
+
msg_type: type,
|
|
260
|
+
content,
|
|
261
|
+
timestamp: msg.timestamp,
|
|
262
|
+
}).catch(() => {});
|
|
263
|
+
|
|
264
|
+
// 通知订阅者
|
|
265
|
+
this.emit("message", { chatId, message: msg });
|
|
266
|
+
|
|
267
|
+
return msg;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** 获取指定会话的消息 */
|
|
271
|
+
getMessages(chatId: string, requesterId: string): SimMessage[] {
|
|
272
|
+
const chat = this.chats.get(chatId);
|
|
273
|
+
if (!chat) return [];
|
|
274
|
+
// 只有成员能看消息
|
|
275
|
+
if (!chat.memberIds.includes(requesterId)) return [];
|
|
276
|
+
return chat.messages;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/** 重置所有状态(测试用) */
|
|
280
|
+
reset(): void {
|
|
281
|
+
this.accounts.clear();
|
|
282
|
+
this.chats.clear();
|
|
283
|
+
this.removeAllListeners();
|
|
284
|
+
this._initDefaults();
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// ---------------------------------------------------------------------------
|
|
289
|
+
// 单例 + 消息分发
|
|
290
|
+
// ---------------------------------------------------------------------------
|
|
291
|
+
|
|
292
|
+
export const simStore = new SimStore();
|
|
293
|
+
|
|
294
|
+
export const SIM_DEFAULT_CHAT_ID = "sim_default";
|
|
295
|
+
|
|
296
|
+
let _messageHandler: MessageHandler | null = null;
|
|
297
|
+
|
|
298
|
+
/** 注册 handleCommand 回调(由 index.ts 在模拟模式启动时调用) */
|
|
299
|
+
export function setMessageHandler(handler: MessageHandler): void {
|
|
300
|
+
_messageHandler = handler;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/** 模拟用户发送消息 → 记录消息 + 触发 bot 处理 */
|
|
304
|
+
export async function dispatchMessage(
|
|
305
|
+
text: string,
|
|
306
|
+
chatId: string,
|
|
307
|
+
openId: string,
|
|
308
|
+
chatType: string,
|
|
309
|
+
): Promise<void> {
|
|
310
|
+
if (!_messageHandler) throw new Error("Message handler not registered — is simulate mode running?");
|
|
311
|
+
simStore.recordMessage(chatId, openId, "text", text);
|
|
312
|
+
await _messageHandler(text, chatId, openId, Date.now(), chatType);
|
|
313
|
+
}
|