chatccc 0.2.4 → 0.2.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +39 -14
- package/package.json +1 -1
- package/src/__tests__/adapter-interface.test.ts +152 -0
- package/src/__tests__/cards.test.ts +122 -5
- package/src/__tests__/claude-adapter.test.ts +529 -0
- package/src/__tests__/cursor-adapter.test.ts +250 -0
- package/src/__tests__/session.test.ts +147 -1
- package/src/adapters/adapter-interface.ts +127 -0
- package/src/adapters/claude-adapter.ts +258 -0
- package/src/adapters/cursor-adapter.ts +229 -0
- package/src/cards.ts +121 -22
- package/src/config.ts +63 -2
- package/src/feishu-api.ts +22 -8
- package/src/index.ts +107 -29
- package/src/session.ts +513 -455
package/src/config.ts
CHANGED
|
@@ -62,10 +62,56 @@ export const CLAUDE_MODEL = process.env.CHATCCC_ANTHROPIC_MODEL?.trim() || "defa
|
|
|
62
62
|
|
|
63
63
|
export const CLAUDE_EFFORT = process.env.CHATCCC_ANTHROPIC_EFFORT?.trim() || "default";
|
|
64
64
|
|
|
65
|
+
/** 探测 cursor-agent 安装路径(优先环境变量,其次 LocalAppData,最后默认 agent) */
|
|
66
|
+
function detectCursorAgent(): string {
|
|
67
|
+
if (process.env.CHATCCC_CURSOR_COMMAND?.trim()) return process.env.CHATCCC_CURSOR_COMMAND.trim();
|
|
68
|
+
const localAppData = process.env.LOCALAPPDATA;
|
|
69
|
+
if (localAppData) {
|
|
70
|
+
const defaultPath = join(localAppData, "cursor-agent", "agent.cmd");
|
|
71
|
+
if (existsSync(defaultPath)) return defaultPath;
|
|
72
|
+
}
|
|
73
|
+
return "agent";
|
|
74
|
+
}
|
|
75
|
+
export const CURSOR_AGENT_COMMAND = detectCursorAgent();
|
|
76
|
+
/** Cursor agent 参数:-p 非交互模式,stream-json 流式 JSONL 输出 */
|
|
77
|
+
export const CURSOR_AGENT_ARGS = (process.env.CHATCCC_CURSOR_ARGS?.trim() || "-p --output-format stream-json --stream-partial-output").split(/\s+/).filter(Boolean);
|
|
78
|
+
|
|
65
79
|
// 新建会话的默认工作路径(/cd 命令设置,持久化到本地文件)
|
|
66
|
-
// 该路径仅影响通过 /new
|
|
80
|
+
// 该路径仅影响通过 /new 新建的会话,不影响已有会话的 resume。
|
|
67
81
|
export const DEFAULT_CWD_FILE = join(PROJECT_ROOT, ".claude", "working_dir.txt");
|
|
68
82
|
|
|
83
|
+
/** 会话工具类型持久化文件 */
|
|
84
|
+
export const SESSIONS_FILE = join(PROJECT_ROOT, ".claude", "sessions.json");
|
|
85
|
+
|
|
86
|
+
/** 最近成功新建会话的工作路径记录(最多 10 条) */
|
|
87
|
+
export const RECENT_DIRS_FILE = join(PROJECT_ROOT, ".claude", "recent_dirs.json");
|
|
88
|
+
export const MAX_RECENT_DIRS = 10;
|
|
89
|
+
|
|
90
|
+
/** 读取最近使用过的工作路径列表(最新的在前) */
|
|
91
|
+
export async function getRecentDirs(): Promise<string[]> {
|
|
92
|
+
try {
|
|
93
|
+
const raw = await readFile(RECENT_DIRS_FILE, "utf-8");
|
|
94
|
+
const arr = JSON.parse(raw);
|
|
95
|
+
if (Array.isArray(arr)) return arr.filter((d: unknown) => typeof d === "string");
|
|
96
|
+
} catch { /* file doesn't exist or corrupted */ }
|
|
97
|
+
return [];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** 添加一个路径到最近使用列表(去重、限制数量、最新的在前) */
|
|
101
|
+
export async function addRecentDir(dir: string): Promise<void> {
|
|
102
|
+
const dirs = await getRecentDirs();
|
|
103
|
+
const filtered = dirs.filter(d => d !== dir);
|
|
104
|
+
filtered.unshift(dir);
|
|
105
|
+
const trimmed = filtered.slice(0, MAX_RECENT_DIRS);
|
|
106
|
+
try {
|
|
107
|
+
await mkdir(dirname(RECENT_DIRS_FILE), { recursive: true });
|
|
108
|
+
await writeFile(RECENT_DIRS_FILE, JSON.stringify(trimmed, null, 2), "utf-8");
|
|
109
|
+
} catch (err) {
|
|
110
|
+
console.error(`[${ts()}] Failed to save recent_dirs.json: ${(err as Error).message}`);
|
|
111
|
+
fileLog.flush();
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
69
115
|
/** 读取 /cd 设置的默认工作路径。若文件不存在或路径已失效,回退到用户主目录。 */
|
|
70
116
|
export async function getDefaultCwd(): Promise<string> {
|
|
71
117
|
try {
|
|
@@ -224,4 +270,19 @@ export function explainMissingFeishuCredentialsAndExit(): never {
|
|
|
224
270
|
process.exit(1);
|
|
225
271
|
}
|
|
226
272
|
|
|
227
|
-
|
|
273
|
+
/** 群描述中用于识别 Claude Code 会话的前缀 */
|
|
274
|
+
export const CLAUDE_SESSION_PREFIX = "Claude Code Session:";
|
|
275
|
+
/** 群描述中用于识别 Cursor 会话的前缀 */
|
|
276
|
+
export const CURSOR_SESSION_PREFIX = "Cursor Session:";
|
|
277
|
+
|
|
278
|
+
/** 根据 tool 名称返回对应的群描述前缀 */
|
|
279
|
+
export function sessionPrefixForTool(tool: string): string {
|
|
280
|
+
if (tool === "cursor") return CURSOR_SESSION_PREFIX;
|
|
281
|
+
return CLAUDE_SESSION_PREFIX;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** 根据 tool 名称返回用于状态展示的标签 */
|
|
285
|
+
export function toolDisplayName(tool: string): string {
|
|
286
|
+
if (tool === "cursor") return "Cursor";
|
|
287
|
+
return "Claude Code";
|
|
288
|
+
}
|
package/src/feishu-api.ts
CHANGED
|
@@ -7,7 +7,8 @@ import {
|
|
|
7
7
|
BASE_URL,
|
|
8
8
|
CHAT_LOGS_DIR,
|
|
9
9
|
PROJECT_ROOT,
|
|
10
|
-
|
|
10
|
+
CLAUDE_SESSION_PREFIX,
|
|
11
|
+
CURSOR_SESSION_PREFIX,
|
|
11
12
|
ts,
|
|
12
13
|
} from "./config.ts";
|
|
13
14
|
import { buildButtons } from "./cards.ts";
|
|
@@ -219,12 +220,24 @@ export async function getChatInfo(
|
|
|
219
220
|
};
|
|
220
221
|
}
|
|
221
222
|
|
|
223
|
+
export function extractSessionInfo(description: string): { sessionId: string; tool: string } | null {
|
|
224
|
+
const PREFIXES: Array<{ prefix: string; tool: string }> = [
|
|
225
|
+
{ prefix: CLAUDE_SESSION_PREFIX, tool: "claude" },
|
|
226
|
+
{ prefix: CURSOR_SESSION_PREFIX, tool: "cursor" },
|
|
227
|
+
];
|
|
228
|
+
for (const { prefix, tool } of PREFIXES) {
|
|
229
|
+
const idx = description.indexOf(prefix);
|
|
230
|
+
if (idx === -1) continue;
|
|
231
|
+
const after = description.slice(idx + prefix.length).trim();
|
|
232
|
+
const match = after.match(/^([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/i);
|
|
233
|
+
if (match) return { sessionId: match[1], tool };
|
|
234
|
+
}
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** @deprecated 使用 extractSessionInfo 代替 */
|
|
222
239
|
export function extractSessionId(description: string): string | null {
|
|
223
|
-
|
|
224
|
-
if (idx === -1) return null;
|
|
225
|
-
const after = description.slice(idx + SESSION_DESC_PREFIX.length).trim();
|
|
226
|
-
const match = after.match(/^([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/i);
|
|
227
|
-
return match ? match[1] : null;
|
|
240
|
+
return extractSessionInfo(description)?.sessionId ?? null;
|
|
228
241
|
}
|
|
229
242
|
|
|
230
243
|
// ---------------------------------------------------------------------------
|
|
@@ -325,9 +338,10 @@ export async function sendRestartCard(token: string): Promise<void> {
|
|
|
325
338
|
config: { wide_screen_mode: true },
|
|
326
339
|
header: { template: "green", title: { content: "ChatCCC Started", tag: "plain_text" } },
|
|
327
340
|
elements: [
|
|
328
|
-
{ tag: "div", text: { tag: "lark_md", content: "Bot 已启动完成,可以继续使用。\n\n
|
|
341
|
+
{ tag: "div", text: { tag: "lark_md", content: "Bot 已启动完成,可以继续使用。\n\n使用 **/new**(默认 Claude Code)或 **/new claude** / **/new cursor** 创建新会话" } },
|
|
329
342
|
buildButtons([
|
|
330
|
-
{ text: "
|
|
343
|
+
{ text: "新建 Claude Code 会话(/new claude)", value: JSON.stringify({ cmd: "new" }), type: "primary" },
|
|
344
|
+
{ text: "新建 Cursor 会话(/new cursor)", value: JSON.stringify({ cmd: "new cursor" }), type: "primary" },
|
|
331
345
|
{ text: "重启Chat CCC(/restart)", value: JSON.stringify({ cmd: "restart" }), type: "danger" },
|
|
332
346
|
{ text: "查看/切换工作路径(/cd)", value: JSON.stringify({ cmd: "cd" }), type: "default" },
|
|
333
347
|
]),
|
package/src/index.ts
CHANGED
|
@@ -22,7 +22,6 @@
|
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
24
|
import { spawn } from "node:child_process";
|
|
25
|
-
import { getSessionInfo } from "@anthropic-ai/claude-agent-sdk";
|
|
26
25
|
import { readdir, stat } from "node:fs/promises";
|
|
27
26
|
import { resolve, dirname, basename } from "node:path";
|
|
28
27
|
|
|
@@ -49,13 +48,17 @@ import {
|
|
|
49
48
|
getDefaultCwd,
|
|
50
49
|
maskAppId,
|
|
51
50
|
setDefaultCwd,
|
|
51
|
+
getRecentDirs,
|
|
52
|
+
addRecentDir,
|
|
53
|
+
sessionPrefixForTool,
|
|
54
|
+
toolDisplayName,
|
|
52
55
|
ts,
|
|
53
56
|
} from "./config.ts";
|
|
54
57
|
import { printServiceDidNotStart, printServiceRunningHint } from "./exit-banner.ts";
|
|
55
58
|
import {
|
|
56
59
|
addReaction,
|
|
57
60
|
createGroupChat,
|
|
58
|
-
|
|
61
|
+
extractSessionInfo,
|
|
59
62
|
getChatInfo,
|
|
60
63
|
getTenantAccessToken,
|
|
61
64
|
recallMessage,
|
|
@@ -67,7 +70,7 @@ import {
|
|
|
67
70
|
verifyAllPermissions,
|
|
68
71
|
reportPermissionResults,
|
|
69
72
|
} from "./feishu-api.ts";
|
|
70
|
-
import { buildHelpCard, buildStatusCard, buildProgressCard, buildCdContent, buildSessionsCard } from "./cards.ts";
|
|
73
|
+
import { buildHelpCard, buildStatusCard, buildProgressCard, buildCdContent, buildCdCard, buildSessionsCard } from "./cards.ts";
|
|
71
74
|
import { updateCardKitCard } from "./cardkit.ts";
|
|
72
75
|
import {
|
|
73
76
|
MAX_PROCESSED,
|
|
@@ -79,6 +82,7 @@ import {
|
|
|
79
82
|
resetState,
|
|
80
83
|
resumeAndPrompt,
|
|
81
84
|
sessionInfoMap,
|
|
85
|
+
getAdapterForTool,
|
|
82
86
|
} from "./session.ts";
|
|
83
87
|
|
|
84
88
|
// ---------------------------------------------------------------------------
|
|
@@ -96,7 +100,11 @@ function getInnerEvent(data: Evt): InnerEvent {
|
|
|
96
100
|
return (data.event ?? data) as InnerEvent;
|
|
97
101
|
}
|
|
98
102
|
|
|
99
|
-
|
|
103
|
+
/**
|
|
104
|
+
* 将飞书消息的原始 content JSON 结构转成可读文本,保留代码块等结构信息。
|
|
105
|
+
* 未知类型直接返回 JSON 原文,让 AI 自行理解。
|
|
106
|
+
*/
|
|
107
|
+
function formatMessageContent(message: { message_type?: string; content?: string }): string {
|
|
100
108
|
const contentStr = message.content ?? "{}";
|
|
101
109
|
let content: Record<string, unknown>;
|
|
102
110
|
try { content = JSON.parse(contentStr); } catch { return ""; }
|
|
@@ -108,7 +116,36 @@ function extractText(message: { message_type?: string; content?: string }): stri
|
|
|
108
116
|
text = text.replace(/ /gi, " ");
|
|
109
117
|
return text.trim();
|
|
110
118
|
}
|
|
111
|
-
|
|
119
|
+
|
|
120
|
+
if (message.message_type === "post") {
|
|
121
|
+
return formatPostContent(content);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// 其他类型(image, file, audio, media, sticker)直接给原始 JSON
|
|
125
|
+
return contentStr;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function formatPostContent(content: Record<string, unknown>): string {
|
|
129
|
+
const paragraphs = content.content as unknown[][];
|
|
130
|
+
if (!Array.isArray(paragraphs)) return "";
|
|
131
|
+
|
|
132
|
+
const parts: string[] = [];
|
|
133
|
+
for (const line of paragraphs) {
|
|
134
|
+
if (!Array.isArray(line)) continue;
|
|
135
|
+
for (const elem of line) {
|
|
136
|
+
const el = elem as Record<string, unknown>;
|
|
137
|
+
if (!el || typeof el !== "object") continue;
|
|
138
|
+
const t = typeof el.text === "string" ? el.text : "";
|
|
139
|
+
|
|
140
|
+
if (el.tag === "code_block") {
|
|
141
|
+
const lang = typeof el.language === "string" ? el.language : "";
|
|
142
|
+
parts.push("```" + lang + "\n" + t + "\n```");
|
|
143
|
+
} else if (el.tag === "p" || el.tag === "text") {
|
|
144
|
+
if (t) parts.push(t);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return parts.join("\n").trim();
|
|
112
149
|
}
|
|
113
150
|
|
|
114
151
|
// ---------------------------------------------------------------------------
|
|
@@ -138,8 +175,12 @@ function parseCardAction(data: unknown): CardActionResult | null {
|
|
|
138
175
|
}
|
|
139
176
|
if (!cmd) return null;
|
|
140
177
|
|
|
141
|
-
const CMD_MAP: Record<string, string> = { stop: "/stop", new: "/new", restart: "/restart", status: "/status", cd: "/cd", sessions: "/sessions" };
|
|
142
|
-
|
|
178
|
+
const CMD_MAP: Record<string, string> = { stop: "/stop", new: "/new", "new cursor": "/new cursor", restart: "/restart", status: "/status", cd: "/cd", sessions: "/sessions" };
|
|
179
|
+
let text = CMD_MAP[cmd] ?? "";
|
|
180
|
+
if (cmd === "cd" && typeof action.value === "object" && action.value !== null) {
|
|
181
|
+
const path = (action.value as Record<string, string>).path;
|
|
182
|
+
if (path) text = `/cd ${path}`;
|
|
183
|
+
}
|
|
143
184
|
if (!text) return null;
|
|
144
185
|
|
|
145
186
|
const chatId =
|
|
@@ -188,9 +229,10 @@ async function handleCommand(text: string, chatId: string, openId: string, msgTi
|
|
|
188
229
|
let sessionCwd: string | undefined;
|
|
189
230
|
try {
|
|
190
231
|
const chatInfo = await getChatInfo(cdToken, chatId);
|
|
191
|
-
const
|
|
192
|
-
if (
|
|
193
|
-
const
|
|
232
|
+
const sessionInfoResult = extractSessionInfo(chatInfo.description);
|
|
233
|
+
if (sessionInfoResult) {
|
|
234
|
+
const adapter = getAdapterForTool(sessionInfoResult.tool);
|
|
235
|
+
const info = await adapter.getSessionInfo(sessionInfoResult.sessionId);
|
|
194
236
|
sessionCwd = info?.cwd;
|
|
195
237
|
}
|
|
196
238
|
} catch { /* 非会话群或获取失败,不显示 */ }
|
|
@@ -223,6 +265,7 @@ async function handleCommand(text: string, chatId: string, openId: string, msgTi
|
|
|
223
265
|
const isUpdate = !!arg && targetDir !== currentDir;
|
|
224
266
|
if (isUpdate) {
|
|
225
267
|
await setDefaultCwd(targetDir);
|
|
268
|
+
await addRecentDir(targetDir);
|
|
226
269
|
}
|
|
227
270
|
|
|
228
271
|
// Read directory entries
|
|
@@ -247,12 +290,39 @@ async function handleCommand(text: string, chatId: string, openId: string, msgTi
|
|
|
247
290
|
return a.name.localeCompare(b.name);
|
|
248
291
|
});
|
|
249
292
|
|
|
250
|
-
|
|
251
|
-
|
|
293
|
+
if (!arg) {
|
|
294
|
+
// /cd 无参数:展示卡片(含最近使用路径按钮)
|
|
295
|
+
const recentDirs = await getRecentDirs();
|
|
296
|
+
const card = buildCdCard(targetDir, withStats, recentDirs, sessionCwd);
|
|
297
|
+
const resp = await fetch(`${BASE_URL}/im/v1/messages?receive_id_type=chat_id`, {
|
|
298
|
+
method: "POST",
|
|
299
|
+
headers: {
|
|
300
|
+
Authorization: `Bearer ${cdToken}`,
|
|
301
|
+
"Content-Type": "application/json",
|
|
302
|
+
},
|
|
303
|
+
body: JSON.stringify({ receive_id: chatId, msg_type: "interactive", content: card }),
|
|
304
|
+
});
|
|
305
|
+
const respData: Record<string, any> = await resp.json().catch(() => ({}));
|
|
306
|
+
console.log(`[${ts()}] [CD] card sent, code=${respData.code}, msgId=${respData.data?.message_id ?? "N/A"}, recentDirs=${recentDirs.length}`);
|
|
307
|
+
} else {
|
|
308
|
+
// /cd <path>:切换目录,发送文本卡片
|
|
309
|
+
const content = buildCdContent(targetDir, withStats, isUpdate, sessionCwd);
|
|
310
|
+
await sendCardReply(cdToken, chatId, "新会话工作路径", content, "blue");
|
|
311
|
+
}
|
|
252
312
|
return;
|
|
253
313
|
}
|
|
254
314
|
|
|
255
|
-
if (text === "/new") {
|
|
315
|
+
if (text === "/new" || text.startsWith("/new ")) {
|
|
316
|
+
const toolArg = text.slice(5).trim();
|
|
317
|
+
const tool = toolArg || "claude";
|
|
318
|
+
const validTools = ["claude", "cursor"];
|
|
319
|
+
if (!validTools.includes(tool)) {
|
|
320
|
+
const warnToken = await getTenantAccessToken();
|
|
321
|
+
await sendCardReply(warnToken, chatId, "Error", `未知的工具类型: "${toolArg}"。支持: claude (Claude Code), cursor (Cursor)。`, "red");
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
const toolLabel = toolDisplayName(tool);
|
|
325
|
+
|
|
256
326
|
if (!openId) {
|
|
257
327
|
console.log(`[${ts()}] [WARN] Cannot get sender open_id`);
|
|
258
328
|
const warnToken = await getTenantAccessToken();
|
|
@@ -264,13 +334,13 @@ async function handleCommand(text: string, chatId: string, openId: string, msgTi
|
|
|
264
334
|
|
|
265
335
|
let sessionId: string;
|
|
266
336
|
try {
|
|
267
|
-
sessionId = await initClaudeSession();
|
|
268
|
-
console.log(`[${ts()}] [STEP 1/4]
|
|
337
|
+
sessionId = await initClaudeSession(tool);
|
|
338
|
+
console.log(`[${ts()}] [STEP 1/4] ${toolLabel} session created: ${sessionId} → OK`);
|
|
269
339
|
} catch (err) {
|
|
270
340
|
console.error(`[${ts()}] [STEP 1/4] FAIL: ${(err as Error).message}`);
|
|
271
341
|
await sendCardReply(
|
|
272
342
|
freshToken, chatId, "Error",
|
|
273
|
-
`Failed to initialize
|
|
343
|
+
`Failed to initialize ${toolLabel} session:\n${(err as Error).message}`,
|
|
274
344
|
"red"
|
|
275
345
|
);
|
|
276
346
|
return;
|
|
@@ -288,8 +358,9 @@ async function handleCommand(text: string, chatId: string, openId: string, msgTi
|
|
|
288
358
|
|
|
289
359
|
try {
|
|
290
360
|
const initialName = `新会话-${sessionId}`;
|
|
291
|
-
|
|
292
|
-
|
|
361
|
+
const descPrefix = sessionPrefixForTool(tool);
|
|
362
|
+
await updateChatInfo(freshToken, newChatId, initialName, `${descPrefix} ${sessionId}`);
|
|
363
|
+
console.log(`[${ts()}] [STEP 3/4] Renamed group → name="${initialName}" (${toolLabel}) → OK`);
|
|
293
364
|
} catch (err) {
|
|
294
365
|
console.error(`[${ts()}] [STEP 3/4] FAIL: ${(err as Error).message}`);
|
|
295
366
|
await sendCardReply(freshToken, chatId, "Error", `Group created but rename failed:\n${(err as Error).message}`, "yellow");
|
|
@@ -297,9 +368,10 @@ async function handleCommand(text: string, chatId: string, openId: string, msgTi
|
|
|
297
368
|
}
|
|
298
369
|
|
|
299
370
|
const cwd = await getDefaultCwd();
|
|
371
|
+
const adapter = getAdapterForTool(tool);
|
|
300
372
|
await sendCardReply(
|
|
301
|
-
freshToken, newChatId,
|
|
302
|
-
`群聊已创建,这是你的
|
|
373
|
+
freshToken, newChatId, `${toolLabel} Session Ready`,
|
|
374
|
+
`群聊已创建,这是你的 **${toolLabel}** 会话群。\n\n**Session ID:** ${sessionId}\n**工作目录:** \`${cwd}\`\n\n直接在这里发消息即可与 ${toolLabel} 对话。\n\n发送 **/sessions** 查看所有会话状态。`,
|
|
303
375
|
"green"
|
|
304
376
|
);
|
|
305
377
|
|
|
@@ -312,10 +384,13 @@ async function handleCommand(text: string, chatId: string, openId: string, msgTi
|
|
|
312
384
|
const token = await getTenantAccessToken();
|
|
313
385
|
const chatInfo = await getChatInfo(token, chatId);
|
|
314
386
|
const description = chatInfo.description;
|
|
315
|
-
const
|
|
387
|
+
const sessionInfo = extractSessionInfo(description);
|
|
316
388
|
|
|
317
|
-
if (
|
|
318
|
-
|
|
389
|
+
if (sessionInfo) {
|
|
390
|
+
const sessionId = sessionInfo.sessionId;
|
|
391
|
+
const descriptionTool = sessionInfo.tool;
|
|
392
|
+
const toolLabel = toolDisplayName(descriptionTool);
|
|
393
|
+
console.log(`[${ts()}] [RESUME] ${toolLabel} session group detected, session=${sessionId} tool=${descriptionTool}`);
|
|
319
394
|
|
|
320
395
|
const freshToken = await getTenantAccessToken();
|
|
321
396
|
|
|
@@ -351,6 +426,7 @@ async function handleCommand(text: string, chatId: string, openId: string, msgTi
|
|
|
351
426
|
const isActive = running && !running.stopped;
|
|
352
427
|
const statusText = [
|
|
353
428
|
`**Session ID:** \`${status?.sessionId ?? sessionId}\``,
|
|
429
|
+
`**工具:** ${toolLabel}`,
|
|
354
430
|
`**状态:** ${isActive ? "🟢 运行中" : "⚪ 空闲"}`,
|
|
355
431
|
`**已对话轮数:** ${status?.turnCount ?? 0}`,
|
|
356
432
|
`**模型:** ${status?.model ?? anthropicConfigDisplay(CLAUDE_MODEL)}`,
|
|
@@ -389,6 +465,7 @@ async function handleCommand(text: string, chatId: string, openId: string, msgTi
|
|
|
389
465
|
turnCount: s.turnCount,
|
|
390
466
|
elapsedSeconds: s.active ? Math.floor((now - s.startTime) / 1000) : null,
|
|
391
467
|
model: s.model,
|
|
468
|
+
tool: s.tool,
|
|
392
469
|
}));
|
|
393
470
|
const card = buildSessionsCard(cardData);
|
|
394
471
|
const sessionsResp = await fetch(`${BASE_URL}/im/v1/messages?receive_id_type=chat_id`, {
|
|
@@ -433,13 +510,14 @@ async function handleCommand(text: string, chatId: string, openId: string, msgTi
|
|
|
433
510
|
}
|
|
434
511
|
|
|
435
512
|
try {
|
|
436
|
-
await resumeAndPrompt(sessionId, text, freshToken, chatId, msgTimestamp);
|
|
513
|
+
await resumeAndPrompt(sessionId, text, freshToken, chatId, msgTimestamp, descriptionTool);
|
|
437
514
|
console.log(`[${ts()}] [RESUME] Session ${sessionId} done`);
|
|
438
515
|
} catch (err) {
|
|
439
516
|
console.error(`[${ts()}] [RESUME] FAIL: ${(err as Error).message}`);
|
|
517
|
+
fileLog.flush();
|
|
440
518
|
await sendCardReply(
|
|
441
519
|
freshToken, chatId, "Error",
|
|
442
|
-
`Failed to resume
|
|
520
|
+
`Failed to resume ${toolLabel} session:\n${(err as Error).message}`,
|
|
443
521
|
"red"
|
|
444
522
|
);
|
|
445
523
|
}
|
|
@@ -579,7 +657,7 @@ async function main(): Promise<void> {
|
|
|
579
657
|
}
|
|
580
658
|
}
|
|
581
659
|
|
|
582
|
-
const text =
|
|
660
|
+
const text = formatMessageContent(message);
|
|
583
661
|
const sender = event.sender;
|
|
584
662
|
const openId = sender?.sender_id?.open_id ?? "";
|
|
585
663
|
const chatId = message.chat_id ?? "";
|
|
@@ -671,7 +749,7 @@ async function main(): Promise<void> {
|
|
|
671
749
|
const event = getInnerEvent(data);
|
|
672
750
|
const message = event.message;
|
|
673
751
|
if (!message) return;
|
|
674
|
-
const text =
|
|
752
|
+
const text = formatMessageContent(message);
|
|
675
753
|
const openId = event.sender?.sender_id?.open_id ?? "";
|
|
676
754
|
const chatId = message.chat_id ?? "";
|
|
677
755
|
appendChatLog(chatId, openId, text);
|
|
@@ -697,8 +775,8 @@ async function main(): Promise<void> {
|
|
|
697
775
|
const wsClient = new WSClient({
|
|
698
776
|
appId: APP_ID,
|
|
699
777
|
appSecret: APP_SECRET,
|
|
700
|
-
onReady: () => resetState(),
|
|
701
|
-
onReconnected: () => resetState(),
|
|
778
|
+
onReady: () => { resetState(); },
|
|
779
|
+
onReconnected: () => { resetState(); },
|
|
702
780
|
});
|
|
703
781
|
|
|
704
782
|
console.log(`\n[启动 6/7] 飞书长连接:正在通过 SDK 建立 WebSocket …`);
|