chatccc 0.2.10 → 0.2.12
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 +63 -11
- package/package.json +4 -1
- package/src/__tests__/cards.test.ts +1 -1
- package/src/__tests__/codex-adapter.test.ts +305 -0
- package/src/__tests__/fixtures/codex_simple_text.jsonl +4 -0
- package/src/__tests__/fixtures/codex_with_tool.jsonl +6 -0
- package/src/adapters/codex-adapter.ts +294 -0
- package/src/adapters/codex-session-meta-store.ts +131 -0
- package/src/cards.ts +28 -7
- package/src/config.ts +4 -0
- package/src/feishu-api.ts +115 -16
- package/src/index.ts +98 -20
- package/src/session.ts +28 -5
package/src/feishu-api.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import { readdir, stat } from "node:fs/promises";
|
|
1
|
+
import { readdir, stat, readFile } from "node:fs/promises";
|
|
2
|
+
import { resolve as resolvePath } from "node:path";
|
|
2
3
|
import { join } from "node:path";
|
|
4
|
+
import sharp from "sharp";
|
|
3
5
|
|
|
4
6
|
import {
|
|
5
7
|
APP_ID,
|
|
@@ -9,9 +11,10 @@ import {
|
|
|
9
11
|
PROJECT_ROOT,
|
|
10
12
|
CLAUDE_SESSION_PREFIX,
|
|
11
13
|
CURSOR_SESSION_PREFIX,
|
|
14
|
+
CODEX_SESSION_PREFIX,
|
|
12
15
|
ts,
|
|
13
16
|
} from "./config.ts";
|
|
14
|
-
import {
|
|
17
|
+
import { buildHelpCard } from "./cards.ts";
|
|
15
18
|
|
|
16
19
|
// ---------------------------------------------------------------------------
|
|
17
20
|
// Auth
|
|
@@ -78,6 +81,18 @@ const REQUIRED_PERMISSIONS: PermissionDef[] = [
|
|
|
78
81
|
path: "/im/v1/messages/om_000000000000000000000000",
|
|
79
82
|
body: JSON.stringify({ content: JSON.stringify({ elements: [{ tag: "markdown", content: " " }] }) }),
|
|
80
83
|
},
|
|
84
|
+
{
|
|
85
|
+
scope: "cardkit:card",
|
|
86
|
+
description: "创建/更新群卡片(流式进度卡片、状态卡片等)",
|
|
87
|
+
method: "POST",
|
|
88
|
+
path: "/cardkit/v1/cards",
|
|
89
|
+
body: JSON.stringify({
|
|
90
|
+
schema: "2.0",
|
|
91
|
+
config: { update_multi: true },
|
|
92
|
+
header: { template: "blue", title: { tag: "plain_text", content: "permcheck" } },
|
|
93
|
+
body: { direction: "vertical", elements: [{ tag: "markdown", content: " " }] },
|
|
94
|
+
}),
|
|
95
|
+
},
|
|
81
96
|
];
|
|
82
97
|
|
|
83
98
|
interface PermissionResult {
|
|
@@ -224,6 +239,7 @@ export function extractSessionInfo(description: string): { sessionId: string; to
|
|
|
224
239
|
const PREFIXES: Array<{ prefix: string; tool: string }> = [
|
|
225
240
|
{ prefix: CLAUDE_SESSION_PREFIX, tool: "claude" },
|
|
226
241
|
{ prefix: CURSOR_SESSION_PREFIX, tool: "cursor" },
|
|
242
|
+
{ prefix: CODEX_SESSION_PREFIX, tool: "codex" },
|
|
227
243
|
];
|
|
228
244
|
for (const { prefix, tool } of PREFIXES) {
|
|
229
245
|
const idx = description.indexOf(prefix);
|
|
@@ -240,6 +256,101 @@ export function extractSessionId(description: string): string | null {
|
|
|
240
256
|
return extractSessionInfo(description)?.sessionId ?? null;
|
|
241
257
|
}
|
|
242
258
|
|
|
259
|
+
// ---------------------------------------------------------------------------
|
|
260
|
+
// Avatar
|
|
261
|
+
// ---------------------------------------------------------------------------
|
|
262
|
+
|
|
263
|
+
const AVATAR_DIR = resolvePath(PROJECT_ROOT, "images", "avatars");
|
|
264
|
+
const AVATAR_SOURCES: Record<string, string> = {
|
|
265
|
+
new: process.env.CHATCCC_AVATAR_NEW_URL?.trim() || resolvePath(AVATAR_DIR, "status_new.png"),
|
|
266
|
+
busy: process.env.CHATCCC_AVATAR_BUSY_URL?.trim() || resolvePath(AVATAR_DIR, "status_busy.png"),
|
|
267
|
+
idle: process.env.CHATCCC_AVATAR_IDLE_URL?.trim() || resolvePath(AVATAR_DIR, "status_idle.png"),
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
const avatarKeyCache = new Map<string, string>();
|
|
271
|
+
|
|
272
|
+
function isHttpUrl(source: string): boolean {
|
|
273
|
+
return /^https?:\/\//i.test(source);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
async function loadAvatarSource(source: string): Promise<{ buffer: Buffer; contentType: string; filename: string }> {
|
|
277
|
+
let input: Buffer;
|
|
278
|
+
if (isHttpUrl(source)) {
|
|
279
|
+
const resp = await fetch(source);
|
|
280
|
+
if (!resp.ok) throw new Error(`download avatar url failed: status=${resp.status}`);
|
|
281
|
+
input = Buffer.from(await resp.arrayBuffer());
|
|
282
|
+
} else {
|
|
283
|
+
input = await readFile(source);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const jpeg = await sharp(input)
|
|
287
|
+
.resize(256, 256, { fit: "cover", position: "center" })
|
|
288
|
+
.flatten({ background: "#ffffff" })
|
|
289
|
+
.removeAlpha()
|
|
290
|
+
.jpeg({ quality: 95, progressive: false })
|
|
291
|
+
.toBuffer();
|
|
292
|
+
|
|
293
|
+
return {
|
|
294
|
+
buffer: jpeg,
|
|
295
|
+
contentType: "image/jpeg",
|
|
296
|
+
filename: "avatar.jpg",
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
async function uploadImage(token: string, source: string): Promise<string> {
|
|
301
|
+
const image = await loadAvatarSource(source);
|
|
302
|
+
const blob = new Blob([new Uint8Array(image.buffer)], { type: image.contentType });
|
|
303
|
+
const form = new FormData();
|
|
304
|
+
form.append("image_type", "avatar");
|
|
305
|
+
form.append("image", blob, image.filename);
|
|
306
|
+
|
|
307
|
+
// Group avatars need an im/v1 image_key uploaded with image_type=avatar.
|
|
308
|
+
const resp = await fetch(`${BASE_URL}/im/v1/images`, {
|
|
309
|
+
method: "POST",
|
|
310
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
311
|
+
body: form,
|
|
312
|
+
});
|
|
313
|
+
const text = await resp.text();
|
|
314
|
+
let data: { code: number; msg?: string; data?: { image_key?: string } };
|
|
315
|
+
try { data = JSON.parse(text); } catch {
|
|
316
|
+
throw new Error(`uploadImage non-JSON response: ${text.slice(0, 200)}`);
|
|
317
|
+
}
|
|
318
|
+
if (data.code !== 0) throw new Error(`[${data.code}] ${data.msg}`);
|
|
319
|
+
return data.data!.image_key!;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
async function getOrUploadAvatarKey(token: string, status: string): Promise<string> {
|
|
323
|
+
const cached = avatarKeyCache.get(status);
|
|
324
|
+
if (cached) return cached;
|
|
325
|
+
const key = await uploadImage(token, AVATAR_SOURCES[status]);
|
|
326
|
+
avatarKeyCache.set(status, key);
|
|
327
|
+
console.log(`[${ts()}] [AVATAR] Uploaded "${status}" → image_key=${key}`);
|
|
328
|
+
return key;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
export async function setChatAvatar(token: string, chatId: string, status: string): Promise<void> {
|
|
332
|
+
try {
|
|
333
|
+
const avatarKey = await getOrUploadAvatarKey(token, status);
|
|
334
|
+
const resp = await fetch(`${BASE_URL}/im/v1/chats/${chatId}`, {
|
|
335
|
+
method: "PUT",
|
|
336
|
+
headers: {
|
|
337
|
+
Authorization: `Bearer ${token}`,
|
|
338
|
+
"Content-Type": "application/json",
|
|
339
|
+
},
|
|
340
|
+
body: JSON.stringify({ avatar: avatarKey }),
|
|
341
|
+
});
|
|
342
|
+
const text = await resp.text();
|
|
343
|
+
let data: { code: number; msg?: string };
|
|
344
|
+
try { data = JSON.parse(text); } catch {
|
|
345
|
+
if (resp.ok) return;
|
|
346
|
+
throw new Error(`setChatAvatar non-JSON response (status=${resp.status}): ${text.slice(0, 200)}`);
|
|
347
|
+
}
|
|
348
|
+
if (data.code !== 0) throw new Error(`[${data.code}] ${data.msg}`);
|
|
349
|
+
} catch (err) {
|
|
350
|
+
console.error(`[${ts()}] [AVATAR] setChatAvatar FAIL: chatId=${chatId} status=${status} ${(err as Error).message}`);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
243
354
|
// ---------------------------------------------------------------------------
|
|
244
355
|
// Messaging
|
|
245
356
|
// ---------------------------------------------------------------------------
|
|
@@ -334,19 +445,7 @@ export async function sendRestartCard(token: string): Promise<void> {
|
|
|
334
445
|
|
|
335
446
|
console.log(`[${ts()}] [RESTART] Latest active chat: ${latestChatId} (mtime=${new Date(latestTime).toISOString()})`);
|
|
336
447
|
|
|
337
|
-
const restartCard =
|
|
338
|
-
config: { wide_screen_mode: true },
|
|
339
|
-
header: { template: "green", title: { content: "ChatCCC Started", tag: "plain_text" } },
|
|
340
|
-
elements: [
|
|
341
|
-
{ tag: "div", text: { tag: "lark_md", content: "Bot 已启动完成,可以继续使用。\n\n发送 **/new** 创建新会话(默认 Claude Code)\n发送 **/new claude** 创建新 Claude 对话\n发送 **/new cursor** 创建新 Cursor 会话(启动需要多等几秒)" } },
|
|
342
|
-
buildButtons([
|
|
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" },
|
|
345
|
-
{ text: "重启Chat CCC(/restart)", value: JSON.stringify({ cmd: "restart" }), type: "danger" },
|
|
346
|
-
{ text: "切换工作路径(/cd)", value: JSON.stringify({ cmd: "cd" }), type: "default" },
|
|
347
|
-
]),
|
|
348
|
-
],
|
|
349
|
-
});
|
|
448
|
+
const restartCard = buildHelpCard("", { greeting: "Bot 已启动完成,可以继续使用。" });
|
|
350
449
|
await fetch(`${BASE_URL}/im/v1/messages?receive_id_type=chat_id`, {
|
|
351
450
|
method: "POST",
|
|
352
451
|
headers: {
|
|
@@ -388,4 +487,4 @@ export async function updateCardMessage(token: string, messageId: string, conten
|
|
|
388
487
|
return false;
|
|
389
488
|
}
|
|
390
489
|
return true;
|
|
391
|
-
}
|
|
490
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* ChatCCC — Feishu Bot Bridge for
|
|
2
|
+
* ChatCCC — Feishu Bot Bridge for AI Coding Tools (TypeScript)
|
|
3
3
|
* =================================================================
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Supported tools: Claude Code, Cursor, Codex (OpenAI).
|
|
5
|
+
*
|
|
6
|
+
* When a user sends "/new [tool]" to the bot:
|
|
7
|
+
* 1. Create an AI tool session via the corresponding adapter, get session ID
|
|
6
8
|
* 2. Create a new Feishu group chat and add the user
|
|
7
9
|
* 3. Rename the group (name + description) to the session ID
|
|
8
|
-
* 4.
|
|
9
|
-
* 5. Reply to the new group with session info and welcome message
|
|
10
|
+
* 4. Reply to the new group with session info and welcome message
|
|
10
11
|
*
|
|
11
|
-
* Auto-resume: when any message is received in a
|
|
12
|
-
* (group description contains
|
|
13
|
-
* session ID, resumes the session
|
|
12
|
+
* Auto-resume: when any message is received in a session group
|
|
13
|
+
* (group description contains a tool-specific prefix), the bot extracts the
|
|
14
|
+
* session ID, resumes the session, sends the user's text, and
|
|
14
15
|
* streams the response to the session's jsonl file.
|
|
15
16
|
*
|
|
16
17
|
* Buttons: progress cards have a 停止 button; help messages have /new and /restart buttons.
|
|
@@ -23,7 +24,7 @@
|
|
|
23
24
|
|
|
24
25
|
import { spawn } from "node:child_process";
|
|
25
26
|
import { readdir, stat } from "node:fs/promises";
|
|
26
|
-
import { resolve, dirname
|
|
27
|
+
import { resolve, dirname } from "node:path";
|
|
27
28
|
|
|
28
29
|
import { WSClient, EventDispatcher } from "@larksuiteoapi/node-sdk";
|
|
29
30
|
import WebSocket from "ws";
|
|
@@ -65,6 +66,7 @@ import {
|
|
|
65
66
|
recallMessage,
|
|
66
67
|
sendCardReply,
|
|
67
68
|
sendTextReply,
|
|
69
|
+
setChatAvatar,
|
|
68
70
|
updateCardMessage,
|
|
69
71
|
updateChatInfo,
|
|
70
72
|
sendRestartCard,
|
|
@@ -87,6 +89,19 @@ import {
|
|
|
87
89
|
getAdapterForTool,
|
|
88
90
|
} from "./session.ts";
|
|
89
91
|
|
|
92
|
+
export function cwdDisplayName(cwd: string): string {
|
|
93
|
+
const trimmed = cwd.trim().replace(/[\\/]+$/, "");
|
|
94
|
+
return trimmed.split(/[\\/]/).filter(Boolean).pop() || trimmed || "cwd";
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function sessionChatName(left: string, cwd: string): string {
|
|
98
|
+
return `${left}-${cwdDisplayName(cwd)}`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function isUntitledSessionChatName(name: string): boolean {
|
|
102
|
+
return name === "新会话" || name.startsWith("新会话-");
|
|
103
|
+
}
|
|
104
|
+
|
|
90
105
|
// ---------------------------------------------------------------------------
|
|
91
106
|
// Event types
|
|
92
107
|
// ---------------------------------------------------------------------------
|
|
@@ -177,7 +192,7 @@ function parseCardAction(data: unknown): CardActionResult | null {
|
|
|
177
192
|
}
|
|
178
193
|
if (!cmd) return null;
|
|
179
194
|
|
|
180
|
-
const CMD_MAP: Record<string, string> = { stop: "/stop", new: "/new", "new cursor": "/new cursor", restart: "/restart", status: "/status", cd: "/cd", sessions: "/sessions" };
|
|
195
|
+
const CMD_MAP: Record<string, string> = { stop: "/stop", new: "/new", "new cursor": "/new cursor", "new codex": "/new codex", restart: "/restart", status: "/status", cd: "/cd", sessions: "/sessions", forget: "/forget" };
|
|
181
196
|
let text = CMD_MAP[cmd] ?? "";
|
|
182
197
|
if (cmd === "cd" && typeof action.value === "object" && action.value !== null) {
|
|
183
198
|
const path = (action.value as Record<string, string>).path;
|
|
@@ -317,10 +332,10 @@ async function handleCommand(text: string, chatId: string, openId: string, msgTi
|
|
|
317
332
|
if (text === "/new" || text.startsWith("/new ")) {
|
|
318
333
|
const toolArg = text.slice(5).trim();
|
|
319
334
|
const tool = toolArg || "claude";
|
|
320
|
-
const validTools = ["claude", "cursor"];
|
|
335
|
+
const validTools = ["claude", "cursor", "codex"];
|
|
321
336
|
if (!validTools.includes(tool)) {
|
|
322
337
|
const warnToken = await getTenantAccessToken();
|
|
323
|
-
await sendCardReply(warnToken, chatId, "Error", `未知的工具类型: "${toolArg}"。支持: claude (Claude Code), cursor (Cursor)。`, "red");
|
|
338
|
+
await sendCardReply(warnToken, chatId, "Error", `未知的工具类型: "${toolArg}"。支持: claude (Claude Code), cursor (Cursor), codex (Codex)。`, "red");
|
|
324
339
|
return;
|
|
325
340
|
}
|
|
326
341
|
const toolLabel = toolDisplayName(tool);
|
|
@@ -335,8 +350,11 @@ async function handleCommand(text: string, chatId: string, openId: string, msgTi
|
|
|
335
350
|
const freshToken = await getTenantAccessToken();
|
|
336
351
|
|
|
337
352
|
let sessionId: string;
|
|
353
|
+
let sessionCwd: string;
|
|
338
354
|
try {
|
|
339
|
-
|
|
355
|
+
const init = await initClaudeSession(tool);
|
|
356
|
+
sessionId = init.sessionId;
|
|
357
|
+
sessionCwd = init.cwd;
|
|
340
358
|
console.log(`[${ts()}] [STEP 1/4] ${toolLabel} session created: ${sessionId} → OK`);
|
|
341
359
|
} catch (err) {
|
|
342
360
|
console.error(`[${ts()}] [STEP 1/4] FAIL: ${(err as Error).message}`);
|
|
@@ -348,9 +366,12 @@ async function handleCommand(text: string, chatId: string, openId: string, msgTi
|
|
|
348
366
|
return;
|
|
349
367
|
}
|
|
350
368
|
|
|
369
|
+
const cwd = sessionCwd;
|
|
370
|
+
const initialName = sessionChatName("新会话", cwd);
|
|
371
|
+
|
|
351
372
|
let newChatId: string;
|
|
352
373
|
try {
|
|
353
|
-
newChatId = await createGroupChat(freshToken,
|
|
374
|
+
newChatId = await createGroupChat(freshToken, initialName, [openId]);
|
|
354
375
|
console.log(`[${ts()}] [STEP 2/4] Created Feishu group: ${newChatId} → OK`);
|
|
355
376
|
} catch (err) {
|
|
356
377
|
console.error(`[${ts()}] [STEP 2/4] FAIL: ${(err as Error).message}`);
|
|
@@ -359,7 +380,6 @@ async function handleCommand(text: string, chatId: string, openId: string, msgTi
|
|
|
359
380
|
}
|
|
360
381
|
|
|
361
382
|
try {
|
|
362
|
-
const initialName = `新会话-${sessionId}`;
|
|
363
383
|
const descPrefix = sessionPrefixForTool(tool);
|
|
364
384
|
await updateChatInfo(freshToken, newChatId, initialName, `${descPrefix} ${sessionId}`);
|
|
365
385
|
console.log(`[${ts()}] [STEP 3/4] Renamed group → name="${initialName}" (${toolLabel}) → OK`);
|
|
@@ -369,7 +389,6 @@ async function handleCommand(text: string, chatId: string, openId: string, msgTi
|
|
|
369
389
|
return;
|
|
370
390
|
}
|
|
371
391
|
|
|
372
|
-
const cwd = await getDefaultCwd();
|
|
373
392
|
const adapter = getAdapterForTool(tool);
|
|
374
393
|
await sendCardReply(
|
|
375
394
|
freshToken, newChatId, `${toolLabel} Session Ready`,
|
|
@@ -383,6 +402,7 @@ async function handleCommand(text: string, chatId: string, openId: string, msgTi
|
|
|
383
402
|
);
|
|
384
403
|
|
|
385
404
|
console.log(`[${ts()}] [STEP 4/4] Replied to new group → OK`);
|
|
405
|
+
setChatAvatar(freshToken, newChatId, "new").catch(() => {});
|
|
386
406
|
console.log(`${"=".repeat(60)}`);
|
|
387
407
|
return;
|
|
388
408
|
}
|
|
@@ -401,10 +421,13 @@ async function handleCommand(text: string, chatId: string, openId: string, msgTi
|
|
|
401
421
|
|
|
402
422
|
const freshToken = await getTenantAccessToken();
|
|
403
423
|
|
|
404
|
-
if (chatInfo.name
|
|
405
|
-
const MAX_PREFIX =
|
|
424
|
+
if (isUntitledSessionChatName(chatInfo.name)) {
|
|
425
|
+
const MAX_PREFIX = 10;
|
|
406
426
|
const prefix = text.slice(0, MAX_PREFIX);
|
|
407
|
-
const
|
|
427
|
+
const adapter = getAdapterForTool(descriptionTool);
|
|
428
|
+
const info = await adapter.getSessionInfo(sessionId).catch(() => undefined);
|
|
429
|
+
const sessionCwd = info?.cwd ?? (await getDefaultCwd());
|
|
430
|
+
const newName = sessionChatName(prefix, sessionCwd);
|
|
408
431
|
try {
|
|
409
432
|
await updateChatInfo(freshToken, chatId, newName, description);
|
|
410
433
|
console.log(`[${ts()}] [RENAME] First message → group renamed to "${newName}"`);
|
|
@@ -492,6 +515,61 @@ async function handleCommand(text: string, chatId: string, openId: string, msgTi
|
|
|
492
515
|
return;
|
|
493
516
|
}
|
|
494
517
|
|
|
518
|
+
if (text === "/forget") {
|
|
519
|
+
const adapter = getAdapterForTool(descriptionTool);
|
|
520
|
+
let cwd: string;
|
|
521
|
+
try {
|
|
522
|
+
const info = await adapter.getSessionInfo(sessionId);
|
|
523
|
+
cwd = info?.cwd ?? (await getDefaultCwd());
|
|
524
|
+
} catch {
|
|
525
|
+
cwd = await getDefaultCwd();
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
const existing = chatSessionMap.get(chatId);
|
|
529
|
+
if (existing) {
|
|
530
|
+
existing.stopped = true;
|
|
531
|
+
if (existing.spinnerTimer) { clearInterval(existing.spinnerTimer); existing.spinnerTimer = null; }
|
|
532
|
+
existing.close();
|
|
533
|
+
chatSessionMap.delete(chatId);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
let newSessionId: string;
|
|
537
|
+
try {
|
|
538
|
+
const init = await initClaudeSession(descriptionTool, cwd);
|
|
539
|
+
newSessionId = init.sessionId;
|
|
540
|
+
} catch (err) {
|
|
541
|
+
await sendCardReply(freshToken, chatId, "Error", `Failed to create new session:\n${(err as Error).message}`, "red");
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
const descPrefix = sessionPrefixForTool(descriptionTool);
|
|
546
|
+
const initialName = sessionChatName("新会话", cwd);
|
|
547
|
+
await updateChatInfo(freshToken, chatId, initialName, `${descPrefix} ${newSessionId}`);
|
|
548
|
+
console.log(`[${ts()}] [FORGET] Group updated: name="${initialName}" desc="${descPrefix} ${newSessionId}"`);
|
|
549
|
+
|
|
550
|
+
sessionInfoMap.set(chatId, {
|
|
551
|
+
sessionId: newSessionId,
|
|
552
|
+
turnCount: 0,
|
|
553
|
+
lastContextTokens: 0,
|
|
554
|
+
startTime: Date.now(),
|
|
555
|
+
tool: descriptionTool,
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
setChatAvatar(freshToken, chatId, "new").catch(() => {});
|
|
559
|
+
|
|
560
|
+
await sendCardReply(
|
|
561
|
+
freshToken, chatId, `${toolLabel} Session Reset`,
|
|
562
|
+
`会话已重置为新的 **${toolLabel}** 会话。\n\n` +
|
|
563
|
+
`**Session ID:** ${newSessionId}\n` +
|
|
564
|
+
`**工作目录:** \`${cwd}\`\n\n` +
|
|
565
|
+
`直接在这里发消息即可继续对话。`,
|
|
566
|
+
"green"
|
|
567
|
+
);
|
|
568
|
+
|
|
569
|
+
console.log(`[${ts()}] [FORGET] Session ${sessionId} → ${newSessionId} (same cwd=${cwd})`);
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
|
|
495
573
|
// /git <args>:在「当前会话工作目录」执行 git 命令,把输出回发到群里。
|
|
496
574
|
// 注意 cwd 必须取自 adapter.getSessionInfo(会话真实 cwd),而非
|
|
497
575
|
// getDefaultCwd(下一次 /new 才会使用的默认路径)。
|
|
@@ -873,4 +951,4 @@ main().catch((err: Error) => {
|
|
|
873
951
|
if (err.stack) console.error(err.stack);
|
|
874
952
|
printServiceDidNotStart(`main() 异常: ${err.message}`);
|
|
875
953
|
process.exit(1);
|
|
876
|
-
});
|
|
954
|
+
});
|
package/src/session.ts
CHANGED
|
@@ -19,11 +19,12 @@ import {
|
|
|
19
19
|
sendCardKitMessage,
|
|
20
20
|
updateCardKitCard,
|
|
21
21
|
} from "./cardkit.ts";
|
|
22
|
-
import { sendTextReply } from "./feishu-api.ts";
|
|
22
|
+
import { sendTextReply, setChatAvatar } from "./feishu-api.ts";
|
|
23
23
|
import type { UnifiedBlock } from "./adapters/adapter-interface.ts";
|
|
24
24
|
import type { ToolAdapter } from "./adapters/adapter-interface.ts";
|
|
25
25
|
import { createClaudeAdapter } from "./adapters/claude-adapter.ts";
|
|
26
26
|
import { createCursorAdapter } from "./adapters/cursor-adapter.ts";
|
|
27
|
+
import { createCodexAdapter } from "./adapters/codex-adapter.ts";
|
|
27
28
|
|
|
28
29
|
// ---------------------------------------------------------------------------
|
|
29
30
|
// Shared state (imported by index.ts)
|
|
@@ -91,6 +92,8 @@ export function getAdapterForTool(tool: string): ToolAdapter {
|
|
|
91
92
|
let adapter: ToolAdapter;
|
|
92
93
|
if (tool === "cursor") {
|
|
93
94
|
adapter = createCursorAdapter();
|
|
95
|
+
} else if (tool === "codex") {
|
|
96
|
+
adapter = createCodexAdapter();
|
|
94
97
|
} else {
|
|
95
98
|
adapter = createClaudeAdapter({
|
|
96
99
|
model: CLAUDE_MODEL,
|
|
@@ -255,11 +258,20 @@ function formatToolConfigForLog(tool: string, sessionModel?: string): string {
|
|
|
255
258
|
if (tool === "cursor") {
|
|
256
259
|
return `model=${sessionModel ?? "(由 cursor-agent 决定,init 事件后学习)"}`;
|
|
257
260
|
}
|
|
261
|
+
if (tool === "codex") {
|
|
262
|
+
const m = process.env.CHATCCC_CODEX_MODEL?.trim();
|
|
263
|
+
const e = process.env.CHATCCC_CODEX_EFFORT?.trim();
|
|
264
|
+
const modelStr = m && m !== "default" ? m : "(由 codex config.toml 决定)";
|
|
265
|
+
const effortStr = e && e !== "default"
|
|
266
|
+
? `effort=${e}`
|
|
267
|
+
: "effort=(由 codex config.toml 决定)";
|
|
268
|
+
return `model=${modelStr}, ${effortStr}`;
|
|
269
|
+
}
|
|
258
270
|
return `model=${anthropicConfigDisplay(CLAUDE_MODEL)}, effort=${anthropicConfigDisplay(CLAUDE_EFFORT)}`;
|
|
259
271
|
}
|
|
260
272
|
|
|
261
|
-
export async function initClaudeSession(tool: string): Promise<string> {
|
|
262
|
-
const cwd = await getDefaultCwd();
|
|
273
|
+
export async function initClaudeSession(tool: string, overrideCwd?: string): Promise<{ sessionId: string; cwd: string }> {
|
|
274
|
+
const cwd = overrideCwd ?? (await getDefaultCwd());
|
|
263
275
|
const adapter = getAdapterForTool(tool);
|
|
264
276
|
console.log(
|
|
265
277
|
`[${ts()}] [STEP 1/5] Creating ${adapter.displayName} session (${formatToolConfigForLog(tool)}, cwd=${cwd})`
|
|
@@ -273,7 +285,7 @@ export async function initClaudeSession(tool: string): Promise<string> {
|
|
|
273
285
|
|
|
274
286
|
await addRecentDir(cwd);
|
|
275
287
|
|
|
276
|
-
return sessionId;
|
|
288
|
+
return { sessionId, cwd };
|
|
277
289
|
}
|
|
278
290
|
|
|
279
291
|
export async function resumeAndPrompt(
|
|
@@ -307,6 +319,8 @@ export async function resumeAndPrompt(
|
|
|
307
319
|
});
|
|
308
320
|
const myGen = sessionGen;
|
|
309
321
|
|
|
322
|
+
setChatAvatar(token, chatId, "busy").catch(() => {});
|
|
323
|
+
|
|
310
324
|
const now = Date.now();
|
|
311
325
|
const existingInfo = sessionInfoMap.get(chatId);
|
|
312
326
|
sessionInfoMap.set(chatId, {
|
|
@@ -437,6 +451,7 @@ export async function resumeAndPrompt(
|
|
|
437
451
|
if (!cEntry || cEntry.gen !== myGen) return;
|
|
438
452
|
const wasStopped = cEntry.stopped;
|
|
439
453
|
chatSessionMap.delete(chatId);
|
|
454
|
+
setChatAvatar(token, chatId, "idle").catch(() => {});
|
|
440
455
|
|
|
441
456
|
const finalCardContent = state.accumulatedContent || " ";
|
|
442
457
|
if (cardId) {
|
|
@@ -547,6 +562,14 @@ async function resolveModelEffort(
|
|
|
547
562
|
}
|
|
548
563
|
return { model, effort: null };
|
|
549
564
|
}
|
|
565
|
+
if (tool === "codex") {
|
|
566
|
+
const m = process.env.CHATCCC_CODEX_MODEL?.trim();
|
|
567
|
+
const e = process.env.CHATCCC_CODEX_EFFORT?.trim();
|
|
568
|
+
return {
|
|
569
|
+
model: m && m !== "default" ? m : UNKNOWN_MODEL_PLACEHOLDER,
|
|
570
|
+
effort: e && e !== "default" ? e : UNKNOWN_MODEL_PLACEHOLDER,
|
|
571
|
+
};
|
|
572
|
+
}
|
|
550
573
|
return {
|
|
551
574
|
model: anthropicConfigDisplay(CLAUDE_MODEL),
|
|
552
575
|
effort: anthropicConfigDisplay(CLAUDE_EFFORT),
|
|
@@ -618,4 +641,4 @@ export function _setAdapterForToolForTest(tool: string, adapter: ToolAdapter): v
|
|
|
618
641
|
|
|
619
642
|
export function _clearAdapterCacheForTest(): void {
|
|
620
643
|
adapterCache.clear();
|
|
621
|
-
}
|
|
644
|
+
}
|