chatccc 0.2.181 → 0.2.183
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__/format-message.test.ts +316 -273
- 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/format-message.ts +293 -214
- 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
|
@@ -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
|
|
|
@@ -1,36 +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
|
-
});
|
|
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,10 +1,10 @@
|
|
|
1
|
-
// ---------------------------------------------------------------------------
|
|
2
|
-
// Button helpers
|
|
3
|
-
// ---------------------------------------------------------------------------
|
|
4
|
-
|
|
5
|
-
import { ABD_HELP_LINE } from "./shared-prefix.ts";
|
|
6
|
-
|
|
7
|
-
export interface ButtonDef {
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// Button helpers
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
|
|
5
|
+
import { ABD_HELP_LINE } from "./shared-prefix.ts";
|
|
6
|
+
|
|
7
|
+
export interface ButtonDef {
|
|
8
8
|
text: string;
|
|
9
9
|
value: string;
|
|
10
10
|
type?: "primary" | "default" | "danger";
|
|
@@ -137,11 +137,11 @@ export function buildHelpCard(
|
|
|
137
137
|
"发送 **/newh** 重置当前会话(沿用当前工作目录,不切换)",
|
|
138
138
|
"发送 **/plan** 以规划模式提问(只读,不执行写操作)",
|
|
139
139
|
"发送 **/ask** 以问答模式提问(只读,不执行写操作)",
|
|
140
|
-
"发送 **/usage** 查看 Codex 5h/周用量,以及查询/使用主动重置卡",
|
|
141
|
-
"发送 **/restart** 重启 ChatCCC 进程",
|
|
142
|
-
"发送 **/update** 更新并重启(仅 npm 全局安装可用)",
|
|
143
|
-
ABD_HELP_LINE,
|
|
144
|
-
].join("\n");
|
|
140
|
+
"发送 **/usage** 查看 Codex 5h/周用量,以及查询/使用主动重置卡",
|
|
141
|
+
"发送 **/restart** 重启 ChatCCC 进程",
|
|
142
|
+
"发送 **/update** 更新并重启(仅 npm 全局安装可用)",
|
|
143
|
+
ABD_HELP_LINE,
|
|
144
|
+
].join("\n");
|
|
145
145
|
return JSON.stringify({
|
|
146
146
|
config: { wide_screen_mode: true },
|
|
147
147
|
header: { template: "blue", title: { content: "ChatCCC", tag: "plain_text" } },
|
|
@@ -410,11 +410,11 @@ export function buildQueueFullCard(): string {
|
|
|
410
410
|
}
|
|
411
411
|
|
|
412
412
|
// 状态卡片(带关闭按钮)
|
|
413
|
-
export function buildStatusCard(statusText: string, template = "blue"): string {
|
|
414
|
-
return JSON.stringify({
|
|
415
|
-
config: { wide_screen_mode: true },
|
|
416
|
-
header: { template, title: { content: "会话状态", tag: "plain_text" } },
|
|
417
|
-
elements: [
|
|
413
|
+
export function buildStatusCard(statusText: string, template = "blue"): string {
|
|
414
|
+
return JSON.stringify({
|
|
415
|
+
config: { wide_screen_mode: true },
|
|
416
|
+
header: { template, title: { content: "会话状态", tag: "plain_text" } },
|
|
417
|
+
elements: [
|
|
418
418
|
{ tag: "div", text: { tag: "lark_md", content: statusText } },
|
|
419
419
|
{ tag: "hr" },
|
|
420
420
|
{
|
|
@@ -426,78 +426,78 @@ export function buildStatusCard(statusText: string, template = "blue"): string {
|
|
|
426
426
|
value: { action: "close" },
|
|
427
427
|
}],
|
|
428
428
|
},
|
|
429
|
-
],
|
|
430
|
-
});
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
export function buildCodexUsageCard(content: string, resetCreditsAvailable: number | null): string {
|
|
434
|
-
const elements: object[] = [
|
|
435
|
-
{ tag: "div", text: { tag: "lark_md", content } },
|
|
436
|
-
];
|
|
437
|
-
if (resetCreditsAvailable !== null && resetCreditsAvailable > 0) {
|
|
438
|
-
elements.push({ tag: "hr" });
|
|
439
|
-
elements.push({
|
|
440
|
-
tag: "action",
|
|
441
|
-
actions: [{
|
|
442
|
-
tag: "button",
|
|
443
|
-
text: { tag: "plain_text", content: "发起重置" },
|
|
444
|
-
type: "primary",
|
|
445
|
-
value: { action: "codex_reset_request", availableCount: resetCreditsAvailable },
|
|
446
|
-
}],
|
|
447
|
-
});
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
return JSON.stringify({
|
|
451
|
-
config: { wide_screen_mode: true },
|
|
452
|
-
header: { template: "blue", title: { content: "Codex Usage", tag: "plain_text" } },
|
|
453
|
-
elements,
|
|
454
|
-
});
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
export function buildCodexResetConfirmCard(params: {
|
|
458
|
-
availableCount: number;
|
|
459
|
-
parentMessageId: string;
|
|
460
|
-
requestId: string;
|
|
461
|
-
}): string {
|
|
462
|
-
const valueBase = {
|
|
463
|
-
parentMessageId: params.parentMessageId,
|
|
464
|
-
requestId: params.requestId,
|
|
465
|
-
};
|
|
466
|
-
return JSON.stringify({
|
|
467
|
-
config: { wide_screen_mode: true },
|
|
468
|
-
header: { template: "yellow", title: { content: "确认 Codex 主动重置", tag: "plain_text" } },
|
|
469
|
-
elements: [
|
|
470
|
-
{
|
|
471
|
-
tag: "div",
|
|
472
|
-
text: {
|
|
473
|
-
tag: "lark_md",
|
|
474
|
-
content: `当前可用主动重置次数:**${params.availableCount}**。\n\n确认后会消耗 1 次主动重置,并重置当前可重置的 Codex 用量窗口。`,
|
|
475
|
-
},
|
|
476
|
-
},
|
|
477
|
-
{ tag: "hr" },
|
|
478
|
-
{
|
|
479
|
-
tag: "action",
|
|
480
|
-
actions: [
|
|
481
|
-
{
|
|
482
|
-
tag: "button",
|
|
483
|
-
text: { tag: "plain_text", content: "是,发起重置" },
|
|
484
|
-
type: "danger",
|
|
485
|
-
value: { action: "codex_reset_confirm", decision: "yes", ...valueBase },
|
|
486
|
-
},
|
|
487
|
-
{
|
|
488
|
-
tag: "button",
|
|
489
|
-
text: { tag: "plain_text", content: "否" },
|
|
490
|
-
type: "default",
|
|
491
|
-
value: { action: "codex_reset_confirm", decision: "no", ...valueBase },
|
|
492
|
-
},
|
|
493
|
-
],
|
|
494
|
-
},
|
|
495
|
-
],
|
|
496
|
-
});
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
/** 模型切换卡片(/model 命令,飞书专用,含切换按钮) */
|
|
500
|
-
export function buildModelCard(
|
|
429
|
+
],
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
export function buildCodexUsageCard(content: string, resetCreditsAvailable: number | null): string {
|
|
434
|
+
const elements: object[] = [
|
|
435
|
+
{ tag: "div", text: { tag: "lark_md", content } },
|
|
436
|
+
];
|
|
437
|
+
if (resetCreditsAvailable !== null && resetCreditsAvailable > 0) {
|
|
438
|
+
elements.push({ tag: "hr" });
|
|
439
|
+
elements.push({
|
|
440
|
+
tag: "action",
|
|
441
|
+
actions: [{
|
|
442
|
+
tag: "button",
|
|
443
|
+
text: { tag: "plain_text", content: "发起重置" },
|
|
444
|
+
type: "primary",
|
|
445
|
+
value: { action: "codex_reset_request", availableCount: resetCreditsAvailable },
|
|
446
|
+
}],
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
return JSON.stringify({
|
|
451
|
+
config: { wide_screen_mode: true },
|
|
452
|
+
header: { template: "blue", title: { content: "Codex Usage", tag: "plain_text" } },
|
|
453
|
+
elements,
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
export function buildCodexResetConfirmCard(params: {
|
|
458
|
+
availableCount: number;
|
|
459
|
+
parentMessageId: string;
|
|
460
|
+
requestId: string;
|
|
461
|
+
}): string {
|
|
462
|
+
const valueBase = {
|
|
463
|
+
parentMessageId: params.parentMessageId,
|
|
464
|
+
requestId: params.requestId,
|
|
465
|
+
};
|
|
466
|
+
return JSON.stringify({
|
|
467
|
+
config: { wide_screen_mode: true },
|
|
468
|
+
header: { template: "yellow", title: { content: "确认 Codex 主动重置", tag: "plain_text" } },
|
|
469
|
+
elements: [
|
|
470
|
+
{
|
|
471
|
+
tag: "div",
|
|
472
|
+
text: {
|
|
473
|
+
tag: "lark_md",
|
|
474
|
+
content: `当前可用主动重置次数:**${params.availableCount}**。\n\n确认后会消耗 1 次主动重置,并重置当前可重置的 Codex 用量窗口。`,
|
|
475
|
+
},
|
|
476
|
+
},
|
|
477
|
+
{ tag: "hr" },
|
|
478
|
+
{
|
|
479
|
+
tag: "action",
|
|
480
|
+
actions: [
|
|
481
|
+
{
|
|
482
|
+
tag: "button",
|
|
483
|
+
text: { tag: "plain_text", content: "是,发起重置" },
|
|
484
|
+
type: "danger",
|
|
485
|
+
value: { action: "codex_reset_confirm", decision: "yes", ...valueBase },
|
|
486
|
+
},
|
|
487
|
+
{
|
|
488
|
+
tag: "button",
|
|
489
|
+
text: { tag: "plain_text", content: "否" },
|
|
490
|
+
type: "default",
|
|
491
|
+
value: { action: "codex_reset_confirm", decision: "no", ...valueBase },
|
|
492
|
+
},
|
|
493
|
+
],
|
|
494
|
+
},
|
|
495
|
+
],
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/** 模型切换卡片(/model 命令,飞书专用,含切换按钮) */
|
|
500
|
+
export function buildModelCard(
|
|
501
501
|
currentModel: string,
|
|
502
502
|
models: string[],
|
|
503
503
|
tool?: string,
|
|
@@ -14,8 +14,10 @@ interface PendingCodexResetRequest {
|
|
|
14
14
|
export interface CodexResetActionDeps {
|
|
15
15
|
getTenantAccessToken(): Promise<string>;
|
|
16
16
|
sendRawCard(token: string, chatId: string, cardJson: string): Promise<boolean>;
|
|
17
|
+
sendTextReply(token: string, chatId: string, text: string): Promise<boolean>;
|
|
17
18
|
sendCardReply(token: string, chatId: string, title: string, content: string, template?: string): Promise<boolean>;
|
|
18
19
|
updateCardMessage(token: string, messageId: string, content: string): Promise<boolean>;
|
|
20
|
+
recallMessage(token: string, messageId: string): Promise<boolean>;
|
|
19
21
|
consumeCodexRateLimitResetCredit(redeemRequestId: string): Promise<CodexResetConsumeResult>;
|
|
20
22
|
createRequestId?: () => string;
|
|
21
23
|
}
|
|
@@ -38,7 +40,13 @@ function eventMessageId(raw: Record<string, unknown>): string {
|
|
|
38
40
|
? raw.open_message_id
|
|
39
41
|
: typeof (raw.context as Record<string, unknown> | undefined)?.open_message_id === "string"
|
|
40
42
|
? (raw.context as Record<string, string>).open_message_id
|
|
41
|
-
: ""
|
|
43
|
+
: typeof raw.message_id === "string"
|
|
44
|
+
? raw.message_id
|
|
45
|
+
: typeof (raw.context as Record<string, unknown> | undefined)?.message_id === "string"
|
|
46
|
+
? (raw.context as Record<string, string>).message_id
|
|
47
|
+
: typeof (raw.message as Record<string, unknown> | undefined)?.message_id === "string"
|
|
48
|
+
? (raw.message as Record<string, string>).message_id
|
|
49
|
+
: "";
|
|
42
50
|
}
|
|
43
51
|
|
|
44
52
|
function eventChatId(raw: Record<string, unknown>): string {
|
|
@@ -124,7 +132,12 @@ async function handleResetConfirmation(
|
|
|
124
132
|
const token = await deps.getTenantAccessToken();
|
|
125
133
|
const confirmMessageId = eventMessageId(raw);
|
|
126
134
|
if (confirmMessageId) {
|
|
127
|
-
await deps.updateCardMessage(token, confirmMessageId, collapsedCard());
|
|
135
|
+
const collapsed = await deps.updateCardMessage(token, confirmMessageId, collapsedCard());
|
|
136
|
+
console.log(`[Codex Reset] collapse confirmation card ${collapsed ? "OK" : "FAILED"}: messageId=${confirmMessageId}`);
|
|
137
|
+
const recalled = await deps.recallMessage(token, confirmMessageId);
|
|
138
|
+
console.log(`[Codex Reset] recall confirmation card ${recalled ? "OK" : "FAILED"}: messageId=${confirmMessageId}`);
|
|
139
|
+
} else {
|
|
140
|
+
console.error("[Codex Reset] missing confirmation card message id; cannot collapse");
|
|
128
141
|
}
|
|
129
142
|
|
|
130
143
|
const pending = pendingCodexResetRequests.get(requestId);
|
|
@@ -132,22 +145,22 @@ async function handleResetConfirmation(
|
|
|
132
145
|
if (!chatId) return true;
|
|
133
146
|
|
|
134
147
|
if (!pending || pending.status !== "pending" || pending.parentMessageId !== parentMessageId) {
|
|
135
|
-
await deps.
|
|
148
|
+
await deps.sendTextReply(token, chatId, "Codex 主动重置:这张确认卡片已经失效或已处理。");
|
|
136
149
|
return true;
|
|
137
150
|
}
|
|
138
151
|
pending.status = "handled";
|
|
139
152
|
|
|
140
153
|
if (decision === "no") {
|
|
141
|
-
await deps.
|
|
154
|
+
await deps.sendTextReply(token, chatId, "Codex 主动重置:用户取消了重置。");
|
|
142
155
|
return true;
|
|
143
156
|
}
|
|
144
157
|
|
|
145
158
|
try {
|
|
146
159
|
const result = await deps.consumeCodexRateLimitResetCredit(requestId);
|
|
147
160
|
const message = resetResultMessage(result);
|
|
148
|
-
await deps.
|
|
161
|
+
await deps.sendTextReply(token, chatId, `Codex 主动重置:${message.content}`);
|
|
149
162
|
} catch (err) {
|
|
150
|
-
await deps.
|
|
163
|
+
await deps.sendTextReply(token, chatId, `Codex 主动重置失败:${(err as Error).message}`);
|
|
151
164
|
}
|
|
152
165
|
return true;
|
|
153
166
|
}
|