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.
@@ -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、displayLoops、sessionInfoMap、processedMessages)。
606
+ // 状态(activePrompts、sessionInfoMap、processedMessages)。
605
607
  // SDK 重连只是底层 WebSocket 抖动,业务层不受影响:
606
608
  // - 后台 generator 仍在跑、stream-state 仍在被写
607
609
  // - display loop 仍在向群推送
@@ -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
- /** displayLoops: sessionId 展示循环的 stop 函数 */
147
- export const displayLoops = new Map<string, () => void>();
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
- for (const stop of displayLoops.values()) stop();
205
- displayLoops.clear();
212
+ if (unifiedDisplayLoopHandle !== null) {
213
+ clearInterval(unifiedDisplayLoopHandle);
214
+ unifiedDisplayLoopHandle = null;
215
+ }
206
216
  }