chatccc 0.2.181 → 0.2.182
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 +69 -69
- package/package.json +1 -1
- package/src/__tests__/card-plain-text.test.ts +5 -5
- package/src/__tests__/cards.test.ts +77 -77
- package/src/__tests__/codex-reset-actions.test.ts +40 -7
- package/src/__tests__/feishu-avatar.test.ts +115 -115
- package/src/__tests__/feishu-platform.test.ts +22 -22
- package/src/__tests__/orchestrator.test.ts +117 -117
- package/src/__tests__/shared-prefix.test.ts +36 -36
- package/src/cards.ts +89 -89
- package/src/codex-reset-actions.ts +19 -6
- package/src/feishu-api.ts +184 -184
- package/src/feishu-platform.ts +20 -20
- package/src/index.ts +2 -0
- package/src/orchestrator.ts +120 -120
- package/src/shared-prefix.ts +29 -29
- package/src/sim-platform.ts +20 -20
|
@@ -39,23 +39,23 @@ async function loadFeishuApiWithHome(homeDir: string, userDataDir: string) {
|
|
|
39
39
|
async function writeCodexAuth(homeDir: string): Promise<void> {
|
|
40
40
|
const codexDir = join(homeDir, ".codex");
|
|
41
41
|
await mkdir(codexDir, { recursive: true });
|
|
42
|
-
await writeFile(
|
|
43
|
-
join(codexDir, "auth.json"),
|
|
44
|
-
JSON.stringify({ tokens: { access_token: "codex-access-token", account_id: "codex-account-id" } }),
|
|
45
|
-
"utf-8",
|
|
46
|
-
);
|
|
47
|
-
}
|
|
42
|
+
await writeFile(
|
|
43
|
+
join(codexDir, "auth.json"),
|
|
44
|
+
JSON.stringify({ tokens: { access_token: "codex-access-token", account_id: "codex-account-id" } }),
|
|
45
|
+
"utf-8",
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
48
|
|
|
49
49
|
function mockAvatarFetch(uploadedNames: string[], usageResponse: Response): void {
|
|
50
50
|
vi.stubGlobal("fetch", vi.fn(async (url: string | URL | Request, init?: RequestInit) => {
|
|
51
51
|
const urlText = String(url);
|
|
52
|
-
if (urlText === "https://chatgpt.com/backend-api/wham/usage") {
|
|
53
|
-
return usageResponse.clone();
|
|
54
|
-
}
|
|
55
|
-
if (urlText === "https://chatgpt.com/backend-api/wham/rate-limit-reset-credits") {
|
|
56
|
-
return new Response(JSON.stringify({ credits: [] }), { status: 200 });
|
|
57
|
-
}
|
|
58
|
-
if (urlText === "https://open.feishu.test/im/v1/images") {
|
|
52
|
+
if (urlText === "https://chatgpt.com/backend-api/wham/usage") {
|
|
53
|
+
return usageResponse.clone();
|
|
54
|
+
}
|
|
55
|
+
if (urlText === "https://chatgpt.com/backend-api/wham/rate-limit-reset-credits") {
|
|
56
|
+
return new Response(JSON.stringify({ credits: [] }), { status: 200 });
|
|
57
|
+
}
|
|
58
|
+
if (urlText === "https://open.feishu.test/im/v1/images") {
|
|
59
59
|
const form = init?.body as FormData;
|
|
60
60
|
const image = form.get("image") as File;
|
|
61
61
|
uploadedNames.push(image.name);
|
|
@@ -103,109 +103,109 @@ describe("Codex avatar usage battery", () => {
|
|
|
103
103
|
}
|
|
104
104
|
});
|
|
105
105
|
|
|
106
|
-
it("returns both usage windows and available reset credit expiries", async () => {
|
|
107
|
-
const homeDir = await mkdtemp(join(tmpdir(), "chatccc-avatar-home-"));
|
|
108
|
-
const userDataDir = await mkdtemp(join(tmpdir(), "chatccc-avatar-data-"));
|
|
109
|
-
await writeCodexAuth(homeDir);
|
|
110
|
-
const fetchMock = vi.fn(async (url: string | URL | Request) => {
|
|
111
|
-
const urlText = String(url);
|
|
112
|
-
if (urlText === "https://chatgpt.com/backend-api/wham/usage") {
|
|
113
|
-
return new Response(JSON.stringify({
|
|
114
|
-
rate_limit: {
|
|
115
|
-
primary_window: { used_percent: 37, reset_after_seconds: 10349, reset_at: 1781528212 },
|
|
116
|
-
secondary_window: { used_percent: 12, reset_after_seconds: 325063, reset_at: 1781842926 },
|
|
117
|
-
},
|
|
118
|
-
rate_limit_reset_credits: { available_count: 1 },
|
|
119
|
-
}), { status: 200 });
|
|
120
|
-
}
|
|
121
|
-
if (urlText === "https://chatgpt.com/backend-api/wham/rate-limit-reset-credits") {
|
|
122
|
-
return new Response(JSON.stringify({
|
|
123
|
-
available_count: 2,
|
|
124
|
-
credits: [
|
|
125
|
-
{
|
|
126
|
-
status: "available",
|
|
127
|
-
granted_at: "2026-06-12T04:01:47.770016Z",
|
|
128
|
-
expires_at: "2026-07-12T04:01:47.770016Z",
|
|
129
|
-
},
|
|
130
|
-
{
|
|
131
|
-
status: "redeemed",
|
|
132
|
-
granted_at: "2026-06-13T04:01:47.770016Z",
|
|
133
|
-
expires_at: "2026-07-13T04:01:47.770016Z",
|
|
134
|
-
},
|
|
135
|
-
{
|
|
136
|
-
status: "available",
|
|
137
|
-
granted_at: "2026-06-18T00:44:23.904386Z",
|
|
138
|
-
expires_at: "2026-07-18T00:44:23.904386Z",
|
|
139
|
-
},
|
|
140
|
-
],
|
|
141
|
-
}), { status: 200 });
|
|
142
|
-
}
|
|
143
|
-
throw new Error(`unexpected fetch: ${urlText}`);
|
|
144
|
-
});
|
|
145
|
-
vi.stubGlobal("fetch", fetchMock);
|
|
146
|
-
|
|
147
|
-
try {
|
|
148
|
-
const { getCodexUsageSummary } = await loadFeishuApiWithHome(homeDir, userDataDir);
|
|
149
|
-
await expect(getCodexUsageSummary()).resolves.toEqual({
|
|
150
|
-
fiveHour: { usedPercent: 37, remainingPercent: 63, resetAfterSeconds: 10349, resetAtEpochSeconds: 1781528212 },
|
|
151
|
-
weekly: { usedPercent: 12, remainingPercent: 88, resetAfterSeconds: 325063, resetAtEpochSeconds: 1781842926 },
|
|
152
|
-
rateLimitResetCreditsAvailable: 2,
|
|
153
|
-
rateLimitResetCredits: [
|
|
154
|
-
{ grantedAt: "2026-06-12T04:01:47.770016Z", expiresAt: "2026-07-12T04:01:47.770016Z" },
|
|
155
|
-
{ grantedAt: "2026-06-18T00:44:23.904386Z", expiresAt: "2026-07-18T00:44:23.904386Z" },
|
|
156
|
-
],
|
|
157
|
-
});
|
|
158
|
-
expect(fetchMock).toHaveBeenCalledWith(
|
|
159
|
-
"https://chatgpt.com/backend-api/wham/rate-limit-reset-credits",
|
|
160
|
-
expect.objectContaining({
|
|
161
|
-
headers: expect.objectContaining({
|
|
162
|
-
Authorization: "Bearer codex-access-token",
|
|
163
|
-
"ChatGPT-Account-ID": "codex-account-id",
|
|
164
|
-
"OpenAI-Beta": "codex-1",
|
|
165
|
-
originator: "Codex Desktop",
|
|
166
|
-
}),
|
|
167
|
-
}),
|
|
168
|
-
);
|
|
169
|
-
} finally {
|
|
170
|
-
await rm(homeDir, { recursive: true, force: true });
|
|
171
|
-
await rm(userDataDir, { recursive: true, force: true });
|
|
172
|
-
}
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
it("consumes a Codex rate-limit reset credit through the WHAM endpoint", async () => {
|
|
176
|
-
const homeDir = await mkdtemp(join(tmpdir(), "chatccc-avatar-home-"));
|
|
177
|
-
const userDataDir = await mkdtemp(join(tmpdir(), "chatccc-avatar-data-"));
|
|
178
|
-
await writeCodexAuth(homeDir);
|
|
179
|
-
const fetchMock = vi.fn(async () => new Response(JSON.stringify({
|
|
180
|
-
code: "reset",
|
|
181
|
-
windows_reset: 2,
|
|
182
|
-
}), { status: 200 }));
|
|
183
|
-
vi.stubGlobal("fetch", fetchMock);
|
|
184
|
-
|
|
185
|
-
try {
|
|
186
|
-
const { consumeCodexRateLimitResetCredit } = await loadFeishuApiWithHome(homeDir, userDataDir);
|
|
187
|
-
await expect(consumeCodexRateLimitResetCredit("request-1")).resolves.toEqual({
|
|
188
|
-
code: "reset",
|
|
189
|
-
windowsReset: 2,
|
|
190
|
-
});
|
|
191
|
-
expect(fetchMock).toHaveBeenCalledWith(
|
|
192
|
-
"https://chatgpt.com/backend-api/wham/rate-limit-reset-credits/consume",
|
|
193
|
-
expect.objectContaining({
|
|
194
|
-
method: "POST",
|
|
195
|
-
headers: expect.objectContaining({
|
|
196
|
-
Authorization: "Bearer codex-access-token",
|
|
197
|
-
"Content-Type": "application/json",
|
|
198
|
-
}),
|
|
199
|
-
body: JSON.stringify({ redeem_request_id: "request-1" }),
|
|
200
|
-
}),
|
|
201
|
-
);
|
|
202
|
-
} finally {
|
|
203
|
-
await rm(homeDir, { recursive: true, force: true });
|
|
204
|
-
await rm(userDataDir, { recursive: true, force: true });
|
|
106
|
+
it("returns both usage windows and available reset credit expiries", async () => {
|
|
107
|
+
const homeDir = await mkdtemp(join(tmpdir(), "chatccc-avatar-home-"));
|
|
108
|
+
const userDataDir = await mkdtemp(join(tmpdir(), "chatccc-avatar-data-"));
|
|
109
|
+
await writeCodexAuth(homeDir);
|
|
110
|
+
const fetchMock = vi.fn(async (url: string | URL | Request) => {
|
|
111
|
+
const urlText = String(url);
|
|
112
|
+
if (urlText === "https://chatgpt.com/backend-api/wham/usage") {
|
|
113
|
+
return new Response(JSON.stringify({
|
|
114
|
+
rate_limit: {
|
|
115
|
+
primary_window: { used_percent: 37, reset_after_seconds: 10349, reset_at: 1781528212 },
|
|
116
|
+
secondary_window: { used_percent: 12, reset_after_seconds: 325063, reset_at: 1781842926 },
|
|
117
|
+
},
|
|
118
|
+
rate_limit_reset_credits: { available_count: 1 },
|
|
119
|
+
}), { status: 200 });
|
|
120
|
+
}
|
|
121
|
+
if (urlText === "https://chatgpt.com/backend-api/wham/rate-limit-reset-credits") {
|
|
122
|
+
return new Response(JSON.stringify({
|
|
123
|
+
available_count: 2,
|
|
124
|
+
credits: [
|
|
125
|
+
{
|
|
126
|
+
status: "available",
|
|
127
|
+
granted_at: "2026-06-12T04:01:47.770016Z",
|
|
128
|
+
expires_at: "2026-07-12T04:01:47.770016Z",
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
status: "redeemed",
|
|
132
|
+
granted_at: "2026-06-13T04:01:47.770016Z",
|
|
133
|
+
expires_at: "2026-07-13T04:01:47.770016Z",
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
status: "available",
|
|
137
|
+
granted_at: "2026-06-18T00:44:23.904386Z",
|
|
138
|
+
expires_at: "2026-07-18T00:44:23.904386Z",
|
|
139
|
+
},
|
|
140
|
+
],
|
|
141
|
+
}), { status: 200 });
|
|
142
|
+
}
|
|
143
|
+
throw new Error(`unexpected fetch: ${urlText}`);
|
|
144
|
+
});
|
|
145
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
146
|
+
|
|
147
|
+
try {
|
|
148
|
+
const { getCodexUsageSummary } = await loadFeishuApiWithHome(homeDir, userDataDir);
|
|
149
|
+
await expect(getCodexUsageSummary()).resolves.toEqual({
|
|
150
|
+
fiveHour: { usedPercent: 37, remainingPercent: 63, resetAfterSeconds: 10349, resetAtEpochSeconds: 1781528212 },
|
|
151
|
+
weekly: { usedPercent: 12, remainingPercent: 88, resetAfterSeconds: 325063, resetAtEpochSeconds: 1781842926 },
|
|
152
|
+
rateLimitResetCreditsAvailable: 2,
|
|
153
|
+
rateLimitResetCredits: [
|
|
154
|
+
{ grantedAt: "2026-06-12T04:01:47.770016Z", expiresAt: "2026-07-12T04:01:47.770016Z" },
|
|
155
|
+
{ grantedAt: "2026-06-18T00:44:23.904386Z", expiresAt: "2026-07-18T00:44:23.904386Z" },
|
|
156
|
+
],
|
|
157
|
+
});
|
|
158
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
159
|
+
"https://chatgpt.com/backend-api/wham/rate-limit-reset-credits",
|
|
160
|
+
expect.objectContaining({
|
|
161
|
+
headers: expect.objectContaining({
|
|
162
|
+
Authorization: "Bearer codex-access-token",
|
|
163
|
+
"ChatGPT-Account-ID": "codex-account-id",
|
|
164
|
+
"OpenAI-Beta": "codex-1",
|
|
165
|
+
originator: "Codex Desktop",
|
|
166
|
+
}),
|
|
167
|
+
}),
|
|
168
|
+
);
|
|
169
|
+
} finally {
|
|
170
|
+
await rm(homeDir, { recursive: true, force: true });
|
|
171
|
+
await rm(userDataDir, { recursive: true, force: true });
|
|
205
172
|
}
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
it("
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it("consumes a Codex rate-limit reset credit through the WHAM endpoint", async () => {
|
|
176
|
+
const homeDir = await mkdtemp(join(tmpdir(), "chatccc-avatar-home-"));
|
|
177
|
+
const userDataDir = await mkdtemp(join(tmpdir(), "chatccc-avatar-data-"));
|
|
178
|
+
await writeCodexAuth(homeDir);
|
|
179
|
+
const fetchMock = vi.fn(async () => new Response(JSON.stringify({
|
|
180
|
+
code: "reset",
|
|
181
|
+
windows_reset: 2,
|
|
182
|
+
}), { status: 200 }));
|
|
183
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
184
|
+
|
|
185
|
+
try {
|
|
186
|
+
const { consumeCodexRateLimitResetCredit } = await loadFeishuApiWithHome(homeDir, userDataDir);
|
|
187
|
+
await expect(consumeCodexRateLimitResetCredit("request-1")).resolves.toEqual({
|
|
188
|
+
code: "reset",
|
|
189
|
+
windowsReset: 2,
|
|
190
|
+
});
|
|
191
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
192
|
+
"https://chatgpt.com/backend-api/wham/rate-limit-reset-credits/consume",
|
|
193
|
+
expect.objectContaining({
|
|
194
|
+
method: "POST",
|
|
195
|
+
headers: expect.objectContaining({
|
|
196
|
+
Authorization: "Bearer codex-access-token",
|
|
197
|
+
"Content-Type": "application/json",
|
|
198
|
+
}),
|
|
199
|
+
body: JSON.stringify({ redeem_request_id: "request-1" }),
|
|
200
|
+
}),
|
|
201
|
+
);
|
|
202
|
+
} finally {
|
|
203
|
+
await rm(homeDir, { recursive: true, force: true });
|
|
204
|
+
await rm(userDataDir, { recursive: true, force: true });
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it("falls back to the pre-combined Codex avatar when usage lookup fails", async () => {
|
|
209
209
|
const homeDir = await mkdtemp(join(tmpdir(), "chatccc-avatar-home-"));
|
|
210
210
|
const userDataDir = await mkdtemp(join(tmpdir(), "chatccc-avatar-data-"));
|
|
211
211
|
const uploadedNames: string[] = [];
|
|
@@ -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
|
+
});
|
|
@@ -65,8 +65,8 @@ vi.mock("../cursor-usage.ts", () => ({
|
|
|
65
65
|
}));
|
|
66
66
|
|
|
67
67
|
import { handleCommand } from "../orchestrator.ts";
|
|
68
|
-
import {
|
|
69
|
-
_clearAdapterCacheForTest,
|
|
68
|
+
import {
|
|
69
|
+
_clearAdapterCacheForTest,
|
|
70
70
|
_resetSessionRegistryFileForTest,
|
|
71
71
|
_resetSessionToolsFileForTest,
|
|
72
72
|
_setAdapterForToolForTest,
|
|
@@ -74,10 +74,10 @@ import {
|
|
|
74
74
|
_setSessionToolsFileForTest,
|
|
75
75
|
loadSessionRegistryForBinding,
|
|
76
76
|
recordSessionRegistry,
|
|
77
|
-
resetState,
|
|
78
|
-
} from "../session.ts";
|
|
79
|
-
import { activePrompts, resetBindingState } from "../session-chat-binding.ts";
|
|
80
|
-
import { ABD_APPEND_PROMPT } from "../shared-prefix.ts";
|
|
77
|
+
resetState,
|
|
78
|
+
} from "../session.ts";
|
|
79
|
+
import { activePrompts, resetBindingState } from "../session-chat-binding.ts";
|
|
80
|
+
import { ABD_APPEND_PROMPT } from "../shared-prefix.ts";
|
|
81
81
|
|
|
82
82
|
function mockPlatform(kind: "wechat" | "feishu" = "wechat"): PlatformAdapter {
|
|
83
83
|
return {
|
|
@@ -129,12 +129,12 @@ describe("handleCommand WeChat processing ack", () => {
|
|
|
129
129
|
mockStreamStates.clear();
|
|
130
130
|
mockGetCodexUsageSummary.mockReset();
|
|
131
131
|
mockGetCursorUsageSummary.mockReset();
|
|
132
|
-
mockGetCodexUsageSummary.mockResolvedValue({
|
|
133
|
-
fiveHour: { usedPercent: 0, remainingPercent: 100, resetAtEpochSeconds: null, resetAfterSeconds: null },
|
|
134
|
-
weekly: { usedPercent: 0, remainingPercent: 100, resetAtEpochSeconds: null, resetAfterSeconds: null },
|
|
135
|
-
rateLimitResetCreditsAvailable: null,
|
|
136
|
-
rateLimitResetCredits: null,
|
|
137
|
-
});
|
|
132
|
+
mockGetCodexUsageSummary.mockResolvedValue({
|
|
133
|
+
fiveHour: { usedPercent: 0, remainingPercent: 100, resetAtEpochSeconds: null, resetAfterSeconds: null },
|
|
134
|
+
weekly: { usedPercent: 0, remainingPercent: 100, resetAtEpochSeconds: null, resetAfterSeconds: null },
|
|
135
|
+
rateLimitResetCreditsAvailable: null,
|
|
136
|
+
rateLimitResetCredits: null,
|
|
137
|
+
});
|
|
138
138
|
mockGetCursorUsageSummary.mockResolvedValue({
|
|
139
139
|
billingCycleStart: "1779357999000",
|
|
140
140
|
billingCycleEnd: "1782036399000",
|
|
@@ -200,8 +200,8 @@ describe("handleCommand WeChat processing ack", () => {
|
|
|
200
200
|
expect(platform.sendCard).not.toHaveBeenCalled();
|
|
201
201
|
});
|
|
202
202
|
|
|
203
|
-
it("sends the WeChat processing ack after the busy check for normal prompts", async () => {
|
|
204
|
-
const platform = mockPlatform();
|
|
203
|
+
it("sends the WeChat processing ack after the busy check for normal prompts", async () => {
|
|
204
|
+
const platform = mockPlatform();
|
|
205
205
|
await recordSessionRegistry({
|
|
206
206
|
chatId: "wx-chat",
|
|
207
207
|
sessionId: "sid-wechat",
|
|
@@ -211,44 +211,44 @@ describe("handleCommand WeChat processing ack", () => {
|
|
|
211
211
|
});
|
|
212
212
|
|
|
213
213
|
await handleCommand(platform, "继续说明", "wx-chat", "wx-user", Date.now(), "p2p");
|
|
214
|
-
|
|
215
|
-
expect(platform.sendText).toHaveBeenCalledWith("wx-chat", "生成中...");
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
it("treats /abd as a shared prompt prefix in an existing session", async () => {
|
|
219
|
-
const platform = mockPlatform();
|
|
220
|
-
const prompt = vi.fn(async function* (_sessionId: string, userText: string) {
|
|
221
|
-
yield {
|
|
222
|
-
type: "assistant" as const,
|
|
223
|
-
blocks: [{ type: "text" as const, text: "done" }],
|
|
224
|
-
};
|
|
225
|
-
});
|
|
226
|
-
_setAdapterForToolForTest("claude", {
|
|
227
|
-
displayName: "Claude",
|
|
228
|
-
sessionDescPrefix: "Claude Session:",
|
|
229
|
-
createSession: vi.fn(async () => ({ sessionId: "sid-wechat" })),
|
|
230
|
-
prompt,
|
|
231
|
-
getSessionInfo: async (sessionId: string): Promise<SessionInfo> => ({
|
|
232
|
-
sessionId,
|
|
233
|
-
cwd: "F:\\repo",
|
|
234
|
-
}),
|
|
235
|
-
closeSession: async () => {},
|
|
236
|
-
});
|
|
237
|
-
await recordSessionRegistry({
|
|
238
|
-
chatId: "wx-chat",
|
|
239
|
-
sessionId: "sid-wechat",
|
|
240
|
-
tool: "claude",
|
|
241
|
-
chatName: "ready-session",
|
|
242
|
-
running: false,
|
|
243
|
-
});
|
|
244
|
-
|
|
245
|
-
await handleCommand(platform, "/abd帮我分析", "wx-chat", "wx-user", Date.now(), "p2p");
|
|
246
|
-
|
|
247
|
-
expect(platform.sendText).toHaveBeenCalledWith("wx-chat", "生成中...");
|
|
248
|
-
const userText = prompt.mock.calls[0][1];
|
|
249
|
-
expect(userText).toContain(`[User message]\n帮我分析\n\n---\n${ABD_APPEND_PROMPT}\n[/User message]`);
|
|
250
|
-
expect(userText).not.toContain("/abd");
|
|
251
|
-
});
|
|
214
|
+
|
|
215
|
+
expect(platform.sendText).toHaveBeenCalledWith("wx-chat", "生成中...");
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it("treats /abd as a shared prompt prefix in an existing session", async () => {
|
|
219
|
+
const platform = mockPlatform();
|
|
220
|
+
const prompt = vi.fn(async function* (_sessionId: string, userText: string) {
|
|
221
|
+
yield {
|
|
222
|
+
type: "assistant" as const,
|
|
223
|
+
blocks: [{ type: "text" as const, text: "done" }],
|
|
224
|
+
};
|
|
225
|
+
});
|
|
226
|
+
_setAdapterForToolForTest("claude", {
|
|
227
|
+
displayName: "Claude",
|
|
228
|
+
sessionDescPrefix: "Claude Session:",
|
|
229
|
+
createSession: vi.fn(async () => ({ sessionId: "sid-wechat" })),
|
|
230
|
+
prompt,
|
|
231
|
+
getSessionInfo: async (sessionId: string): Promise<SessionInfo> => ({
|
|
232
|
+
sessionId,
|
|
233
|
+
cwd: "F:\\repo",
|
|
234
|
+
}),
|
|
235
|
+
closeSession: async () => {},
|
|
236
|
+
});
|
|
237
|
+
await recordSessionRegistry({
|
|
238
|
+
chatId: "wx-chat",
|
|
239
|
+
sessionId: "sid-wechat",
|
|
240
|
+
tool: "claude",
|
|
241
|
+
chatName: "ready-session",
|
|
242
|
+
running: false,
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
await handleCommand(platform, "/abd帮我分析", "wx-chat", "wx-user", Date.now(), "p2p");
|
|
246
|
+
|
|
247
|
+
expect(platform.sendText).toHaveBeenCalledWith("wx-chat", "生成中...");
|
|
248
|
+
const userText = prompt.mock.calls[0][1];
|
|
249
|
+
expect(userText).toContain(`[User message]\n帮我分析\n\n---\n${ABD_APPEND_PROMPT}\n[/User message]`);
|
|
250
|
+
expect(userText).not.toContain("/abd");
|
|
251
|
+
});
|
|
252
252
|
|
|
253
253
|
it("does not send the stopped success text until the running prompt really exits", async () => {
|
|
254
254
|
const platform = mockPlatform();
|
|
@@ -270,7 +270,7 @@ describe("handleCommand WeChat processing ack", () => {
|
|
|
270
270
|
expect(platform.sendText).not.toHaveBeenCalledWith("wx-chat", "会话已停止。");
|
|
271
271
|
});
|
|
272
272
|
|
|
273
|
-
it("cleans stale Feishu p2p binding, creates a group, and sends the private message as first prompt", async () => {
|
|
273
|
+
it("cleans stale Feishu p2p binding, creates a group, and sends the private message as first prompt", async () => {
|
|
274
274
|
const platform = mockPlatform("feishu");
|
|
275
275
|
const prompt = vi.fn(async function* (_sessionId: string, userText: string) {
|
|
276
276
|
yield {
|
|
@@ -315,40 +315,40 @@ describe("handleCommand WeChat processing ack", () => {
|
|
|
315
315
|
|
|
316
316
|
const registry = await loadSessionRegistryForBinding();
|
|
317
317
|
expect(registry["feishu-p2p"]).toBeUndefined();
|
|
318
|
-
expect(registry["feishu-group"]?.sessionId).toBe("sid-feishu-new");
|
|
319
|
-
});
|
|
320
|
-
|
|
321
|
-
it("auto-creates a Feishu group for /abd private messages and sends the transformed prompt", async () => {
|
|
322
|
-
const platform = mockPlatform("feishu");
|
|
323
|
-
const prompt = vi.fn(async function* (_sessionId: string, userText: string) {
|
|
324
|
-
yield {
|
|
325
|
-
type: "assistant" as const,
|
|
326
|
-
blocks: [{ type: "text" as const, text: "done" }],
|
|
327
|
-
};
|
|
328
|
-
});
|
|
329
|
-
_setAdapterForToolForTest("claude", {
|
|
330
|
-
displayName: "Claude",
|
|
331
|
-
sessionDescPrefix: "Claude Session:",
|
|
332
|
-
createSession: vi.fn(async () => ({ sessionId: "sid-feishu-abd" })),
|
|
333
|
-
prompt,
|
|
334
|
-
getSessionInfo: async (sessionId: string): Promise<SessionInfo> => ({
|
|
335
|
-
sessionId,
|
|
336
|
-
cwd: "F:\\repo",
|
|
337
|
-
}),
|
|
338
|
-
closeSession: async () => {},
|
|
339
|
-
});
|
|
340
|
-
|
|
341
|
-
await handleCommand(platform, "/abd帮我看一下日志", "feishu-p2p", "ou-user", Date.now(), "p2p");
|
|
342
|
-
|
|
343
|
-
expect(platform.createGroup).toHaveBeenCalledWith(expect.stringContaining("帮我看一下日志"), ["ou-user"]);
|
|
344
|
-
expect(platform.createGroup).not.toHaveBeenCalledWith(expect.stringContaining("---"), expect.anything());
|
|
345
|
-
const updateCall = vi.mocked(platform.updateChatInfo).mock.calls[0];
|
|
346
|
-
expect(updateCall[1]).not.toContain("---");
|
|
347
|
-
expect(updateCall[1]).not.toContain(ABD_APPEND_PROMPT);
|
|
348
|
-
const userText = prompt.mock.calls[0][1];
|
|
349
|
-
expect(userText).toContain(`[User message]\n帮我看一下日志\n\n---\n${ABD_APPEND_PROMPT}\n[/User message]`);
|
|
350
|
-
expect(userText).not.toContain("/abd");
|
|
351
|
-
});
|
|
318
|
+
expect(registry["feishu-group"]?.sessionId).toBe("sid-feishu-new");
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
it("auto-creates a Feishu group for /abd private messages and sends the transformed prompt", async () => {
|
|
322
|
+
const platform = mockPlatform("feishu");
|
|
323
|
+
const prompt = vi.fn(async function* (_sessionId: string, userText: string) {
|
|
324
|
+
yield {
|
|
325
|
+
type: "assistant" as const,
|
|
326
|
+
blocks: [{ type: "text" as const, text: "done" }],
|
|
327
|
+
};
|
|
328
|
+
});
|
|
329
|
+
_setAdapterForToolForTest("claude", {
|
|
330
|
+
displayName: "Claude",
|
|
331
|
+
sessionDescPrefix: "Claude Session:",
|
|
332
|
+
createSession: vi.fn(async () => ({ sessionId: "sid-feishu-abd" })),
|
|
333
|
+
prompt,
|
|
334
|
+
getSessionInfo: async (sessionId: string): Promise<SessionInfo> => ({
|
|
335
|
+
sessionId,
|
|
336
|
+
cwd: "F:\\repo",
|
|
337
|
+
}),
|
|
338
|
+
closeSession: async () => {},
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
await handleCommand(platform, "/abd帮我看一下日志", "feishu-p2p", "ou-user", Date.now(), "p2p");
|
|
342
|
+
|
|
343
|
+
expect(platform.createGroup).toHaveBeenCalledWith(expect.stringContaining("帮我看一下日志"), ["ou-user"]);
|
|
344
|
+
expect(platform.createGroup).not.toHaveBeenCalledWith(expect.stringContaining("---"), expect.anything());
|
|
345
|
+
const updateCall = vi.mocked(platform.updateChatInfo).mock.calls[0];
|
|
346
|
+
expect(updateCall[1]).not.toContain("---");
|
|
347
|
+
expect(updateCall[1]).not.toContain(ABD_APPEND_PROMPT);
|
|
348
|
+
const userText = prompt.mock.calls[0][1];
|
|
349
|
+
expect(userText).toContain(`[User message]\n帮我看一下日志\n\n---\n${ABD_APPEND_PROMPT}\n[/User message]`);
|
|
350
|
+
expect(userText).not.toContain("/abd");
|
|
351
|
+
});
|
|
352
352
|
|
|
353
353
|
it("cleans stale Feishu p2p binding but keeps valid commands from auto-creating a group", async () => {
|
|
354
354
|
const platform = mockPlatform("feishu");
|
|
@@ -370,35 +370,35 @@ describe("handleCommand WeChat processing ack", () => {
|
|
|
370
370
|
|
|
371
371
|
it("handles /usage without creating a new Feishu group", async () => {
|
|
372
372
|
const platform = mockPlatform("feishu");
|
|
373
|
-
mockGetCodexUsageSummary.mockResolvedValue({
|
|
374
|
-
fiveHour: { usedPercent: 37, remainingPercent: 63, resetAtEpochSeconds: 1781528212, resetAfterSeconds: 10349 },
|
|
375
|
-
weekly: { usedPercent: 12, remainingPercent: 88, resetAtEpochSeconds: 1781842926, resetAfterSeconds: 325063 },
|
|
376
|
-
rateLimitResetCreditsAvailable: 2,
|
|
377
|
-
rateLimitResetCredits: [
|
|
378
|
-
{ grantedAt: "2026-06-12T04:01:47.770016Z", expiresAt: "2026-07-12T04:01:47.770016Z" },
|
|
379
|
-
{ grantedAt: "2026-06-18T00:44:23.904386Z", expiresAt: "2026-07-18T00:44:23.904386Z" },
|
|
380
|
-
],
|
|
381
|
-
});
|
|
382
|
-
|
|
383
|
-
await handleCommand(platform, "/usage", "feishu-p2p", "ou-user", Date.now(), "p2p");
|
|
384
|
-
|
|
385
|
-
expect(platform.createGroup).not.toHaveBeenCalled();
|
|
386
|
-
expect(platform.sendCard).not.toHaveBeenCalled();
|
|
387
|
-
expect(platform.sendRawCard).toHaveBeenCalledTimes(1);
|
|
388
|
-
const card = JSON.parse(vi.mocked(platform.sendRawCard).mock.calls[0][1]);
|
|
389
|
-
expect(card.header.title.content).toBe("Codex Usage");
|
|
390
|
-
expect(card.elements[0].text.content).toContain("2026-07-12");
|
|
391
|
-
expect(card.elements[0].text.content).toContain("2026-07-18");
|
|
392
|
-
expect(card.elements[0].text.content).toContain("**主动重置:** 剩余 2 次");
|
|
393
|
-
expect(card.elements[0].text.content).toContain("**5h:** 已用 37%,剩余 63%,重置:");
|
|
394
|
-
expect(card.elements[0].text.content).toContain("约 2小时52分钟后");
|
|
395
|
-
expect(card.elements[0].text.content).toContain("[███████░░░░░░░░░░░░░]");
|
|
396
|
-
expect(card.elements[0].text.content).toContain("**周:** 已用 12%,剩余 88%,重置:");
|
|
397
|
-
expect(card.elements[0].text.content).toContain("约 3天18小时17分钟后");
|
|
398
|
-
expect(card.elements[0].text.content).toContain("[██░░░░░░░░░░░░░░░░░░]");
|
|
399
|
-
expect(card.elements[2].actions[0].text.content).toBe("发起重置");
|
|
400
|
-
expect(card.elements[2].actions[0].value).toEqual({ action: "codex_reset_request", availableCount: 2 });
|
|
401
|
-
});
|
|
373
|
+
mockGetCodexUsageSummary.mockResolvedValue({
|
|
374
|
+
fiveHour: { usedPercent: 37, remainingPercent: 63, resetAtEpochSeconds: 1781528212, resetAfterSeconds: 10349 },
|
|
375
|
+
weekly: { usedPercent: 12, remainingPercent: 88, resetAtEpochSeconds: 1781842926, resetAfterSeconds: 325063 },
|
|
376
|
+
rateLimitResetCreditsAvailable: 2,
|
|
377
|
+
rateLimitResetCredits: [
|
|
378
|
+
{ grantedAt: "2026-06-12T04:01:47.770016Z", expiresAt: "2026-07-12T04:01:47.770016Z" },
|
|
379
|
+
{ grantedAt: "2026-06-18T00:44:23.904386Z", expiresAt: "2026-07-18T00:44:23.904386Z" },
|
|
380
|
+
],
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
await handleCommand(platform, "/usage", "feishu-p2p", "ou-user", Date.now(), "p2p");
|
|
384
|
+
|
|
385
|
+
expect(platform.createGroup).not.toHaveBeenCalled();
|
|
386
|
+
expect(platform.sendCard).not.toHaveBeenCalled();
|
|
387
|
+
expect(platform.sendRawCard).toHaveBeenCalledTimes(1);
|
|
388
|
+
const card = JSON.parse(vi.mocked(platform.sendRawCard).mock.calls[0][1]);
|
|
389
|
+
expect(card.header.title.content).toBe("Codex Usage");
|
|
390
|
+
expect(card.elements[0].text.content).toContain("2026-07-12");
|
|
391
|
+
expect(card.elements[0].text.content).toContain("2026-07-18");
|
|
392
|
+
expect(card.elements[0].text.content).toContain("**主动重置:** 剩余 2 次");
|
|
393
|
+
expect(card.elements[0].text.content).toContain("**5h:** 已用 37%,剩余 63%,重置:");
|
|
394
|
+
expect(card.elements[0].text.content).toContain("约 2小时52分钟后");
|
|
395
|
+
expect(card.elements[0].text.content).toContain("[███████░░░░░░░░░░░░░]");
|
|
396
|
+
expect(card.elements[0].text.content).toContain("**周:** 已用 12%,剩余 88%,重置:");
|
|
397
|
+
expect(card.elements[0].text.content).toContain("约 3天18小时17分钟后");
|
|
398
|
+
expect(card.elements[0].text.content).toContain("[██░░░░░░░░░░░░░░░░░░]");
|
|
399
|
+
expect(card.elements[2].actions[0].text.content).toBe("发起重置");
|
|
400
|
+
expect(card.elements[2].actions[0].value).toEqual({ action: "codex_reset_request", availableCount: 2 });
|
|
401
|
+
});
|
|
402
402
|
|
|
403
403
|
it("handles /usage as Cursor usage in Cursor chats", async () => {
|
|
404
404
|
const platform = mockPlatform("feishu");
|
|
@@ -438,7 +438,7 @@ describe("handleCommand WeChat processing ack", () => {
|
|
|
438
438
|
expect(codexPlatform.sendCard).toHaveBeenCalledWith(
|
|
439
439
|
"feishu-group",
|
|
440
440
|
"Codex Session Ready",
|
|
441
|
-
expect.stringContaining("发送 **/usage** 查看 Codex 5h/周用量,以及查询/使用主动重置卡。"),
|
|
441
|
+
expect.stringContaining("发送 **/usage** 查看 Codex 5h/周用量,以及查询/使用主动重置卡。"),
|
|
442
442
|
"green",
|
|
443
443
|
);
|
|
444
444
|
|