chatccc 0.2.88 → 0.2.89

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
@@ -1,63 +1,63 @@
1
- {
2
- "name": "chatccc",
3
- "version": "0.2.88",
4
- "description": "Feishu bot bridge for Claude Code",
5
- "license": "Apache-2.0",
6
- "type": "module",
7
- "main": "./src/index.ts",
8
- "bin": {
9
- "chatccc": "bin/chatccc.mjs"
10
- },
11
- "files": [
12
- "src/",
13
- "bin/",
14
- "scripts/postinstall-sharp-check.mjs",
15
- "demo/ilink_echo_probe.ts",
16
- "im-skills/",
17
- "images/img_readme_*.jpg",
18
- "images/img_readme_*.png",
19
- "images/avatars/status_*.png",
20
- "images/avatars/badges/",
21
- "package.json",
22
- "README.md",
23
- "config.sample.json"
24
- ],
25
- "scripts": {
26
- "dev": "tsx src/index.ts",
27
- "chatccc": "tsx src/index.ts",
28
- "start": "tsx src/index.ts",
29
- "demo:bot-test": "tsx demo/bot_test.ts",
30
- "demo:bot-test:local": "tsx demo/bot_test.ts --local",
31
- "demo:create-group": "tsx src/index.ts",
32
- "demo:create-group:local": "tsx src/index.ts --local",
33
- "demo:permission-check": "tsx demo/permission_check.ts",
34
- "demo:claude-hi": "tsx demo/claude_say_hi.ts",
35
- "demo:codex-hi": "tsx demo/codex_say_hi.ts",
36
- "demo:ilink-echo": "tsx demo/ilink_echo_probe.ts",
37
- "test": "vitest run",
38
- "test:watch": "vitest",
39
- "postinstall": "node scripts/postinstall-sharp-check.mjs"
40
- },
41
- "dependencies": {
42
- "@ai-sdk/openai-compatible": "^2.0.47",
43
- "@anthropic-ai/claude-agent-sdk": "0.2.133",
44
- "@larksuiteoapi/node-sdk": "^1.59.0",
45
- "@openilink/openilink-sdk-node": "^0.6.0",
46
- "ai": "^6.0.184",
47
- "nodemailer": "^8.0.7",
48
- "qrcode-terminal": "^0.12.0",
49
- "sharp": "^0.34.5",
50
- "tsx": "^4.0.0",
51
- "ws": "^8.18.0"
52
- },
53
- "devDependencies": {
54
- "@types/node": "^20.0.0",
55
- "@types/qrcode-terminal": "^0.12.2",
56
- "@types/ws": "^8.18.1",
57
- "typescript": "^5.0.0",
58
- "vitest": "^3.2.4"
59
- },
60
- "engines": {
61
- "node": ">=20"
62
- }
63
- }
1
+ {
2
+ "name": "chatccc",
3
+ "version": "0.2.89",
4
+ "description": "Feishu bot bridge for Claude Code",
5
+ "license": "Apache-2.0",
6
+ "type": "module",
7
+ "main": "./src/index.ts",
8
+ "bin": {
9
+ "chatccc": "bin/chatccc.mjs"
10
+ },
11
+ "files": [
12
+ "src/",
13
+ "bin/",
14
+ "scripts/postinstall-sharp-check.mjs",
15
+ "demo/ilink_echo_probe.ts",
16
+ "im-skills/",
17
+ "images/img_readme_*.jpg",
18
+ "images/img_readme_*.png",
19
+ "images/avatars/status_*.png",
20
+ "images/avatars/badges/",
21
+ "package.json",
22
+ "README.md",
23
+ "config.sample.json"
24
+ ],
25
+ "scripts": {
26
+ "dev": "tsx src/index.ts",
27
+ "chatccc": "tsx src/index.ts",
28
+ "start": "tsx src/index.ts",
29
+ "demo:bot-test": "tsx demo/bot_test.ts",
30
+ "demo:bot-test:local": "tsx demo/bot_test.ts --local",
31
+ "demo:create-group": "tsx src/index.ts",
32
+ "demo:create-group:local": "tsx src/index.ts --local",
33
+ "demo:permission-check": "tsx demo/permission_check.ts",
34
+ "demo:claude-hi": "tsx demo/claude_say_hi.ts",
35
+ "demo:codex-hi": "tsx demo/codex_say_hi.ts",
36
+ "demo:ilink-echo": "tsx demo/ilink_echo_probe.ts",
37
+ "test": "vitest run",
38
+ "test:watch": "vitest",
39
+ "postinstall": "node scripts/postinstall-sharp-check.mjs"
40
+ },
41
+ "dependencies": {
42
+ "@ai-sdk/openai-compatible": "^2.0.47",
43
+ "@anthropic-ai/claude-agent-sdk": "0.2.133",
44
+ "@larksuiteoapi/node-sdk": "^1.59.0",
45
+ "@openilink/openilink-sdk-node": "^0.6.0",
46
+ "ai": "^6.0.184",
47
+ "nodemailer": "^8.0.7",
48
+ "qrcode-terminal": "^0.12.0",
49
+ "sharp": "^0.34.5",
50
+ "tsx": "^4.0.0",
51
+ "ws": "^8.18.0"
52
+ },
53
+ "devDependencies": {
54
+ "@types/node": "^20.0.0",
55
+ "@types/qrcode-terminal": "^0.12.2",
56
+ "@types/ws": "^8.18.1",
57
+ "typescript": "^5.0.0",
58
+ "vitest": "^3.2.4"
59
+ },
60
+ "engines": {
61
+ "node": ">=20"
62
+ }
63
+ }
@@ -1,59 +1,59 @@
1
- // postinstall 检测 sharp 原生模块是否可用(常见于 Linux 缺少 libvips)
2
- import { createRequire } from "node:module";
3
-
4
- const require = createRequire(import.meta.url);
5
- let ok = false;
6
-
7
- try {
8
- require("sharp");
9
- ok = true;
10
- } catch (err) {
11
- const msg = err.message;
12
- const platform = process.platform;
13
-
14
- console.warn("");
15
- console.warn("============================================================");
16
- console.warn(" ChatCCC: sharp 图像处理模块检测失败");
17
- console.warn("============================================================");
18
-
19
- if (platform === "linux") {
20
- if (msg.includes("libvips") || msg.includes("vips") || msg.includes("libglib")) {
21
- console.warn(" 原因:系统缺少 libvips 图像处理库。");
22
- console.warn("");
23
- console.warn(" 请先安装 libvips 然后重建 sharp:");
24
- console.warn("");
25
- console.warn(" Debian / Ubuntu:");
26
- console.warn(" sudo apt install libvips-dev");
27
- console.warn("");
28
- console.warn(" CentOS / RHEL / Fedora:");
29
- console.warn(" sudo yum install vips-devel");
30
- console.warn("");
31
- console.warn(" Alpine:");
32
- console.warn(" sudo apk add vips-dev");
33
- console.warn("");
34
- console.warn(" 安装 libvips 后重建 sharp:");
35
- console.warn(" npm install -g chatccc --force");
36
- } else if (msg.includes("Cannot find module")) {
37
- console.warn(" 原因:sharp 模块未成功安装。");
38
- console.warn("");
39
- console.warn(" 请尝试重建(可能需要先安装 libvips):");
40
- console.warn(" npm install -g chatccc --force");
41
- } else {
42
- console.warn(" 错误详情:", msg);
43
- }
44
- } else {
45
- console.warn(" 错误详情:", msg);
46
- console.warn(" 平台:", platform);
47
- console.warn(" 请尝试:npm install -g chatccc --force");
48
- }
49
-
50
- console.warn("");
51
- console.warn(" 缺少 sharp 不影响消息收发,但群聊头像状态指示");
52
- console.warn(" (运行中/空闲/新建)将不会更新。");
53
- console.warn("============================================================");
54
- console.warn("");
55
- }
56
-
57
- if (!ok && process.env.npm_config_verbose) {
58
- process.exitCode = 0; // 永远不阻塞安装
1
+ // postinstall 检测 sharp 原生模块是否可用(常见于 Linux 缺少 libvips)
2
+ import { createRequire } from "node:module";
3
+
4
+ const require = createRequire(import.meta.url);
5
+ let ok = false;
6
+
7
+ try {
8
+ require("sharp");
9
+ ok = true;
10
+ } catch (err) {
11
+ const msg = err.message;
12
+ const platform = process.platform;
13
+
14
+ console.warn("");
15
+ console.warn("============================================================");
16
+ console.warn(" ChatCCC: sharp 图像处理模块检测失败");
17
+ console.warn("============================================================");
18
+
19
+ if (platform === "linux") {
20
+ if (msg.includes("libvips") || msg.includes("vips") || msg.includes("libglib")) {
21
+ console.warn(" 原因:系统缺少 libvips 图像处理库。");
22
+ console.warn("");
23
+ console.warn(" 请先安装 libvips 然后重建 sharp:");
24
+ console.warn("");
25
+ console.warn(" Debian / Ubuntu:");
26
+ console.warn(" sudo apt install libvips-dev");
27
+ console.warn("");
28
+ console.warn(" CentOS / RHEL / Fedora:");
29
+ console.warn(" sudo yum install vips-devel");
30
+ console.warn("");
31
+ console.warn(" Alpine:");
32
+ console.warn(" sudo apk add vips-dev");
33
+ console.warn("");
34
+ console.warn(" 安装 libvips 后重建 sharp:");
35
+ console.warn(" npm install -g chatccc --force");
36
+ } else if (msg.includes("Cannot find module")) {
37
+ console.warn(" 原因:sharp 模块未成功安装。");
38
+ console.warn("");
39
+ console.warn(" 请尝试重建(可能需要先安装 libvips):");
40
+ console.warn(" npm install -g chatccc --force");
41
+ } else {
42
+ console.warn(" 错误详情:", msg);
43
+ }
44
+ } else {
45
+ console.warn(" 错误详情:", msg);
46
+ console.warn(" 平台:", platform);
47
+ console.warn(" 请尝试:npm install -g chatccc --force");
48
+ }
49
+
50
+ console.warn("");
51
+ console.warn(" 缺少 sharp 不影响消息收发,但群聊头像状态指示");
52
+ console.warn(" (运行中/空闲/新建)将不会更新。");
53
+ console.warn("============================================================");
54
+ console.warn("");
55
+ }
56
+
57
+ if (!ok && process.env.npm_config_verbose) {
58
+ process.exitCode = 0; // 永远不阻塞安装
59
59
  }
@@ -1,197 +1,197 @@
1
- /**
2
- * builtin/cli.ts — ChatCCC 内置 Agent 终端 REPL
3
- *
4
- * 用法:
5
- * npx tsx src/builtin/cli.ts
6
- * npx tsx src/builtin/cli.ts --model deepseek-chat
7
- * npx tsx src/builtin/cli.ts --cwd /path/to/project
8
- *
9
- * 环境变量:
10
- * DEEPSEEK_API_KEY — DeepSeek API Key(必需)
11
- * DEEPSEEK_BASE_URL — API 地址(可选,默认 https://api.deepseek.com/v1)
12
- */
13
-
14
- import * as readline from "node:readline";
15
- import * as process from "node:process";
16
- import { ChatSession, type ChatSessionConfig, type ChatSessionOptions } from "./index.js";
17
-
18
- // ---------------------------------------------------------------------------
19
- // 命令行参数解析
20
- // ---------------------------------------------------------------------------
21
-
22
- function parseArgs(): { config: ChatSessionConfig; options: ChatSessionOptions } {
23
- const args = process.argv.slice(2);
24
- const config: ChatSessionConfig = {};
25
- const options: ChatSessionOptions = {};
26
-
27
- for (let i = 0; i < args.length; i++) {
28
- const arg = args[i];
29
- const next = args[i + 1];
30
- if (arg === "--model" && next) {
31
- config.model = next;
32
- i++;
33
- } else if (arg === "--base-url" && next) {
34
- config.baseURL = next;
35
- i++;
36
- } else if (arg === "--api-key" && next) {
37
- config.apiKey = next;
38
- i++;
39
- } else if (arg === "--cwd" && next) {
40
- options.cwd = next;
41
- i++;
42
- } else if (arg === "--help" || arg === "-h") {
43
- printHelp();
44
- process.exit(0);
45
- }
46
- }
47
-
48
- return { config, options };
49
- }
50
-
51
- function printHelp(): void {
52
- console.log([
53
- "ChatCCC 内置 Agent 终端 REPL",
54
- "",
55
- "用法: npx tsx src/builtin/cli.ts [选项]",
56
- "",
57
- "选项:",
58
- " --model <name> 模型名称(默认 deepseek-chat)",
59
- " --base-url <url> API 地址(默认 https://api.deepseek.com/v1)",
60
- " --api-key <key> API Key(默认读 DEEPSEEK_API_KEY 环境变量)",
61
- " --cwd <path> 工作目录",
62
- " --help, -h 显示帮助",
63
- "",
64
- "环境变量:",
65
- " DEEPSEEK_API_KEY DeepSeek API Key",
66
- " DEEPSEEK_BASE_URL API 地址",
67
- "",
68
- ].join("\n"));
69
- }
70
-
71
- // ---------------------------------------------------------------------------
72
- // ANSI 颜色
73
- // ---------------------------------------------------------------------------
74
-
75
- const C = {
76
- reset: "\x1b[0m",
77
- dim: "\x1b[2m",
78
- green: "\x1b[32m",
79
- cyan: "\x1b[36m",
80
- yellow: "\x1b[33m",
81
- };
82
-
83
- // ---------------------------------------------------------------------------
84
- // 主程序
85
- // ---------------------------------------------------------------------------
86
-
87
- async function main(): Promise<void> {
88
- const { config, options } = parseArgs();
89
-
90
- // 环境变量回退
91
- if (!config.baseURL) config.baseURL = process.env.DEEPSEEK_BASE_URL;
92
- if (!config.apiKey) config.apiKey = process.env.DEEPSEEK_API_KEY;
93
-
94
- console.log(`${C.dim}ChatCCC 内置 Agent 原型${C.reset}`);
95
- console.log(`${C.dim}模型: ${config.model ?? "deepseek-chat"}${C.reset}`);
96
- if (options.cwd) {
97
- console.log(`${C.dim}目录: ${options.cwd}${C.reset}`);
98
- }
99
- console.log(`${C.dim}输入消息开始对话,Ctrl+C 中断当前回复,/exit 退出${C.reset}`);
100
- console.log("");
101
-
102
- let session: ChatSession;
103
- try {
104
- session = new ChatSession(config, options);
105
- } catch (err) {
106
- console.error(`${C.yellow}${(err as Error).message}${C.reset}`);
107
- process.exit(1);
108
- }
109
-
110
- const rl = readline.createInterface({
111
- input: process.stdin,
112
- output: process.stdout,
113
- prompt: `${C.green}>${C.reset} `,
114
- });
115
-
116
- // 用于中断当前 LLM 调用的 AbortController
117
- let currentAbort: AbortController | null = null;
118
-
119
- rl.prompt();
120
-
121
- rl.on("line", async (line: string) => {
122
- const input = line.trim();
123
- if (!input) {
124
- rl.prompt();
125
- return;
126
- }
127
-
128
- // 特殊命令
129
- if (input === "/exit" || input === "/quit") {
130
- console.log(`${C.dim}再见${C.reset}`);
131
- rl.close();
132
- return;
133
- }
134
-
135
- if (input === "/clear") {
136
- session.reset();
137
- console.log(`${C.dim}会话已重置${C.reset}`);
138
- rl.prompt();
139
- return;
140
- }
141
-
142
- if (input === "/history") {
143
- console.log(`${C.dim}共 ${session.turnCount} 轮对话${C.reset}`);
144
- rl.prompt();
145
- return;
146
- }
147
-
148
- // 发送消息
149
- currentAbort = new AbortController();
150
- const signal = currentAbort.signal;
151
-
152
- try {
153
- let lastAccumulated = "";
154
- for await (const event of session.chat(input, signal)) {
155
- if (event.type === "text") {
156
- // 增量输出(仅在首次和行首时不换行)
157
- const newText = event.accumulated.slice(lastAccumulated.length);
158
- process.stdout.write(newText);
159
- lastAccumulated = event.accumulated;
160
- } else if (event.type === "done") {
161
- if (lastAccumulated) console.log("");
162
- console.log(`${C.dim}[完成]${C.reset}`);
163
- } else if (event.type === "error") {
164
- console.log(`\n${C.yellow}[错误] ${event.message}${C.reset}`);
165
- }
166
- }
167
- } catch (err) {
168
- console.log(`\n${C.yellow}[错误] ${(err as Error).message}${C.reset}`);
169
- } finally {
170
- currentAbort = null;
171
- }
172
-
173
- rl.prompt();
174
- });
175
-
176
- // Ctrl+C → 中断当前 LLM 调用(不退出程序)
177
- rl.on("SIGINT", () => {
178
- if (currentAbort) {
179
- console.log(`\n${C.yellow}[中断中...]${C.reset}`);
180
- currentAbort.abort();
181
- currentAbort = null;
182
- } else {
183
- console.log(`\n${C.dim}输入 /exit 退出${C.reset}`);
184
- rl.prompt();
185
- }
186
- });
187
-
188
- rl.on("close", () => {
189
- console.log("");
190
- process.exit(0);
191
- });
192
- }
193
-
194
- main().catch((err) => {
195
- console.error(`${C.yellow}启动失败: ${(err as Error).message}${C.reset}`);
196
- process.exit(1);
1
+ /**
2
+ * builtin/cli.ts — ChatCCC 内置 Agent 终端 REPL
3
+ *
4
+ * 用法:
5
+ * npx tsx src/builtin/cli.ts
6
+ * npx tsx src/builtin/cli.ts --model deepseek-chat
7
+ * npx tsx src/builtin/cli.ts --cwd /path/to/project
8
+ *
9
+ * 环境变量:
10
+ * DEEPSEEK_API_KEY — DeepSeek API Key(必需)
11
+ * DEEPSEEK_BASE_URL — API 地址(可选,默认 https://api.deepseek.com/v1)
12
+ */
13
+
14
+ import * as readline from "node:readline";
15
+ import * as process from "node:process";
16
+ import { ChatSession, type ChatSessionConfig, type ChatSessionOptions } from "./index.js";
17
+
18
+ // ---------------------------------------------------------------------------
19
+ // 命令行参数解析
20
+ // ---------------------------------------------------------------------------
21
+
22
+ function parseArgs(): { config: ChatSessionConfig; options: ChatSessionOptions } {
23
+ const args = process.argv.slice(2);
24
+ const config: ChatSessionConfig = {};
25
+ const options: ChatSessionOptions = {};
26
+
27
+ for (let i = 0; i < args.length; i++) {
28
+ const arg = args[i];
29
+ const next = args[i + 1];
30
+ if (arg === "--model" && next) {
31
+ config.model = next;
32
+ i++;
33
+ } else if (arg === "--base-url" && next) {
34
+ config.baseURL = next;
35
+ i++;
36
+ } else if (arg === "--api-key" && next) {
37
+ config.apiKey = next;
38
+ i++;
39
+ } else if (arg === "--cwd" && next) {
40
+ options.cwd = next;
41
+ i++;
42
+ } else if (arg === "--help" || arg === "-h") {
43
+ printHelp();
44
+ process.exit(0);
45
+ }
46
+ }
47
+
48
+ return { config, options };
49
+ }
50
+
51
+ function printHelp(): void {
52
+ console.log([
53
+ "ChatCCC 内置 Agent 终端 REPL",
54
+ "",
55
+ "用法: npx tsx src/builtin/cli.ts [选项]",
56
+ "",
57
+ "选项:",
58
+ " --model <name> 模型名称(默认 deepseek-chat)",
59
+ " --base-url <url> API 地址(默认 https://api.deepseek.com/v1)",
60
+ " --api-key <key> API Key(默认读 DEEPSEEK_API_KEY 环境变量)",
61
+ " --cwd <path> 工作目录",
62
+ " --help, -h 显示帮助",
63
+ "",
64
+ "环境变量:",
65
+ " DEEPSEEK_API_KEY DeepSeek API Key",
66
+ " DEEPSEEK_BASE_URL API 地址",
67
+ "",
68
+ ].join("\n"));
69
+ }
70
+
71
+ // ---------------------------------------------------------------------------
72
+ // ANSI 颜色
73
+ // ---------------------------------------------------------------------------
74
+
75
+ const C = {
76
+ reset: "\x1b[0m",
77
+ dim: "\x1b[2m",
78
+ green: "\x1b[32m",
79
+ cyan: "\x1b[36m",
80
+ yellow: "\x1b[33m",
81
+ };
82
+
83
+ // ---------------------------------------------------------------------------
84
+ // 主程序
85
+ // ---------------------------------------------------------------------------
86
+
87
+ async function main(): Promise<void> {
88
+ const { config, options } = parseArgs();
89
+
90
+ // 环境变量回退
91
+ if (!config.baseURL) config.baseURL = process.env.DEEPSEEK_BASE_URL;
92
+ if (!config.apiKey) config.apiKey = process.env.DEEPSEEK_API_KEY;
93
+
94
+ console.log(`${C.dim}ChatCCC 内置 Agent 原型${C.reset}`);
95
+ console.log(`${C.dim}模型: ${config.model ?? "deepseek-chat"}${C.reset}`);
96
+ if (options.cwd) {
97
+ console.log(`${C.dim}目录: ${options.cwd}${C.reset}`);
98
+ }
99
+ console.log(`${C.dim}输入消息开始对话,Ctrl+C 中断当前回复,/exit 退出${C.reset}`);
100
+ console.log("");
101
+
102
+ let session: ChatSession;
103
+ try {
104
+ session = new ChatSession(config, options);
105
+ } catch (err) {
106
+ console.error(`${C.yellow}${(err as Error).message}${C.reset}`);
107
+ process.exit(1);
108
+ }
109
+
110
+ const rl = readline.createInterface({
111
+ input: process.stdin,
112
+ output: process.stdout,
113
+ prompt: `${C.green}>${C.reset} `,
114
+ });
115
+
116
+ // 用于中断当前 LLM 调用的 AbortController
117
+ let currentAbort: AbortController | null = null;
118
+
119
+ rl.prompt();
120
+
121
+ rl.on("line", async (line: string) => {
122
+ const input = line.trim();
123
+ if (!input) {
124
+ rl.prompt();
125
+ return;
126
+ }
127
+
128
+ // 特殊命令
129
+ if (input === "/exit" || input === "/quit") {
130
+ console.log(`${C.dim}再见${C.reset}`);
131
+ rl.close();
132
+ return;
133
+ }
134
+
135
+ if (input === "/clear") {
136
+ session.reset();
137
+ console.log(`${C.dim}会话已重置${C.reset}`);
138
+ rl.prompt();
139
+ return;
140
+ }
141
+
142
+ if (input === "/history") {
143
+ console.log(`${C.dim}共 ${session.turnCount} 轮对话${C.reset}`);
144
+ rl.prompt();
145
+ return;
146
+ }
147
+
148
+ // 发送消息
149
+ currentAbort = new AbortController();
150
+ const signal = currentAbort.signal;
151
+
152
+ try {
153
+ let lastAccumulated = "";
154
+ for await (const event of session.chat(input, signal)) {
155
+ if (event.type === "text") {
156
+ // 增量输出(仅在首次和行首时不换行)
157
+ const newText = event.accumulated.slice(lastAccumulated.length);
158
+ process.stdout.write(newText);
159
+ lastAccumulated = event.accumulated;
160
+ } else if (event.type === "done") {
161
+ if (lastAccumulated) console.log("");
162
+ console.log(`${C.dim}[完成]${C.reset}`);
163
+ } else if (event.type === "error") {
164
+ console.log(`\n${C.yellow}[错误] ${event.message}${C.reset}`);
165
+ }
166
+ }
167
+ } catch (err) {
168
+ console.log(`\n${C.yellow}[错误] ${(err as Error).message}${C.reset}`);
169
+ } finally {
170
+ currentAbort = null;
171
+ }
172
+
173
+ rl.prompt();
174
+ });
175
+
176
+ // Ctrl+C → 中断当前 LLM 调用(不退出程序)
177
+ rl.on("SIGINT", () => {
178
+ if (currentAbort) {
179
+ console.log(`\n${C.yellow}[中断中...]${C.reset}`);
180
+ currentAbort.abort();
181
+ currentAbort = null;
182
+ } else {
183
+ console.log(`\n${C.dim}输入 /exit 退出${C.reset}`);
184
+ rl.prompt();
185
+ }
186
+ });
187
+
188
+ rl.on("close", () => {
189
+ console.log("");
190
+ process.exit(0);
191
+ });
192
+ }
193
+
194
+ main().catch((err) => {
195
+ console.error(`${C.yellow}启动失败: ${(err as Error).message}${C.reset}`);
196
+ process.exit(1);
197
197
  });
@@ -1,167 +1,167 @@
1
- /**
2
- * builtin/index.ts — ChatCCC 内置 Agent 核心 API
3
- *
4
- * ChatSession 是程序化入口,既可以被 CLI 调用,也可以被其他模块(如 ToolAdapter)调用。
5
- */
6
-
7
- import { streamText } from "ai";
8
-
9
- // ---------------------------------------------------------------------------
10
- // 系统提示词 — 编译期冻结常量
11
- // ---------------------------------------------------------------------------
12
-
13
- const SYSTEM_PROMPT = [
14
- "你是 ChatCCC 内置 AI 编程助手,运行在终端环境中。",
15
- "",
16
- "## 基本规则",
17
- "- 用中文回复,但代码、命令、文件名保持原文",
18
- "- 优先给出直接可用的方案,而非长篇解释",
19
- "- 如果用户的问题涉及代码,直接给出代码并说明用法",
20
- "- 保持简洁,一次聚焦一个问题",
21
- ].join("\n");
22
-
23
- // ---------------------------------------------------------------------------
24
- // 类型定义
25
- // ---------------------------------------------------------------------------
26
-
27
- export interface ChatSessionConfig {
28
- /** DeepSeek API 兼容的服务地址,默认 https://api.deepseek.com/v1 */
29
- baseURL?: string;
30
- /** API Key(优先用 DEEPSEEK_API_KEY 环境变量) */
31
- apiKey?: string;
32
- /** 模型名称,默认 deepseek-chat */
33
- model?: string;
34
- }
35
-
36
- export interface ChatSessionOptions {
37
- /** 会话工作目录 */
38
- cwd?: string;
39
- /** 自定义系统提示词(会拼接到默认提示词之后) */
40
- systemPrompt?: string;
41
- }
42
-
43
- /**
44
- * 流式响应事件
45
- */
46
- export type ChatEvent =
47
- | { type: "text"; text: string; accumulated: string }
48
- | { type: "done"; text: string }
49
- | { type: "error"; message: string };
50
-
51
- // ---------------------------------------------------------------------------
52
- // ChatSession
53
- // ---------------------------------------------------------------------------
54
-
55
- /** 消息角色 */
56
- type MessageRole = "system" | "user" | "assistant" | "tool";
57
-
58
- /** 内部消息类型 */
59
- interface ChatMessage {
60
- role: MessageRole;
61
- content: string;
62
- }
63
-
64
- export class ChatSession {
65
- private model: any;
66
- private messages: ChatMessage[];
67
-
68
- constructor(
69
- config: ChatSessionConfig = {},
70
- options: ChatSessionOptions = {},
71
- ) {
72
- const apiKey = config.apiKey ?? process.env.DEEPSEEK_API_KEY;
73
- if (!apiKey) {
74
- throw new Error(
75
- "DEEPSEEK_API_KEY 未设置。请设置环境变量 DEEPSEEK_API_KEY 或传入 apiKey",
76
- );
77
- }
78
-
79
- const baseURL = config.baseURL ?? "https://api.deepseek.com/v1";
80
- const modelId = config.model ?? "deepseek-chat";
81
-
82
- // 动态 import 以支持 ESM 包在 CJS 环境下的加载(tsx 处理)
83
- const { createOpenAICompatible } = require("@ai-sdk/openai-compatible");
84
- const provider = createOpenAICompatible({
85
- name: "deepseek",
86
- baseURL,
87
- apiKey,
88
- });
89
- this.model = provider(modelId);
90
-
91
- // 构建系统提示词
92
- const systemContent = [SYSTEM_PROMPT];
93
- if (options?.systemPrompt) {
94
- systemContent.push("", options.systemPrompt);
95
- }
96
- if (options?.cwd) {
97
- systemContent.push("", `当前工作目录: ${options.cwd}`);
98
- }
99
-
100
- this.messages = [{ role: "system", content: systemContent.join("\n") }];
101
- }
102
-
103
- /**
104
- * 发送用户消息,返回异步可迭代的文本流。
105
- *
106
- * 使用方式:
107
- * ```typescript
108
- * const session = new ChatSession();
109
- * for await (const event of session.chat("帮我看看 package.json")) {
110
- * if (event.type === "text") process.stdout.write(event.text);
111
- * }
112
- * console.log("完成");
113
- * ```
114
- */
115
- async *chat(
116
- userMessage: string,
117
- signal?: AbortSignal,
118
- ): AsyncIterable<ChatEvent> {
119
- this.messages.push({ role: "user", content: userMessage });
120
-
121
- let fullText = "";
122
-
123
- try {
124
- const result = streamText({
125
- model: this.model,
126
- messages: this.messages as any,
127
- abortSignal: signal,
128
- });
129
-
130
- for await (const chunk of result.textStream) {
131
- fullText += chunk;
132
- yield { type: "text", text: chunk, accumulated: fullText };
133
- }
134
-
135
- this.messages.push({ role: "assistant", content: fullText });
136
- yield { type: "done", text: fullText };
137
- } catch (err) {
138
- const message = err instanceof Error ? err.message : String(err);
139
- if ((err as Error).name === "AbortError" || signal?.aborted) {
140
- // 被中断时,不保存不完整的助手消息
141
- if (fullText) {
142
- this.messages.push({ role: "assistant", content: fullText + "\n[已中断]" });
143
- }
144
- yield { type: "done", text: fullText };
145
- return;
146
- }
147
- yield { type: "error", message };
148
- throw err;
149
- }
150
- }
151
-
152
- /** 返回当前的会话历史(只读) */
153
- get history(): ReadonlyArray<ChatMessage> {
154
- return this.messages;
155
- }
156
-
157
- /** 返回当前轮数(不含 system 消息) */
158
- get turnCount(): number {
159
- return this.messages.length - 1;
160
- }
161
-
162
- /** 清空会话历史,保留 system 消息 */
163
- reset(): void {
164
- const system = this.messages[0];
165
- this.messages = [system];
166
- }
1
+ /**
2
+ * builtin/index.ts — ChatCCC 内置 Agent 核心 API
3
+ *
4
+ * ChatSession 是程序化入口,既可以被 CLI 调用,也可以被其他模块(如 ToolAdapter)调用。
5
+ */
6
+
7
+ import { streamText } from "ai";
8
+
9
+ // ---------------------------------------------------------------------------
10
+ // 系统提示词 — 编译期冻结常量
11
+ // ---------------------------------------------------------------------------
12
+
13
+ const SYSTEM_PROMPT = [
14
+ "你是 ChatCCC 内置 AI 编程助手,运行在终端环境中。",
15
+ "",
16
+ "## 基本规则",
17
+ "- 用中文回复,但代码、命令、文件名保持原文",
18
+ "- 优先给出直接可用的方案,而非长篇解释",
19
+ "- 如果用户的问题涉及代码,直接给出代码并说明用法",
20
+ "- 保持简洁,一次聚焦一个问题",
21
+ ].join("\n");
22
+
23
+ // ---------------------------------------------------------------------------
24
+ // 类型定义
25
+ // ---------------------------------------------------------------------------
26
+
27
+ export interface ChatSessionConfig {
28
+ /** DeepSeek API 兼容的服务地址,默认 https://api.deepseek.com/v1 */
29
+ baseURL?: string;
30
+ /** API Key(优先用 DEEPSEEK_API_KEY 环境变量) */
31
+ apiKey?: string;
32
+ /** 模型名称,默认 deepseek-chat */
33
+ model?: string;
34
+ }
35
+
36
+ export interface ChatSessionOptions {
37
+ /** 会话工作目录 */
38
+ cwd?: string;
39
+ /** 自定义系统提示词(会拼接到默认提示词之后) */
40
+ systemPrompt?: string;
41
+ }
42
+
43
+ /**
44
+ * 流式响应事件
45
+ */
46
+ export type ChatEvent =
47
+ | { type: "text"; text: string; accumulated: string }
48
+ | { type: "done"; text: string }
49
+ | { type: "error"; message: string };
50
+
51
+ // ---------------------------------------------------------------------------
52
+ // ChatSession
53
+ // ---------------------------------------------------------------------------
54
+
55
+ /** 消息角色 */
56
+ type MessageRole = "system" | "user" | "assistant" | "tool";
57
+
58
+ /** 内部消息类型 */
59
+ interface ChatMessage {
60
+ role: MessageRole;
61
+ content: string;
62
+ }
63
+
64
+ export class ChatSession {
65
+ private model: any;
66
+ private messages: ChatMessage[];
67
+
68
+ constructor(
69
+ config: ChatSessionConfig = {},
70
+ options: ChatSessionOptions = {},
71
+ ) {
72
+ const apiKey = config.apiKey ?? process.env.DEEPSEEK_API_KEY;
73
+ if (!apiKey) {
74
+ throw new Error(
75
+ "DEEPSEEK_API_KEY 未设置。请设置环境变量 DEEPSEEK_API_KEY 或传入 apiKey",
76
+ );
77
+ }
78
+
79
+ const baseURL = config.baseURL ?? "https://api.deepseek.com/v1";
80
+ const modelId = config.model ?? "deepseek-chat";
81
+
82
+ // 动态 import 以支持 ESM 包在 CJS 环境下的加载(tsx 处理)
83
+ const { createOpenAICompatible } = require("@ai-sdk/openai-compatible");
84
+ const provider = createOpenAICompatible({
85
+ name: "deepseek",
86
+ baseURL,
87
+ apiKey,
88
+ });
89
+ this.model = provider(modelId);
90
+
91
+ // 构建系统提示词
92
+ const systemContent = [SYSTEM_PROMPT];
93
+ if (options?.systemPrompt) {
94
+ systemContent.push("", options.systemPrompt);
95
+ }
96
+ if (options?.cwd) {
97
+ systemContent.push("", `当前工作目录: ${options.cwd}`);
98
+ }
99
+
100
+ this.messages = [{ role: "system", content: systemContent.join("\n") }];
101
+ }
102
+
103
+ /**
104
+ * 发送用户消息,返回异步可迭代的文本流。
105
+ *
106
+ * 使用方式:
107
+ * ```typescript
108
+ * const session = new ChatSession();
109
+ * for await (const event of session.chat("帮我看看 package.json")) {
110
+ * if (event.type === "text") process.stdout.write(event.text);
111
+ * }
112
+ * console.log("完成");
113
+ * ```
114
+ */
115
+ async *chat(
116
+ userMessage: string,
117
+ signal?: AbortSignal,
118
+ ): AsyncIterable<ChatEvent> {
119
+ this.messages.push({ role: "user", content: userMessage });
120
+
121
+ let fullText = "";
122
+
123
+ try {
124
+ const result = streamText({
125
+ model: this.model,
126
+ messages: this.messages as any,
127
+ abortSignal: signal,
128
+ });
129
+
130
+ for await (const chunk of result.textStream) {
131
+ fullText += chunk;
132
+ yield { type: "text", text: chunk, accumulated: fullText };
133
+ }
134
+
135
+ this.messages.push({ role: "assistant", content: fullText });
136
+ yield { type: "done", text: fullText };
137
+ } catch (err) {
138
+ const message = err instanceof Error ? err.message : String(err);
139
+ if ((err as Error).name === "AbortError" || signal?.aborted) {
140
+ // 被中断时,不保存不完整的助手消息
141
+ if (fullText) {
142
+ this.messages.push({ role: "assistant", content: fullText + "\n[已中断]" });
143
+ }
144
+ yield { type: "done", text: fullText };
145
+ return;
146
+ }
147
+ yield { type: "error", message };
148
+ throw err;
149
+ }
150
+ }
151
+
152
+ /** 返回当前的会话历史(只读) */
153
+ get history(): ReadonlyArray<ChatMessage> {
154
+ return this.messages;
155
+ }
156
+
157
+ /** 返回当前轮数(不含 system 消息) */
158
+ get turnCount(): number {
159
+ return this.messages.length - 1;
160
+ }
161
+
162
+ /** 清空会话历史,保留 system 消息 */
163
+ reset(): void {
164
+ const system = this.messages[0];
165
+ this.messages = [system];
166
+ }
167
167
  }
package/src/session.ts CHANGED
@@ -791,6 +791,44 @@ export async function runAgentSession(
791
791
  running: true,
792
792
  });
793
793
 
794
+ // 在覆盖 stream state 前,先终结上一轮的展示卡片并发送最终回复。
795
+ // 竞态根因:上一轮 finally 写入 "done" 状态后,200ms setTimeout 即启动
796
+ // 新一轮 runAgentSession,立即覆盖为 "running" 状态并 kill 旧 display
797
+ // loop。旧 loop 的 3s 间隔 tick 只有约 6.7% 概率在 200ms 窗口内命中,
798
+ // 导致 finalReply 丢失、完成卡片空白。此处主动读取上一轮终端状态完成
799
+ // 卡片终结和回复发送,不依赖 display loop 时序,保证"先发完上一个回答
800
+ // 再开始缓存问题对应的任务"。
801
+ const prevState = await readStreamState(sessionId);
802
+ if (prevState && prevState.status !== "running") {
803
+ const displayChatId = pickDisplayChat(sessionId);
804
+ if (displayChatId) {
805
+ const pp = platformForChat(displayChatId);
806
+ const display = displayCards.get(displayChatId);
807
+ const oldLoopExisted = displayLoops.has(sessionId);
808
+
809
+ if (display && pp) {
810
+ // 旧 display loop 被 kill 前来不及终结卡片 → 现在终结
811
+ while (display.cardBusy) await new Promise(r => setTimeout(r, 20));
812
+ const nextSeq = display.sequence + 1;
813
+ const headerTitle = prevState.status === "stopped" ? "已停止" : "完成";
814
+ const headerTemplate = prevState.status === "stopped" ? "red" : undefined;
815
+ const cardContent = prevState.accumulatedContent || " ";
816
+ const doneCard = buildProgressCard(cardContent, { showStop: false, headerTitle, headerTemplate });
817
+ await pp.cardUpdate(display.cardId, doneCard, nextSeq).catch(() => {});
818
+ displayCards.delete(displayChatId);
819
+
820
+ if (prevState.finalReply) {
821
+ await pp.sendText(displayChatId, prevState.finalReply).catch(() => {});
822
+ }
823
+ pp.setChatAvatar(displayChatId, prevState.tool, "idle").catch(() => {});
824
+ } else if (oldLoopExisted && pp && prevState.finalReply) {
825
+ // display loop 存在但尚未创建卡片(极快轮次),至少发送 finalReply
826
+ await pp.sendText(displayChatId, prevState.finalReply).catch(() => {});
827
+ }
828
+ // else: display loop 已自行终结(displayCards 无记录)→ 无需处理
829
+ }
830
+ }
831
+
794
832
  // 初始化 stream-state.json
795
833
  const initialState = createEmptyStreamState(sessionId, cwd, tool, nextTurnCount);
796
834
  await writeStreamState(initialState);