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.
- package/package.json +1 -1
- package/src/__tests__/feishu-platform.test.ts +55 -0
- package/src/__tests__/sim-agent.test.ts +174 -0
- package/src/__tests__/sim-platform.test.ts +77 -0
- package/src/__tests__/sim-store.test.ts +214 -0
- package/src/agent-file-rpc.ts +1 -1
- package/src/agent-image-rpc.ts +1 -1
- package/src/config.ts +1 -0
- package/src/feishu-api.ts +27 -0
- package/src/feishu-platform.ts +134 -0
- package/src/index.ts +121 -49
- package/src/session.ts +1 -1
- package/src/sim-agent.ts +156 -0
- package/src/sim-platform.ts +148 -0
- package/src/sim-store.ts +313 -0
package/package.json
CHANGED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll, afterAll } from "vitest";
|
|
2
|
+
import { getPlatform, setPlatform, getTenantAccessToken, getChatInfo, createGroupChat, updateChatInfo, sendTextReply, sendCardReply, extractSessionInfo, formatDelayNotice, reportPermissionResults, verifyAllPermissions, addReaction, recallMessage, updateCardMessage, setChatAvatar, sendRestartCard } from "../feishu-platform.ts";
|
|
3
|
+
import type { FeishuPlatform } from "../feishu-platform.ts";
|
|
4
|
+
|
|
5
|
+
const realPlatform = getPlatform();
|
|
6
|
+
|
|
7
|
+
describe("feishu-platform", () => {
|
|
8
|
+
it("默认使用真实实现", () => {
|
|
9
|
+
expect(realPlatform).toBeDefined();
|
|
10
|
+
expect(typeof realPlatform.getTenantAccessToken).toBe("function");
|
|
11
|
+
expect(typeof realPlatform.sendTextReply).toBe("function");
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("setPlatform 可替换实现,包装函数委托到新实现", async () => {
|
|
15
|
+
const mock: FeishuPlatform = {
|
|
16
|
+
getTenantAccessToken: async () => "mock_token",
|
|
17
|
+
sendTextReply: async () => true,
|
|
18
|
+
sendCardReply: async () => true,
|
|
19
|
+
sendImageReply: async () => true,
|
|
20
|
+
sendFileReply: async () => true,
|
|
21
|
+
addReaction: async () => {},
|
|
22
|
+
recallMessage: async () => false,
|
|
23
|
+
updateCardMessage: async () => true,
|
|
24
|
+
createGroupChat: async () => "mock_chat",
|
|
25
|
+
updateChatInfo: async () => {},
|
|
26
|
+
getChatInfo: async () => ({ name: "x", description: "y" }),
|
|
27
|
+
setChatAvatar: async () => {},
|
|
28
|
+
getOrDownloadImage: async () => "/tmp/img.png",
|
|
29
|
+
verifyAllPermissions: async () => [],
|
|
30
|
+
reportPermissionResults: realPlatform.reportPermissionResults,
|
|
31
|
+
extractSessionInfo: realPlatform.extractSessionInfo,
|
|
32
|
+
extractSessionId: realPlatform.extractSessionId,
|
|
33
|
+
formatDelayNotice: realPlatform.formatDelayNotice,
|
|
34
|
+
sendRestartCard: async () => {},
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
setPlatform(mock);
|
|
38
|
+
try {
|
|
39
|
+
expect(await getTenantAccessToken()).toBe("mock_token");
|
|
40
|
+
expect(await sendTextReply("t", "c", "hi")).toBe(true);
|
|
41
|
+
expect(await createGroupChat("t", "g", [])).toBe("mock_chat");
|
|
42
|
+
expect(await getChatInfo("t", "mock_chat")).toEqual({ name: "x", description: "y" });
|
|
43
|
+
const perms = await verifyAllPermissions("t");
|
|
44
|
+
expect(perms).toEqual([]);
|
|
45
|
+
} finally {
|
|
46
|
+
// 恢复到真实实现,避免影响后续测试
|
|
47
|
+
setPlatform(realPlatform);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("恢复真实实现后函数正常工作", async () => {
|
|
52
|
+
setPlatform(realPlatform);
|
|
53
|
+
expect(getPlatform()).toBe(realPlatform);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
@@ -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,77 @@
|
|
|
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 { SimulatedPlatform, SIM_DEFAULT_CHAT_ID } from "../sim-platform.ts";
|
|
7
|
+
|
|
8
|
+
const MESSAGES_FILE = join(homedir(), ".chatccc", "sim", "messages.jsonl");
|
|
9
|
+
|
|
10
|
+
describe("SimulatedPlatform", () => {
|
|
11
|
+
afterAll(async () => {
|
|
12
|
+
// 清理测试消息文件
|
|
13
|
+
try { await unlink(MESSAGES_FILE); } catch { /* ok */ }
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("getTenantAccessToken 返回固定 token", async () => {
|
|
17
|
+
const token = await SimulatedPlatform.getTenantAccessToken();
|
|
18
|
+
expect(token).toBe("sim_token");
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("createGroupChat 创建群并返回 sim_xxx ID", async () => {
|
|
22
|
+
const chatId = await SimulatedPlatform.createGroupChat("t", "测试群", ["u1"]);
|
|
23
|
+
expect(chatId).toMatch(/^sim_[0-9a-f]{8}$/);
|
|
24
|
+
const info = await SimulatedPlatform.getChatInfo("t", chatId);
|
|
25
|
+
expect(info.name).toBe("测试群");
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("getChatInfo 默认群存在", async () => {
|
|
29
|
+
const info = await SimulatedPlatform.getChatInfo("t", SIM_DEFAULT_CHAT_ID);
|
|
30
|
+
expect(info.name).toBe("默认模拟会话");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("updateChatInfo 更新群信息", async () => {
|
|
34
|
+
const chatId = await SimulatedPlatform.createGroupChat("t", "原群名", ["u1"]);
|
|
35
|
+
await SimulatedPlatform.updateChatInfo("t", chatId, "新群名", "Claude Session: abc123");
|
|
36
|
+
const info = await SimulatedPlatform.getChatInfo("t", chatId);
|
|
37
|
+
expect(info.name).toBe("新群名");
|
|
38
|
+
expect(info.description).toContain("Claude Session");
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("sendTextReply 返回 true", async () => {
|
|
42
|
+
const ok = await SimulatedPlatform.sendTextReply("t", "ch1", "你好");
|
|
43
|
+
expect(ok).toBe(true);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("sendCardReply 返回 true", async () => {
|
|
47
|
+
const ok = await SimulatedPlatform.sendCardReply("t", "ch1", "标题", "内容", "blue");
|
|
48
|
+
expect(ok).toBe(true);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("verifyAllPermissions 全部通过", async () => {
|
|
52
|
+
const results = await SimulatedPlatform.verifyAllPermissions("t");
|
|
53
|
+
expect(results.length).toBe(5);
|
|
54
|
+
expect(results.every((r) => r.ok)).toBe(true);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("addReaction 无异常", async () => {
|
|
58
|
+
await expect(SimulatedPlatform.addReaction("t", "msg1")).resolves.toBeUndefined();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("setChatAvatar 无异常", async () => {
|
|
62
|
+
await expect(SimulatedPlatform.setChatAvatar("t", "ch1", "claude", "busy")).resolves.toBeUndefined();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("纯函数 extractSessionInfo 正常工作", () => {
|
|
66
|
+
const result = SimulatedPlatform.extractSessionInfo("Claude Code Session: a1b2c3d4-e5f6-7890-abcd-ef1234567890");
|
|
67
|
+
expect(result).toEqual({ sessionId: "a1b2c3d4-e5f6-7890-abcd-ef1234567890", tool: "claude" });
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("纯函数 formatDelayNotice 正常工作", () => {
|
|
71
|
+
const notice = SimulatedPlatform.formatDelayNotice(Date.now() - 20 * 60 * 1000, "测试消息");
|
|
72
|
+
expect(notice).toBeDefined();
|
|
73
|
+
expect(notice).toContain("延迟送达");
|
|
74
|
+
// 近期消息不触发
|
|
75
|
+
expect(SimulatedPlatform.formatDelayNotice(Date.now(), "test")).toBeNull();
|
|
76
|
+
});
|
|
77
|
+
});
|
|
@@ -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/agent-file-rpc.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { IncomingMessage, ServerResponse } from "node:http";
|
|
|
3
3
|
import { extname, isAbsolute, resolve } from "node:path";
|
|
4
4
|
import { stat } from "node:fs/promises";
|
|
5
5
|
|
|
6
|
-
import { getTenantAccessToken, sendFileReply, sendTextReply } from "./feishu-
|
|
6
|
+
import { getTenantAccessToken, sendFileReply, sendTextReply } from "./feishu-platform.ts";
|
|
7
7
|
import { ts } from "./config.ts";
|
|
8
8
|
import { logTrace } from "./trace.ts";
|
|
9
9
|
import { readUtf8JsonBody } from "./agent-rpc-body.ts";
|
package/src/agent-image-rpc.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { IncomingMessage, ServerResponse } from "node:http";
|
|
|
3
3
|
import { extname, isAbsolute, resolve } from "node:path";
|
|
4
4
|
import { stat } from "node:fs/promises";
|
|
5
5
|
|
|
6
|
-
import { getTenantAccessToken, sendImageReply, sendTextReply } from "./feishu-
|
|
6
|
+
import { getTenantAccessToken, sendImageReply, sendTextReply } from "./feishu-platform.ts";
|
|
7
7
|
import { ts } from "./config.ts";
|
|
8
8
|
import { logTrace } from "./trace.ts";
|
|
9
9
|
import { readUtf8JsonBody } from "./agent-rpc-body.ts";
|
package/src/config.ts
CHANGED
|
@@ -454,6 +454,7 @@ export const config: AppConfig = loadConfig();
|
|
|
454
454
|
// (前提是导入端在函数体内读,不是在模块顶层读)。
|
|
455
455
|
|
|
456
456
|
export const USE_LOCAL = process.argv.includes("--local");
|
|
457
|
+
export const USE_SIMULATE = process.argv.includes("--simulate");
|
|
457
458
|
export let APP_ID = config.feishu.appId;
|
|
458
459
|
export let APP_SECRET = config.feishu.appSecret;
|
|
459
460
|
export const BASE_URL = "https://open.feishu.cn/open-apis";
|
package/src/feishu-api.ts
CHANGED
|
@@ -794,6 +794,33 @@ export async function sendCardReply(
|
|
|
794
794
|
}
|
|
795
795
|
}
|
|
796
796
|
|
|
797
|
+
export async function sendRawCard(
|
|
798
|
+
token: string,
|
|
799
|
+
chatId: string,
|
|
800
|
+
cardJson: string
|
|
801
|
+
): Promise<boolean> {
|
|
802
|
+
try {
|
|
803
|
+
const resp = await fetch(`${BASE_URL}/im/v1/messages?receive_id_type=chat_id`, {
|
|
804
|
+
method: "POST",
|
|
805
|
+
headers: {
|
|
806
|
+
Authorization: `Bearer ${token}`,
|
|
807
|
+
"Content-Type": "application/json",
|
|
808
|
+
},
|
|
809
|
+
body: JSON.stringify({ receive_id: chatId, msg_type: "interactive", content: cardJson }),
|
|
810
|
+
});
|
|
811
|
+
const data = (await resp.json().catch(() => ({}))) as { code: number; msg?: string; data?: { message_id?: string } };
|
|
812
|
+
if (data.code !== 0) {
|
|
813
|
+
console.error(`[${ts()}] [SEND] raw_card FAIL: chatId=${chatId} code=${data.code} msg="${data.msg ?? ""}"`);
|
|
814
|
+
return false;
|
|
815
|
+
}
|
|
816
|
+
console.log(`[${ts()}] [SEND] raw_card OK: chatId=${chatId} msgId=${data.data?.message_id ?? "N/A"}`);
|
|
817
|
+
return true;
|
|
818
|
+
} catch (err) {
|
|
819
|
+
console.error(`[${ts()}] [SEND] raw_card FAIL: chatId=${chatId} ${(err as Error).message}`);
|
|
820
|
+
return false;
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
|
|
797
824
|
// 重启后,向最后有发言的会话发送 "已重启" 卡片(基于 chat_logs 的文件修改时间)
|
|
798
825
|
export async function sendRestartCard(token: string): Promise<void> {
|
|
799
826
|
try {
|