chatccc 0.2.33 → 0.2.34
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-platform.test.ts +77 -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 +111 -49
- package/src/session.ts +1 -1
- package/src/sim-platform.ts +224 -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,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
|
+
});
|
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 {
|
|
@@ -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,14 @@ import {
|
|
|
79
81
|
sendRestartCard,
|
|
80
82
|
verifyAllPermissions,
|
|
81
83
|
reportPermissionResults,
|
|
82
|
-
|
|
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";
|
|
88
92
|
import { formatGitResult, gitResultHeaderTemplate, runGitCommand } from "./git-command.ts";
|
|
89
93
|
import {
|
|
90
94
|
MAX_PROCESSED,
|
|
@@ -251,6 +255,53 @@ function parseCardAction(data: unknown): CardActionResult | null {
|
|
|
251
255
|
|
|
252
256
|
let broadcastToRelay: (data: unknown) => void = () => {};
|
|
253
257
|
|
|
258
|
+
// ---------------------------------------------------------------------------
|
|
259
|
+
// Simulate mode: inject message via HTTP
|
|
260
|
+
// ---------------------------------------------------------------------------
|
|
261
|
+
|
|
262
|
+
async function handleSimInjectMessage(req: IncomingMessage, res: ServerResponse): Promise<boolean> {
|
|
263
|
+
const url = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
264
|
+
if (url.pathname !== "/api/sim/inject-message") return false;
|
|
265
|
+
if (req.method !== "POST") {
|
|
266
|
+
res.writeHead(405, { "Content-Type": "application/json; charset=utf-8" });
|
|
267
|
+
res.end(JSON.stringify({ ok: false, error: "Method not allowed, use POST" }));
|
|
268
|
+
return true;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const chunks: Buffer[] = [];
|
|
272
|
+
for await (const chunk of req) { chunks.push(Buffer.from(chunk)); }
|
|
273
|
+
const body = Buffer.concat(chunks).toString("utf-8");
|
|
274
|
+
let parsed: { text?: string; chat_id?: string; open_id?: string; chat_type?: string };
|
|
275
|
+
try { parsed = JSON.parse(body); } catch {
|
|
276
|
+
res.writeHead(400, { "Content-Type": "application/json; charset=utf-8" });
|
|
277
|
+
res.end(JSON.stringify({ ok: false, error: "Invalid JSON body" }));
|
|
278
|
+
return true;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const text = parsed.text;
|
|
282
|
+
if (!text) {
|
|
283
|
+
res.writeHead(400, { "Content-Type": "application/json; charset=utf-8" });
|
|
284
|
+
res.end(JSON.stringify({ ok: false, error: "Missing 'text' field" }));
|
|
285
|
+
return true;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const chatId = parsed.chat_id || SIM_DEFAULT_CHAT_ID;
|
|
289
|
+
const openId = parsed.open_id || "sim_user_001";
|
|
290
|
+
const chatType = parsed.chat_type || "group";
|
|
291
|
+
|
|
292
|
+
console.log(`[${ts()}] [SIM:INJECT] chat=${chatId} text="${text.slice(0, 80)}"`);
|
|
293
|
+
appendChatLog(chatId, openId, text);
|
|
294
|
+
|
|
295
|
+
// Fire and forget: process command, respond 202 immediately
|
|
296
|
+
handleCommand(text, chatId, openId, Date.now(), chatType).catch((err) =>
|
|
297
|
+
console.error(`[${ts()}] [SIM:INJECT] handleCommand error: ${(err as Error).message}`)
|
|
298
|
+
);
|
|
299
|
+
|
|
300
|
+
res.writeHead(202, { "Content-Type": "application/json; charset=utf-8" });
|
|
301
|
+
res.end(JSON.stringify({ ok: true, chat_id: chatId }));
|
|
302
|
+
return true;
|
|
303
|
+
}
|
|
304
|
+
|
|
254
305
|
// ---------------------------------------------------------------------------
|
|
255
306
|
// Command handler
|
|
256
307
|
// ---------------------------------------------------------------------------
|
|
@@ -351,17 +402,9 @@ async function handleCommand(text: string, chatId: string, openId: string, msgTi
|
|
|
351
402
|
// /cd 无参数:展示卡片(含最近使用路径按钮)
|
|
352
403
|
const recentDirs = await getRecentDirs();
|
|
353
404
|
const card = buildCdCard(targetDir, withStats, recentDirs, sessionCwd);
|
|
354
|
-
const
|
|
355
|
-
|
|
356
|
-
|
|
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 });
|
|
405
|
+
const ok = await sendRawCard(cdToken, chatId, card);
|
|
406
|
+
console.log(`[${ts()}] [CD] card sent, ok=${ok}, recentDirs=${recentDirs.length}`);
|
|
407
|
+
logTrace(tid, "DONE", { outcome: "cd_card", ok });
|
|
365
408
|
} else {
|
|
366
409
|
// /cd <path>:切换目录,发送文本卡片
|
|
367
410
|
const content = buildCdContent(targetDir, withStats, isUpdate, sessionCwd);
|
|
@@ -537,17 +580,9 @@ async function handleCommand(text: string, chatId: string, openId: string, msgTi
|
|
|
537
580
|
statusText.push(`**上下文 Token 数:** ~${status.lastContextTokens.toLocaleString()}`);
|
|
538
581
|
}
|
|
539
582
|
const card = buildStatusCard(statusText.join("\n"), isActive ? "blue" : "green");
|
|
540
|
-
const
|
|
541
|
-
|
|
542
|
-
|
|
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 });
|
|
583
|
+
const ok = await sendRawCard(freshToken, chatId, card);
|
|
584
|
+
console.log(`[${ts()}] [STATUS] card sent, ok=${ok}`);
|
|
585
|
+
logTrace(tid, "DONE", { outcome: "status", ok });
|
|
551
586
|
return;
|
|
552
587
|
}
|
|
553
588
|
|
|
@@ -564,17 +599,9 @@ async function handleCommand(text: string, chatId: string, openId: string, msgTi
|
|
|
564
599
|
tool: s.tool,
|
|
565
600
|
}));
|
|
566
601
|
const card = buildSessionsCard(cardData);
|
|
567
|
-
const
|
|
568
|
-
|
|
569
|
-
|
|
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 });
|
|
602
|
+
const ok = await sendRawCard(freshToken, chatId, card);
|
|
603
|
+
console.log(`[${ts()}] [SESSIONS] card sent, ok=${ok}, count=${allSessions.length}`);
|
|
604
|
+
logTrace(tid, "DONE", { outcome: "sessions", ok, count: allSessions.length });
|
|
578
605
|
return;
|
|
579
606
|
}
|
|
580
607
|
|
|
@@ -757,21 +784,13 @@ async function handleCommand(text: string, chatId: string, openId: string, msgTi
|
|
|
757
784
|
logTrace(tid, "SEND", { method: "help_card", chatId });
|
|
758
785
|
const replyToken = await getTenantAccessToken();
|
|
759
786
|
const card = buildHelpCard(text);
|
|
760
|
-
const
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
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 });
|
|
787
|
+
const ok = await sendRawCard(replyToken, chatId, card);
|
|
788
|
+
if (!ok) {
|
|
789
|
+
console.error(`[${ts()}] [SEND] help_card FAIL: chatId=${chatId}`);
|
|
790
|
+
logTrace(tid, "DONE", { outcome: "help_card_fail" });
|
|
772
791
|
} else {
|
|
773
|
-
console.log(`[${ts()}] [SEND] help_card OK: chatId=${chatId}
|
|
774
|
-
logTrace(tid, "DONE", { outcome: "help_card_sent"
|
|
792
|
+
console.log(`[${ts()}] [SEND] help_card OK: chatId=${chatId}`);
|
|
793
|
+
logTrace(tid, "DONE", { outcome: "help_card_sent" });
|
|
775
794
|
}
|
|
776
795
|
}
|
|
777
796
|
|
|
@@ -1053,6 +1072,49 @@ async function main(): Promise<void> {
|
|
|
1053
1072
|
// server.close)——只负责诊断与默认致命退出,不替代清理逻辑。
|
|
1054
1073
|
installCrashLogging({ flush: () => fileLog.flush() });
|
|
1055
1074
|
|
|
1075
|
+
// 模拟模式:独立端口 18079,不与 SDK 实例冲突,不走飞书凭证/权限/WSClient
|
|
1076
|
+
if (USE_SIMULATE) {
|
|
1077
|
+
const SIM_PORT = 18079;
|
|
1078
|
+
console.log("\n[Simulate] 模拟飞书环境模式");
|
|
1079
|
+
setPlatform(SimulatedPlatform);
|
|
1080
|
+
console.log(" 已切换到 SimulatedPlatform(零飞书依赖)");
|
|
1081
|
+
appendStartupTrace("main: simulate mode", { port: SIM_PORT });
|
|
1082
|
+
|
|
1083
|
+
setExtraApiHandler(async (req, res) => {
|
|
1084
|
+
const injected = await handleSimInjectMessage(req, res);
|
|
1085
|
+
if (injected) return true;
|
|
1086
|
+
return (await handleAgentGrantsRequest(req, res)) || (await handleAgentImageRequest(req, res)) || (await handleAgentFileRequest(req, res));
|
|
1087
|
+
});
|
|
1088
|
+
|
|
1089
|
+
const simServer = createServer(createUiRouter());
|
|
1090
|
+
await new Promise<void>((resolveListen, rejectListen) => {
|
|
1091
|
+
const onError = (err: NodeJS.ErrnoException): void => {
|
|
1092
|
+
simServer.removeListener("listening", onListening);
|
|
1093
|
+
rejectListen(err);
|
|
1094
|
+
};
|
|
1095
|
+
const onListening = (): void => {
|
|
1096
|
+
simServer.removeListener("error", onError);
|
|
1097
|
+
resolveListen();
|
|
1098
|
+
};
|
|
1099
|
+
simServer.once("error", onError);
|
|
1100
|
+
simServer.once("listening", onListening);
|
|
1101
|
+
simServer.listen(SIM_PORT, "127.0.0.1");
|
|
1102
|
+
}).catch((err: NodeJS.ErrnoException) => {
|
|
1103
|
+
console.error(`\n[启动] 监听失败:端口 ${SIM_PORT}(${err.code ?? "?"} — ${err.message})`);
|
|
1104
|
+
process.exit(1);
|
|
1105
|
+
});
|
|
1106
|
+
|
|
1107
|
+
console.log(`\n${"=".repeat(60)}`);
|
|
1108
|
+
console.log(` ChatCCC — 模拟飞书环境模式`);
|
|
1109
|
+
console.log(`${"=".repeat(60)}`);
|
|
1110
|
+
console.log(` 发送消息: POST http://127.0.0.1:${SIM_PORT}/api/sim/inject-message`);
|
|
1111
|
+
console.log(` 消息日志: ~/.chatccc/sim/messages.jsonl`);
|
|
1112
|
+
console.log(`${"=".repeat(60)}\n`);
|
|
1113
|
+
|
|
1114
|
+
installShutdownHandlers(simServer);
|
|
1115
|
+
return;
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1056
1118
|
if (Number.isNaN(CHATCCC_PORT) || CHATCCC_PORT < 1 || CHATCCC_PORT > 65535) {
|
|
1057
1119
|
console.error("\n[启动] 预检失败: config.json 的 port 字段不是有效端口号(1–65535)。");
|
|
1058
1120
|
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-
|
|
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,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sim-platform.ts — SimulatedPlatform: 零飞书依赖的本地模拟实现
|
|
3
|
+
*
|
|
4
|
+
* 所有飞书 API 调用都由本地逻辑替代:
|
|
5
|
+
* - 消息写入本地 JSONL(~/.chatccc/sim/messages.jsonl)
|
|
6
|
+
* - 群聊信息存在内存 Map 中
|
|
7
|
+
* - createGroupChat 生成 "sim_<uuid>" 格式的 chat_id
|
|
8
|
+
*
|
|
9
|
+
* 纯函数(extractSessionInfo / formatDelayNotice / reportPermissionResults)
|
|
10
|
+
* 直接从 feishu-api.ts re-export,不重新实现。
|
|
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
|
+
|
|
18
|
+
import {
|
|
19
|
+
extractSessionInfo as realExtractSessionInfo,
|
|
20
|
+
extractSessionId as realExtractSessionId,
|
|
21
|
+
formatDelayNotice as realFormatDelayNotice,
|
|
22
|
+
reportPermissionResults as realReportPermissionResults,
|
|
23
|
+
} from "./feishu-api.ts";
|
|
24
|
+
import type { FeishuPlatform } from "./feishu-platform.ts";
|
|
25
|
+
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
// 持久化路径
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
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
|
+
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
// 工具函数
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
|
|
57
|
+
function ts(): string {
|
|
58
|
+
return new Date().toISOString().replace("T", " ").slice(0, 19);
|
|
59
|
+
}
|
|
60
|
+
|
|
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
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
// SimulatedPlatform
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
|
|
75
|
+
export const SimulatedPlatform: FeishuPlatform = {
|
|
76
|
+
// ---- 认证 ----
|
|
77
|
+
async getTenantAccessToken() {
|
|
78
|
+
return "sim_token";
|
|
79
|
+
},
|
|
80
|
+
|
|
81
|
+
// ---- 消息发送:全部写本地 JSONL ----
|
|
82
|
+
async sendTextReply(_token, chatId, text) {
|
|
83
|
+
console.log(`[${ts()}] [SIM:SEND] text → ${chatId}: ${text.slice(0, 80)}`);
|
|
84
|
+
await appendJsonl({
|
|
85
|
+
direction: "send",
|
|
86
|
+
chat_id: chatId,
|
|
87
|
+
msg_type: "text",
|
|
88
|
+
content: text,
|
|
89
|
+
timestamp: Date.now(),
|
|
90
|
+
});
|
|
91
|
+
return true;
|
|
92
|
+
},
|
|
93
|
+
|
|
94
|
+
async sendCardReply(_token, chatId, title, content, _template) {
|
|
95
|
+
console.log(`[${ts()}] [SIM:SEND] card → ${chatId}: [${title}]`);
|
|
96
|
+
const text = `**[${title}]**\n${content}`;
|
|
97
|
+
await appendJsonl({
|
|
98
|
+
direction: "send",
|
|
99
|
+
chat_id: chatId,
|
|
100
|
+
msg_type: "card",
|
|
101
|
+
header: title,
|
|
102
|
+
content: content,
|
|
103
|
+
timestamp: Date.now(),
|
|
104
|
+
});
|
|
105
|
+
return true;
|
|
106
|
+
},
|
|
107
|
+
|
|
108
|
+
async sendImageReply(_token, chatId, imagePath) {
|
|
109
|
+
console.log(`[${ts()}] [SIM:SEND] image → ${chatId}: ${imagePath}`);
|
|
110
|
+
await appendJsonl({
|
|
111
|
+
direction: "send",
|
|
112
|
+
chat_id: chatId,
|
|
113
|
+
msg_type: "image",
|
|
114
|
+
image_path: imagePath,
|
|
115
|
+
timestamp: Date.now(),
|
|
116
|
+
});
|
|
117
|
+
return true;
|
|
118
|
+
},
|
|
119
|
+
|
|
120
|
+
async sendFileReply(_token, chatId, filePath) {
|
|
121
|
+
console.log(`[${ts()}] [SIM:SEND] file → ${chatId}: ${filePath}`);
|
|
122
|
+
await appendJsonl({
|
|
123
|
+
direction: "send",
|
|
124
|
+
chat_id: chatId,
|
|
125
|
+
msg_type: "file",
|
|
126
|
+
file_path: filePath,
|
|
127
|
+
timestamp: Date.now(),
|
|
128
|
+
});
|
|
129
|
+
return true;
|
|
130
|
+
},
|
|
131
|
+
|
|
132
|
+
async sendRawCard(_token, chatId, cardJson) {
|
|
133
|
+
console.log(`[${ts()}] [SIM:SEND] raw_card → ${chatId}: ${cardJson.slice(0, 80)}...`);
|
|
134
|
+
await appendJsonl({
|
|
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
|
+
});
|
|
141
|
+
return true;
|
|
142
|
+
},
|
|
143
|
+
|
|
144
|
+
// ---- 消息管理 ----
|
|
145
|
+
async addReaction(_token, _messageId, _emojiType) {
|
|
146
|
+
// 模拟模式不需要表情回应
|
|
147
|
+
},
|
|
148
|
+
|
|
149
|
+
async recallMessage(_token, _messageId) {
|
|
150
|
+
return true; // 假装撤回成功
|
|
151
|
+
},
|
|
152
|
+
|
|
153
|
+
async updateCardMessage(_token, _messageId, content) {
|
|
154
|
+
// 模拟模式:写更新记录到 JSONL
|
|
155
|
+
await appendJsonl({
|
|
156
|
+
type: "card_update",
|
|
157
|
+
message_id: _messageId,
|
|
158
|
+
content_preview: content.slice(0, 200),
|
|
159
|
+
timestamp: Date.now(),
|
|
160
|
+
});
|
|
161
|
+
return true;
|
|
162
|
+
},
|
|
163
|
+
|
|
164
|
+
// ---- 群聊管理 ----
|
|
165
|
+
async createGroupChat(_token, name, userIds) {
|
|
166
|
+
const chatId = `sim_${randomUUID().slice(0, 8)}`;
|
|
167
|
+
chats.set(chatId, { name, description: "Creating...", members: userIds });
|
|
168
|
+
console.log(`[${ts()}] [SIM:CHAT] created: ${chatId} name="${name}" members=${userIds.length}`);
|
|
169
|
+
return chatId;
|
|
170
|
+
},
|
|
171
|
+
|
|
172
|
+
async updateChatInfo(_token, chatId, name, description) {
|
|
173
|
+
const existing = chats.get(chatId);
|
|
174
|
+
if (existing) {
|
|
175
|
+
existing.name = name;
|
|
176
|
+
existing.description = description;
|
|
177
|
+
} else {
|
|
178
|
+
chats.set(chatId, { name, description, members: ["sim_user_001"] });
|
|
179
|
+
}
|
|
180
|
+
console.log(`[${ts()}] [SIM:CHAT] updated: ${chatId} name="${name}"`);
|
|
181
|
+
},
|
|
182
|
+
|
|
183
|
+
async getChatInfo(_token, chatId) {
|
|
184
|
+
const chat = chats.get(chatId);
|
|
185
|
+
if (!chat) throw new Error(`[99999] chat not found: ${chatId}`);
|
|
186
|
+
return { name: chat.name, description: chat.description };
|
|
187
|
+
},
|
|
188
|
+
|
|
189
|
+
// ---- 头像 ----
|
|
190
|
+
async setChatAvatar(_token, _chatId, _tool, _status) {
|
|
191
|
+
// 模拟模式不需要头像
|
|
192
|
+
},
|
|
193
|
+
|
|
194
|
+
// ---- 图片下载 ----
|
|
195
|
+
async getOrDownloadImage(_token, _messageId, fileKey) {
|
|
196
|
+
// 模拟模式下图片不需要从飞书下载,返回占位路径
|
|
197
|
+
return join(SIM_DIR, "images", fileKey);
|
|
198
|
+
},
|
|
199
|
+
|
|
200
|
+
// ---- 权限验证 ----
|
|
201
|
+
async verifyAllPermissions(_token) {
|
|
202
|
+
return [
|
|
203
|
+
{ scope: "im:chat", description: "模拟", ok: true, detail: "模拟模式跳过" },
|
|
204
|
+
{ scope: "im:message:send_as_bot", description: "模拟", ok: true, detail: "模拟模式跳过" },
|
|
205
|
+
{ scope: "im:message:reaction", description: "模拟", ok: true, detail: "模拟模式跳过" },
|
|
206
|
+
{ scope: "im:message", description: "模拟", ok: true, detail: "模拟模式跳过" },
|
|
207
|
+
{ scope: "cardkit:card", description: "模拟", ok: true, detail: "模拟模式跳过" },
|
|
208
|
+
];
|
|
209
|
+
},
|
|
210
|
+
|
|
211
|
+
// ---- 纯函数:直接从真实实现 re-export ----
|
|
212
|
+
extractSessionInfo: realExtractSessionInfo,
|
|
213
|
+
extractSessionId: realExtractSessionId,
|
|
214
|
+
formatDelayNotice: realFormatDelayNotice,
|
|
215
|
+
reportPermissionResults: realReportPermissionResults,
|
|
216
|
+
|
|
217
|
+
// ---- 其他 ----
|
|
218
|
+
async sendRestartCard(_token) {
|
|
219
|
+
// 模拟模式不需要重启通知
|
|
220
|
+
},
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
/** 模拟模式下的默认 chat_id */
|
|
224
|
+
export const SIM_DEFAULT_CHAT_ID = DEFAULT_CHAT_ID;
|