chatccc 0.2.89 → 0.2.90
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 +1 -1
- package/scripts/postinstall-sharp-check.mjs +58 -58
- package/src/builtin/cli.ts +196 -196
- package/src/builtin/index.ts +166 -166
- package/src/session-chat-binding.ts +2 -2
- package/src/session.ts +45 -31
- package/src/turn-cards.ts +118 -0
package/package.json
CHANGED
|
@@ -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
|
}
|
package/src/builtin/cli.ts
CHANGED
|
@@ -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
|
});
|
package/src/builtin/index.ts
CHANGED
|
@@ -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,6 +818,10 @@ 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
|
}
|
|
@@ -833,6 +838,31 @@ export async function runAgentSession(
|
|
|
833
838
|
const initialState = createEmptyStreamState(sessionId, cwd, tool, nextTurnCount);
|
|
834
839
|
await writeStreamState(initialState);
|
|
835
840
|
|
|
841
|
+
// 为新 turn 创建第一张展示卡片,同时注册到 turn-cards 持久化。
|
|
842
|
+
// 放在 ensureDisplayLoop 之前,确保卡片立即可见(不等 3s 首次 tick)。
|
|
843
|
+
const displayChatIdForNew = pickDisplayChat(sessionId);
|
|
844
|
+
if (displayChatIdForNew) {
|
|
845
|
+
const ppNew = platformForChat(displayChatIdForNew);
|
|
846
|
+
if (ppNew && ppNew.kind !== "wechat") {
|
|
847
|
+
const cardId = await ppNew.cardCreate(
|
|
848
|
+
buildProgressCard("", { showStop: true, headerTitle: "生成中..." }),
|
|
849
|
+
).catch(() => null);
|
|
850
|
+
if (cardId) {
|
|
851
|
+
await ppNew.cardSend(displayChatIdForNew, cardId).catch(() => null);
|
|
852
|
+
displayCards.set(displayChatIdForNew, {
|
|
853
|
+
cardId,
|
|
854
|
+
sequence: 1,
|
|
855
|
+
cardBusy: false,
|
|
856
|
+
cardCreatedAt: Date.now(),
|
|
857
|
+
lastSentContent: "",
|
|
858
|
+
streamErrorNotified: false,
|
|
859
|
+
turnCount: nextTurnCount,
|
|
860
|
+
});
|
|
861
|
+
addCardToTurn(sessionId, nextTurnCount, cardId).catch(() => {});
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
|
|
836
866
|
// 启动 display loop
|
|
837
867
|
ensureDisplayLoop(sessionId);
|
|
838
868
|
|
|
@@ -1078,6 +1108,8 @@ export function ensureDisplayLoop(sessionId: string): void {
|
|
|
1078
1108
|
const cardContent = state.accumulatedContent || " ";
|
|
1079
1109
|
const doneCard = buildProgressCard(cardContent, { showStop: false, headerTitle, headerTemplate });
|
|
1080
1110
|
await p.cardUpdate(display.cardId, doneCard, nextSeq).catch(() => {});
|
|
1111
|
+
const finalSt = state.status === "stopped" ? "stopped" : "done";
|
|
1112
|
+
finalizeTurnCards(sessionId, state.turnCount, finalSt).catch(() => {});
|
|
1081
1113
|
displayCards.delete(chatId);
|
|
1082
1114
|
}
|
|
1083
1115
|
if (state.finalReply) {
|
|
@@ -1144,6 +1176,7 @@ export function ensureDisplayLoop(sessionId: string): void {
|
|
|
1144
1176
|
} else {
|
|
1145
1177
|
// 非 WeChat: 卡片流程
|
|
1146
1178
|
if (!display) {
|
|
1179
|
+
// 兜底:runAgentSession 应已创建卡片,若没有则补建
|
|
1147
1180
|
const cardId = await p.cardCreate(buildProgressCard("", { showStop: true, headerTitle: "生成中..." })).catch(() => null);
|
|
1148
1181
|
if (cardId) {
|
|
1149
1182
|
await p.cardSend(chatId, cardId).catch(() => null);
|
|
@@ -1154,57 +1187,38 @@ export function ensureDisplayLoop(sessionId: string): void {
|
|
|
1154
1187
|
cardCreatedAt: Date.now(),
|
|
1155
1188
|
lastSentContent: "",
|
|
1156
1189
|
streamErrorNotified: false,
|
|
1157
|
-
|
|
1190
|
+
turnCount: state.turnCount,
|
|
1158
1191
|
});
|
|
1192
|
+
addCardToTurn(sessionId, state.turnCount, cardId).catch(() => {});
|
|
1159
1193
|
}
|
|
1160
1194
|
} else {
|
|
1161
1195
|
if (display.cardBusy) return;
|
|
1162
1196
|
|
|
1163
|
-
//
|
|
1164
|
-
//
|
|
1165
|
-
//
|
|
1166
|
-
if (display.
|
|
1167
|
-
display.
|
|
1168
|
-
|
|
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
|
-
}
|
|
1197
|
+
// 守卫:display 归属的 turn 与当前 stream state 不一致。
|
|
1198
|
+
// 正常流程 runAgentSession 会终结旧 turn 并建新卡,
|
|
1199
|
+
// 此处仅兜底处理异常情况(如进程重启后 display 残留)。
|
|
1200
|
+
if (display.turnCount !== state.turnCount) {
|
|
1201
|
+
console.log(`[${ts()}] [DISPLAY] turn mismatch for ${chatId}: display.turnCount=${display.turnCount} state.turnCount=${state.turnCount}, resetting`);
|
|
1202
|
+
displayCards.delete(chatId);
|
|
1189
1203
|
return;
|
|
1190
1204
|
}
|
|
1191
1205
|
|
|
1192
|
-
// 更新当前 turn 的 lastTurnCount
|
|
1193
|
-
display.lastTurnCount = state.turnCount;
|
|
1194
|
-
|
|
1195
1206
|
// 卡片轮转
|
|
1196
1207
|
if (Date.now() - display.cardCreatedAt > CARD_ROTATE_MS) {
|
|
1197
1208
|
display.cardBusy = true;
|
|
1198
1209
|
try {
|
|
1199
1210
|
const oldSeqBase = display.sequence;
|
|
1200
|
-
const
|
|
1211
|
+
const oldContent = state.accumulatedContent + state.finalReply;
|
|
1212
|
+
const oldCard = buildProgressCard(truncateContent(oldContent) || " ", { showStop: false, headerTitle: "完成" });
|
|
1201
1213
|
await p.cardUpdate(display.cardId, oldCard, oldSeqBase + 1).catch(() => {});
|
|
1214
|
+
markCardDone(sessionId, display.turnCount, display.cardId).catch(() => {});
|
|
1202
1215
|
const newCardId = await p.cardCreate(buildProgressCard(
|
|
1203
1216
|
"",
|
|
1204
1217
|
{ showStop: true, headerTitle: "生成中..." },
|
|
1205
1218
|
));
|
|
1206
1219
|
if (newCardId) {
|
|
1207
1220
|
await p.cardSend(chatId, newCardId);
|
|
1221
|
+
addCardToTurn(sessionId, display.turnCount, newCardId).catch(() => {});
|
|
1208
1222
|
display.cardId = newCardId;
|
|
1209
1223
|
display.sequence = 1;
|
|
1210
1224
|
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
|
+
}
|