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.
Files changed (49) hide show
  1. package/README.md +1 -1
  2. package/agent-prompts/claude_specific.md +45 -45
  3. package/agent-prompts/codex_specific.md +2 -2
  4. package/im-skills/feishu-skill/receive-send-file.md +63 -63
  5. package/im-skills/feishu-skill/receive-send-image.md +24 -24
  6. package/im-skills/feishu-skill/skill.md +3 -3
  7. package/im-skills/wechat-file-skill/receive-send-file.md +38 -38
  8. package/im-skills/wechat-file-skill/send-file.mjs +83 -83
  9. package/im-skills/wechat-file-skill/skill.md +10 -10
  10. package/im-skills/wechat-image-skill/skill.md +10 -10
  11. package/im-skills/wechat-video-skill/receive-send-video.md +38 -38
  12. package/im-skills/wechat-video-skill/send-video.mjs +79 -79
  13. package/im-skills/wechat-video-skill/skill.md +10 -10
  14. package/package.json +1 -1
  15. package/scripts/postinstall-sharp-check.mjs +58 -58
  16. package/src/__tests__/agent-delegate-task-rpc.test.ts +165 -165
  17. package/src/__tests__/card-plain-text.test.ts +5 -5
  18. package/src/__tests__/cardkit.test.ts +60 -60
  19. package/src/__tests__/cards.test.ts +1 -1
  20. package/src/__tests__/claude-adapter.test.ts +592 -592
  21. package/src/__tests__/codex-reset-actions.test.ts +146 -146
  22. package/src/__tests__/cursor-adapter.test.ts +66 -7
  23. package/src/__tests__/feishu-api.test.ts +60 -60
  24. package/src/__tests__/feishu-avatar.test.ts +47 -5
  25. package/src/__tests__/feishu-platform.test.ts +22 -22
  26. package/src/__tests__/format-message.test.ts +47 -47
  27. package/src/__tests__/orchestrator.test.ts +27 -2
  28. package/src/__tests__/privacy.test.ts +198 -198
  29. package/src/__tests__/shared-prefix.test.ts +36 -36
  30. package/src/__tests__/stream-state.test.ts +42 -42
  31. package/src/adapters/claude-session-meta-store.ts +120 -120
  32. package/src/adapters/cursor-adapter.ts +39 -6
  33. package/src/adapters/resource-monitor.ts +140 -140
  34. package/src/agent-delegate-task-rpc.ts +153 -153
  35. package/src/agent-delegate-task.ts +81 -81
  36. package/src/agent-stop-stuck.ts +129 -129
  37. package/src/cards.ts +1 -1
  38. package/src/codex-reset-actions.ts +184 -184
  39. package/src/feishu-api.ts +41 -18
  40. package/src/feishu-platform.ts +20 -20
  41. package/src/format-message.ts +293 -293
  42. package/src/litellm-proxy.ts +374 -374
  43. package/src/orchestrator.ts +4 -4
  44. package/src/privacy.ts +118 -118
  45. package/src/session-chat-binding.ts +6 -6
  46. package/src/session-name.ts +8 -8
  47. package/src/shared-prefix.ts +29 -29
  48. package/src/sim-platform.ts +20 -20
  49. 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 "Cursor Agent 没有返回内容。检测到认证相关错误,可能需要重新登录 Cursor Agent,或配置 CURSOR_API_KEY。请在本机运行 agent status 检查状态;如未登录,请运行 agent login 后重试。";
190
+ return `Cursor Agent 没有返回内容。检测到认证相关错误,可能需要重新登录 Cursor Agent,或配置 CURSOR_API_KEY。请在本机运行 agent status 检查状态;如未登录,请运行 agent login 后重试。\n\n${stderrBlock}`;
170
191
  }
171
192
 
172
- return "Cursor Agent 没有返回内容。底层命令异常退出,请检查本机 Cursor Agent 状态后重试。";
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
- constructor(metaStore: CursorSessionMetaStore, modelOverride?: string, spawnImpl: CursorSpawn = spawn) {
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
  }