chatccc 0.2.68 → 0.2.69
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 +3 -1
- package/src/builtin/cli.ts +197 -0
- package/src/builtin/index.ts +167 -0
- package/src/orchestrator.ts +3 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "chatccc",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.69",
|
|
4
4
|
"description": "Feishu bot bridge for Claude Code",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -37,9 +37,11 @@
|
|
|
37
37
|
"test:watch": "vitest"
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
+
"@ai-sdk/openai-compatible": "^2.0.47",
|
|
40
41
|
"@anthropic-ai/claude-agent-sdk": "0.2.133",
|
|
41
42
|
"@larksuiteoapi/node-sdk": "^1.59.0",
|
|
42
43
|
"@openilink/openilink-sdk-node": "^0.6.0",
|
|
44
|
+
"ai": "^6.0.184",
|
|
43
45
|
"nodemailer": "^8.0.7",
|
|
44
46
|
"qrcode-terminal": "^0.12.0",
|
|
45
47
|
"sharp": "^0.34.5",
|
|
@@ -0,0 +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);
|
|
197
|
+
});
|
|
@@ -0,0 +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
|
+
}
|
|
167
|
+
}
|
package/src/orchestrator.ts
CHANGED
|
@@ -295,8 +295,9 @@ export async function handleCommand(
|
|
|
295
295
|
const cwd = sessionCwd;
|
|
296
296
|
const initialName = sessionChatName("新会话", cwd);
|
|
297
297
|
|
|
298
|
-
//
|
|
299
|
-
|
|
298
|
+
// 微信私聊:不创建群,直接绑定 session 到当前私聊
|
|
299
|
+
// 飞书私聊:也要建群
|
|
300
|
+
if (chatType === "p2p" && platform.kind === "wechat") {
|
|
300
301
|
// 先解绑旧 session(如果存在),避免旧 session 的 display loop
|
|
301
302
|
// 继续往同一个 chat 推送内容(/newh 走 switchChatBinding 已有此逻辑,
|
|
302
303
|
// 但 /new p2p 之前遗漏了解绑)。
|