chatccc 0.2.20 → 0.2.21
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
CHANGED
package/src/feishu-api.ts
CHANGED
|
@@ -405,6 +405,80 @@ export async function setChatAvatar(token: string, chatId: string, tool: string,
|
|
|
405
405
|
}
|
|
406
406
|
}
|
|
407
407
|
|
|
408
|
+
// ---------------------------------------------------------------------------
|
|
409
|
+
// Image download & cache
|
|
410
|
+
// ---------------------------------------------------------------------------
|
|
411
|
+
|
|
412
|
+
const IMAGE_DOWNLOAD_DIR = resolvePath(PROJECT_ROOT, "images", "downloads");
|
|
413
|
+
const IMAGE_CACHE_FILE = resolvePath(PROJECT_ROOT, "state", "image-cache.json");
|
|
414
|
+
|
|
415
|
+
const imageCache = new Map<string, string>();
|
|
416
|
+
let imageCacheLoaded = false;
|
|
417
|
+
|
|
418
|
+
async function loadImageCache(): Promise<void> {
|
|
419
|
+
if (imageCacheLoaded) return;
|
|
420
|
+
imageCacheLoaded = true;
|
|
421
|
+
try {
|
|
422
|
+
const raw = await readFile(IMAGE_CACHE_FILE, "utf-8");
|
|
423
|
+
const parsed = JSON.parse(raw) as Record<string, unknown>;
|
|
424
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
425
|
+
if (typeof value === "string" && value.trim()) imageCache.set(key, value);
|
|
426
|
+
}
|
|
427
|
+
} catch { /* missing or malformed cache is not fatal */ }
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
async function persistImageCache(): Promise<void> {
|
|
431
|
+
await mkdir(dirname(IMAGE_CACHE_FILE), { recursive: true });
|
|
432
|
+
await writeFile(
|
|
433
|
+
IMAGE_CACHE_FILE,
|
|
434
|
+
JSON.stringify(Object.fromEntries(imageCache.entries()), null, 2),
|
|
435
|
+
"utf-8",
|
|
436
|
+
);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function extFromContentType(contentType: string | null): string {
|
|
440
|
+
if (!contentType) return ".png";
|
|
441
|
+
const mime = contentType.split(";")[0].trim().toLowerCase();
|
|
442
|
+
const map: Record<string, string> = {
|
|
443
|
+
"image/png": ".png",
|
|
444
|
+
"image/jpeg": ".jpg",
|
|
445
|
+
"image/webp": ".webp",
|
|
446
|
+
"image/gif": ".gif",
|
|
447
|
+
"image/bmp": ".bmp",
|
|
448
|
+
"image/svg+xml": ".svg",
|
|
449
|
+
};
|
|
450
|
+
return map[mime] ?? ".png";
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
async function downloadImage(token: string, messageId: string, fileKey: string): Promise<string> {
|
|
454
|
+
const resp = await fetch(`${BASE_URL}/im/v1/messages/${messageId}/resources/${fileKey}?type=image`, {
|
|
455
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
456
|
+
});
|
|
457
|
+
if (!resp.ok) {
|
|
458
|
+
const text = await resp.text().catch(() => "");
|
|
459
|
+
throw new Error(`downloadImage HTTP ${resp.status}: ${text.slice(0, 200)}`);
|
|
460
|
+
}
|
|
461
|
+
const ext = extFromContentType(resp.headers.get("content-type"));
|
|
462
|
+
const buffer = Buffer.from(await resp.arrayBuffer());
|
|
463
|
+
await mkdir(IMAGE_DOWNLOAD_DIR, { recursive: true });
|
|
464
|
+
const localPath = resolvePath(IMAGE_DOWNLOAD_DIR, `${fileKey}${ext}`);
|
|
465
|
+
await writeFile(localPath, buffer);
|
|
466
|
+
return localPath;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
export async function getOrDownloadImage(token: string, messageId: string, fileKey: string): Promise<string> {
|
|
470
|
+
await loadImageCache();
|
|
471
|
+
const cached = imageCache.get(fileKey);
|
|
472
|
+
if (cached) return cached;
|
|
473
|
+
const localPath = await downloadImage(token, messageId, fileKey);
|
|
474
|
+
imageCache.set(fileKey, localPath);
|
|
475
|
+
await persistImageCache().catch((err) => {
|
|
476
|
+
console.error(`[${ts()}] [IMAGE] persist cache FAIL: ${(err as Error).message}`);
|
|
477
|
+
});
|
|
478
|
+
console.log(`[${ts()}] [IMAGE] Downloaded ${fileKey} -> ${localPath}`);
|
|
479
|
+
return localPath;
|
|
480
|
+
}
|
|
481
|
+
|
|
408
482
|
// ---------------------------------------------------------------------------
|
|
409
483
|
// Messaging
|
|
410
484
|
// ---------------------------------------------------------------------------
|
package/src/index.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* =================================================================
|
|
4
4
|
* Supported tools: Claude Code, Cursor, Codex (OpenAI).
|
|
5
5
|
*
|
|
6
|
-
* When a user sends "/new [tool]" to the bot (omitting tool uses the configured default Agent):
|
|
6
|
+
* When a user sends "/new [tool]" to the bot (omitting tool uses the configured default Agent):
|
|
7
7
|
* 1. Create an AI tool session via the corresponding adapter, get session ID
|
|
8
8
|
* 2. Create a new Feishu group chat and add the user
|
|
9
9
|
* 3. Rename the group (name + description) to the session ID
|
|
@@ -54,12 +54,12 @@ import {
|
|
|
54
54
|
maskAppId,
|
|
55
55
|
setDefaultCwd,
|
|
56
56
|
getRecentDirs,
|
|
57
|
-
addRecentDir,
|
|
58
|
-
sessionPrefixForTool,
|
|
59
|
-
resolveDefaultAgentTool,
|
|
60
|
-
toolDisplayName,
|
|
61
|
-
ts,
|
|
62
|
-
} from "./config.ts";
|
|
57
|
+
addRecentDir,
|
|
58
|
+
sessionPrefixForTool,
|
|
59
|
+
resolveDefaultAgentTool,
|
|
60
|
+
toolDisplayName,
|
|
61
|
+
ts,
|
|
62
|
+
} from "./config.ts";
|
|
63
63
|
import { printServiceDidNotStart, printServiceRunningHint } from "./exit-banner.ts";
|
|
64
64
|
import {
|
|
65
65
|
addReaction,
|
|
@@ -74,6 +74,7 @@ import {
|
|
|
74
74
|
setChatAvatar,
|
|
75
75
|
updateCardMessage,
|
|
76
76
|
updateChatInfo,
|
|
77
|
+
getOrDownloadImage,
|
|
77
78
|
sendRestartCard,
|
|
78
79
|
verifyAllPermissions,
|
|
79
80
|
reportPermissionResults,
|
|
@@ -126,7 +127,7 @@ function getInnerEvent(data: Evt): InnerEvent {
|
|
|
126
127
|
* 将飞书消息的原始 content JSON 结构转成可读文本,保留代码块等结构信息。
|
|
127
128
|
* 未知类型直接返回 JSON 原文,让 AI 自行理解。
|
|
128
129
|
*/
|
|
129
|
-
function formatMessageContent(message: { message_type?: string; content?: string }): string {
|
|
130
|
+
async function formatMessageContent(message: { message_id?: string; message_type?: string; content?: string }): Promise<string> {
|
|
130
131
|
const contentStr = message.content ?? "{}";
|
|
131
132
|
let content: Record<string, unknown>;
|
|
132
133
|
try { content = JSON.parse(contentStr); } catch { return ""; }
|
|
@@ -143,7 +144,21 @@ function formatMessageContent(message: { message_type?: string; content?: string
|
|
|
143
144
|
return formatPostContent(content);
|
|
144
145
|
}
|
|
145
146
|
|
|
146
|
-
|
|
147
|
+
if (message.message_type === "image") {
|
|
148
|
+
const imageKey = content.image_key as string | undefined;
|
|
149
|
+
const messageId = message.message_id;
|
|
150
|
+
if (!imageKey || !messageId) return contentStr;
|
|
151
|
+
try {
|
|
152
|
+
const token = await getTenantAccessToken();
|
|
153
|
+
const localPath = await getOrDownloadImage(token, messageId, imageKey);
|
|
154
|
+
return `[图片] ${localPath}`;
|
|
155
|
+
} catch (err) {
|
|
156
|
+
console.error(`[${ts()}] [IMAGE] download failed for ${imageKey}: ${(err as Error).message}`);
|
|
157
|
+
return `[图片: ${imageKey}]`;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// 其他类型(file, audio, media, sticker)直接给原始 JSON
|
|
147
162
|
return contentStr;
|
|
148
163
|
}
|
|
149
164
|
|
|
@@ -197,7 +212,7 @@ function parseCardAction(data: unknown): CardActionResult | null {
|
|
|
197
212
|
}
|
|
198
213
|
if (!cmd) return null;
|
|
199
214
|
|
|
200
|
-
const CMD_MAP: Record<string, string> = { stop: "/stop", new: "/new", "new claude": "/new claude", "new cursor": "/new cursor", "new codex": "/new codex", restart: "/restart", status: "/status", cd: "/cd", sessions: "/sessions", forget: "/forget" };
|
|
215
|
+
const CMD_MAP: Record<string, string> = { stop: "/stop", new: "/new", "new claude": "/new claude", "new cursor": "/new cursor", "new codex": "/new codex", restart: "/restart", status: "/status", cd: "/cd", sessions: "/sessions", forget: "/forget" };
|
|
201
216
|
let text = CMD_MAP[cmd] ?? "";
|
|
202
217
|
if (cmd === "cd" && typeof action.value === "object" && action.value !== null) {
|
|
203
218
|
const path = (action.value as Record<string, string>).path;
|
|
@@ -723,8 +738,8 @@ async function startBotServiceCore(): Promise<void> {
|
|
|
723
738
|
console.log(`${"=".repeat(60)}`);
|
|
724
739
|
console.log(` ChatCCC — Feishu Bot Bridge for Claude Code${modeTag}`);
|
|
725
740
|
console.log(`${"=".repeat(60)}`);
|
|
726
|
-
console.log(` Send "/new" to the bot to create a new group + ${toolDisplayName(resolveDefaultAgentTool())} session.`);
|
|
727
|
-
console.log(` In a session group, send any message to resume & prompt.`);
|
|
741
|
+
console.log(` Send "/new" to the bot to create a new group + ${toolDisplayName(resolveDefaultAgentTool())} session.`);
|
|
742
|
+
console.log(` In a session group, send any message to resume & prompt.`);
|
|
728
743
|
console.log(`${"=".repeat(60)}`);
|
|
729
744
|
|
|
730
745
|
console.log(`\n[启动 4/7] 向飞书开放平台申请 tenant_access_token …`);
|
|
@@ -784,7 +799,7 @@ async function startBotServiceCore(): Promise<void> {
|
|
|
784
799
|
}
|
|
785
800
|
}
|
|
786
801
|
|
|
787
|
-
const text = formatMessageContent(message);
|
|
802
|
+
const text = await formatMessageContent(message);
|
|
788
803
|
const sender = event.sender;
|
|
789
804
|
const openId = sender?.sender_id?.open_id ?? "";
|
|
790
805
|
const chatId = message.chat_id ?? "";
|
|
@@ -868,7 +883,7 @@ async function startBotServiceCore(): Promise<void> {
|
|
|
868
883
|
console.log("[启动 7/7] 已连接本地中继,可接收转发事件。\n");
|
|
869
884
|
printServiceRunningHint("local", `http://127.0.0.1:${CHATCCC_PORT}`);
|
|
870
885
|
});
|
|
871
|
-
ws.on("message", (raw: Buffer) => {
|
|
886
|
+
ws.on("message", async (raw: Buffer) => {
|
|
872
887
|
try {
|
|
873
888
|
const data = JSON.parse(raw.toString()) as Evt;
|
|
874
889
|
const action = parseCardAction(data);
|
|
@@ -881,7 +896,7 @@ async function startBotServiceCore(): Promise<void> {
|
|
|
881
896
|
const event = getInnerEvent(data);
|
|
882
897
|
const message = event.message;
|
|
883
898
|
if (!message) return;
|
|
884
|
-
const text = formatMessageContent(message);
|
|
899
|
+
const text = await formatMessageContent(message);
|
|
885
900
|
const openId = event.sender?.sender_id?.open_id ?? "";
|
|
886
901
|
const chatId = message.chat_id ?? "";
|
|
887
902
|
appendChatLog(chatId, openId, text);
|