chatccc 0.2.201 → 0.2.202

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 (92) hide show
  1. package/README.md +17 -17
  2. package/agent-prompts/claude_specific.md +45 -45
  3. package/agent-prompts/codex_specific.md +2 -2
  4. package/agent-prompts/cursor_specific.md +13 -13
  5. package/bin/cccagent.mjs +17 -17
  6. package/config.sample.json +27 -27
  7. package/im-skills/feishu-skill/receive-send-file.md +63 -63
  8. package/im-skills/feishu-skill/receive-send-image.md +24 -24
  9. package/im-skills/feishu-skill/skill.md +3 -3
  10. package/im-skills/wechat-file-skill/receive-send-file.md +38 -38
  11. package/im-skills/wechat-file-skill/send-file.mjs +83 -83
  12. package/im-skills/wechat-file-skill/skill.md +10 -10
  13. package/im-skills/wechat-image-skill/skill.md +10 -10
  14. package/im-skills/wechat-video-skill/receive-send-video.md +38 -38
  15. package/im-skills/wechat-video-skill/send-video.mjs +79 -79
  16. package/im-skills/wechat-video-skill/skill.md +10 -10
  17. package/package.json +1 -1
  18. package/scripts/postinstall-sharp-check.mjs +58 -58
  19. package/src/__tests__/agent-delegate-task-rpc.test.ts +165 -165
  20. package/src/__tests__/agent-reload-config-rpc.test.ts +99 -99
  21. package/src/__tests__/builtin-chat-session.test.ts +277 -277
  22. package/src/__tests__/builtin-cli-json.test.ts +39 -39
  23. package/src/__tests__/builtin-config.test.ts +33 -33
  24. package/src/__tests__/builtin-context.test.ts +163 -163
  25. package/src/__tests__/builtin-file-tools.test.ts +224 -224
  26. package/src/__tests__/builtin-session-select.test.ts +116 -116
  27. package/src/__tests__/builtin-sigint.test.ts +56 -56
  28. package/src/__tests__/card-plain-text.test.ts +5 -5
  29. package/src/__tests__/cardkit.test.ts +60 -60
  30. package/src/__tests__/cards.test.ts +133 -122
  31. package/src/__tests__/ccc-adapter.test.ts +114 -114
  32. package/src/__tests__/chatgpt-subscription-rpc.test.ts +89 -89
  33. package/src/__tests__/chatgpt-subscription.test.ts +135 -135
  34. package/src/__tests__/chrome-devtools-guard.test.ts +165 -165
  35. package/src/__tests__/claude-adapter.test.ts +592 -592
  36. package/src/__tests__/claude-raw-stream-log.test.ts +87 -87
  37. package/src/__tests__/codex-raw-stream-log.test.ts +163 -163
  38. package/src/__tests__/codex-reset-actions.test.ts +146 -146
  39. package/src/__tests__/config-reload.test.ts +10 -10
  40. package/src/__tests__/config-sample.test.ts +18 -18
  41. package/src/__tests__/cursor-adapter.test.ts +226 -226
  42. package/src/__tests__/feishu-api.test.ts +60 -60
  43. package/src/__tests__/feishu-avatar.test.ts +90 -90
  44. package/src/__tests__/feishu-platform.test.ts +22 -22
  45. package/src/__tests__/format-message.test.ts +47 -47
  46. package/src/__tests__/jsonl-stream.test.ts +79 -79
  47. package/src/__tests__/orchestrator.test.ts +464 -301
  48. package/src/__tests__/privacy.test.ts +198 -198
  49. package/src/__tests__/raw-stream-log.test.ts +106 -106
  50. package/src/__tests__/session.test.ts +46 -44
  51. package/src/__tests__/shared-prefix.test.ts +36 -36
  52. package/src/__tests__/sim-platform.test.ts +12 -12
  53. package/src/__tests__/stream-state.test.ts +42 -42
  54. package/src/__tests__/web-ui.test.ts +209 -209
  55. package/src/adapters/ccc-adapter.ts +121 -121
  56. package/src/adapters/claude-adapter.ts +603 -603
  57. package/src/adapters/claude-session-meta-store.ts +120 -120
  58. package/src/adapters/codex-adapter.ts +380 -380
  59. package/src/adapters/cursor-adapter.ts +673 -673
  60. package/src/adapters/jsonl-stream.ts +157 -157
  61. package/src/adapters/raw-stream-log.ts +124 -124
  62. package/src/adapters/resource-monitor.ts +140 -140
  63. package/src/agent-delegate-task-rpc.ts +153 -153
  64. package/src/agent-delegate-task.ts +81 -81
  65. package/src/agent-reload-config-rpc.ts +34 -34
  66. package/src/agent-stop-stuck.ts +129 -129
  67. package/src/builtin/cli.ts +473 -473
  68. package/src/builtin/context.ts +323 -323
  69. package/src/builtin/file-tools.ts +1072 -1072
  70. package/src/builtin/index.ts +404 -404
  71. package/src/builtin/session-select.ts +48 -48
  72. package/src/builtin/sigint.ts +50 -50
  73. package/src/cards.ts +222 -210
  74. package/src/chatgpt-subscription-rpc.ts +27 -27
  75. package/src/chatgpt-subscription.ts +299 -299
  76. package/src/chrome-devtools-guard.ts +318 -318
  77. package/src/codex-reset-actions.ts +184 -184
  78. package/src/config.ts +125 -125
  79. package/src/feishu-api.ts +118 -118
  80. package/src/feishu-platform.ts +20 -20
  81. package/src/format-message.ts +293 -293
  82. package/src/litellm-proxy.ts +374 -374
  83. package/src/orchestrator.ts +348 -329
  84. package/src/privacy.ts +118 -118
  85. package/src/runtime-reload.ts +34 -34
  86. package/src/session-chat-binding.ts +6 -6
  87. package/src/session-name.ts +8 -8
  88. package/src/session.ts +177 -169
  89. package/src/shared-prefix.ts +29 -29
  90. package/src/sim-platform.ts +20 -20
  91. package/src/turn-cards.ts +117 -117
  92. package/src/web-ui.ts +205 -205
@@ -1,4 +1,4 @@
1
- import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
1
+ import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
2
2
  import { tmpdir } from "node:os";
3
3
  import { join } from "node:path";
4
4
 
@@ -68,7 +68,7 @@ function mockAvatarFetch(uploadedNames: string[], usageResponse: Response): void
68
68
  }));
69
69
  }
70
70
 
71
- function mockAvatarUploadOnlyFetch(uploadedNames: string[]): ReturnType<typeof vi.fn> {
71
+ function mockAvatarUploadOnlyFetch(uploadedNames: string[]): ReturnType<typeof vi.fn> {
72
72
  const fetchMock = vi.fn(async (url: string | URL | Request, init?: RequestInit) => {
73
73
  const urlText = String(url);
74
74
  if (urlText === "https://open.feishu.test/im/v1/images") {
@@ -83,10 +83,44 @@ function mockAvatarUploadOnlyFetch(uploadedNames: string[]): ReturnType<typeof v
83
83
  throw new Error(`unexpected fetch: ${urlText}`);
84
84
  });
85
85
  vi.stubGlobal("fetch", fetchMock);
86
- return fetchMock;
87
- }
88
-
89
- describe("Plain avatar fallback", () => {
86
+ return fetchMock;
87
+ }
88
+
89
+ describe("Plain avatar fallback", () => {
90
+ afterEach(() => {
91
+ vi.unstubAllGlobals();
92
+ vi.doUnmock("node:os");
93
+ vi.doUnmock("../config.ts");
94
+ vi.doUnmock("../cursor-usage.ts");
95
+ vi.restoreAllMocks();
96
+ getCursorUsageSummaryMock.mockReset();
97
+ mockConfig.cursor.avatarBatteryMode = "apiPercent";
98
+ mockConfig.cursor.onDemandMonthlyBudget = 1000;
99
+ });
100
+
101
+ it("uses a status-only avatar for unknown tools instead of falling back to Claude", async () => {
102
+ const homeDir = await mkdtemp(join(tmpdir(), "chatccc-avatar-home-"));
103
+ const userDataDir = await mkdtemp(join(tmpdir(), "chatccc-avatar-data-"));
104
+ const uploadedNames: string[] = [];
105
+ mockAvatarUploadOnlyFetch(uploadedNames);
106
+
107
+ try {
108
+ const { setChatAvatar } = await loadFeishuApiWithHome(homeDir, userDataDir);
109
+ await setChatAvatar("tenant-token", "chat_1", "ccc", "new");
110
+
111
+ expect(uploadedNames).toEqual(["avatar_plain_new.jpg"]);
112
+ const cacheRaw = await readFile(join(userDataDir, "state", "avatar-image-keys.json"), "utf-8");
113
+ const cache = JSON.parse(cacheRaw) as Record<string, string>;
114
+ expect(cache["plain:new"]).toBe("img_test");
115
+ expect(cache["claude:new"]).toBeUndefined();
116
+ } finally {
117
+ await rm(homeDir, { recursive: true, force: true });
118
+ await rm(userDataDir, { recursive: true, force: true });
119
+ }
120
+ });
121
+ });
122
+
123
+ describe("Codex avatar usage battery", () => {
90
124
  afterEach(() => {
91
125
  vi.unstubAllGlobals();
92
126
  vi.doUnmock("node:os");
@@ -98,104 +132,70 @@ describe("Plain avatar fallback", () => {
98
132
  mockConfig.cursor.onDemandMonthlyBudget = 1000;
99
133
  });
100
134
 
101
- it("uses a status-only avatar for unknown tools instead of falling back to Claude", async () => {
102
- const homeDir = await mkdtemp(join(tmpdir(), "chatccc-avatar-home-"));
103
- const userDataDir = await mkdtemp(join(tmpdir(), "chatccc-avatar-data-"));
104
- const uploadedNames: string[] = [];
105
- mockAvatarUploadOnlyFetch(uploadedNames);
106
-
107
- try {
108
- const { setChatAvatar } = await loadFeishuApiWithHome(homeDir, userDataDir);
109
- await setChatAvatar("tenant-token", "chat_1", "ccc", "new");
110
-
111
- expect(uploadedNames).toEqual(["avatar_plain_new.jpg"]);
112
- const cacheRaw = await readFile(join(userDataDir, "state", "avatar-image-keys.json"), "utf-8");
113
- const cache = JSON.parse(cacheRaw) as Record<string, string>;
114
- expect(cache["plain:new"]).toBe("img_test");
115
- expect(cache["claude:new"]).toBeUndefined();
116
- } finally {
117
- await rm(homeDir, { recursive: true, force: true });
118
- await rm(userDataDir, { recursive: true, force: true });
119
- }
120
- });
121
- });
122
-
123
- describe("Codex avatar usage battery", () => {
124
- afterEach(() => {
125
- vi.unstubAllGlobals();
126
- vi.doUnmock("node:os");
127
- vi.doUnmock("../config.ts");
128
- vi.doUnmock("../cursor-usage.ts");
129
- vi.restoreAllMocks();
130
- getCursorUsageSummaryMock.mockReset();
131
- mockConfig.cursor.avatarBatteryMode = "apiPercent";
132
- mockConfig.cursor.onDemandMonthlyBudget = 1000;
133
- });
134
-
135
- it("adds 7-day battery and 5h ring percentages to Codex avatar uploads when both windows exist", async () => {
135
+ it("adds 7-day battery and 5h ring percentages to Codex avatar uploads when both windows exist", async () => {
136
136
  const homeDir = await mkdtemp(join(tmpdir(), "chatccc-avatar-home-"));
137
137
  const userDataDir = await mkdtemp(join(tmpdir(), "chatccc-avatar-data-"));
138
138
  const uploadedNames: string[] = [];
139
139
  await writeCodexAuth(homeDir);
140
140
  mockAvatarFetch(uploadedNames, new Response(JSON.stringify({
141
- rate_limit: {
142
- primary_window: { used_percent: 37, limit_window_seconds: 18000 },
143
- secondary_window: { used_percent: 12, limit_window_seconds: 604800 },
144
- },
141
+ rate_limit: {
142
+ primary_window: { used_percent: 37, limit_window_seconds: 18000 },
143
+ secondary_window: { used_percent: 12, limit_window_seconds: 604800 },
144
+ },
145
145
  }), { status: 200 }));
146
146
 
147
147
  try {
148
148
  const { setChatAvatar } = await loadFeishuApiWithHome(homeDir, userDataDir);
149
149
  await setChatAvatar("tenant-token", "chat_1", "codex", "busy");
150
150
 
151
- expect(uploadedNames).toEqual(["avatar_codex_busy_7d_88_5h_63.jpg"]);
151
+ expect(uploadedNames).toEqual(["avatar_codex_busy_7d_88_5h_63.jpg"]);
152
152
  } finally {
153
153
  await rm(homeDir, { recursive: true, force: true });
154
154
  await rm(userDataDir, { recursive: true, force: true });
155
155
  }
156
- });
157
-
158
- it("uses a 7-day battery without a 5h ring when the API only returns a 7-day window", async () => {
159
- const homeDir = await mkdtemp(join(tmpdir(), "chatccc-avatar-home-"));
160
- const userDataDir = await mkdtemp(join(tmpdir(), "chatccc-avatar-data-"));
161
- const uploadedNames: string[] = [];
162
- await writeCodexAuth(homeDir);
163
- mockAvatarFetch(uploadedNames, new Response(JSON.stringify({
164
- rate_limit: {
165
- primary_window: {
166
- used_percent: 23,
167
- limit_window_seconds: 604800,
168
- reset_after_seconds: 500000,
169
- reset_at: 1784510226,
170
- },
171
- secondary_window: null,
172
- },
173
- }), { status: 200 }));
174
-
175
- try {
176
- const { getCodexUsageSummary, setChatAvatar } = await loadFeishuApiWithHome(homeDir, userDataDir);
177
-
178
- await expect(getCodexUsageSummary()).resolves.toEqual({
179
- fiveHour: null,
180
- weekly: {
181
- usedPercent: 23,
182
- remainingPercent: 77,
183
- resetAfterSeconds: 500000,
184
- resetAtEpochSeconds: 1784510226,
185
- limitWindowSeconds: 604800,
186
- },
187
- rateLimitResetCreditsAvailable: null,
188
- rateLimitResetCredits: [],
189
- });
190
-
191
- await setChatAvatar("tenant-token", "chat_1", "codex", "busy");
192
-
193
- expect(uploadedNames).toEqual(["avatar_codex_busy_7d_77.jpg"]);
194
- } finally {
195
- await rm(homeDir, { recursive: true, force: true });
196
- await rm(userDataDir, { recursive: true, force: true });
197
- }
198
- });
156
+ });
157
+
158
+ it("uses a 7-day battery without a 5h ring when the API only returns a 7-day window", async () => {
159
+ const homeDir = await mkdtemp(join(tmpdir(), "chatccc-avatar-home-"));
160
+ const userDataDir = await mkdtemp(join(tmpdir(), "chatccc-avatar-data-"));
161
+ const uploadedNames: string[] = [];
162
+ await writeCodexAuth(homeDir);
163
+ mockAvatarFetch(uploadedNames, new Response(JSON.stringify({
164
+ rate_limit: {
165
+ primary_window: {
166
+ used_percent: 23,
167
+ limit_window_seconds: 604800,
168
+ reset_after_seconds: 500000,
169
+ reset_at: 1784510226,
170
+ },
171
+ secondary_window: null,
172
+ },
173
+ }), { status: 200 }));
174
+
175
+ try {
176
+ const { getCodexUsageSummary, setChatAvatar } = await loadFeishuApiWithHome(homeDir, userDataDir);
177
+
178
+ await expect(getCodexUsageSummary()).resolves.toEqual({
179
+ fiveHour: null,
180
+ weekly: {
181
+ usedPercent: 23,
182
+ remainingPercent: 77,
183
+ resetAfterSeconds: 500000,
184
+ resetAtEpochSeconds: 1784510226,
185
+ limitWindowSeconds: 604800,
186
+ },
187
+ rateLimitResetCreditsAvailable: null,
188
+ rateLimitResetCredits: [],
189
+ });
190
+
191
+ await setChatAvatar("tenant-token", "chat_1", "codex", "busy");
192
+
193
+ expect(uploadedNames).toEqual(["avatar_codex_busy_7d_77.jpg"]);
194
+ } finally {
195
+ await rm(homeDir, { recursive: true, force: true });
196
+ await rm(userDataDir, { recursive: true, force: true });
197
+ }
198
+ });
199
199
 
200
200
  it("uses provided Codex usage without fetching usage again", async () => {
201
201
  const homeDir = await mkdtemp(join(tmpdir(), "chatccc-avatar-home-"));
@@ -214,7 +214,7 @@ describe("Codex avatar usage battery", () => {
214
214
  },
215
215
  });
216
216
 
217
- expect(uploadedNames).toEqual(["avatar_codex_busy_7d_88_5h_63.jpg"]);
217
+ expect(uploadedNames).toEqual(["avatar_codex_busy_7d_88_5h_63.jpg"]);
218
218
  expect(fetchMock).not.toHaveBeenCalledWith("https://chatgpt.com/backend-api/wham/usage", expect.anything());
219
219
  } finally {
220
220
  await rm(homeDir, { recursive: true, force: true });
@@ -1,5 +1,5 @@
1
1
  import { describe, it, expect, beforeAll, afterAll } from "vitest";
2
- import { getPlatform, setPlatform, getTenantAccessToken, getChatInfo, createGroupChat, updateChatInfo, sendTextReply, sendCardReply, sendRawCard, sendPostMessage, extractSessionInfo, formatDelayNotice, reportPermissionResults, verifyAllPermissions, addReaction, recallMessage, updateCardMessage, setChatAvatar, getCodexUsageSummary, consumeCodexRateLimitResetCredit, disbandChat, sendRestartCard, getMergeForwardMessages } from "../feishu-platform.ts";
2
+ import { getPlatform, setPlatform, getTenantAccessToken, getChatInfo, createGroupChat, updateChatInfo, sendTextReply, sendCardReply, sendRawCard, sendPostMessage, extractSessionInfo, formatDelayNotice, reportPermissionResults, verifyAllPermissions, addReaction, recallMessage, updateCardMessage, setChatAvatar, getCodexUsageSummary, consumeCodexRateLimitResetCredit, disbandChat, sendRestartCard, getMergeForwardMessages } from "../feishu-platform.ts";
3
3
  import type { FeishuPlatform } from "../feishu-platform.ts";
4
4
 
5
5
  const realPlatform = getPlatform();
@@ -26,16 +26,16 @@ describe("feishu-platform", () => {
26
26
  createGroupChat: async () => "mock_chat",
27
27
  updateChatInfo: async () => {},
28
28
  getChatInfo: async () => ({ name: "x", description: "y" }),
29
- disbandChat: async () => {},
30
- setChatAvatar: async () => {},
31
- getCodexUsageSummary: async () => ({
32
- fiveHour: { usedPercent: 10, remainingPercent: 90, resetAtEpochSeconds: 1781528212, resetAfterSeconds: 10349 },
33
- weekly: { usedPercent: 20, remainingPercent: 80, resetAtEpochSeconds: 1781842926, resetAfterSeconds: 325063 },
34
- rateLimitResetCreditsAvailable: 1,
35
- rateLimitResetCredits: [{ grantedAt: null, expiresAt: "2026-07-12T04:01:47.770016Z" }],
36
- }),
37
- consumeCodexRateLimitResetCredit: async () => ({ code: "reset", windowsReset: 2 }),
38
- getOrDownloadImage: async () => "/tmp/img.png",
29
+ disbandChat: async () => {},
30
+ setChatAvatar: async () => {},
31
+ getCodexUsageSummary: async () => ({
32
+ fiveHour: { usedPercent: 10, remainingPercent: 90, resetAtEpochSeconds: 1781528212, resetAfterSeconds: 10349 },
33
+ weekly: { usedPercent: 20, remainingPercent: 80, resetAtEpochSeconds: 1781842926, resetAfterSeconds: 325063 },
34
+ rateLimitResetCreditsAvailable: 1,
35
+ rateLimitResetCredits: [{ grantedAt: null, expiresAt: "2026-07-12T04:01:47.770016Z" }],
36
+ }),
37
+ consumeCodexRateLimitResetCredit: async () => ({ code: "reset", windowsReset: 2 }),
38
+ getOrDownloadImage: async () => "/tmp/img.png",
39
39
  verifyAllPermissions: async () => [],
40
40
  reportPermissionResults: realPlatform.reportPermissionResults,
41
41
  extractSessionInfo: realPlatform.extractSessionInfo,
@@ -53,16 +53,16 @@ describe("feishu-platform", () => {
53
53
  expect(await getChatInfo("t", "mock_chat")).toEqual({ name: "x", description: "y" });
54
54
  const perms = await verifyAllPermissions("t");
55
55
  expect(perms).toEqual([]);
56
- const mergeMsgs = await getMergeForwardMessages("t", "om_test");
57
- expect(mergeMsgs).toEqual([]);
58
- expect(await getCodexUsageSummary()).toEqual({
59
- fiveHour: { usedPercent: 10, remainingPercent: 90, resetAtEpochSeconds: 1781528212, resetAfterSeconds: 10349 },
60
- weekly: { usedPercent: 20, remainingPercent: 80, resetAtEpochSeconds: 1781842926, resetAfterSeconds: 325063 },
61
- rateLimitResetCreditsAvailable: 1,
62
- rateLimitResetCredits: [{ grantedAt: null, expiresAt: "2026-07-12T04:01:47.770016Z" }],
63
- });
64
- expect(await consumeCodexRateLimitResetCredit("request-1")).toEqual({ code: "reset", windowsReset: 2 });
65
- } finally {
56
+ const mergeMsgs = await getMergeForwardMessages("t", "om_test");
57
+ expect(mergeMsgs).toEqual([]);
58
+ expect(await getCodexUsageSummary()).toEqual({
59
+ fiveHour: { usedPercent: 10, remainingPercent: 90, resetAtEpochSeconds: 1781528212, resetAfterSeconds: 10349 },
60
+ weekly: { usedPercent: 20, remainingPercent: 80, resetAtEpochSeconds: 1781842926, resetAfterSeconds: 325063 },
61
+ rateLimitResetCreditsAvailable: 1,
62
+ rateLimitResetCredits: [{ grantedAt: null, expiresAt: "2026-07-12T04:01:47.770016Z" }],
63
+ });
64
+ expect(await consumeCodexRateLimitResetCredit("request-1")).toEqual({ code: "reset", windowsReset: 2 });
65
+ } finally {
66
66
  // 恢复到真实实现,避免影响后续测试
67
67
  setPlatform(realPlatform);
68
68
  }
@@ -72,4 +72,4 @@ describe("feishu-platform", () => {
72
72
  setPlatform(realPlatform);
73
73
  expect(getPlatform()).toBe(realPlatform);
74
74
  });
75
- });
75
+ });
@@ -11,52 +11,52 @@ vi.mock("../feishu-platform.ts", () => ({
11
11
  getMergeForwardMessages: (...args: unknown[]) => mockGetMergeForwardMessages(...args),
12
12
  }));
13
13
 
14
- import { formatMessageContent, formatPostContent, formatMergeForward } from "../format-message.ts";
15
-
16
- describe("formatMessageContent mixed post images", () => {
17
- beforeEach(() => {
18
- vi.clearAllMocks();
19
- mockGetTenantAccessToken.mockResolvedValue("mock_token");
20
- });
21
-
22
- it("includes downloaded images from mixed post messages", async () => {
23
- mockGetOrDownloadImage.mockResolvedValue("C:\\tmp\\img_001.png");
24
-
25
- const result = await formatMessageContent({
26
- message_id: "om_post",
27
- message_type: "post",
28
- content: JSON.stringify({
29
- content: [[
30
- { tag: "text", text: "use this image" },
31
- { tag: "img", image_key: "img_001" },
32
- ]],
33
- }),
34
- });
35
-
36
- expect(result).toBe("use this image\n[图片] C:\\tmp\\img_001.png");
37
- expect(mockGetTenantAccessToken).toHaveBeenCalled();
38
- expect(mockGetOrDownloadImage).toHaveBeenCalledWith("mock_token", "om_post", "img_001");
39
- });
40
-
41
- it("keeps post image key when mixed post image download fails", async () => {
42
- mockGetOrDownloadImage.mockRejectedValue(new Error("download failed"));
43
-
44
- const result = await formatMessageContent({
45
- message_id: "om_post",
46
- message_type: "post",
47
- content: JSON.stringify({
48
- content: [[
49
- { tag: "text", text: "caption" },
50
- { tag: "img", image_key: "img_002" },
51
- ]],
52
- }),
53
- });
54
-
55
- expect(result).toBe("caption\n[图片: img_002]");
56
- });
57
- });
58
-
59
- describe("formatMessageContent", () => {
14
+ import { formatMessageContent, formatPostContent, formatMergeForward } from "../format-message.ts";
15
+
16
+ describe("formatMessageContent mixed post images", () => {
17
+ beforeEach(() => {
18
+ vi.clearAllMocks();
19
+ mockGetTenantAccessToken.mockResolvedValue("mock_token");
20
+ });
21
+
22
+ it("includes downloaded images from mixed post messages", async () => {
23
+ mockGetOrDownloadImage.mockResolvedValue("C:\\tmp\\img_001.png");
24
+
25
+ const result = await formatMessageContent({
26
+ message_id: "om_post",
27
+ message_type: "post",
28
+ content: JSON.stringify({
29
+ content: [[
30
+ { tag: "text", text: "use this image" },
31
+ { tag: "img", image_key: "img_001" },
32
+ ]],
33
+ }),
34
+ });
35
+
36
+ expect(result).toBe("use this image\n[图片] C:\\tmp\\img_001.png");
37
+ expect(mockGetTenantAccessToken).toHaveBeenCalled();
38
+ expect(mockGetOrDownloadImage).toHaveBeenCalledWith("mock_token", "om_post", "img_001");
39
+ });
40
+
41
+ it("keeps post image key when mixed post image download fails", async () => {
42
+ mockGetOrDownloadImage.mockRejectedValue(new Error("download failed"));
43
+
44
+ const result = await formatMessageContent({
45
+ message_id: "om_post",
46
+ message_type: "post",
47
+ content: JSON.stringify({
48
+ content: [[
49
+ { tag: "text", text: "caption" },
50
+ { tag: "img", image_key: "img_002" },
51
+ ]],
52
+ }),
53
+ });
54
+
55
+ expect(result).toBe("caption\n[图片: img_002]");
56
+ });
57
+ });
58
+
59
+ describe("formatMessageContent", () => {
60
60
  beforeEach(() => {
61
61
  vi.clearAllMocks();
62
62
  mockGetTenantAccessToken.mockResolvedValue("mock_token");
@@ -313,4 +313,4 @@ describe("formatPostContent", () => {
313
313
  const result = formatPostContent({ content: [null, undefined, "string"] });
314
314
  expect(result).toBe("");
315
315
  });
316
- });
316
+ });
@@ -1,79 +1,79 @@
1
- import { PassThrough } from "node:stream";
2
-
3
- import { afterEach, describe, expect, it, vi } from "vitest";
4
-
5
- import {
6
- BadJsonIdleTimeoutError,
7
- readJsonLinesWithBadJsonIdleWatchdog,
8
- } from "../adapters/jsonl-stream.ts";
9
-
10
- function createReader(input: PassThrough, idleTimeoutMs = 100): AsyncIterator<unknown> {
11
- return readJsonLinesWithBadJsonIdleWatchdog({
12
- input,
13
- tool: "test-agent",
14
- tag: "sid-test",
15
- idleTimeoutMs,
16
- parse: (line) => JSON.parse(line) as unknown,
17
- })[Symbol.asyncIterator]();
18
- }
19
-
20
- afterEach(() => {
21
- vi.useRealTimers();
22
- });
23
-
24
- describe("readJsonLinesWithBadJsonIdleWatchdog", () => {
25
- it("throws when a JSON-like bad line remains the last stdout past the idle timeout", async () => {
26
- vi.useFakeTimers();
27
- const input = new PassThrough();
28
- const iterator = createReader(input);
29
-
30
- const pending = iterator.next().catch((error: unknown) => error);
31
- input.write("{\"type\":\"tool_call\",\"subtype\":\"started\"\n");
32
-
33
- await vi.advanceTimersByTimeAsync(100);
34
-
35
- const error = await pending;
36
- expect(error).toBeInstanceOf(BadJsonIdleTimeoutError);
37
- expect(error).toMatchObject({
38
- code: "BAD_JSON_IDLE_TIMEOUT",
39
- tool: "test-agent",
40
- tag: "sid-test",
41
- });
42
- });
43
-
44
- it("does not throw when a valid JSON line arrives after the bad line before timeout", async () => {
45
- vi.useFakeTimers();
46
- const input = new PassThrough();
47
- const iterator = createReader(input);
48
-
49
- const pending = iterator.next();
50
- input.write("{\"type\":\"tool_call\"\n");
51
- await vi.advanceTimersByTimeAsync(50);
52
- input.write("{\"type\":\"ok\"}\n");
53
-
54
- await expect(pending).resolves.toEqual({
55
- done: false,
56
- value: { type: "ok" },
57
- });
58
-
59
- const done = iterator.next();
60
- input.end();
61
- await expect(done).resolves.toEqual({ done: true, value: undefined });
62
- });
63
-
64
- it("ignores non-JSON banner lines for the bad JSON idle watchdog", async () => {
65
- vi.useFakeTimers();
66
- const input = new PassThrough();
67
- const iterator = createReader(input);
68
-
69
- const pending = iterator.next();
70
- input.write("Reading prompt from stdin...\n");
71
- await vi.advanceTimersByTimeAsync(200);
72
- input.write("{\"type\":\"ok\"}\n");
73
-
74
- await expect(pending).resolves.toEqual({
75
- done: false,
76
- value: { type: "ok" },
77
- });
78
- });
79
- });
1
+ import { PassThrough } from "node:stream";
2
+
3
+ import { afterEach, describe, expect, it, vi } from "vitest";
4
+
5
+ import {
6
+ BadJsonIdleTimeoutError,
7
+ readJsonLinesWithBadJsonIdleWatchdog,
8
+ } from "../adapters/jsonl-stream.ts";
9
+
10
+ function createReader(input: PassThrough, idleTimeoutMs = 100): AsyncIterator<unknown> {
11
+ return readJsonLinesWithBadJsonIdleWatchdog({
12
+ input,
13
+ tool: "test-agent",
14
+ tag: "sid-test",
15
+ idleTimeoutMs,
16
+ parse: (line) => JSON.parse(line) as unknown,
17
+ })[Symbol.asyncIterator]();
18
+ }
19
+
20
+ afterEach(() => {
21
+ vi.useRealTimers();
22
+ });
23
+
24
+ describe("readJsonLinesWithBadJsonIdleWatchdog", () => {
25
+ it("throws when a JSON-like bad line remains the last stdout past the idle timeout", async () => {
26
+ vi.useFakeTimers();
27
+ const input = new PassThrough();
28
+ const iterator = createReader(input);
29
+
30
+ const pending = iterator.next().catch((error: unknown) => error);
31
+ input.write("{\"type\":\"tool_call\",\"subtype\":\"started\"\n");
32
+
33
+ await vi.advanceTimersByTimeAsync(100);
34
+
35
+ const error = await pending;
36
+ expect(error).toBeInstanceOf(BadJsonIdleTimeoutError);
37
+ expect(error).toMatchObject({
38
+ code: "BAD_JSON_IDLE_TIMEOUT",
39
+ tool: "test-agent",
40
+ tag: "sid-test",
41
+ });
42
+ });
43
+
44
+ it("does not throw when a valid JSON line arrives after the bad line before timeout", async () => {
45
+ vi.useFakeTimers();
46
+ const input = new PassThrough();
47
+ const iterator = createReader(input);
48
+
49
+ const pending = iterator.next();
50
+ input.write("{\"type\":\"tool_call\"\n");
51
+ await vi.advanceTimersByTimeAsync(50);
52
+ input.write("{\"type\":\"ok\"}\n");
53
+
54
+ await expect(pending).resolves.toEqual({
55
+ done: false,
56
+ value: { type: "ok" },
57
+ });
58
+
59
+ const done = iterator.next();
60
+ input.end();
61
+ await expect(done).resolves.toEqual({ done: true, value: undefined });
62
+ });
63
+
64
+ it("ignores non-JSON banner lines for the bad JSON idle watchdog", async () => {
65
+ vi.useFakeTimers();
66
+ const input = new PassThrough();
67
+ const iterator = createReader(input);
68
+
69
+ const pending = iterator.next();
70
+ input.write("Reading prompt from stdin...\n");
71
+ await vi.advanceTimersByTimeAsync(200);
72
+ input.write("{\"type\":\"ok\"}\n");
73
+
74
+ await expect(pending).resolves.toEqual({
75
+ done: false,
76
+ value: { type: "ok" },
77
+ });
78
+ });
79
+ });