chatccc 0.2.89 → 0.2.91

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,6 +1,6 @@
1
1
  {
2
2
  "name": "chatccc",
3
- "version": "0.2.89",
3
+ "version": "0.2.91",
4
4
  "description": "Feishu bot bridge for Claude Code",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -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
  }
@@ -130,6 +130,8 @@ export interface DisplayCardState {
130
130
  cardCreatedAt: number;
131
131
  lastSentContent: string;
132
132
  streamErrorNotified: boolean;
133
+ /** 所属 turn */
134
+ turnCount: number;
133
135
  /** 卡片轮转时记录的基线,轮转后只展示增量 */
134
136
  rotationAccLen?: number;
135
137
  rotationFinalReply?: string;
@@ -137,8 +139,6 @@ export interface DisplayCardState {
137
139
  lastSentAccLen?: number;
138
140
  /** WeChat delta: 上次发送时的 finalReply */
139
141
  lastSentFinalReply?: string;
140
- /** 上次 loop 读取到的 turnCount,用于检测轮次切换 */
141
- lastTurnCount?: number;
142
142
  }
143
143
 
144
144
  export const displayCards = new Map<string, DisplayCardState>();
package/src/session.ts CHANGED
@@ -38,6 +38,7 @@ function compressWechatDisplayText(text: string): string {
38
38
  return [...lines.slice(0, 5), "...", ...lines.slice(-5)].join("\n");
39
39
  }
40
40
  import { readStreamState, writeStreamState, createEmptyStreamState, fixStaleStreamStates } from "./stream-state.ts";
41
+ import { addCardToTurn, finalizeTurnCards, markCardDone } from "./turn-cards.ts";
41
42
  import {
42
43
  bindChatToSession,
43
44
  unbindChatFromSession,
@@ -817,12 +818,18 @@ export async function runAgentSession(
817
818
  await pp.cardUpdate(display.cardId, doneCard, nextSeq).catch(() => {});
818
819
  displayCards.delete(displayChatId);
819
820
 
821
+ // 持久化:标记上一轮所有卡片为终态
822
+ const finalStatus = prevState.status === "stopped" ? "stopped" : "done";
823
+ finalizeTurnCards(sessionId, prevState.turnCount, finalStatus).catch(() => {});
824
+
820
825
  if (prevState.finalReply) {
821
826
  await pp.sendText(displayChatId, prevState.finalReply).catch(() => {});
822
827
  }
823
828
  pp.setChatAvatar(displayChatId, prevState.tool, "idle").catch(() => {});
824
829
  } else if (oldLoopExisted && pp && prevState.finalReply) {
825
830
  // display loop 存在但尚未创建卡片(极快轮次),至少发送 finalReply
831
+ // 同时持久化终结旧 turn 卡片:旧 loop 的 mismatch 守卫可能已删 display entry
832
+ finalizeTurnCards(sessionId, prevState.turnCount, finalStatus).catch(() => {});
826
833
  await pp.sendText(displayChatId, prevState.finalReply).catch(() => {});
827
834
  }
828
835
  // else: display loop 已自行终结(displayCards 无记录)→ 无需处理
@@ -833,6 +840,31 @@ export async function runAgentSession(
833
840
  const initialState = createEmptyStreamState(sessionId, cwd, tool, nextTurnCount);
834
841
  await writeStreamState(initialState);
835
842
 
843
+ // 为新 turn 创建第一张展示卡片,同时注册到 turn-cards 持久化。
844
+ // 放在 ensureDisplayLoop 之前,确保卡片立即可见(不等 3s 首次 tick)。
845
+ const displayChatIdForNew = pickDisplayChat(sessionId);
846
+ if (displayChatIdForNew) {
847
+ const ppNew = platformForChat(displayChatIdForNew);
848
+ if (ppNew && ppNew.kind !== "wechat") {
849
+ const cardId = await ppNew.cardCreate(
850
+ buildProgressCard("", { showStop: true, headerTitle: "生成中..." }),
851
+ ).catch(() => null);
852
+ if (cardId) {
853
+ await ppNew.cardSend(displayChatIdForNew, cardId).catch(() => null);
854
+ displayCards.set(displayChatIdForNew, {
855
+ cardId,
856
+ sequence: 1,
857
+ cardBusy: false,
858
+ cardCreatedAt: Date.now(),
859
+ lastSentContent: "",
860
+ streamErrorNotified: false,
861
+ turnCount: nextTurnCount,
862
+ });
863
+ addCardToTurn(sessionId, nextTurnCount, cardId).catch(() => {});
864
+ }
865
+ }
866
+ }
867
+
836
868
  // 启动 display loop
837
869
  ensureDisplayLoop(sessionId);
838
870
 
@@ -1078,6 +1110,8 @@ export function ensureDisplayLoop(sessionId: string): void {
1078
1110
  const cardContent = state.accumulatedContent || " ";
1079
1111
  const doneCard = buildProgressCard(cardContent, { showStop: false, headerTitle, headerTemplate });
1080
1112
  await p.cardUpdate(display.cardId, doneCard, nextSeq).catch(() => {});
1113
+ const finalSt = state.status === "stopped" ? "stopped" : "done";
1114
+ finalizeTurnCards(sessionId, state.turnCount, finalSt).catch(() => {});
1081
1115
  displayCards.delete(chatId);
1082
1116
  }
1083
1117
  if (state.finalReply) {
@@ -1144,6 +1178,7 @@ export function ensureDisplayLoop(sessionId: string): void {
1144
1178
  } else {
1145
1179
  // 非 WeChat: 卡片流程
1146
1180
  if (!display) {
1181
+ // 兜底:runAgentSession 应已创建卡片,若没有则补建
1147
1182
  const cardId = await p.cardCreate(buildProgressCard("", { showStop: true, headerTitle: "生成中..." })).catch(() => null);
1148
1183
  if (cardId) {
1149
1184
  await p.cardSend(chatId, cardId).catch(() => null);
@@ -1154,57 +1189,41 @@ export function ensureDisplayLoop(sessionId: string): void {
1154
1189
  cardCreatedAt: Date.now(),
1155
1190
  lastSentContent: "",
1156
1191
  streamErrorNotified: false,
1157
- lastTurnCount: state.turnCount,
1192
+ turnCount: state.turnCount,
1158
1193
  });
1194
+ addCardToTurn(sessionId, state.turnCount, cardId).catch(() => {});
1159
1195
  }
1160
1196
  } else {
1161
1197
  if (display.cardBusy) return;
1162
1198
 
1163
- // 检测轮次切换:turnCount 变化说明上一轮已完成、本 tick 是新轮,
1164
- // 但上一轮的 display 卡片因 setImmediate/setTimeout 时序问题
1165
- // 还没被终结分支清除。此处主动终结旧卡、创建新卡。
1166
- if (display.lastTurnCount !== undefined && display.lastTurnCount !== state.turnCount) {
1167
- display.cardBusy = true;
1168
- try {
1169
- const doneSeq = display.sequence + 1;
1170
- const doneCard = buildProgressCard("", { showStop: false, headerTitle: "完成" });
1171
- await p.cardUpdate(display.cardId, doneCard, doneSeq).catch(() => {});
1172
- displayCards.delete(chatId);
1173
- } catch (_) { /* ignore */ }
1174
- display.cardBusy = false;
1175
- // 新建卡片
1176
- const cardId = await p.cardCreate(buildProgressCard("", { showStop: true, headerTitle: "生成中..." })).catch(() => null);
1177
- if (cardId) {
1178
- await p.cardSend(chatId, cardId).catch(() => null);
1179
- displayCards.set(chatId, {
1180
- cardId,
1181
- sequence: 1,
1182
- cardBusy: false,
1183
- cardCreatedAt: Date.now(),
1184
- lastSentContent: "",
1185
- streamErrorNotified: false,
1186
- lastTurnCount: state.turnCount,
1187
- });
1188
- }
1199
+ // 守卫:display 归属的 turn 与当前 stream state 不一致。
1200
+ // 正常流程 runAgentSession 会终结旧 turn 并建新卡,
1201
+ // 此处仅兜底处理异常情况(如进程重启后 display 残留)。
1202
+ if (display.turnCount !== state.turnCount) {
1203
+ console.log(`[${ts()}] [DISPLAY] turn mismatch for ${chatId}: display.turnCount=${display.turnCount} state.turnCount=${state.turnCount}, resetting`);
1204
+ // 兜底:turn mismatch 意味着旧 turn 已结束但卡片未在飞书端终结,
1205
+ // 持久化标记为 done,避免 turn-cards.json 残留 "active"
1206
+ finalizeTurnCards(sessionId, display.turnCount, "done").catch(() => {});
1207
+ displayCards.delete(chatId);
1189
1208
  return;
1190
1209
  }
1191
1210
 
1192
- // 更新当前 turn 的 lastTurnCount
1193
- display.lastTurnCount = state.turnCount;
1194
-
1195
1211
  // 卡片轮转
1196
1212
  if (Date.now() - display.cardCreatedAt > CARD_ROTATE_MS) {
1197
1213
  display.cardBusy = true;
1198
1214
  try {
1199
1215
  const oldSeqBase = display.sequence;
1200
- const oldCard = buildProgressCard("", { showStop: false, headerTitle: "完成" });
1216
+ const oldContent = state.accumulatedContent + state.finalReply;
1217
+ const oldCard = buildProgressCard(truncateContent(oldContent) || " ", { showStop: false, headerTitle: "完成" });
1201
1218
  await p.cardUpdate(display.cardId, oldCard, oldSeqBase + 1).catch(() => {});
1219
+ markCardDone(sessionId, display.turnCount, display.cardId).catch(() => {});
1202
1220
  const newCardId = await p.cardCreate(buildProgressCard(
1203
1221
  "",
1204
1222
  { showStop: true, headerTitle: "生成中..." },
1205
1223
  ));
1206
1224
  if (newCardId) {
1207
1225
  await p.cardSend(chatId, newCardId);
1226
+ addCardToTurn(sessionId, display.turnCount, newCardId).catch(() => {});
1208
1227
  display.cardId = newCardId;
1209
1228
  display.sequence = 1;
1210
1229
  display.cardCreatedAt = Date.now();
@@ -0,0 +1,118 @@
1
+ import { readFile, writeFile, mkdir } from "node:fs/promises";
2
+ import { dirname, join } from "node:path";
3
+
4
+ import { USER_DATA_DIR, ts } from "./config.ts";
5
+
6
+ // ---------------------------------------------------------------------------
7
+ // turn-cards.json — 每个 session 的卡片归属持久化
8
+ // 记录每个 turn 有哪些卡片(轮转会产生多张),以及每张卡片的状态
9
+ // ---------------------------------------------------------------------------
10
+
11
+ export const TURNS_DIR = join(USER_DATA_DIR, "state", "turns");
12
+
13
+ export interface TurnCardEntry {
14
+ cardId: string;
15
+ status: "active" | "done" | "stopped";
16
+ createdAt: number;
17
+ }
18
+
19
+ export interface TurnCardsFile {
20
+ sessionId: string;
21
+ turns: { turnCount: number; cards: TurnCardEntry[] }[];
22
+ }
23
+
24
+ function getTurnCardsPath(sessionId: string): string {
25
+ return join(TURNS_DIR, `${sessionId}.json`);
26
+ }
27
+
28
+ export function createEmptyTurnCards(sessionId: string): TurnCardsFile {
29
+ return { sessionId, turns: [] };
30
+ }
31
+
32
+ export async function readTurnCards(sessionId: string): Promise<TurnCardsFile | null> {
33
+ try {
34
+ const raw = await readFile(getTurnCardsPath(sessionId), "utf-8");
35
+ const parsed = JSON.parse(raw) as TurnCardsFile;
36
+ if (parsed && typeof parsed.sessionId === "string" && Array.isArray(parsed.turns)) {
37
+ return parsed;
38
+ }
39
+ return null;
40
+ } catch {
41
+ return null;
42
+ }
43
+ }
44
+
45
+ export async function writeTurnCards(sessionId: string, data: TurnCardsFile): Promise<void> {
46
+ try {
47
+ const filePath = getTurnCardsPath(sessionId);
48
+ await mkdir(dirname(filePath), { recursive: true });
49
+ await writeFile(filePath, JSON.stringify(data, null, 2), "utf-8");
50
+ } catch (err) {
51
+ console.error(`[${ts()}] Failed to write turn-cards for ${sessionId}: ${(err as Error).message}`);
52
+ }
53
+ }
54
+
55
+ /** 向指定 turn 追加一张卡片并持久化。若 turn 不存在则创建。 */
56
+ export async function addCardToTurn(
57
+ sessionId: string,
58
+ turnCount: number,
59
+ cardId: string,
60
+ ): Promise<void> {
61
+ const data = (await readTurnCards(sessionId)) ?? createEmptyTurnCards(sessionId);
62
+ let turn = data.turns.find(t => t.turnCount === turnCount);
63
+ if (!turn) {
64
+ turn = { turnCount, cards: [] };
65
+ data.turns.push(turn);
66
+ }
67
+ turn.cards.push({ cardId, status: "active", createdAt: Date.now() });
68
+ await writeTurnCards(sessionId, data);
69
+ }
70
+
71
+ /** 将指定 turn 的所有 active 卡片标记为终态并持久化。返回被标记的 cardId 列表。 */
72
+ export async function finalizeTurnCards(
73
+ sessionId: string,
74
+ turnCount: number,
75
+ finalStatus: "done" | "stopped",
76
+ ): Promise<string[]> {
77
+ const data = await readTurnCards(sessionId);
78
+ if (!data) return [];
79
+ const turn = data.turns.find(t => t.turnCount === turnCount);
80
+ if (!turn) return [];
81
+ const finalized: string[] = [];
82
+ for (const card of turn.cards) {
83
+ if (card.status === "active") {
84
+ card.status = finalStatus;
85
+ finalized.push(card.cardId);
86
+ }
87
+ }
88
+ if (finalized.length > 0) {
89
+ await writeTurnCards(sessionId, data);
90
+ }
91
+ return finalized;
92
+ }
93
+
94
+ /** 将指定 turn 中的一张 active 卡片标记为 done(轮转时用),并持久化。 */
95
+ export async function markCardDone(
96
+ sessionId: string,
97
+ turnCount: number,
98
+ cardId: string,
99
+ ): Promise<void> {
100
+ const data = await readTurnCards(sessionId);
101
+ if (!data) return;
102
+ const turn = data.turns.find(t => t.turnCount === turnCount);
103
+ if (!turn) return;
104
+ const card = turn.cards.find(c => c.cardId === cardId && c.status === "active");
105
+ if (!card) return;
106
+ card.status = "done";
107
+ await writeTurnCards(sessionId, data);
108
+ }
109
+
110
+ /** 清理 session 的 turn-cards 文件 */
111
+ export async function removeTurnCards(sessionId: string): Promise<void> {
112
+ try {
113
+ const { unlink } = await import("node:fs/promises");
114
+ await unlink(getTurnCardsPath(sessionId));
115
+ } catch {
116
+ // 文件不存在,忽略
117
+ }
118
+ }