chatccc 0.2.198 → 0.2.200
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/README.md +1 -1
- package/agent-prompts/claude_specific.md +45 -45
- package/agent-prompts/codex_specific.md +2 -2
- 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__/card-plain-text.test.ts +5 -5
- package/src/__tests__/cardkit.test.ts +60 -60
- package/src/__tests__/cards.test.ts +1 -1
- package/src/__tests__/claude-adapter.test.ts +592 -592
- package/src/__tests__/codex-reset-actions.test.ts +146 -146
- package/src/__tests__/cursor-adapter.test.ts +66 -7
- package/src/__tests__/feishu-api.test.ts +60 -60
- package/src/__tests__/feishu-avatar.test.ts +47 -5
- package/src/__tests__/feishu-platform.test.ts +22 -22
- package/src/__tests__/format-message.test.ts +47 -47
- package/src/__tests__/orchestrator.test.ts +27 -2
- package/src/__tests__/privacy.test.ts +198 -198
- package/src/__tests__/shared-prefix.test.ts +36 -36
- package/src/__tests__/stream-state.test.ts +42 -42
- package/src/adapters/claude-session-meta-store.ts +120 -120
- package/src/adapters/cursor-adapter.ts +39 -6
- 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/cards.ts +1 -1
- package/src/codex-reset-actions.ts +184 -184
- package/src/feishu-api.ts +41 -18
- 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 +4 -4
- package/src/privacy.ts +118 -118
- package/src/session-chat-binding.ts +6 -6
- package/src/session-name.ts +8 -8
- package/src/shared-prefix.ts +29 -29
- package/src/sim-platform.ts +20 -20
- package/src/turn-cards.ts +117 -117
|
@@ -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
|
+
});
|
|
@@ -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();
|
|
@@ -156,6 +156,26 @@ function isCursorAuthRelatedError(stderr: string): boolean {
|
|
|
156
156
|
);
|
|
157
157
|
}
|
|
158
158
|
|
|
159
|
+
const CURSOR_VISIBLE_STDERR_MAX_CHARS = 1200;
|
|
160
|
+
|
|
161
|
+
function sanitizeCursorStderr(stderr: string): string {
|
|
162
|
+
return stderr
|
|
163
|
+
.replace(/\x1B\[[0-?]*[ -/]*[@-~]/g, "")
|
|
164
|
+
.replace(/\bBearer\s+[A-Za-z0-9._~+/=-]{12,}\b/gi, "Bearer <redacted>")
|
|
165
|
+
.replace(/\bsk-[A-Za-z0-9_-]{12,}\b/g, "<redacted-api-key>")
|
|
166
|
+
.replace(
|
|
167
|
+
/\b(api[_-]?key|token|authorization|password)\s*[:=]\s*["']?[^\s"']+/gi,
|
|
168
|
+
"$1=<redacted>",
|
|
169
|
+
)
|
|
170
|
+
.trim();
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function formatCursorVisibleStderr(stderr: string): string {
|
|
174
|
+
const sanitized = sanitizeCursorStderr(stderr);
|
|
175
|
+
if (sanitized.length <= CURSOR_VISIBLE_STDERR_MAX_CHARS) return sanitized;
|
|
176
|
+
return `${sanitized.slice(0, CURSOR_VISIBLE_STDERR_MAX_CHARS)}\n...(stderr truncated)`;
|
|
177
|
+
}
|
|
178
|
+
|
|
159
179
|
export function formatCursorAgentEmptyOutputMessage(args: {
|
|
160
180
|
exitCode: number | null;
|
|
161
181
|
stdoutLength: number;
|
|
@@ -165,11 +185,12 @@ export function formatCursorAgentEmptyOutputMessage(args: {
|
|
|
165
185
|
if (args.stdoutLength !== 0) return null;
|
|
166
186
|
if (args.stderr.trim().length === 0) return null;
|
|
167
187
|
|
|
188
|
+
const stderrBlock = `[Cursor stderr] exit=${args.exitCode ?? "unknown"}:\n${formatCursorVisibleStderr(args.stderr)}`;
|
|
168
189
|
if (isCursorAuthRelatedError(args.stderr)) {
|
|
169
|
-
return
|
|
190
|
+
return `Cursor Agent 没有返回内容。检测到认证相关错误,可能需要重新登录 Cursor Agent,或配置 CURSOR_API_KEY。请在本机运行 agent status 检查状态;如未登录,请运行 agent login 后重试。\n\n${stderrBlock}`;
|
|
170
191
|
}
|
|
171
192
|
|
|
172
|
-
return
|
|
193
|
+
return `Cursor Agent 没有返回内容。底层命令异常退出,错误信息如下:\n\n${stderrBlock}`;
|
|
173
194
|
}
|
|
174
195
|
|
|
175
196
|
function createCursorAgentFailureError(info: CursorProcessCloseInfo): Error {
|
|
@@ -437,6 +458,7 @@ async function* readJsonLines(
|
|
|
437
458
|
debugTag?: string,
|
|
438
459
|
rawLog?: RawStreamLogHandle | null,
|
|
439
460
|
stats?: CursorStreamStats,
|
|
461
|
+
idleTimeoutMs?: number,
|
|
440
462
|
): AsyncGenerator<CursorMessageLine> {
|
|
441
463
|
const tag = debugTag ?? "cursor";
|
|
442
464
|
yield* readJsonLinesWithBadJsonIdleWatchdog<CursorMessageLine>({
|
|
@@ -445,6 +467,7 @@ async function* readJsonLines(
|
|
|
445
467
|
tag,
|
|
446
468
|
signal,
|
|
447
469
|
rawLog,
|
|
470
|
+
idleTimeoutMs,
|
|
448
471
|
parse: (line) => JSON.parse(line) as CursorMessageLine,
|
|
449
472
|
onRawLine: (line) => {
|
|
450
473
|
if (stats) {
|
|
@@ -472,11 +495,18 @@ class CursorAdapter implements ToolAdapter {
|
|
|
472
495
|
private metaStore: CursorSessionMetaStore;
|
|
473
496
|
private modelOverride: string | undefined;
|
|
474
497
|
private spawnImpl: CursorSpawn;
|
|
475
|
-
|
|
476
|
-
|
|
498
|
+
private badJsonIdleTimeoutMs: number | undefined;
|
|
499
|
+
|
|
500
|
+
constructor(
|
|
501
|
+
metaStore: CursorSessionMetaStore,
|
|
502
|
+
modelOverride?: string,
|
|
503
|
+
spawnImpl: CursorSpawn = spawn,
|
|
504
|
+
badJsonIdleTimeoutMs?: number,
|
|
505
|
+
) {
|
|
477
506
|
this.metaStore = metaStore;
|
|
478
507
|
this.modelOverride = modelOverride;
|
|
479
508
|
this.spawnImpl = spawnImpl;
|
|
509
|
+
this.badJsonIdleTimeoutMs = badJsonIdleTimeoutMs;
|
|
480
510
|
}
|
|
481
511
|
|
|
482
512
|
async createSession(cwd: string): Promise<CreateSessionResult> {
|
|
@@ -485,7 +515,7 @@ class CursorAdapter implements ToolAdapter {
|
|
|
485
515
|
const stats = createCursorStreamStats();
|
|
486
516
|
this.activeProcs.add(proc);
|
|
487
517
|
|
|
488
|
-
for await (const msg of readJsonLines(proc, undefined, "createSession", null, stats)) {
|
|
518
|
+
for await (const msg of readJsonLines(proc, undefined, "createSession", null, stats, this.badJsonIdleTimeoutMs)) {
|
|
489
519
|
if (msg.type === "system" && msg.subtype === "init" && msg.session_id) {
|
|
490
520
|
const sessionId = msg.session_id;
|
|
491
521
|
await this.metaStore
|
|
@@ -554,7 +584,7 @@ class CursorAdapter implements ToolAdapter {
|
|
|
554
584
|
const stats = createCursorStreamStats();
|
|
555
585
|
|
|
556
586
|
try {
|
|
557
|
-
for await (const raw of readJsonLines(proc, signal, sessionId, rawLog, stats)) {
|
|
587
|
+
for await (const raw of readJsonLines(proc, signal, sessionId, rawLog, stats, this.badJsonIdleTimeoutMs)) {
|
|
558
588
|
if (signal?.aborted) break;
|
|
559
589
|
if (
|
|
560
590
|
raw.type === "system" &&
|
|
@@ -627,6 +657,8 @@ export interface CreateCursorAdapterOptions {
|
|
|
627
657
|
model?: string;
|
|
628
658
|
/** 注入自定义 spawn 实现(测试用)。 */
|
|
629
659
|
spawn?: CursorSpawn;
|
|
660
|
+
/** Test-only override for the bad JSON idle watchdog threshold. */
|
|
661
|
+
badJsonIdleTimeoutMs?: number;
|
|
630
662
|
}
|
|
631
663
|
|
|
632
664
|
export function createCursorAdapter(
|
|
@@ -636,5 +668,6 @@ export function createCursorAdapter(
|
|
|
636
668
|
options.metaStore ?? defaultCursorSessionMetaStore,
|
|
637
669
|
options.model,
|
|
638
670
|
options.spawn,
|
|
671
|
+
options.badJsonIdleTimeoutMs,
|
|
639
672
|
);
|
|
640
673
|
}
|