chatccc 0.2.178 → 0.2.180
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 +67 -49
- package/config.sample.json +3 -1
- package/package.json +1 -1
- package/src/__tests__/card-plain-text.test.ts +5 -4
- package/src/__tests__/cards.test.ts +22 -13
- package/src/__tests__/config-reload.test.ts +29 -22
- package/src/__tests__/feishu-avatar.test.ts +219 -129
- package/src/__tests__/orchestrator.test.ts +244 -106
- package/src/__tests__/shared-prefix.test.ts +36 -0
- package/src/cards.ts +13 -10
- package/src/config.ts +31 -2
- package/src/cursor-usage.ts +128 -0
- package/src/feishu-api.ts +65 -9
- package/src/index.ts +1 -1
- package/src/orchestrator.ts +260 -170
- package/src/shared-prefix.ts +29 -0
- package/src/web-ui.ts +95 -11
|
@@ -6,8 +6,9 @@ import { join } from "node:path";
|
|
|
6
6
|
import type { PlatformAdapter } from "../platform-adapter.ts";
|
|
7
7
|
import type { SessionInfo, ToolAdapter } from "../adapters/adapter-interface.ts";
|
|
8
8
|
|
|
9
|
-
const mockStreamStates = new Map<string, { status: "running" | "done" | "stopped"; finalReply: string }>();
|
|
10
|
-
const mockGetCodexUsageSummary = vi.hoisted(() => vi.fn());
|
|
9
|
+
const mockStreamStates = new Map<string, { status: "running" | "done" | "stopped"; finalReply: string }>();
|
|
10
|
+
const mockGetCodexUsageSummary = vi.hoisted(() => vi.fn());
|
|
11
|
+
const mockGetCursorUsageSummary = vi.hoisted(() => vi.fn());
|
|
11
12
|
|
|
12
13
|
vi.mock("../im-skills.ts", () => ({
|
|
13
14
|
buildImSkillsPrompt: async () => "",
|
|
@@ -15,7 +16,7 @@ vi.mock("../im-skills.ts", () => ({
|
|
|
15
16
|
exportSkillSubDocs: async () => {},
|
|
16
17
|
}));
|
|
17
18
|
|
|
18
|
-
vi.mock("../stream-state.ts", () => ({
|
|
19
|
+
vi.mock("../stream-state.ts", () => ({
|
|
19
20
|
readStreamState: async (sessionId: string) => {
|
|
20
21
|
const state = mockStreamStates.get(sessionId);
|
|
21
22
|
if (!state) return null;
|
|
@@ -50,18 +51,22 @@ vi.mock("../stream-state.ts", () => ({
|
|
|
50
51
|
cwd,
|
|
51
52
|
tool,
|
|
52
53
|
}),
|
|
53
|
-
fixStaleStreamStates: async () => {},
|
|
54
|
-
}));
|
|
55
|
-
|
|
56
|
-
vi.mock("../feishu-platform.ts", () => ({
|
|
57
|
-
getCodexUsageSummary: mockGetCodexUsageSummary,
|
|
58
|
-
getTenantAccessToken: vi.fn(async () => "tenant-token"),
|
|
59
|
-
sendPostMessage: vi.fn(async () => true),
|
|
60
|
-
}));
|
|
54
|
+
fixStaleStreamStates: async () => {},
|
|
55
|
+
}));
|
|
56
|
+
|
|
57
|
+
vi.mock("../feishu-platform.ts", () => ({
|
|
58
|
+
getCodexUsageSummary: mockGetCodexUsageSummary,
|
|
59
|
+
getTenantAccessToken: vi.fn(async () => "tenant-token"),
|
|
60
|
+
sendPostMessage: vi.fn(async () => true),
|
|
61
|
+
}));
|
|
62
|
+
|
|
63
|
+
vi.mock("../cursor-usage.ts", () => ({
|
|
64
|
+
getCursorUsageSummary: mockGetCursorUsageSummary,
|
|
65
|
+
}));
|
|
61
66
|
|
|
62
67
|
import { handleCommand } from "../orchestrator.ts";
|
|
63
|
-
import {
|
|
64
|
-
_clearAdapterCacheForTest,
|
|
68
|
+
import {
|
|
69
|
+
_clearAdapterCacheForTest,
|
|
65
70
|
_resetSessionRegistryFileForTest,
|
|
66
71
|
_resetSessionToolsFileForTest,
|
|
67
72
|
_setAdapterForToolForTest,
|
|
@@ -69,9 +74,10 @@ import {
|
|
|
69
74
|
_setSessionToolsFileForTest,
|
|
70
75
|
loadSessionRegistryForBinding,
|
|
71
76
|
recordSessionRegistry,
|
|
72
|
-
resetState,
|
|
73
|
-
} from "../session.ts";
|
|
74
|
-
import { activePrompts, resetBindingState } from "../session-chat-binding.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";
|
|
75
81
|
|
|
76
82
|
function mockPlatform(kind: "wechat" | "feishu" = "wechat"): PlatformAdapter {
|
|
77
83
|
return {
|
|
@@ -120,14 +126,40 @@ describe("handleCommand WeChat processing ack", () => {
|
|
|
120
126
|
_setSessionToolsFileForTest(join(tempDir, "sessions.json"));
|
|
121
127
|
resetState();
|
|
122
128
|
resetBindingState();
|
|
123
|
-
mockStreamStates.clear();
|
|
124
|
-
mockGetCodexUsageSummary.mockReset();
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
129
|
+
mockStreamStates.clear();
|
|
130
|
+
mockGetCodexUsageSummary.mockReset();
|
|
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
|
+
});
|
|
136
|
+
mockGetCursorUsageSummary.mockResolvedValue({
|
|
137
|
+
billingCycleStart: "1779357999000",
|
|
138
|
+
billingCycleEnd: "1782036399000",
|
|
139
|
+
planUsage: {
|
|
140
|
+
totalSpend: 8159,
|
|
141
|
+
includedSpend: 2000,
|
|
142
|
+
bonusSpend: 6159,
|
|
143
|
+
limit: 2000,
|
|
144
|
+
remainingBonus: false,
|
|
145
|
+
autoPercentUsed: 0,
|
|
146
|
+
apiPercentUsed: 100,
|
|
147
|
+
totalPercentUsed: 100,
|
|
148
|
+
},
|
|
149
|
+
spendLimitUsage: {
|
|
150
|
+
pooledLimit: 48950000,
|
|
151
|
+
pooledUsed: 31808224,
|
|
152
|
+
pooledRemaining: 17141776,
|
|
153
|
+
individualUsed: 101252,
|
|
154
|
+
limitType: "team",
|
|
155
|
+
},
|
|
156
|
+
displayThreshold: 200,
|
|
157
|
+
enabled: true,
|
|
158
|
+
displayMessage: "You've hit your usage limit",
|
|
159
|
+
autoBucketModels: ["default"],
|
|
160
|
+
});
|
|
161
|
+
_setAdapterForToolForTest("claude", mockAdapter());
|
|
162
|
+
});
|
|
131
163
|
|
|
132
164
|
afterEach(async () => {
|
|
133
165
|
resetState();
|
|
@@ -166,8 +198,8 @@ describe("handleCommand WeChat processing ack", () => {
|
|
|
166
198
|
expect(platform.sendCard).not.toHaveBeenCalled();
|
|
167
199
|
});
|
|
168
200
|
|
|
169
|
-
it("sends the WeChat processing ack after the busy check for normal prompts", async () => {
|
|
170
|
-
const platform = mockPlatform();
|
|
201
|
+
it("sends the WeChat processing ack after the busy check for normal prompts", async () => {
|
|
202
|
+
const platform = mockPlatform();
|
|
171
203
|
await recordSessionRegistry({
|
|
172
204
|
chatId: "wx-chat",
|
|
173
205
|
sessionId: "sid-wechat",
|
|
@@ -177,9 +209,44 @@ describe("handleCommand WeChat processing ack", () => {
|
|
|
177
209
|
});
|
|
178
210
|
|
|
179
211
|
await handleCommand(platform, "继续说明", "wx-chat", "wx-user", Date.now(), "p2p");
|
|
180
|
-
|
|
181
|
-
expect(platform.sendText).toHaveBeenCalledWith("wx-chat", "生成中...");
|
|
182
|
-
});
|
|
212
|
+
|
|
213
|
+
expect(platform.sendText).toHaveBeenCalledWith("wx-chat", "生成中...");
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it("treats /abd as a shared prompt prefix in an existing session", async () => {
|
|
217
|
+
const platform = mockPlatform();
|
|
218
|
+
const prompt = vi.fn(async function* (_sessionId: string, userText: string) {
|
|
219
|
+
yield {
|
|
220
|
+
type: "assistant" as const,
|
|
221
|
+
blocks: [{ type: "text" as const, text: "done" }],
|
|
222
|
+
};
|
|
223
|
+
});
|
|
224
|
+
_setAdapterForToolForTest("claude", {
|
|
225
|
+
displayName: "Claude",
|
|
226
|
+
sessionDescPrefix: "Claude Session:",
|
|
227
|
+
createSession: vi.fn(async () => ({ sessionId: "sid-wechat" })),
|
|
228
|
+
prompt,
|
|
229
|
+
getSessionInfo: async (sessionId: string): Promise<SessionInfo> => ({
|
|
230
|
+
sessionId,
|
|
231
|
+
cwd: "F:\\repo",
|
|
232
|
+
}),
|
|
233
|
+
closeSession: async () => {},
|
|
234
|
+
});
|
|
235
|
+
await recordSessionRegistry({
|
|
236
|
+
chatId: "wx-chat",
|
|
237
|
+
sessionId: "sid-wechat",
|
|
238
|
+
tool: "claude",
|
|
239
|
+
chatName: "ready-session",
|
|
240
|
+
running: false,
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
await handleCommand(platform, "/abd帮我分析", "wx-chat", "wx-user", Date.now(), "p2p");
|
|
244
|
+
|
|
245
|
+
expect(platform.sendText).toHaveBeenCalledWith("wx-chat", "生成中...");
|
|
246
|
+
const userText = prompt.mock.calls[0][1];
|
|
247
|
+
expect(userText).toContain(`[User message]\n帮我分析\n\n---\n${ABD_APPEND_PROMPT}\n[/User message]`);
|
|
248
|
+
expect(userText).not.toContain("/abd");
|
|
249
|
+
});
|
|
183
250
|
|
|
184
251
|
it("does not send the stopped success text until the running prompt really exits", async () => {
|
|
185
252
|
const platform = mockPlatform();
|
|
@@ -201,7 +268,7 @@ describe("handleCommand WeChat processing ack", () => {
|
|
|
201
268
|
expect(platform.sendText).not.toHaveBeenCalledWith("wx-chat", "会话已停止。");
|
|
202
269
|
});
|
|
203
270
|
|
|
204
|
-
it("cleans stale Feishu p2p binding, creates a group, and sends the private message as first prompt", async () => {
|
|
271
|
+
it("cleans stale Feishu p2p binding, creates a group, and sends the private message as first prompt", async () => {
|
|
205
272
|
const platform = mockPlatform("feishu");
|
|
206
273
|
const prompt = vi.fn(async function* (_sessionId: string, userText: string) {
|
|
207
274
|
yield {
|
|
@@ -246,10 +313,42 @@ describe("handleCommand WeChat processing ack", () => {
|
|
|
246
313
|
|
|
247
314
|
const registry = await loadSessionRegistryForBinding();
|
|
248
315
|
expect(registry["feishu-p2p"]).toBeUndefined();
|
|
249
|
-
expect(registry["feishu-group"]?.sessionId).toBe("sid-feishu-new");
|
|
250
|
-
});
|
|
316
|
+
expect(registry["feishu-group"]?.sessionId).toBe("sid-feishu-new");
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
it("auto-creates a Feishu group for /abd private messages and sends the transformed prompt", async () => {
|
|
320
|
+
const platform = mockPlatform("feishu");
|
|
321
|
+
const prompt = vi.fn(async function* (_sessionId: string, userText: string) {
|
|
322
|
+
yield {
|
|
323
|
+
type: "assistant" as const,
|
|
324
|
+
blocks: [{ type: "text" as const, text: "done" }],
|
|
325
|
+
};
|
|
326
|
+
});
|
|
327
|
+
_setAdapterForToolForTest("claude", {
|
|
328
|
+
displayName: "Claude",
|
|
329
|
+
sessionDescPrefix: "Claude Session:",
|
|
330
|
+
createSession: vi.fn(async () => ({ sessionId: "sid-feishu-abd" })),
|
|
331
|
+
prompt,
|
|
332
|
+
getSessionInfo: async (sessionId: string): Promise<SessionInfo> => ({
|
|
333
|
+
sessionId,
|
|
334
|
+
cwd: "F:\\repo",
|
|
335
|
+
}),
|
|
336
|
+
closeSession: async () => {},
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
await handleCommand(platform, "/abd帮我看一下日志", "feishu-p2p", "ou-user", Date.now(), "p2p");
|
|
340
|
+
|
|
341
|
+
expect(platform.createGroup).toHaveBeenCalledWith(expect.stringContaining("帮我看一下日志"), ["ou-user"]);
|
|
342
|
+
expect(platform.createGroup).not.toHaveBeenCalledWith(expect.stringContaining("---"), expect.anything());
|
|
343
|
+
const updateCall = vi.mocked(platform.updateChatInfo).mock.calls[0];
|
|
344
|
+
expect(updateCall[1]).not.toContain("---");
|
|
345
|
+
expect(updateCall[1]).not.toContain(ABD_APPEND_PROMPT);
|
|
346
|
+
const userText = prompt.mock.calls[0][1];
|
|
347
|
+
expect(userText).toContain(`[User message]\n帮我看一下日志\n\n---\n${ABD_APPEND_PROMPT}\n[/User message]`);
|
|
348
|
+
expect(userText).not.toContain("/abd");
|
|
349
|
+
});
|
|
251
350
|
|
|
252
|
-
it("cleans stale Feishu p2p binding but keeps valid commands from auto-creating a group", async () => {
|
|
351
|
+
it("cleans stale Feishu p2p binding but keeps valid commands from auto-creating a group", async () => {
|
|
253
352
|
const platform = mockPlatform("feishu");
|
|
254
353
|
await recordSessionRegistry({
|
|
255
354
|
chatId: "feishu-p2p",
|
|
@@ -264,76 +363,115 @@ describe("handleCommand WeChat processing ack", () => {
|
|
|
264
363
|
expect(platform.createGroup).not.toHaveBeenCalled();
|
|
265
364
|
expect(platform.sendRawCard).toHaveBeenCalled();
|
|
266
365
|
const registry = await loadSessionRegistryForBinding();
|
|
267
|
-
expect(registry["feishu-p2p"]).toBeUndefined();
|
|
268
|
-
});
|
|
269
|
-
|
|
270
|
-
it("handles /usage without creating a new Feishu group", async () => {
|
|
271
|
-
const platform = mockPlatform("feishu");
|
|
272
|
-
mockGetCodexUsageSummary.mockResolvedValue({
|
|
273
|
-
fiveHour: { usedPercent: 37, remainingPercent: 63, resetAtEpochSeconds: 1781528212, resetAfterSeconds: 10349 },
|
|
274
|
-
weekly: { usedPercent: 12, remainingPercent: 88, resetAtEpochSeconds: 1781842926, resetAfterSeconds: 325063 },
|
|
275
|
-
});
|
|
276
|
-
|
|
277
|
-
await handleCommand(platform, "/usage", "feishu-p2p", "ou-user", Date.now(), "p2p");
|
|
278
|
-
|
|
279
|
-
expect(platform.createGroup).not.toHaveBeenCalled();
|
|
280
|
-
expect(platform.sendCard).toHaveBeenCalledWith(
|
|
281
|
-
"feishu-p2p",
|
|
282
|
-
"Codex Usage",
|
|
283
|
-
expect.stringContaining("**5h:** 已用 37%,剩余 63%,重置:"),
|
|
284
|
-
"blue",
|
|
285
|
-
);
|
|
286
|
-
expect(platform.sendCard).toHaveBeenCalledWith(
|
|
287
|
-
"feishu-p2p",
|
|
288
|
-
"Codex Usage",
|
|
289
|
-
expect.stringContaining("约 2小时52分钟后"),
|
|
290
|
-
"blue",
|
|
291
|
-
);
|
|
292
|
-
expect(platform.sendCard).toHaveBeenCalledWith(
|
|
293
|
-
"feishu-p2p",
|
|
294
|
-
"Codex Usage",
|
|
295
|
-
expect.stringContaining("[███████░░░░░░░░░░░░░]"),
|
|
296
|
-
"blue",
|
|
297
|
-
);
|
|
298
|
-
expect(platform.sendCard).toHaveBeenCalledWith(
|
|
299
|
-
"feishu-p2p",
|
|
300
|
-
"Codex Usage",
|
|
301
|
-
expect.stringContaining("**周:** 已用 12%,剩余 88%,重置:"),
|
|
302
|
-
"blue",
|
|
303
|
-
);
|
|
304
|
-
expect(platform.sendCard).toHaveBeenCalledWith(
|
|
305
|
-
"feishu-p2p",
|
|
306
|
-
"Codex Usage",
|
|
307
|
-
expect.stringContaining("约 3天18小时17分钟后"),
|
|
308
|
-
"blue",
|
|
309
|
-
);
|
|
310
|
-
expect(platform.sendCard).toHaveBeenCalledWith(
|
|
311
|
-
"feishu-p2p",
|
|
312
|
-
"Codex Usage",
|
|
313
|
-
expect.stringContaining("[██░░░░░░░░░░░░░░░░░░]"),
|
|
314
|
-
"blue",
|
|
315
|
-
);
|
|
316
|
-
});
|
|
317
|
-
|
|
318
|
-
it("
|
|
319
|
-
const
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
366
|
+
expect(registry["feishu-p2p"]).toBeUndefined();
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
it("handles /usage without creating a new Feishu group", async () => {
|
|
370
|
+
const platform = mockPlatform("feishu");
|
|
371
|
+
mockGetCodexUsageSummary.mockResolvedValue({
|
|
372
|
+
fiveHour: { usedPercent: 37, remainingPercent: 63, resetAtEpochSeconds: 1781528212, resetAfterSeconds: 10349 },
|
|
373
|
+
weekly: { usedPercent: 12, remainingPercent: 88, resetAtEpochSeconds: 1781842926, resetAfterSeconds: 325063 },
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
await handleCommand(platform, "/usage", "feishu-p2p", "ou-user", Date.now(), "p2p");
|
|
377
|
+
|
|
378
|
+
expect(platform.createGroup).not.toHaveBeenCalled();
|
|
379
|
+
expect(platform.sendCard).toHaveBeenCalledWith(
|
|
380
|
+
"feishu-p2p",
|
|
381
|
+
"Codex Usage",
|
|
382
|
+
expect.stringContaining("**5h:** 已用 37%,剩余 63%,重置:"),
|
|
383
|
+
"blue",
|
|
384
|
+
);
|
|
385
|
+
expect(platform.sendCard).toHaveBeenCalledWith(
|
|
386
|
+
"feishu-p2p",
|
|
387
|
+
"Codex Usage",
|
|
388
|
+
expect.stringContaining("约 2小时52分钟后"),
|
|
389
|
+
"blue",
|
|
390
|
+
);
|
|
391
|
+
expect(platform.sendCard).toHaveBeenCalledWith(
|
|
392
|
+
"feishu-p2p",
|
|
393
|
+
"Codex Usage",
|
|
394
|
+
expect.stringContaining("[███████░░░░░░░░░░░░░]"),
|
|
395
|
+
"blue",
|
|
396
|
+
);
|
|
397
|
+
expect(platform.sendCard).toHaveBeenCalledWith(
|
|
398
|
+
"feishu-p2p",
|
|
399
|
+
"Codex Usage",
|
|
400
|
+
expect.stringContaining("**周:** 已用 12%,剩余 88%,重置:"),
|
|
401
|
+
"blue",
|
|
402
|
+
);
|
|
403
|
+
expect(platform.sendCard).toHaveBeenCalledWith(
|
|
404
|
+
"feishu-p2p",
|
|
405
|
+
"Codex Usage",
|
|
406
|
+
expect.stringContaining("约 3天18小时17分钟后"),
|
|
407
|
+
"blue",
|
|
408
|
+
);
|
|
409
|
+
expect(platform.sendCard).toHaveBeenCalledWith(
|
|
410
|
+
"feishu-p2p",
|
|
411
|
+
"Codex Usage",
|
|
412
|
+
expect.stringContaining("[██░░░░░░░░░░░░░░░░░░]"),
|
|
413
|
+
"blue",
|
|
414
|
+
);
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
it("handles /usage as Cursor usage in Cursor chats", async () => {
|
|
418
|
+
const platform = mockPlatform("feishu");
|
|
419
|
+
await recordSessionRegistry({
|
|
420
|
+
chatId: "cursor-chat",
|
|
421
|
+
sessionId: "sid-cursor",
|
|
422
|
+
tool: "cursor",
|
|
423
|
+
chatName: "cursor-session",
|
|
424
|
+
running: false,
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
await handleCommand(platform, "/usage", "cursor-chat", "ou-user", Date.now(), "group");
|
|
428
|
+
|
|
429
|
+
expect(platform.createGroup).not.toHaveBeenCalled();
|
|
430
|
+
expect(mockGetCodexUsageSummary).not.toHaveBeenCalled();
|
|
431
|
+
expect(mockGetCursorUsageSummary).toHaveBeenCalled();
|
|
432
|
+
expect(platform.sendCard).toHaveBeenCalledWith(
|
|
433
|
+
"cursor-chat",
|
|
434
|
+
"Cursor Usage",
|
|
435
|
+
expect.stringContaining("Individual used: $1012.52"),
|
|
436
|
+
"blue",
|
|
437
|
+
);
|
|
438
|
+
expect(platform.sendCard).toHaveBeenCalledWith(
|
|
439
|
+
"cursor-chat",
|
|
440
|
+
"Cursor Usage",
|
|
441
|
+
expect.stringContaining("Pool remaining: $171417.76"),
|
|
442
|
+
"blue",
|
|
443
|
+
);
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
it("advertises /usage in new Codex and Cursor session ready messages", async () => {
|
|
447
|
+
const codexPlatform = mockPlatform("feishu");
|
|
448
|
+
_setAdapterForToolForTest("codex", mockAdapter("sid-codex"));
|
|
449
|
+
|
|
450
|
+
await handleCommand(codexPlatform, "/new codex", "feishu-p2p", "ou-user", Date.now(), "p2p");
|
|
451
|
+
|
|
452
|
+
expect(codexPlatform.sendCard).toHaveBeenCalledWith(
|
|
453
|
+
"feishu-group",
|
|
454
|
+
"Codex Session Ready",
|
|
455
|
+
expect.stringContaining("发送 **/usage** 查看 Codex 5h 和周用量。"),
|
|
456
|
+
"green",
|
|
457
|
+
);
|
|
458
|
+
|
|
459
|
+
const cursorPlatform = mockPlatform("feishu");
|
|
460
|
+
_setAdapterForToolForTest("cursor", mockAdapter("sid-cursor"));
|
|
461
|
+
|
|
462
|
+
await handleCommand(cursorPlatform, "/new cursor", "feishu-p2p-cursor", "ou-user", Date.now(), "p2p");
|
|
463
|
+
|
|
464
|
+
const cursorReadyCall = vi.mocked(cursorPlatform.sendCard).mock.calls.find(
|
|
465
|
+
([chatId, title]) => chatId === "feishu-group" && title === "Cursor Session Ready",
|
|
466
|
+
);
|
|
467
|
+
expect(cursorReadyCall?.[2]).toContain("/usage");
|
|
468
|
+
|
|
469
|
+
const claudePlatform = mockPlatform("feishu");
|
|
470
|
+
await handleCommand(claudePlatform, "/new claude", "feishu-p2p-2", "ou-user", Date.now(), "p2p");
|
|
471
|
+
|
|
472
|
+
const claudeReadyCall = vi.mocked(claudePlatform.sendCard).mock.calls.find(
|
|
473
|
+
([chatId, title]) => chatId === "feishu-group" && title === "Claude Code Session Ready",
|
|
474
|
+
);
|
|
475
|
+
expect(claudeReadyCall?.[2]).not.toContain("/usage");
|
|
476
|
+
});
|
|
477
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
ABD_APPEND_PROMPT,
|
|
5
|
+
applySharedPrefix,
|
|
6
|
+
} from "../shared-prefix.ts";
|
|
7
|
+
|
|
8
|
+
describe("applySharedPrefix", () => {
|
|
9
|
+
it("leaves normal messages unchanged", () => {
|
|
10
|
+
expect(applySharedPrefix("帮我分析")).toEqual({
|
|
11
|
+
matched: false,
|
|
12
|
+
text: "帮我分析",
|
|
13
|
+
body: "帮我分析",
|
|
14
|
+
});
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("removes /abd without requiring a space", () => {
|
|
18
|
+
const result = applySharedPrefix("/abd帮我分析");
|
|
19
|
+
|
|
20
|
+
expect(result.matched).toBe(true);
|
|
21
|
+
expect(result.body).toBe("帮我分析");
|
|
22
|
+
expect(result.text).toBe(`帮我分析\n\n---\n${ABD_APPEND_PROMPT}`);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("removes whitespace after /abd", () => {
|
|
26
|
+
const result = applySharedPrefix("/abd 帮我分析");
|
|
27
|
+
|
|
28
|
+
expect(result.text).toBe(`帮我分析\n\n---\n${ABD_APPEND_PROMPT}`);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("keeps the appendix when the user body is empty", () => {
|
|
32
|
+
const result = applySharedPrefix("/abd ");
|
|
33
|
+
|
|
34
|
+
expect(result.text).toBe(`---\n${ABD_APPEND_PROMPT}`);
|
|
35
|
+
});
|
|
36
|
+
});
|
package/src/cards.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
// ---------------------------------------------------------------------------
|
|
2
|
-
// Button helpers
|
|
3
|
-
// ---------------------------------------------------------------------------
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// Button helpers
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
|
|
5
|
+
import { ABD_HELP_LINE } from "./shared-prefix.ts";
|
|
6
|
+
|
|
7
|
+
export interface ButtonDef {
|
|
6
8
|
text: string;
|
|
7
9
|
value: string;
|
|
8
10
|
type?: "primary" | "default" | "danger";
|
|
@@ -133,12 +135,13 @@ export function buildHelpCard(
|
|
|
133
135
|
"发送 **/new cursor** 创建新 Cursor 会话",
|
|
134
136
|
"发送 **/new codex** 创建新 Codex 会话",
|
|
135
137
|
"发送 **/newh** 重置当前会话(沿用当前工作目录,不切换)",
|
|
136
|
-
"发送 **/plan** 以规划模式提问(只读,不执行写操作)",
|
|
137
|
-
"发送 **/ask** 以问答模式提问(只读,不执行写操作)",
|
|
138
|
+
"发送 **/plan** 以规划模式提问(只读,不执行写操作)",
|
|
139
|
+
"发送 **/ask** 以问答模式提问(只读,不执行写操作)",
|
|
138
140
|
"发送 **/usage** 查看 Codex 5h 和周用量",
|
|
139
141
|
"发送 **/restart** 重启 ChatCCC 进程",
|
|
140
|
-
"发送 **/
|
|
141
|
-
|
|
142
|
+
"发送 **/update** 更新并重启(仅 npm 全局安装可用)",
|
|
143
|
+
ABD_HELP_LINE,
|
|
144
|
+
].join("\n");
|
|
142
145
|
return JSON.stringify({
|
|
143
146
|
config: { wide_screen_mode: true },
|
|
144
147
|
header: { template: "blue", title: { content: "ChatCCC", tag: "plain_text" } },
|
|
@@ -151,7 +154,7 @@ export function buildHelpCard(
|
|
|
151
154
|
{ text: "新建 Cursor 会话(/new cursor)", value: JSON.stringify({ cmd: "new cursor" }), type: "primary" },
|
|
152
155
|
{ text: "新建 Codex 会话(/new codex)", value: JSON.stringify({ cmd: "new codex" }), type: "primary" },
|
|
153
156
|
{ text: "重启 ChatCCC(/restart)", value: JSON.stringify({ cmd: "restart" }), type: "danger" },
|
|
154
|
-
{ text: "更新并重启(/
|
|
157
|
+
{ text: "更新并重启(/update)", value: JSON.stringify({ cmd: "update" }), type: "danger" },
|
|
155
158
|
{ text: "切换工作路径(/cd)", value: JSON.stringify({ cmd: "cd" }), type: "default" },
|
|
156
159
|
]),
|
|
157
160
|
],
|
package/src/config.ts
CHANGED
|
@@ -81,6 +81,8 @@ export interface CursorConfig {
|
|
|
81
81
|
/** Cursor Agent CLI 可执行文件绝对路径;留空时由运行时按 LocalAppData / PATH 兜底 */
|
|
82
82
|
path: string;
|
|
83
83
|
model: string;
|
|
84
|
+
avatarBatteryMode: CursorAvatarBatteryMode;
|
|
85
|
+
onDemandMonthlyBudget: number;
|
|
84
86
|
}
|
|
85
87
|
|
|
86
88
|
export interface CodexConfig {
|
|
@@ -125,6 +127,7 @@ export interface AppConfig {
|
|
|
125
127
|
|
|
126
128
|
export type AgentTool = "claude" | "cursor" | "codex";
|
|
127
129
|
export const AGENT_TOOLS: AgentTool[] = ["claude", "cursor", "codex"];
|
|
130
|
+
export type CursorAvatarBatteryMode = "apiPercent" | "onDemandUse";
|
|
128
131
|
|
|
129
132
|
/** 获取指定 agent 配置中所有模型相关的值(最多 100 个,去重) */
|
|
130
133
|
export function getAllModelsForTool(tool: AgentTool, cfg: AppConfig = config): string[] {
|
|
@@ -326,6 +329,15 @@ function normalizePlatformType(raw: string): "feishu" | "lark" {
|
|
|
326
329
|
return "feishu";
|
|
327
330
|
}
|
|
328
331
|
|
|
332
|
+
function normalizeCursorAvatarBatteryMode(raw: unknown): CursorAvatarBatteryMode {
|
|
333
|
+
return raw === "onDemandUse" ? "onDemandUse" : "apiPercent";
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function normalizeCursorOnDemandMonthlyBudget(raw: unknown): number {
|
|
337
|
+
const value = typeof raw === "string" ? Number(raw.trim()) : Number(raw);
|
|
338
|
+
return Number.isFinite(value) && value > 0 ? value : 1000;
|
|
339
|
+
}
|
|
340
|
+
|
|
329
341
|
function loadConfig(): AppConfig {
|
|
330
342
|
const defaults: AppConfig = {
|
|
331
343
|
feishu: { appId: "", appSecret: "" },
|
|
@@ -334,7 +346,14 @@ function loadConfig(): AppConfig {
|
|
|
334
346
|
gitTimeoutSeconds: 180,
|
|
335
347
|
allowInterrupt: false,
|
|
336
348
|
claude: { enabled: false, defaultAgent: true, model: "", subagentModel: "", effort: "", apiKey: "", baseUrl: "", maxTurn: 0 },
|
|
337
|
-
cursor: {
|
|
349
|
+
cursor: {
|
|
350
|
+
enabled: false,
|
|
351
|
+
defaultAgent: false,
|
|
352
|
+
path: "",
|
|
353
|
+
model: "claude-opus-4-7-max",
|
|
354
|
+
avatarBatteryMode: "apiPercent",
|
|
355
|
+
onDemandMonthlyBudget: 1000,
|
|
356
|
+
},
|
|
338
357
|
codex: { enabled: false, defaultAgent: false, path: "", model: "", effort: "" },
|
|
339
358
|
};
|
|
340
359
|
|
|
@@ -377,7 +396,15 @@ function loadConfig(): AppConfig {
|
|
|
377
396
|
|
|
378
397
|
let parsed: Partial<AppConfig> & {
|
|
379
398
|
claude?: Partial<ClaudeConfig> & { enabled?: unknown };
|
|
380
|
-
cursor?: {
|
|
399
|
+
cursor?: {
|
|
400
|
+
enabled?: unknown;
|
|
401
|
+
defaultAgent?: unknown;
|
|
402
|
+
path?: unknown;
|
|
403
|
+
command?: unknown;
|
|
404
|
+
model?: unknown;
|
|
405
|
+
avatarBatteryMode?: unknown;
|
|
406
|
+
onDemandMonthlyBudget?: unknown;
|
|
407
|
+
};
|
|
381
408
|
codex?: { enabled?: unknown; defaultAgent?: unknown; path?: unknown; command?: unknown; model?: unknown; effort?: unknown };
|
|
382
409
|
};
|
|
383
410
|
try {
|
|
@@ -491,6 +518,8 @@ function loadConfig(): AppConfig {
|
|
|
491
518
|
defaultAgent: defaultTool === "cursor",
|
|
492
519
|
path: readToolCliPath(cursorRaw, { label: "cursor", onLegacyField }),
|
|
493
520
|
model: normalizeOptionalConfigField(cursorRaw.model, { label: "cursor.model", fallback: "claude-opus-4-7-max" }),
|
|
521
|
+
avatarBatteryMode: normalizeCursorAvatarBatteryMode(cursorRaw.avatarBatteryMode),
|
|
522
|
+
onDemandMonthlyBudget: normalizeCursorOnDemandMonthlyBudget(cursorRaw.onDemandMonthlyBudget),
|
|
494
523
|
},
|
|
495
524
|
codex: {
|
|
496
525
|
enabled: codexEnabled,
|