chatccc 0.2.94 → 0.2.95
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 +91 -1
- package/src/__tests__/stream-state.test.ts +42 -42
- package/src/adapters/adapter-interface.ts +13 -0
- package/src/adapters/codex-adapter.ts +4 -0
- package/src/adapters/cursor-adapter.ts +4 -0
- package/src/builtin/cli.ts +196 -196
- package/src/builtin/index.ts +166 -166
- package/src/session-chat-binding.ts +9 -0
- package/src/session.ts +148 -12
- package/src/stream-state.ts +33 -33
- 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
|
}
|
|
@@ -65,6 +65,12 @@ export interface ActivePrompt {
|
|
|
65
65
|
controller: AbortController;
|
|
66
66
|
stopped: boolean;
|
|
67
67
|
startTime: number;
|
|
68
|
+
/** Root PID for the CLI process currently serving this prompt, if the adapter exposes one. */
|
|
69
|
+
processPid?: number;
|
|
70
|
+
processMonitor?: ReturnType<typeof setInterval>;
|
|
71
|
+
/** Set when the watchdog detects that the CLI process disappeared before stream finalization. */
|
|
72
|
+
abnormalExit?: boolean;
|
|
73
|
+
abnormalExitNotified?: boolean;
|
|
68
74
|
}
|
|
69
75
|
|
|
70
76
|
export const activePrompts = new Map<string, ActivePrompt>();
|
|
@@ -206,6 +212,9 @@ export function consumeQueuedMessage(platform: PlatformAdapter, msg: QueuedMessa
|
|
|
206
212
|
export function resetBindingState(): void {
|
|
207
213
|
sessionChatsMap.clear();
|
|
208
214
|
lastActiveChatMap.clear();
|
|
215
|
+
for (const prompt of activePrompts.values()) {
|
|
216
|
+
if (prompt.processMonitor) clearInterval(prompt.processMonitor);
|
|
217
|
+
}
|
|
209
218
|
activePrompts.clear();
|
|
210
219
|
queuedMessages.clear();
|
|
211
220
|
displayCards.clear();
|
package/src/session.ts
CHANGED
|
@@ -25,6 +25,7 @@ import { simplifyToolUse, simplifyToolResult } from "./simplify.ts";
|
|
|
25
25
|
import { logTrace } from "./trace.ts";
|
|
26
26
|
import type { UnifiedBlock } from "./adapters/adapter-interface.ts";
|
|
27
27
|
import type { ToolAdapter } from "./adapters/adapter-interface.ts";
|
|
28
|
+
import type { ToolProcessInfo } from "./adapters/adapter-interface.ts";
|
|
28
29
|
import { createClaudeAdapter } from "./adapters/claude-adapter.ts";
|
|
29
30
|
import { createCursorAdapter } from "./adapters/cursor-adapter.ts";
|
|
30
31
|
import { createCodexAdapter } from "./adapters/codex-adapter.ts";
|
|
@@ -114,6 +115,110 @@ function platformForChat(chatId: string): PlatformAdapter | null {
|
|
|
114
115
|
return chatPlatformMap.get(chatId) ?? platformRef;
|
|
115
116
|
}
|
|
116
117
|
|
|
118
|
+
const DEFAULT_PROCESS_MONITOR_INTERVAL_MS = 5000;
|
|
119
|
+
let processMonitorIntervalMs = DEFAULT_PROCESS_MONITOR_INTERVAL_MS;
|
|
120
|
+
let isProcessAliveImpl = (pid: number): boolean => {
|
|
121
|
+
try {
|
|
122
|
+
process.kill(pid, 0);
|
|
123
|
+
return true;
|
|
124
|
+
} catch {
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
export function _setProcessAliveForTest(impl: (pid: number) => boolean): void {
|
|
130
|
+
isProcessAliveImpl = impl;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function _resetProcessAliveForTest(): void {
|
|
134
|
+
isProcessAliveImpl = (pid: number): boolean => {
|
|
135
|
+
try {
|
|
136
|
+
process.kill(pid, 0);
|
|
137
|
+
return true;
|
|
138
|
+
} catch {
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function _setProcessMonitorIntervalForTest(ms: number): void {
|
|
145
|
+
processMonitorIntervalMs = ms;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function _resetProcessMonitorIntervalForTest(): void {
|
|
149
|
+
processMonitorIntervalMs = DEFAULT_PROCESS_MONITOR_INTERVAL_MS;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function clearPromptProcessMonitor(sessionId: string): void {
|
|
153
|
+
const prompt = activePrompts.get(sessionId);
|
|
154
|
+
if (!prompt?.processMonitor) return;
|
|
155
|
+
clearInterval(prompt.processMonitor);
|
|
156
|
+
prompt.processMonitor = undefined;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function formatTerminalHeader(status: "running" | "done" | "stopped" | "error"): {
|
|
160
|
+
title: string;
|
|
161
|
+
template?: string;
|
|
162
|
+
} {
|
|
163
|
+
if (status === "stopped") return { title: "已停止", template: "red" };
|
|
164
|
+
if (status === "error") return { title: "异常结束", template: "red" };
|
|
165
|
+
return { title: "完成" };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function turnFinalStatus(status: "running" | "done" | "stopped" | "error"): "done" | "stopped" {
|
|
169
|
+
return status === "stopped" || status === "error" ? "stopped" : "done";
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function startPromptProcessMonitor(sessionId: string, info: ToolProcessInfo): void {
|
|
173
|
+
const prompt = activePrompts.get(sessionId);
|
|
174
|
+
if (!prompt) return;
|
|
175
|
+
prompt.processPid = info.pid;
|
|
176
|
+
clearPromptProcessMonitor(sessionId);
|
|
177
|
+
|
|
178
|
+
const check = async () => {
|
|
179
|
+
const current = activePrompts.get(sessionId);
|
|
180
|
+
if (!current || current !== prompt) {
|
|
181
|
+
clearPromptProcessMonitor(sessionId);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
if (current.stopped || current.abnormalExit) return;
|
|
185
|
+
if (isProcessAliveImpl(info.pid)) return;
|
|
186
|
+
|
|
187
|
+
current.abnormalExit = true;
|
|
188
|
+
clearPromptProcessMonitor(sessionId);
|
|
189
|
+
|
|
190
|
+
const state = await readStreamState(sessionId);
|
|
191
|
+
if (state?.status === "running") {
|
|
192
|
+
await writeStreamState({
|
|
193
|
+
...state,
|
|
194
|
+
status: "error",
|
|
195
|
+
updatedAt: Date.now(),
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const chatId = pickDisplayChat(sessionId) ?? getLastActiveChat(sessionId) ?? getChatsForSession(sessionId)[0];
|
|
200
|
+
const p = chatId ? platformForChat(chatId) : null;
|
|
201
|
+
if (chatId && p && !current.abnormalExitNotified) {
|
|
202
|
+
current.abnormalExitNotified = true;
|
|
203
|
+
await p.sendText(
|
|
204
|
+
chatId,
|
|
205
|
+
`⚠️ 进程异常结束:session ${sessionId} 对应的 CLI 进程 PID ${info.pid} 已不存在,已按完成处理。若回复不完整,请重新发送上一条指令。`,
|
|
206
|
+
).catch(() => {});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// 主动关闭 readline,让 runAgentSession 的 finally 落盘 error 终态并清理 activePrompts。
|
|
210
|
+
current.controller.abort();
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
const handle = setInterval(() => {
|
|
214
|
+
void check().catch((err) => {
|
|
215
|
+
console.warn(`[${ts()}] [PROCESS-MONITOR] check failed for ${sessionId}: ${(err as Error).message}`);
|
|
216
|
+
});
|
|
217
|
+
}, processMonitorIntervalMs);
|
|
218
|
+
handle.unref?.();
|
|
219
|
+
prompt.processMonitor = handle;
|
|
220
|
+
}
|
|
221
|
+
|
|
117
222
|
export function _getPlatformForChatForTest(chatId: string): PlatformAdapter | null {
|
|
118
223
|
return platformForChat(chatId);
|
|
119
224
|
}
|
|
@@ -189,6 +294,9 @@ export function resetState(): void {
|
|
|
189
294
|
processedMessages.clear();
|
|
190
295
|
lastMsgTimestamps.clear();
|
|
191
296
|
chatPlatformMap.clear();
|
|
297
|
+
for (const prompt of activePrompts.values()) {
|
|
298
|
+
if (prompt.processMonitor) clearInterval(prompt.processMonitor);
|
|
299
|
+
}
|
|
192
300
|
activePrompts.clear();
|
|
193
301
|
displayCards.clear();
|
|
194
302
|
stopUnifiedDisplayLoop();
|
|
@@ -833,13 +941,12 @@ export async function runAgentSession(
|
|
|
833
941
|
// terminal state,发送了 finalReply 并删除/替换了 displayCards 条目。
|
|
834
942
|
// 通过引用比较检测——若统一 loop 已处理则只补持久化和头像,不重复发。
|
|
835
943
|
if (displayCards.get(displayChatId) !== display) {
|
|
836
|
-
const finalStatus = prevState.status
|
|
944
|
+
const finalStatus = turnFinalStatus(prevState.status);
|
|
837
945
|
finalizeTurnCards(sessionId, prevState.turnCount, finalStatus).catch(() => {});
|
|
838
946
|
pp.setChatAvatar(displayChatId, prevState.tool, "idle").catch(() => {});
|
|
839
947
|
} else {
|
|
840
948
|
const nextSeq = display.sequence + 1;
|
|
841
|
-
const headerTitle = prevState.status
|
|
842
|
-
const headerTemplate = prevState.status === "stopped" ? "red" : undefined;
|
|
949
|
+
const { title: headerTitle, template: headerTemplate } = formatTerminalHeader(prevState.status);
|
|
843
950
|
const cardContent = truncateContent(prevState.accumulatedContent + prevState.finalReply) || " ";
|
|
844
951
|
const doneCard = buildProgressCard(cardContent, { showStop: false, headerTitle, headerTemplate });
|
|
845
952
|
await pp.cardUpdate(display.cardId, doneCard, nextSeq).catch(() => {});
|
|
@@ -848,7 +955,7 @@ export async function runAgentSession(
|
|
|
848
955
|
displayCards.delete(displayChatId);
|
|
849
956
|
|
|
850
957
|
// 持久化:标记上一轮所有卡片为终态
|
|
851
|
-
const finalStatus = prevState.status
|
|
958
|
+
const finalStatus = turnFinalStatus(prevState.status);
|
|
852
959
|
finalizeTurnCards(sessionId, prevState.turnCount, finalStatus).catch(() => {});
|
|
853
960
|
|
|
854
961
|
if (prevState.finalReply && stillOursAfterUpdate && !isFinalReplySentForTurn(prevState)) {
|
|
@@ -858,7 +965,7 @@ export async function runAgentSession(
|
|
|
858
965
|
}
|
|
859
966
|
} else if (pp && prevState.finalReply && !isFinalReplySentForTurn(prevState)) {
|
|
860
967
|
// 无 display 记录但上一轮有 finalReply(极快轮次),至少发送
|
|
861
|
-
const finalStatus = prevState.status
|
|
968
|
+
const finalStatus = turnFinalStatus(prevState.status);
|
|
862
969
|
finalizeTurnCards(sessionId, prevState.turnCount, finalStatus).catch(() => {});
|
|
863
970
|
await sendFinalReplyTextOnce(pp, displayChatId, sessionId, prevState.turnCount, prevState.finalReply);
|
|
864
971
|
}
|
|
@@ -928,7 +1035,14 @@ export async function runAgentSession(
|
|
|
928
1035
|
const toolCallMap = new Map<string, { name: string; input: unknown }>();
|
|
929
1036
|
|
|
930
1037
|
try {
|
|
931
|
-
for await (const unifiedMsg of adapter.prompt(sessionId, userTextWithCapabilities, cwd, controller.signal
|
|
1038
|
+
for await (const unifiedMsg of adapter.prompt(sessionId, userTextWithCapabilities, cwd, controller.signal, {
|
|
1039
|
+
onProcessStart: (processInfo) => {
|
|
1040
|
+
startPromptProcessMonitor(sessionId, processInfo);
|
|
1041
|
+
},
|
|
1042
|
+
onProcessExit: () => {
|
|
1043
|
+
clearPromptProcessMonitor(sessionId);
|
|
1044
|
+
},
|
|
1045
|
+
})) {
|
|
932
1046
|
for (const block of unifiedMsg.blocks) {
|
|
933
1047
|
accumulateBlockContent(block, state, toolCallMap);
|
|
934
1048
|
|
|
@@ -971,13 +1085,15 @@ export async function runAgentSession(
|
|
|
971
1085
|
// 标记 prompt 结束
|
|
972
1086
|
const prompt = activePrompts.get(sessionId);
|
|
973
1087
|
const wasStopped = prompt?.stopped ?? false;
|
|
1088
|
+
const wasAbnormalExit = prompt?.abnormalExit ?? false;
|
|
1089
|
+
clearPromptProcessMonitor(sessionId);
|
|
974
1090
|
activePrompts.delete(sessionId);
|
|
975
1091
|
|
|
976
1092
|
// 先写最终状态(done/stopped),确保 display loop 在下一轮消费前
|
|
977
1093
|
// 读到新状态并终结旧卡片。否则 setImmediate 在 CHECK 阶段先于
|
|
978
1094
|
// writeFile I/O(POLL 阶段)执行,display loop 会误以为旧轮仍在
|
|
979
1095
|
// 运行中并更新旧卡片,而不是新建卡片。
|
|
980
|
-
const finalStatus = wasStopped ? "stopped" : "done";
|
|
1096
|
+
const finalStatus = wasAbnormalExit ? "error" : wasStopped ? "stopped" : "done";
|
|
981
1097
|
const finalReply = pickFinalReply(state).trim();
|
|
982
1098
|
await writeStreamState({
|
|
983
1099
|
sessionId,
|
|
@@ -1039,6 +1155,23 @@ export async function runAgentSession(
|
|
|
1039
1155
|
if (active1) platform.setChatAvatar(active1, tool, "idle").catch(() => {});
|
|
1040
1156
|
console.log(`[${ts()}] Session ${sessionId} stopped (content chunks: ${state.chunkCount})`);
|
|
1041
1157
|
if (tid) logTrace(tid, "SESSION_END", { sessionId, outcome: "stopped", chunks: state.chunkCount });
|
|
1158
|
+
} else if (wasAbnormalExit) {
|
|
1159
|
+
for (const cid of getChatsForSession(sessionId)) {
|
|
1160
|
+
const finfo = sessionInfoMap.get(cid);
|
|
1161
|
+
await recordSessionRegistry({
|
|
1162
|
+
chatId: cid,
|
|
1163
|
+
sessionId,
|
|
1164
|
+
tool,
|
|
1165
|
+
turnCount: finfo?.turnCount ?? nextTurnCount,
|
|
1166
|
+
lastContextTokens: finfo?.lastContextTokens ?? nextContextTokens,
|
|
1167
|
+
startTime: finfo?.startTime ?? now,
|
|
1168
|
+
running: false,
|
|
1169
|
+
});
|
|
1170
|
+
}
|
|
1171
|
+
const activeErr = getLastActiveChat(sessionId) ?? getChatsForSession(sessionId)[0];
|
|
1172
|
+
if (activeErr) platform.setChatAvatar(activeErr, tool, "idle").catch(() => {});
|
|
1173
|
+
console.log(`[${ts()}] Session ${sessionId} process exited unexpectedly (content chunks: ${state.chunkCount})`);
|
|
1174
|
+
if (tid) logTrace(tid, "SESSION_END", { sessionId, outcome: "process_missing", chunks: state.chunkCount });
|
|
1042
1175
|
} else {
|
|
1043
1176
|
for (const cid of getChatsForSession(sessionId)) {
|
|
1044
1177
|
const finfo = sessionInfoMap.get(cid);
|
|
@@ -1140,8 +1273,7 @@ export function startUnifiedDisplayLoop(): void {
|
|
|
1140
1273
|
// 发送最终结果(卡片平台)
|
|
1141
1274
|
while (display.cardBusy) await new Promise(r => setTimeout(r, 20));
|
|
1142
1275
|
const nextSeq = display.sequence + 1;
|
|
1143
|
-
const headerTitle = state.status
|
|
1144
|
-
const headerTemplate = state.status === "stopped" ? "red" : undefined;
|
|
1276
|
+
const { title: headerTitle, template: headerTemplate } = formatTerminalHeader(state.status);
|
|
1145
1277
|
const cardContent = truncateContent(state.accumulatedContent + state.finalReply) || " ";
|
|
1146
1278
|
const doneCard = buildProgressCard(cardContent, { showStop: false, headerTitle, headerTemplate });
|
|
1147
1279
|
await p.cardUpdate(display.cardId, doneCard, nextSeq).catch(() => {});
|
|
@@ -1156,7 +1288,7 @@ export function startUnifiedDisplayLoop(): void {
|
|
|
1156
1288
|
continue;
|
|
1157
1289
|
}
|
|
1158
1290
|
|
|
1159
|
-
const finalSt = state.status
|
|
1291
|
+
const finalSt = turnFinalStatus(state.status);
|
|
1160
1292
|
finalizeTurnCards(sessionId, state.turnCount, finalSt).catch(() => {});
|
|
1161
1293
|
displayCards.delete(chatId);
|
|
1162
1294
|
if (state.finalReply) {
|
|
@@ -1343,6 +1475,7 @@ export function stopSession(sessionId: string): boolean {
|
|
|
1343
1475
|
const prompt = activePrompts.get(sessionId);
|
|
1344
1476
|
if (!prompt) return false;
|
|
1345
1477
|
prompt.stopped = true;
|
|
1478
|
+
clearPromptProcessMonitor(sessionId);
|
|
1346
1479
|
cancelQueuedMessage(sessionId);
|
|
1347
1480
|
prompt.controller.abort();
|
|
1348
1481
|
console.log(`[${ts()}] [STOP] Session ${sessionId} aborted`);
|
|
@@ -1434,7 +1567,8 @@ export async function getSessionStatus(chatId: string): Promise<SessionStatus |
|
|
|
1434
1567
|
const info = sessionInfoMap.get(chatId);
|
|
1435
1568
|
if (!info) return null;
|
|
1436
1569
|
|
|
1437
|
-
const
|
|
1570
|
+
const activePrompt = activePrompts.get(info.sessionId);
|
|
1571
|
+
const isActive = !!activePrompt && !activePrompt.stopped && !activePrompt.abnormalExit;
|
|
1438
1572
|
const { model, effort } = await resolveModelEffort(info.tool, info.sessionId);
|
|
1439
1573
|
|
|
1440
1574
|
const registry = await loadSessionRegistry();
|
|
@@ -1509,7 +1643,9 @@ export async function getAllSessionsStatus(): Promise<SessionsListEntry[]> {
|
|
|
1509
1643
|
chatId: info.chatId,
|
|
1510
1644
|
sessionId: info.sessionId,
|
|
1511
1645
|
chatName: info.chatName || "",
|
|
1512
|
-
active: activePrompts.
|
|
1646
|
+
active: !!activePrompts.get(info.sessionId) &&
|
|
1647
|
+
!activePrompts.get(info.sessionId)?.stopped &&
|
|
1648
|
+
!activePrompts.get(info.sessionId)?.abnormalExit,
|
|
1513
1649
|
turnCount: info.turnCount,
|
|
1514
1650
|
startTime: info.startTime,
|
|
1515
1651
|
model,
|