chatccc 0.2.94 → 0.2.96
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/cards.ts +60 -0
- package/src/config.ts +5 -0
- package/src/index.ts +1 -1
- package/src/orchestrator.ts +116 -6
- package/src/session-chat-binding.ts +9 -0
- package/src/session.ts +185 -23
- package/src/stream-state.ts +33 -33
- package/src/turn-cards.ts +117 -117
|
@@ -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
|
}
|
|
@@ -90,6 +90,10 @@ import {
|
|
|
90
90
|
runAgentSession,
|
|
91
91
|
startUnifiedDisplayLoop,
|
|
92
92
|
stopUnifiedDisplayLoop,
|
|
93
|
+
_setProcessAliveForTest,
|
|
94
|
+
_resetProcessAliveForTest,
|
|
95
|
+
_setProcessMonitorIntervalForTest,
|
|
96
|
+
_resetProcessMonitorIntervalForTest,
|
|
93
97
|
} from "../session.ts";
|
|
94
98
|
import {
|
|
95
99
|
activePrompts,
|
|
@@ -103,7 +107,7 @@ import {
|
|
|
103
107
|
displayCards,
|
|
104
108
|
} from "../session-chat-binding.ts";
|
|
105
109
|
import type { AccumulatorState } from "../session.ts";
|
|
106
|
-
import type { ToolAdapter, UnifiedBlock, SessionInfo } from "../adapters/adapter-interface.ts";
|
|
110
|
+
import type { ToolAdapter, ToolPromptOptions, UnifiedBlock, SessionInfo } from "../adapters/adapter-interface.ts";
|
|
107
111
|
import type { PlatformAdapter } from "../platform-adapter.ts";
|
|
108
112
|
|
|
109
113
|
// Helper to create a mock active session entry
|
|
@@ -308,6 +312,92 @@ describe("runAgentSession previous final delivery guard", () => {
|
|
|
308
312
|
});
|
|
309
313
|
});
|
|
310
314
|
|
|
315
|
+
describe("runAgentSession process monitor", () => {
|
|
316
|
+
let registryFile = "";
|
|
317
|
+
let toolsFile = "";
|
|
318
|
+
|
|
319
|
+
beforeEach(async () => {
|
|
320
|
+
vi.useFakeTimers();
|
|
321
|
+
resetState();
|
|
322
|
+
resetBindingState();
|
|
323
|
+
mockStreamStates.clear();
|
|
324
|
+
const dir = await mkdtemp(join(tmpdir(), "chatccc-process-monitor-"));
|
|
325
|
+
registryFile = join(dir, "session-registry.json");
|
|
326
|
+
toolsFile = join(dir, "session-tools.json");
|
|
327
|
+
_setSessionRegistryFileForTest(registryFile);
|
|
328
|
+
_setSessionToolsFileForTest(toolsFile);
|
|
329
|
+
_setProcessMonitorIntervalForTest(50);
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
afterEach(() => {
|
|
333
|
+
_resetSessionRegistryFileForTest();
|
|
334
|
+
_resetSessionToolsFileForTest();
|
|
335
|
+
_clearAdapterCacheForTest();
|
|
336
|
+
_resetProcessAliveForTest();
|
|
337
|
+
_resetProcessMonitorIntervalForTest();
|
|
338
|
+
resetBindingState();
|
|
339
|
+
vi.useRealTimers();
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
it("marks the turn as error and sends a separate notice when the CLI process disappears", async () => {
|
|
343
|
+
const platform = mockPlatform("feishu");
|
|
344
|
+
setSessionPlatform(platform);
|
|
345
|
+
bindChatToSession("sid-process", "chat-process");
|
|
346
|
+
recordLastActiveChat("sid-process", "chat-process");
|
|
347
|
+
_setProcessAliveForTest(() => false);
|
|
348
|
+
|
|
349
|
+
const adapter: ToolAdapter = {
|
|
350
|
+
displayName: "Cursor",
|
|
351
|
+
sessionDescPrefix: "Cursor Session:",
|
|
352
|
+
createSession: async () => ({ sessionId: "sid-process" }),
|
|
353
|
+
getSessionInfo: async (sid) => ({ sessionId: sid, cwd: "/tmp" }),
|
|
354
|
+
closeSession: async () => {},
|
|
355
|
+
prompt: async function* (
|
|
356
|
+
_sid: string,
|
|
357
|
+
_text: string,
|
|
358
|
+
_cwd: string,
|
|
359
|
+
signal?: AbortSignal,
|
|
360
|
+
options?: ToolPromptOptions,
|
|
361
|
+
) {
|
|
362
|
+
options?.onProcessStart?.({ pid: 12345 });
|
|
363
|
+
yield { type: "assistant", blocks: [{ type: "text", text: "partial answer" }] };
|
|
364
|
+
await new Promise<void>((resolve) => {
|
|
365
|
+
if (signal?.aborted) {
|
|
366
|
+
resolve();
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
signal?.addEventListener("abort", () => resolve(), { once: true });
|
|
370
|
+
});
|
|
371
|
+
},
|
|
372
|
+
};
|
|
373
|
+
_setAdapterForToolForTest("cursor", adapter);
|
|
374
|
+
|
|
375
|
+
const runPromise = runAgentSession(
|
|
376
|
+
"sid-process",
|
|
377
|
+
"prompt",
|
|
378
|
+
platform,
|
|
379
|
+
"chat-process",
|
|
380
|
+
Date.now(),
|
|
381
|
+
"cursor",
|
|
382
|
+
);
|
|
383
|
+
|
|
384
|
+
await vi.waitFor(() => {
|
|
385
|
+
expect(activePrompts.get("sid-process")?.processPid).toBe(12345);
|
|
386
|
+
});
|
|
387
|
+
await vi.advanceTimersByTimeAsync(60);
|
|
388
|
+
await runPromise;
|
|
389
|
+
|
|
390
|
+
const state = mockStreamStates.get("sid-process");
|
|
391
|
+
expect(state?.status).toBe("error");
|
|
392
|
+
expect(state?.finalReply).toContain("partial answer");
|
|
393
|
+
expect(activePrompts.has("sid-process")).toBe(false);
|
|
394
|
+
expect(platform.sendText).toHaveBeenCalledWith(
|
|
395
|
+
"chat-process",
|
|
396
|
+
expect.stringContaining("进程异常结束"),
|
|
397
|
+
);
|
|
398
|
+
});
|
|
399
|
+
});
|
|
400
|
+
|
|
311
401
|
describe("unified display loop WeChat delta", () => {
|
|
312
402
|
beforeEach(() => {
|
|
313
403
|
vi.useFakeTimers();
|
|
@@ -15,12 +15,12 @@ vi.mock("../config.ts", async () => {
|
|
|
15
15
|
});
|
|
16
16
|
|
|
17
17
|
const {
|
|
18
|
-
writeStreamState,
|
|
19
|
-
readStreamState,
|
|
20
|
-
createEmptyStreamState,
|
|
21
|
-
isFinalReplySentForTurn,
|
|
22
|
-
markFinalReplySent,
|
|
23
|
-
STREAMS_DIR,
|
|
18
|
+
writeStreamState,
|
|
19
|
+
readStreamState,
|
|
20
|
+
createEmptyStreamState,
|
|
21
|
+
isFinalReplySentForTurn,
|
|
22
|
+
markFinalReplySent,
|
|
23
|
+
STREAMS_DIR,
|
|
24
24
|
_setRenameForTest,
|
|
25
25
|
_resetRenameForTest,
|
|
26
26
|
} = await import("../stream-state.ts");
|
|
@@ -119,36 +119,36 @@ describe("writeStreamState — atomic rename", () => {
|
|
|
119
119
|
const got = await readStreamState("never-existed");
|
|
120
120
|
expect(got).toBeNull();
|
|
121
121
|
});
|
|
122
|
-
it("marks final reply delivery for the matching terminal turn only", async () => {
|
|
123
|
-
const state = createEmptyStreamState("sid-delivered", "/tmp", "claude", 2);
|
|
124
|
-
state.status = "done";
|
|
125
|
-
state.finalReply = "done";
|
|
126
|
-
await writeStreamState(state);
|
|
127
|
-
|
|
128
|
-
await markFinalReplySent("sid-delivered", 2, 12345);
|
|
129
|
-
|
|
130
|
-
const got = await readStreamState("sid-delivered");
|
|
131
|
-
expect(got).not.toBeNull();
|
|
132
|
-
expect(got!.finalReplySentTurn).toBe(2);
|
|
133
|
-
expect(got!.finalReplySentAt).toBe(12345);
|
|
134
|
-
expect(isFinalReplySentForTurn(got!)).toBe(true);
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
it("does not mark a running state or a different turn as delivered", async () => {
|
|
138
|
-
const state = createEmptyStreamState("sid-running", "/tmp", "claude", 3);
|
|
139
|
-
await writeStreamState(state);
|
|
140
|
-
|
|
141
|
-
await markFinalReplySent("sid-running", 2, 12345);
|
|
142
|
-
await markFinalReplySent("sid-running", 3, 12345);
|
|
143
|
-
|
|
144
|
-
const got = await readStreamState("sid-running");
|
|
145
|
-
expect(got).not.toBeNull();
|
|
146
|
-
expect(got!.finalReplySentTurn).toBeUndefined();
|
|
147
|
-
expect(isFinalReplySentForTurn(got!)).toBe(false);
|
|
148
|
-
});
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
describe("createEmptyStreamState", () => {
|
|
122
|
+
it("marks final reply delivery for the matching terminal turn only", async () => {
|
|
123
|
+
const state = createEmptyStreamState("sid-delivered", "/tmp", "claude", 2);
|
|
124
|
+
state.status = "done";
|
|
125
|
+
state.finalReply = "done";
|
|
126
|
+
await writeStreamState(state);
|
|
127
|
+
|
|
128
|
+
await markFinalReplySent("sid-delivered", 2, 12345);
|
|
129
|
+
|
|
130
|
+
const got = await readStreamState("sid-delivered");
|
|
131
|
+
expect(got).not.toBeNull();
|
|
132
|
+
expect(got!.finalReplySentTurn).toBe(2);
|
|
133
|
+
expect(got!.finalReplySentAt).toBe(12345);
|
|
134
|
+
expect(isFinalReplySentForTurn(got!)).toBe(true);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("does not mark a running state or a different turn as delivered", async () => {
|
|
138
|
+
const state = createEmptyStreamState("sid-running", "/tmp", "claude", 3);
|
|
139
|
+
await writeStreamState(state);
|
|
140
|
+
|
|
141
|
+
await markFinalReplySent("sid-running", 2, 12345);
|
|
142
|
+
await markFinalReplySent("sid-running", 3, 12345);
|
|
143
|
+
|
|
144
|
+
const got = await readStreamState("sid-running");
|
|
145
|
+
expect(got).not.toBeNull();
|
|
146
|
+
expect(got!.finalReplySentTurn).toBeUndefined();
|
|
147
|
+
expect(isFinalReplySentForTurn(got!)).toBe(false);
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
describe("createEmptyStreamState", () => {
|
|
152
152
|
it("生成的 state 字段正确且 status=running", () => {
|
|
153
153
|
const s = createEmptyStreamState("sid-X", "/cwd", "cursor", 5);
|
|
154
154
|
expect(s.sessionId).toBe("sid-X");
|
|
@@ -156,9 +156,9 @@ describe("createEmptyStreamState", () => {
|
|
|
156
156
|
expect(s.tool).toBe("cursor");
|
|
157
157
|
expect(s.turnCount).toBe(5);
|
|
158
158
|
expect(s.status).toBe("running");
|
|
159
|
-
expect(s.accumulatedContent).toBe("");
|
|
160
|
-
expect(s.finalReply).toBe("");
|
|
161
|
-
expect(s.finalReplySentTurn).toBeUndefined();
|
|
162
|
-
expect(s.chunkCount).toBe(0);
|
|
163
|
-
});
|
|
164
|
-
});
|
|
159
|
+
expect(s.accumulatedContent).toBe("");
|
|
160
|
+
expect(s.finalReply).toBe("");
|
|
161
|
+
expect(s.finalReplySentTurn).toBeUndefined();
|
|
162
|
+
expect(s.chunkCount).toBe(0);
|
|
163
|
+
});
|
|
164
|
+
});
|
|
@@ -113,6 +113,18 @@ export interface SessionInfo {
|
|
|
113
113
|
model?: string;
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
+
export interface ToolProcessInfo {
|
|
117
|
+
/** Root PID returned by child_process.spawn. With shell:true this is the shell wrapper PID. */
|
|
118
|
+
pid: number;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export interface ToolPromptOptions {
|
|
122
|
+
/** Called once a CLI-backed prompt process has been spawned. */
|
|
123
|
+
onProcessStart?: (info: ToolProcessInfo) => void;
|
|
124
|
+
/** Called when the adapter leaves the prompt process scope normally or by abort. */
|
|
125
|
+
onProcessExit?: (info: ToolProcessInfo) => void;
|
|
126
|
+
}
|
|
127
|
+
|
|
116
128
|
// ---------------------------------------------------------------------------
|
|
117
129
|
// ToolAdapter — 统一的 AI 工具适配器接口
|
|
118
130
|
// ---------------------------------------------------------------------------
|
|
@@ -139,6 +151,7 @@ export interface ToolAdapter {
|
|
|
139
151
|
userText: string,
|
|
140
152
|
cwd: string,
|
|
141
153
|
signal?: AbortSignal,
|
|
154
|
+
options?: ToolPromptOptions,
|
|
142
155
|
): AsyncIterable<UnifiedStreamMessage>;
|
|
143
156
|
|
|
144
157
|
/**
|
|
@@ -13,6 +13,7 @@ import { randomUUID } from "node:crypto";
|
|
|
13
13
|
|
|
14
14
|
import type {
|
|
15
15
|
ToolAdapter,
|
|
16
|
+
ToolPromptOptions,
|
|
16
17
|
UnifiedBlock,
|
|
17
18
|
UnifiedStreamMessage,
|
|
18
19
|
CreateSessionResult,
|
|
@@ -238,6 +239,7 @@ class CodexAdapter implements ToolAdapter {
|
|
|
238
239
|
userText: string,
|
|
239
240
|
cwd: string,
|
|
240
241
|
signal?: AbortSignal,
|
|
242
|
+
options?: ToolPromptOptions,
|
|
241
243
|
): AsyncIterable<UnifiedStreamMessage> {
|
|
242
244
|
let meta = await this.metaStore.get(sessionId);
|
|
243
245
|
const threadId = meta?.threadId;
|
|
@@ -250,6 +252,7 @@ class CodexAdapter implements ToolAdapter {
|
|
|
250
252
|
: [...CODEX_BASE_ARGS, "resume", threadId, "-"];
|
|
251
253
|
|
|
252
254
|
const proc = spawnCodex(args, cwd, userText);
|
|
255
|
+
if (proc.pid !== undefined) options?.onProcessStart?.({ pid: proc.pid });
|
|
253
256
|
|
|
254
257
|
// 关键:spawn 用了 shell:true,proc.pid 指向的是壳进程(cmd.exe / sh)。
|
|
255
258
|
// 真正干活的是壳的孙子 codex.exe。普通 proc.kill() 在 Windows 上只杀第一层,
|
|
@@ -278,6 +281,7 @@ class CodexAdapter implements ToolAdapter {
|
|
|
278
281
|
} finally {
|
|
279
282
|
signal?.removeEventListener("abort", onAbort);
|
|
280
283
|
await killProcessTree(proc.pid);
|
|
284
|
+
if (proc.pid !== undefined) options?.onProcessExit?.({ pid: proc.pid });
|
|
281
285
|
}
|
|
282
286
|
}
|
|
283
287
|
|
|
@@ -10,6 +10,7 @@ import { createInterface } from "node:readline";
|
|
|
10
10
|
|
|
11
11
|
import type {
|
|
12
12
|
ToolAdapter,
|
|
13
|
+
ToolPromptOptions,
|
|
13
14
|
UnifiedBlock,
|
|
14
15
|
UnifiedStreamMessage,
|
|
15
16
|
CreateSessionResult,
|
|
@@ -354,10 +355,12 @@ class CursorAdapter implements ToolAdapter {
|
|
|
354
355
|
userText: string,
|
|
355
356
|
cwd: string,
|
|
356
357
|
signal?: AbortSignal,
|
|
358
|
+
options?: ToolPromptOptions,
|
|
357
359
|
): AsyncIterable<UnifiedStreamMessage> {
|
|
358
360
|
console.log(`[Cursor debug] prompt start: sessionId=${sessionId}, cwd=${cwd}, userTextLen=${userText.length}`);
|
|
359
361
|
const proc = spawnAgent(["--resume", sessionId], cwd, userText);
|
|
360
362
|
this.activeProcs.add(proc);
|
|
363
|
+
if (proc.pid !== undefined) options?.onProcessStart?.({ pid: proc.pid });
|
|
361
364
|
|
|
362
365
|
// 见 codex-adapter.ts 同位置注释:spawn 用了 shell:true,必须杀整棵树,
|
|
363
366
|
// 否则 abort 后真正在跑的孙进程 cursor-agent 还会继续输出 & 占用资源。
|
|
@@ -384,6 +387,7 @@ class CursorAdapter implements ToolAdapter {
|
|
|
384
387
|
signal?.removeEventListener("abort", onAbort);
|
|
385
388
|
await killProcessTree(proc.pid);
|
|
386
389
|
this.activeProcs.delete(proc);
|
|
390
|
+
if (proc.pid !== undefined) options?.onProcessExit?.({ pid: proc.pid });
|
|
387
391
|
console.log(`[Cursor debug] prompt end: sessionId=${sessionId}, signalAborted=${signal?.aborted ?? false}`);
|
|
388
392
|
}
|
|
389
393
|
}
|