chatccc 0.2.92 → 0.2.94
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/im-skills/wechat-file-skill/receive-send-file.md +38 -38
- package/im-skills/wechat-file-skill/send-file.mjs +83 -83
- package/im-skills/wechat-file-skill/skill.md +10 -10
- package/im-skills/wechat-image-skill/skill.md +10 -10
- package/im-skills/wechat-video-skill/receive-send-video.md +38 -38
- package/im-skills/wechat-video-skill/send-video.mjs +79 -79
- package/im-skills/wechat-video-skill/skill.md +10 -10
- package/package.json +1 -1
- package/scripts/postinstall-sharp-check.mjs +58 -58
- package/src/__tests__/session.test.ts +148 -6
- package/src/__tests__/stream-state.test.ts +30 -0
- package/src/builtin/cli.ts +196 -196
- package/src/builtin/index.ts +166 -166
- package/src/index.ts +3 -1
- package/src/orchestrator.ts +0 -10
- package/src/session-chat-binding.ts +14 -4
- package/src/session.ts +212 -209
- package/src/stream-state.ts +20 -1
- package/src/turn-cards.ts +117 -117
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
|
}
|
package/src/index.ts
CHANGED
|
@@ -92,6 +92,7 @@ import {
|
|
|
92
92
|
rebuildBindingsFromRegistry,
|
|
93
93
|
resetState,
|
|
94
94
|
setSessionPlatform,
|
|
95
|
+
startUnifiedDisplayLoop,
|
|
95
96
|
} from "./session.ts";
|
|
96
97
|
import {
|
|
97
98
|
rebuildSessionChatsFromRegistry,
|
|
@@ -595,13 +596,14 @@ async function startBotServiceCore(): Promise<void> {
|
|
|
595
596
|
// 进程首次启动:此时所有 Map 都是空的,resetState 主要是打个 LOG 标识"开始
|
|
596
597
|
// 干净状态"。修正残留的 running stream-state 并重建 session→chat 映射。
|
|
597
598
|
resetState();
|
|
599
|
+
startUnifiedDisplayLoop();
|
|
598
600
|
fixStaleStreamStates().then(async () => {
|
|
599
601
|
const registry = await loadSessionRegistryForBinding();
|
|
600
602
|
rebuildSessionChatsFromRegistry(registry);
|
|
601
603
|
}).catch((err) => console.error(`[${ts()}] Init bindings failed: ${(err as Error).message}`));
|
|
602
604
|
|
|
603
605
|
// ⚠️ 关键设计:onReady / onReconnected 只重建只读映射,**绝不**清空运行时
|
|
604
|
-
// 状态(activePrompts、
|
|
606
|
+
// 状态(activePrompts、sessionInfoMap、processedMessages)。
|
|
605
607
|
// SDK 重连只是底层 WebSocket 抖动,业务层不受影响:
|
|
606
608
|
// - 后台 generator 仍在跑、stream-state 仍在被写
|
|
607
609
|
// - display loop 仍在向群推送
|
package/src/orchestrator.ts
CHANGED
|
@@ -740,11 +740,6 @@ export async function handleCommand(
|
|
|
740
740
|
.setChatAvatar(chatId, descriptionTool, "new")
|
|
741
741
|
.catch(() => {});
|
|
742
742
|
|
|
743
|
-
if (isSessionRunning(newSessionId)) {
|
|
744
|
-
const { ensureDisplayLoop } = await import("./session.ts");
|
|
745
|
-
ensureDisplayLoop(newSessionId);
|
|
746
|
-
}
|
|
747
|
-
|
|
748
743
|
await platform.sendCard(
|
|
749
744
|
chatId,
|
|
750
745
|
`${toolLabel} Session Reset`,
|
|
@@ -901,11 +896,6 @@ export async function handleCommand(
|
|
|
901
896
|
|
|
902
897
|
platform.setChatAvatar(chatId, target.tool, "new").catch(() => {});
|
|
903
898
|
|
|
904
|
-
if (isSessionRunning(target.sessionId)) {
|
|
905
|
-
const { ensureDisplayLoop } = await import("./session.ts");
|
|
906
|
-
ensureDisplayLoop(target.sessionId);
|
|
907
|
-
}
|
|
908
|
-
|
|
909
899
|
const targetToolLabel = toolDisplayName(target.tool);
|
|
910
900
|
const busyNote = isSessionRunning(target.sessionId)
|
|
911
901
|
? "\n\n⚠️ 该会话当前正在生成中,请等待完成后再发送消息。"
|
|
@@ -130,6 +130,8 @@ export interface DisplayCardState {
|
|
|
130
130
|
cardCreatedAt: number;
|
|
131
131
|
lastSentContent: string;
|
|
132
132
|
streamErrorNotified: boolean;
|
|
133
|
+
/** 所属 session */
|
|
134
|
+
sessionId: string;
|
|
133
135
|
/** 所属 turn */
|
|
134
136
|
turnCount: number;
|
|
135
137
|
/** 卡片轮转时记录的基线,轮转后只展示增量 */
|
|
@@ -139,12 +141,18 @@ export interface DisplayCardState {
|
|
|
139
141
|
lastSentAccLen?: number;
|
|
140
142
|
/** WeChat delta: 上次发送时的 finalReply */
|
|
141
143
|
lastSentFinalReply?: string;
|
|
144
|
+
/** 点点点动画计数器(统一 display loop 每个卡片独立计数) */
|
|
145
|
+
dotCount: number;
|
|
142
146
|
}
|
|
143
147
|
|
|
144
148
|
export const displayCards = new Map<string, DisplayCardState>();
|
|
145
149
|
|
|
146
|
-
/**
|
|
147
|
-
export
|
|
150
|
+
/** 统一 display loop 的 interval handle,由 session.ts 管理 */
|
|
151
|
+
export let unifiedDisplayLoopHandle: ReturnType<typeof setInterval> | null = null;
|
|
152
|
+
|
|
153
|
+
export function setUnifiedDisplayLoopHandle(h: ReturnType<typeof setInterval> | null): void {
|
|
154
|
+
unifiedDisplayLoopHandle = h;
|
|
155
|
+
}
|
|
148
156
|
|
|
149
157
|
// ---------------------------------------------------------------------------
|
|
150
158
|
// queuedMessages: sessionId → 缓存消息(生成中排队,队列最大长度 1)
|
|
@@ -201,6 +209,8 @@ export function resetBindingState(): void {
|
|
|
201
209
|
activePrompts.clear();
|
|
202
210
|
queuedMessages.clear();
|
|
203
211
|
displayCards.clear();
|
|
204
|
-
|
|
205
|
-
|
|
212
|
+
if (unifiedDisplayLoopHandle !== null) {
|
|
213
|
+
clearInterval(unifiedDisplayLoopHandle);
|
|
214
|
+
unifiedDisplayLoopHandle = null;
|
|
215
|
+
}
|
|
206
216
|
}
|