chatccc 0.2.188 → 0.2.189
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/agent-prompts/claude_specific.md +45 -45
- package/agent-prompts/codex_specific.md +2 -2
- package/agent-prompts/cursor_specific.md +13 -13
- package/config.sample.json +14 -14
- package/im-skills/feishu-skill/receive-send-file.md +63 -63
- package/im-skills/feishu-skill/receive-send-image.md +24 -24
- package/im-skills/feishu-skill/skill.md +3 -3
- 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__/agent-delegate-task-rpc.test.ts +165 -165
- package/src/__tests__/builtin-config.test.ts +33 -33
- package/src/__tests__/card-plain-text.test.ts +5 -5
- package/src/__tests__/cardkit.test.ts +60 -60
- package/src/__tests__/cards.test.ts +77 -77
- package/src/__tests__/chatgpt-subscription-rpc.test.ts +89 -89
- package/src/__tests__/chatgpt-subscription.test.ts +135 -135
- package/src/__tests__/chrome-devtools-guard.test.ts +165 -165
- package/src/__tests__/claude-adapter.test.ts +592 -592
- package/src/__tests__/codex-reset-actions.test.ts +146 -146
- package/src/__tests__/config-reload.test.ts +4 -4
- package/src/__tests__/config-sample.test.ts +17 -17
- package/src/__tests__/feishu-api.test.ts +60 -60
- package/src/__tests__/feishu-platform.test.ts +22 -22
- package/src/__tests__/format-message.test.ts +47 -47
- package/src/__tests__/orchestrator.test.ts +112 -112
- package/src/__tests__/privacy.test.ts +198 -198
- package/src/__tests__/raw-stream-log.test.ts +106 -106
- package/src/__tests__/session.test.ts +17 -17
- package/src/__tests__/shared-prefix.test.ts +36 -36
- package/src/__tests__/stream-state.test.ts +42 -42
- package/src/__tests__/web-ui.test.ts +209 -130
- package/src/adapters/claude-adapter.ts +566 -566
- package/src/adapters/claude-session-meta-store.ts +120 -120
- package/src/adapters/codex-adapter.ts +27 -28
- package/src/adapters/cursor-adapter.ts +46 -46
- package/src/adapters/raw-stream-log.ts +124 -124
- package/src/adapters/resource-monitor.ts +140 -140
- package/src/agent-delegate-task-rpc.ts +153 -153
- package/src/agent-delegate-task.ts +81 -81
- package/src/agent-stop-stuck.ts +129 -129
- package/src/builtin/cli.ts +189 -189
- package/src/builtin/index.ts +168 -168
- package/src/cards.ts +137 -137
- package/src/chatgpt-subscription-rpc.ts +27 -27
- package/src/chatgpt-subscription.ts +299 -299
- package/src/chrome-devtools-guard.ts +318 -318
- package/src/codex-reset-actions.ts +184 -184
- package/src/config.ts +86 -86
- package/src/feishu-platform.ts +20 -20
- package/src/format-message.ts +293 -293
- package/src/litellm-proxy.ts +374 -374
- package/src/orchestrator.ts +143 -143
- package/src/privacy.ts +118 -118
- package/src/session-chat-binding.ts +6 -6
- package/src/session-name.ts +8 -8
- package/src/session.ts +98 -98
- package/src/shared-prefix.ts +29 -29
- package/src/sim-platform.ts +20 -20
- package/src/turn-cards.ts +117 -117
- package/src/web-ui.ts +142 -24
|
@@ -1,120 +1,120 @@
|
|
|
1
|
-
// =============================================================================
|
|
2
|
-
// claude-session-meta-store.ts — Claude 会话 sessionId → meta 持久化映射
|
|
3
|
-
// =============================================================================
|
|
4
|
-
// 背景:Claude Agent SDK 的 getSessionInfo 可能因 cwd / 本地记录缺失而查不到。
|
|
5
|
-
// ChatCCC 额外维护 sessionId → { cwd, model } 映射作为展示与恢复兜底。
|
|
6
|
-
//
|
|
7
|
-
// 存储:
|
|
8
|
-
// 文件 state/claude-session-meta.json,结构:
|
|
9
|
-
// { "<sessionId>": { "cwd": "...", "model": "..." } }
|
|
10
|
-
//
|
|
11
|
-
// API 设计:
|
|
12
|
-
// set(sid, partial) → 部分合并写入;只更新非空字段,不会清空其他字段
|
|
13
|
-
// =============================================================================
|
|
14
|
-
|
|
15
|
-
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
16
|
-
import { dirname, join } from "node:path";
|
|
17
|
-
|
|
18
|
-
import { USER_DATA_DIR } from "../config.ts";
|
|
19
|
-
|
|
20
|
-
export const CLAUDE_SESSION_META_FILE = join(
|
|
21
|
-
USER_DATA_DIR,
|
|
22
|
-
"state",
|
|
23
|
-
"claude-session-meta.json",
|
|
24
|
-
);
|
|
25
|
-
|
|
26
|
-
export interface ClaudeSessionMeta {
|
|
27
|
-
cwd: string;
|
|
28
|
-
model?: string;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export interface ClaudeSessionMetaStore {
|
|
32
|
-
get(sessionId: string): Promise<ClaudeSessionMeta | undefined>;
|
|
33
|
-
set(sessionId: string, partial: Partial<ClaudeSessionMeta>): Promise<void>;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
interface RawEntry {
|
|
37
|
-
cwd?: string;
|
|
38
|
-
model?: string;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function isNonEmptyString(v: unknown): v is string {
|
|
42
|
-
return typeof v === "string" && v.length > 0;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function parseEntry(raw: unknown): RawEntry | null {
|
|
46
|
-
if (typeof raw === "string" && raw.length > 0) {
|
|
47
|
-
return { cwd: raw };
|
|
48
|
-
}
|
|
49
|
-
if (raw && typeof raw === "object" && !Array.isArray(raw)) {
|
|
50
|
-
const obj = raw as Record<string, unknown>;
|
|
51
|
-
const out: RawEntry = {};
|
|
52
|
-
if (isNonEmptyString(obj.cwd)) out.cwd = obj.cwd;
|
|
53
|
-
if (isNonEmptyString(obj.model)) out.model = obj.model;
|
|
54
|
-
return out;
|
|
55
|
-
}
|
|
56
|
-
return null;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export function createClaudeSessionMetaStore(
|
|
60
|
-
filePath: string = CLAUDE_SESSION_META_FILE,
|
|
61
|
-
): ClaudeSessionMetaStore {
|
|
62
|
-
let cache: Record<string, RawEntry> | null = null;
|
|
63
|
-
|
|
64
|
-
async function load(): Promise<Record<string, RawEntry>> {
|
|
65
|
-
if (cache) return cache;
|
|
66
|
-
try {
|
|
67
|
-
const raw = await readFile(filePath, "utf-8");
|
|
68
|
-
const parsed = JSON.parse(raw);
|
|
69
|
-
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
70
|
-
const out: Record<string, RawEntry> = {};
|
|
71
|
-
for (const [k, v] of Object.entries(parsed as Record<string, unknown>)) {
|
|
72
|
-
const entry = parseEntry(v);
|
|
73
|
-
if (entry) out[k] = entry;
|
|
74
|
-
}
|
|
75
|
-
cache = out;
|
|
76
|
-
return out;
|
|
77
|
-
}
|
|
78
|
-
} catch {
|
|
79
|
-
// 文件不存在 / JSON 损坏 → 视为空映射
|
|
80
|
-
}
|
|
81
|
-
cache = {};
|
|
82
|
-
return cache;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
return {
|
|
86
|
-
async get(sessionId: string): Promise<ClaudeSessionMeta | undefined> {
|
|
87
|
-
const map = await load();
|
|
88
|
-
const entry = map[sessionId];
|
|
89
|
-
if (!entry || !isNonEmptyString(entry.cwd)) return undefined;
|
|
90
|
-
return entry.model
|
|
91
|
-
? { cwd: entry.cwd, model: entry.model }
|
|
92
|
-
: { cwd: entry.cwd };
|
|
93
|
-
},
|
|
94
|
-
|
|
95
|
-
async set(
|
|
96
|
-
sessionId: string,
|
|
97
|
-
partial: Partial<ClaudeSessionMeta>,
|
|
98
|
-
): Promise<void> {
|
|
99
|
-
const map = await load();
|
|
100
|
-
const existing = map[sessionId] ?? {};
|
|
101
|
-
const merged: RawEntry = { ...existing };
|
|
102
|
-
if (isNonEmptyString(partial.cwd)) merged.cwd = partial.cwd;
|
|
103
|
-
if (isNonEmptyString(partial.model)) merged.model = partial.model;
|
|
104
|
-
|
|
105
|
-
if (existing.cwd === merged.cwd && existing.model === merged.model) return;
|
|
106
|
-
|
|
107
|
-
map[sessionId] = merged;
|
|
108
|
-
try {
|
|
109
|
-
await mkdir(dirname(filePath), { recursive: true });
|
|
110
|
-
await writeFile(filePath, JSON.stringify(map, null, 2), "utf-8");
|
|
111
|
-
} catch (err) {
|
|
112
|
-
console.error(
|
|
113
|
-
`[claude-session-meta] failed to persist ${filePath}: ${(err as Error).message}`,
|
|
114
|
-
);
|
|
115
|
-
}
|
|
116
|
-
},
|
|
117
|
-
};
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
export const defaultClaudeSessionMetaStore = createClaudeSessionMetaStore();
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// claude-session-meta-store.ts — Claude 会话 sessionId → meta 持久化映射
|
|
3
|
+
// =============================================================================
|
|
4
|
+
// 背景:Claude Agent SDK 的 getSessionInfo 可能因 cwd / 本地记录缺失而查不到。
|
|
5
|
+
// ChatCCC 额外维护 sessionId → { cwd, model } 映射作为展示与恢复兜底。
|
|
6
|
+
//
|
|
7
|
+
// 存储:
|
|
8
|
+
// 文件 state/claude-session-meta.json,结构:
|
|
9
|
+
// { "<sessionId>": { "cwd": "...", "model": "..." } }
|
|
10
|
+
//
|
|
11
|
+
// API 设计:
|
|
12
|
+
// set(sid, partial) → 部分合并写入;只更新非空字段,不会清空其他字段
|
|
13
|
+
// =============================================================================
|
|
14
|
+
|
|
15
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
16
|
+
import { dirname, join } from "node:path";
|
|
17
|
+
|
|
18
|
+
import { USER_DATA_DIR } from "../config.ts";
|
|
19
|
+
|
|
20
|
+
export const CLAUDE_SESSION_META_FILE = join(
|
|
21
|
+
USER_DATA_DIR,
|
|
22
|
+
"state",
|
|
23
|
+
"claude-session-meta.json",
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
export interface ClaudeSessionMeta {
|
|
27
|
+
cwd: string;
|
|
28
|
+
model?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface ClaudeSessionMetaStore {
|
|
32
|
+
get(sessionId: string): Promise<ClaudeSessionMeta | undefined>;
|
|
33
|
+
set(sessionId: string, partial: Partial<ClaudeSessionMeta>): Promise<void>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface RawEntry {
|
|
37
|
+
cwd?: string;
|
|
38
|
+
model?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function isNonEmptyString(v: unknown): v is string {
|
|
42
|
+
return typeof v === "string" && v.length > 0;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function parseEntry(raw: unknown): RawEntry | null {
|
|
46
|
+
if (typeof raw === "string" && raw.length > 0) {
|
|
47
|
+
return { cwd: raw };
|
|
48
|
+
}
|
|
49
|
+
if (raw && typeof raw === "object" && !Array.isArray(raw)) {
|
|
50
|
+
const obj = raw as Record<string, unknown>;
|
|
51
|
+
const out: RawEntry = {};
|
|
52
|
+
if (isNonEmptyString(obj.cwd)) out.cwd = obj.cwd;
|
|
53
|
+
if (isNonEmptyString(obj.model)) out.model = obj.model;
|
|
54
|
+
return out;
|
|
55
|
+
}
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function createClaudeSessionMetaStore(
|
|
60
|
+
filePath: string = CLAUDE_SESSION_META_FILE,
|
|
61
|
+
): ClaudeSessionMetaStore {
|
|
62
|
+
let cache: Record<string, RawEntry> | null = null;
|
|
63
|
+
|
|
64
|
+
async function load(): Promise<Record<string, RawEntry>> {
|
|
65
|
+
if (cache) return cache;
|
|
66
|
+
try {
|
|
67
|
+
const raw = await readFile(filePath, "utf-8");
|
|
68
|
+
const parsed = JSON.parse(raw);
|
|
69
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
70
|
+
const out: Record<string, RawEntry> = {};
|
|
71
|
+
for (const [k, v] of Object.entries(parsed as Record<string, unknown>)) {
|
|
72
|
+
const entry = parseEntry(v);
|
|
73
|
+
if (entry) out[k] = entry;
|
|
74
|
+
}
|
|
75
|
+
cache = out;
|
|
76
|
+
return out;
|
|
77
|
+
}
|
|
78
|
+
} catch {
|
|
79
|
+
// 文件不存在 / JSON 损坏 → 视为空映射
|
|
80
|
+
}
|
|
81
|
+
cache = {};
|
|
82
|
+
return cache;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
async get(sessionId: string): Promise<ClaudeSessionMeta | undefined> {
|
|
87
|
+
const map = await load();
|
|
88
|
+
const entry = map[sessionId];
|
|
89
|
+
if (!entry || !isNonEmptyString(entry.cwd)) return undefined;
|
|
90
|
+
return entry.model
|
|
91
|
+
? { cwd: entry.cwd, model: entry.model }
|
|
92
|
+
: { cwd: entry.cwd };
|
|
93
|
+
},
|
|
94
|
+
|
|
95
|
+
async set(
|
|
96
|
+
sessionId: string,
|
|
97
|
+
partial: Partial<ClaudeSessionMeta>,
|
|
98
|
+
): Promise<void> {
|
|
99
|
+
const map = await load();
|
|
100
|
+
const existing = map[sessionId] ?? {};
|
|
101
|
+
const merged: RawEntry = { ...existing };
|
|
102
|
+
if (isNonEmptyString(partial.cwd)) merged.cwd = partial.cwd;
|
|
103
|
+
if (isNonEmptyString(partial.model)) merged.model = partial.model;
|
|
104
|
+
|
|
105
|
+
if (existing.cwd === merged.cwd && existing.model === merged.model) return;
|
|
106
|
+
|
|
107
|
+
map[sessionId] = merged;
|
|
108
|
+
try {
|
|
109
|
+
await mkdir(dirname(filePath), { recursive: true });
|
|
110
|
+
await writeFile(filePath, JSON.stringify(map, null, 2), "utf-8");
|
|
111
|
+
} catch (err) {
|
|
112
|
+
console.error(
|
|
113
|
+
`[claude-session-meta] failed to persist ${filePath}: ${(err as Error).message}`,
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export const defaultClaudeSessionMetaStore = createClaudeSessionMetaStore();
|
|
@@ -72,7 +72,6 @@ function buildCodexPromptText(userText: string): string {
|
|
|
72
72
|
function detectCodexCommand(): string {
|
|
73
73
|
return config.codex.path || "codex";
|
|
74
74
|
}
|
|
75
|
-
const CODEX_COMMAND = detectCodexCommand();
|
|
76
75
|
|
|
77
76
|
/** exec 模式共用参数:JSONL 输出、绕过沙盒和确认、跳过 git 仓库检查 */
|
|
78
77
|
const CODEX_BASE_ARGS = [
|
|
@@ -184,13 +183,13 @@ export function normalizeCodexMessage(
|
|
|
184
183
|
// 子进程辅助函数
|
|
185
184
|
// ---------------------------------------------------------------------------
|
|
186
185
|
|
|
187
|
-
function spawnCodex(
|
|
188
|
-
args: string[],
|
|
189
|
-
cwd?: string,
|
|
190
|
-
stdinText?: string,
|
|
191
|
-
modelOverride?: string,
|
|
192
|
-
effortOverride?: string,
|
|
193
|
-
): ChildProcess {
|
|
186
|
+
function spawnCodex(
|
|
187
|
+
args: string[],
|
|
188
|
+
cwd?: string,
|
|
189
|
+
stdinText?: string,
|
|
190
|
+
modelOverride?: string,
|
|
191
|
+
effortOverride?: string,
|
|
192
|
+
): ChildProcess {
|
|
194
193
|
const allArgs = [...args];
|
|
195
194
|
const model = modelOverride ?? resolveCodexModel();
|
|
196
195
|
if (model) {
|
|
@@ -198,12 +197,12 @@ function spawnCodex(
|
|
|
198
197
|
const execIdx = allArgs.indexOf("exec");
|
|
199
198
|
allArgs.splice(execIdx + 1, 0, "-m", model);
|
|
200
199
|
}
|
|
201
|
-
const effort = effortOverride ?? resolveCodexEffort();
|
|
200
|
+
const effort = effortOverride ?? resolveCodexEffort();
|
|
202
201
|
if (effort) {
|
|
203
202
|
allArgs.push("-c", `model_reasoning_effort="${effort}"`);
|
|
204
203
|
}
|
|
205
204
|
|
|
206
|
-
const proc = spawn(
|
|
205
|
+
const proc = spawn(detectCodexCommand(), allArgs, {
|
|
207
206
|
cwd,
|
|
208
207
|
stdio: [stdinText !== undefined ? "pipe" : "ignore", "pipe", "pipe"],
|
|
209
208
|
windowsHide: true,
|
|
@@ -259,17 +258,17 @@ async function* readJsonLines(
|
|
|
259
258
|
// ---------------------------------------------------------------------------
|
|
260
259
|
|
|
261
260
|
class CodexAdapter implements ToolAdapter {
|
|
262
|
-
readonly displayName = "Codex";
|
|
263
|
-
readonly sessionDescPrefix = "Codex Session:";
|
|
264
|
-
private metaStore: CodexSessionMetaStore;
|
|
265
|
-
private modelOverride: string | undefined;
|
|
266
|
-
private effortOverride: string | undefined;
|
|
267
|
-
|
|
268
|
-
constructor(metaStore: CodexSessionMetaStore, modelOverride?: string, effortOverride?: string) {
|
|
269
|
-
this.metaStore = metaStore;
|
|
270
|
-
this.modelOverride = modelOverride;
|
|
271
|
-
this.effortOverride = effortOverride;
|
|
272
|
-
}
|
|
261
|
+
readonly displayName = "Codex";
|
|
262
|
+
readonly sessionDescPrefix = "Codex Session:";
|
|
263
|
+
private metaStore: CodexSessionMetaStore;
|
|
264
|
+
private modelOverride: string | undefined;
|
|
265
|
+
private effortOverride: string | undefined;
|
|
266
|
+
|
|
267
|
+
constructor(metaStore: CodexSessionMetaStore, modelOverride?: string, effortOverride?: string) {
|
|
268
|
+
this.metaStore = metaStore;
|
|
269
|
+
this.modelOverride = modelOverride;
|
|
270
|
+
this.effortOverride = effortOverride;
|
|
271
|
+
}
|
|
273
272
|
|
|
274
273
|
// createSession: 生成 sessionId,记录 cwd,不创建 Codex 线程(延迟到首次 prompt)
|
|
275
274
|
async createSession(cwd: string): Promise<CreateSessionResult> {
|
|
@@ -299,7 +298,7 @@ class CodexAdapter implements ToolAdapter {
|
|
|
299
298
|
? [...baseArgs, "-C", cwd, "-"]
|
|
300
299
|
: [...baseArgs, "resume", threadId, "-"];
|
|
301
300
|
|
|
302
|
-
const proc = spawnCodex(args, cwd, buildCodexPromptText(userText), this.modelOverride, this.effortOverride);
|
|
301
|
+
const proc = spawnCodex(args, cwd, buildCodexPromptText(userText), this.modelOverride, this.effortOverride);
|
|
303
302
|
if (proc.pid !== undefined) options?.onProcessStart?.({ pid: proc.pid });
|
|
304
303
|
|
|
305
304
|
// 关键:spawn 用了 shell:true,proc.pid 指向的是壳进程(cmd.exe / sh)。
|
|
@@ -353,8 +352,8 @@ class CodexAdapter implements ToolAdapter {
|
|
|
353
352
|
export interface CreateCodexAdapterOptions {
|
|
354
353
|
metaStore?: CodexSessionMetaStore;
|
|
355
354
|
/** per-session 模型覆盖(/model 命令);传了就用,不传走全局 codex.model */
|
|
356
|
-
model?: string;
|
|
357
|
-
effort?: string;
|
|
355
|
+
model?: string;
|
|
356
|
+
effort?: string;
|
|
358
357
|
}
|
|
359
358
|
|
|
360
359
|
export function createCodexAdapter(
|
|
@@ -362,7 +361,7 @@ export function createCodexAdapter(
|
|
|
362
361
|
): ToolAdapter {
|
|
363
362
|
return new CodexAdapter(
|
|
364
363
|
options.metaStore ?? defaultCodexSessionMetaStore,
|
|
365
|
-
options.model,
|
|
366
|
-
options.effort,
|
|
367
|
-
);
|
|
368
|
-
}
|
|
364
|
+
options.model,
|
|
365
|
+
options.effort,
|
|
366
|
+
);
|
|
367
|
+
}
|
|
@@ -20,16 +20,16 @@ import type {
|
|
|
20
20
|
SessionInfo,
|
|
21
21
|
} from "./adapter-interface.ts";
|
|
22
22
|
import { parseUserCommand } from "./adapter-interface.ts";
|
|
23
|
-
import { config, CURSOR_AGENT_COMMAND, CURSOR_AGENT_ARGS, RAW_STREAM_LOGS_DIR } from "../config.ts";
|
|
23
|
+
import { config, CURSOR_AGENT_COMMAND, CURSOR_AGENT_ARGS, RAW_STREAM_LOGS_DIR } from "../config.ts";
|
|
24
24
|
import {
|
|
25
25
|
defaultCursorSessionMetaStore,
|
|
26
26
|
type CursorSessionMetaStore,
|
|
27
27
|
} from "./cursor-session-meta-store.ts";
|
|
28
|
-
import { killProcessTree } from "./proc-tree-kill.ts";
|
|
29
|
-
import {
|
|
30
|
-
createRawStreamLog,
|
|
31
|
-
type RawStreamLogHandle,
|
|
32
|
-
} from "./raw-stream-log.ts";
|
|
28
|
+
import { killProcessTree } from "./proc-tree-kill.ts";
|
|
29
|
+
import {
|
|
30
|
+
createRawStreamLog,
|
|
31
|
+
type RawStreamLogHandle,
|
|
32
|
+
} from "./raw-stream-log.ts";
|
|
33
33
|
|
|
34
34
|
// ---------------------------------------------------------------------------
|
|
35
35
|
// 特殊注入提示
|
|
@@ -348,12 +348,12 @@ function spawnAgent(
|
|
|
348
348
|
return proc;
|
|
349
349
|
}
|
|
350
350
|
|
|
351
|
-
async function* readJsonLines(
|
|
352
|
-
proc: ChildProcess,
|
|
353
|
-
signal?: AbortSignal,
|
|
354
|
-
debugTag?: string,
|
|
355
|
-
rawLog?: RawStreamLogHandle | null,
|
|
356
|
-
): AsyncGenerator<CursorMessageLine> {
|
|
351
|
+
async function* readJsonLines(
|
|
352
|
+
proc: ChildProcess,
|
|
353
|
+
signal?: AbortSignal,
|
|
354
|
+
debugTag?: string,
|
|
355
|
+
rawLog?: RawStreamLogHandle | null,
|
|
356
|
+
): AsyncGenerator<CursorMessageLine> {
|
|
357
357
|
const tag = debugTag ?? "cursor";
|
|
358
358
|
const rl = createInterface({ input: proc.stdout!, crlfDelay: Infinity });
|
|
359
359
|
// abort 时主动 close readline,避免等待 Windows 管道自然关闭(可能延迟数分钟)
|
|
@@ -364,10 +364,10 @@ async function* readJsonLines(
|
|
|
364
364
|
for await (const line of rl) {
|
|
365
365
|
if (signal?.aborted) break;
|
|
366
366
|
lineCount++;
|
|
367
|
-
const trimmed = line.trim();
|
|
368
|
-
if (!trimmed) continue;
|
|
369
|
-
rawLog?.writeLine(trimmed);
|
|
370
|
-
try {
|
|
367
|
+
const trimmed = line.trim();
|
|
368
|
+
if (!trimmed) continue;
|
|
369
|
+
rawLog?.writeLine(trimmed);
|
|
370
|
+
try {
|
|
371
371
|
yield JSON.parse(trimmed) as CursorMessageLine;
|
|
372
372
|
} catch { /* 非 JSON 行静默跳过 */ }
|
|
373
373
|
}
|
|
@@ -432,32 +432,32 @@ class CursorAdapter implements ToolAdapter {
|
|
|
432
432
|
cmd.mode ?? undefined,
|
|
433
433
|
);
|
|
434
434
|
this.activeProcs.add(proc);
|
|
435
|
-
if (proc.pid !== undefined) options?.onProcessStart?.({ pid: proc.pid });
|
|
436
|
-
|
|
437
|
-
const rawLogConfig = config.rawStreamLogs.cursor;
|
|
438
|
-
let rawLog: RawStreamLogHandle | null = null;
|
|
439
|
-
try {
|
|
440
|
-
rawLog = await createRawStreamLog({
|
|
441
|
-
enabled: rawLogConfig.enabled,
|
|
442
|
-
rootDir: RAW_STREAM_LOGS_DIR,
|
|
443
|
-
tool: "cursor",
|
|
444
|
-
sessionId,
|
|
445
|
-
label: "prompt",
|
|
446
|
-
maxBytesPerTurn: rawLogConfig.maxBytesPerTurn,
|
|
447
|
-
retentionDays: rawLogConfig.retentionDays,
|
|
448
|
-
});
|
|
449
|
-
} catch (err) {
|
|
450
|
-
console.error(`[Cursor raw stream log] create failed: ${(err as Error).message}`);
|
|
451
|
-
}
|
|
435
|
+
if (proc.pid !== undefined) options?.onProcessStart?.({ pid: proc.pid });
|
|
436
|
+
|
|
437
|
+
const rawLogConfig = config.rawStreamLogs.cursor;
|
|
438
|
+
let rawLog: RawStreamLogHandle | null = null;
|
|
439
|
+
try {
|
|
440
|
+
rawLog = await createRawStreamLog({
|
|
441
|
+
enabled: rawLogConfig.enabled,
|
|
442
|
+
rootDir: RAW_STREAM_LOGS_DIR,
|
|
443
|
+
tool: "cursor",
|
|
444
|
+
sessionId,
|
|
445
|
+
label: "prompt",
|
|
446
|
+
maxBytesPerTurn: rawLogConfig.maxBytesPerTurn,
|
|
447
|
+
retentionDays: rawLogConfig.retentionDays,
|
|
448
|
+
});
|
|
449
|
+
} catch (err) {
|
|
450
|
+
console.error(`[Cursor raw stream log] create failed: ${(err as Error).message}`);
|
|
451
|
+
}
|
|
452
452
|
|
|
453
453
|
// 见 codex-adapter.ts 同位置注释:spawn 用了 shell:true,必须杀整棵树,
|
|
454
454
|
// 否则 abort 后真正在跑的孙进程 cursor-agent 还会继续输出 & 占用资源。
|
|
455
|
-
const onAbort = () => { void killProcessTree(proc.pid); };
|
|
456
|
-
signal?.addEventListener("abort", onAbort, { once: true });
|
|
457
|
-
let sawResult = false;
|
|
458
|
-
|
|
459
|
-
try {
|
|
460
|
-
for await (const raw of readJsonLines(proc, signal, sessionId, rawLog)) {
|
|
455
|
+
const onAbort = () => { void killProcessTree(proc.pid); };
|
|
456
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
457
|
+
let sawResult = false;
|
|
458
|
+
|
|
459
|
+
try {
|
|
460
|
+
for await (const raw of readJsonLines(proc, signal, sessionId, rawLog)) {
|
|
461
461
|
if (signal?.aborted) break;
|
|
462
462
|
if (
|
|
463
463
|
raw.type === "system" &&
|
|
@@ -473,17 +473,17 @@ class CursorAdapter implements ToolAdapter {
|
|
|
473
473
|
if (normalized) yield normalized;
|
|
474
474
|
|
|
475
475
|
// result 是流末事件,收到后立即结束进程,防止 CLI 僵死导致 readline 挂起。
|
|
476
|
-
if (raw.type === "result") {
|
|
477
|
-
sawResult = true;
|
|
478
|
-
void killProcessTree(proc.pid);
|
|
476
|
+
if (raw.type === "result") {
|
|
477
|
+
sawResult = true;
|
|
478
|
+
void killProcessTree(proc.pid);
|
|
479
479
|
break;
|
|
480
480
|
}
|
|
481
481
|
}
|
|
482
482
|
} finally {
|
|
483
483
|
signal?.removeEventListener("abort", onAbort);
|
|
484
|
-
await killProcessTree(proc.pid);
|
|
485
|
-
await rawLog?.close({ keep: rawLogConfig.keepCompleted || signal?.aborted === true || !sawResult });
|
|
486
|
-
this.activeProcs.delete(proc);
|
|
484
|
+
await killProcessTree(proc.pid);
|
|
485
|
+
await rawLog?.close({ keep: rawLogConfig.keepCompleted || signal?.aborted === true || !sawResult });
|
|
486
|
+
this.activeProcs.delete(proc);
|
|
487
487
|
if (proc.pid !== undefined) options?.onProcessExit?.({ pid: proc.pid });
|
|
488
488
|
console.log(`[Cursor debug] prompt end: sessionId=${sessionId}, signalAborted=${signal?.aborted ?? false}`);
|
|
489
489
|
}
|
|
@@ -519,4 +519,4 @@ export function createCursorAdapter(
|
|
|
519
519
|
options: CreateCursorAdapterOptions = {},
|
|
520
520
|
): ToolAdapter {
|
|
521
521
|
return new CursorAdapter(options.metaStore ?? defaultCursorSessionMetaStore, options.model);
|
|
522
|
-
}
|
|
522
|
+
}
|