chatccc 0.2.199 → 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/agent-prompts/cursor_specific.md +13 -13
- package/bin/cccagent.mjs +17 -17
- package/config.sample.json +27 -27
- 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__/agent-reload-config-rpc.test.ts +99 -99
- package/src/__tests__/builtin-chat-session.test.ts +277 -277
- package/src/__tests__/builtin-cli-json.test.ts +39 -39
- package/src/__tests__/builtin-config.test.ts +33 -33
- package/src/__tests__/builtin-context.test.ts +163 -163
- package/src/__tests__/builtin-file-tools.test.ts +224 -224
- package/src/__tests__/builtin-session-select.test.ts +116 -116
- package/src/__tests__/builtin-sigint.test.ts +56 -56
- package/src/__tests__/card-plain-text.test.ts +5 -5
- package/src/__tests__/cardkit.test.ts +60 -60
- package/src/__tests__/cards.test.ts +109 -109
- package/src/__tests__/ccc-adapter.test.ts +114 -114
- 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__/claude-raw-stream-log.test.ts +87 -87
- package/src/__tests__/codex-raw-stream-log.test.ts +163 -163
- package/src/__tests__/codex-reset-actions.test.ts +146 -146
- package/src/__tests__/config-reload.test.ts +10 -10
- package/src/__tests__/config-sample.test.ts +18 -18
- package/src/__tests__/feishu-api.test.ts +60 -60
- package/src/__tests__/feishu-avatar.test.ts +87 -45
- package/src/__tests__/feishu-platform.test.ts +22 -22
- package/src/__tests__/format-message.test.ts +47 -47
- package/src/__tests__/jsonl-stream.test.ts +79 -79
- package/src/__tests__/orchestrator.test.ts +227 -202
- package/src/__tests__/privacy.test.ts +198 -198
- package/src/__tests__/raw-stream-log.test.ts +106 -106
- package/src/__tests__/session.test.ts +40 -40
- package/src/__tests__/shared-prefix.test.ts +36 -36
- package/src/__tests__/sim-platform.test.ts +12 -12
- package/src/__tests__/stream-state.test.ts +42 -42
- package/src/__tests__/web-ui.test.ts +209 -209
- package/src/adapters/ccc-adapter.ts +121 -121
- package/src/adapters/claude-adapter.ts +603 -603
- package/src/adapters/claude-session-meta-store.ts +120 -120
- package/src/adapters/codex-adapter.ts +380 -380
- package/src/adapters/jsonl-stream.ts +157 -157
- 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-reload-config-rpc.ts +34 -34
- package/src/agent-stop-stuck.ts +129 -129
- package/src/builtin/cli.ts +473 -473
- package/src/builtin/context.ts +323 -323
- package/src/builtin/file-tools.ts +1072 -1072
- package/src/builtin/index.ts +404 -404
- package/src/builtin/session-select.ts +48 -48
- package/src/builtin/sigint.ts +50 -50
- package/src/cards.ts +195 -195
- 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 +125 -125
- package/src/feishu-api.ts +86 -63
- 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 +183 -183
- package/src/privacy.ts +118 -118
- package/src/runtime-reload.ts +34 -34
- package/src/session-chat-binding.ts +6 -6
- package/src/session-name.ts +8 -8
- package/src/session.ts +141 -141
- 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 +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,44 +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", () => {
|
|
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", () => {
|
|
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", () => {
|
|
124
124
|
afterEach(() => {
|
|
125
125
|
vi.unstubAllGlobals();
|
|
126
126
|
vi.doUnmock("node:os");
|
|
@@ -132,15 +132,15 @@ describe("Codex avatar usage battery", () => {
|
|
|
132
132
|
mockConfig.cursor.onDemandMonthlyBudget = 1000;
|
|
133
133
|
});
|
|
134
134
|
|
|
135
|
-
it("adds
|
|
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
141
|
rate_limit: {
|
|
142
|
-
primary_window: { used_percent: 37 },
|
|
143
|
-
secondary_window: { used_percent: 12 },
|
|
142
|
+
primary_window: { used_percent: 37, limit_window_seconds: 18000 },
|
|
143
|
+
secondary_window: { used_percent: 12, limit_window_seconds: 604800 },
|
|
144
144
|
},
|
|
145
145
|
}), { status: 200 }));
|
|
146
146
|
|
|
@@ -148,7 +148,49 @@ describe("Codex avatar usage battery", () => {
|
|
|
148
148
|
const { setChatAvatar } = await loadFeishuApiWithHome(homeDir, userDataDir);
|
|
149
149
|
await setChatAvatar("tenant-token", "chat_1", "codex", "busy");
|
|
150
150
|
|
|
151
|
-
expect(uploadedNames).toEqual(["
|
|
151
|
+
expect(uploadedNames).toEqual(["avatar_codex_busy_7d_88_5h_63.jpg"]);
|
|
152
|
+
} finally {
|
|
153
|
+
await rm(homeDir, { recursive: true, force: true });
|
|
154
|
+
await rm(userDataDir, { recursive: true, force: true });
|
|
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"]);
|
|
152
194
|
} finally {
|
|
153
195
|
await rm(homeDir, { recursive: true, force: true });
|
|
154
196
|
await rm(userDataDir, { recursive: true, force: true });
|
|
@@ -172,7 +214,7 @@ describe("Codex avatar usage battery", () => {
|
|
|
172
214
|
},
|
|
173
215
|
});
|
|
174
216
|
|
|
175
|
-
expect(uploadedNames).toEqual(["
|
|
217
|
+
expect(uploadedNames).toEqual(["avatar_codex_busy_7d_88_5h_63.jpg"]);
|
|
176
218
|
expect(fetchMock).not.toHaveBeenCalledWith("https://chatgpt.com/backend-api/wham/usage", expect.anything());
|
|
177
219
|
} finally {
|
|
178
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
|
+
});
|