chatccc 0.2.183 → 0.2.184
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/im-skills/feishu-skill/skill.md +2 -1
- package/package.json +1 -1
- package/src/__tests__/agent-delegate-task-rpc.test.ts +165 -0
- package/src/__tests__/codex-reset-actions.test.ts +146 -146
- package/src/__tests__/format-message.test.ts +47 -47
- package/src/agent-delegate-task-rpc.ts +153 -0
- package/src/agent-delegate-task.ts +81 -0
- package/src/codex-reset-actions.ts +184 -184
- package/src/format-message.ts +293 -293
- package/src/index.ts +9 -2
- package/src/orchestrator.ts +136 -237
- package/src/session-name.ts +8 -0
- package/src/session.ts +1 -0
|
@@ -8,4 +8,5 @@ Current working directory: {{cwd}}
|
|
|
8
8
|
Use local endpoints instead of calling Feishu Open Platform directly.
|
|
9
9
|
|
|
10
10
|
- **Send images**: POST `{{send_image_url}}` with `{"session_id":"{{session_id}}","path":"<absolute path>","caption":"<optional>"}` — read `{{im_skills_cache_dir}}/feishu-skill/receive-send-image.md`
|
|
11
|
-
- **Send files/videos**: POST `{{send_file_url}}` with `{"session_id":"{{session_id}}","path":"<absolute path>","caption":"<optional>"}` — read `{{im_skills_cache_dir}}/feishu-skill/receive-send-file.md`
|
|
11
|
+
- **Send files/videos**: POST `{{send_file_url}}` with `{"session_id":"{{session_id}}","path":"<absolute path>","caption":"<optional>"}` — read `{{im_skills_cache_dir}}/feishu-skill/receive-send-file.md`
|
|
12
|
+
- **Delegate a task to a new agent conversation**: POST `{{delegate_task_url}}` with `{"tool":"codex|claude|cursor","cwd":"<absolute working directory>","open_id":"<Feishu user open_id>","prompt":"<first task>"}`. Use `open_ids` for multiple users. This uses the normal ChatCCC prompt flow, so project prompt injection and IM skills still apply.
|
package/package.json
CHANGED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { Readable } from "node:stream";
|
|
2
|
+
import { describe, expect, it, vi, beforeEach } from "vitest";
|
|
3
|
+
|
|
4
|
+
import type { PlatformAdapter } from "../platform-adapter.ts";
|
|
5
|
+
|
|
6
|
+
const mockDelegateAgentTask = vi.hoisted(() => vi.fn());
|
|
7
|
+
|
|
8
|
+
vi.mock("../agent-delegate-task.ts", () => ({
|
|
9
|
+
delegateAgentTask: mockDelegateAgentTask,
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
AGENT_DELEGATE_TASK_PATH,
|
|
14
|
+
buildAgentDelegateTaskCapabilityPrompt,
|
|
15
|
+
handleAgentDelegateTaskRequest,
|
|
16
|
+
} from "../agent-delegate-task-rpc.ts";
|
|
17
|
+
import { ABD_APPEND_PROMPT } from "../shared-prefix.ts";
|
|
18
|
+
|
|
19
|
+
function request(body: unknown, path = AGENT_DELEGATE_TASK_PATH, method = "POST"): Readable & {
|
|
20
|
+
url?: string;
|
|
21
|
+
method?: string;
|
|
22
|
+
headers: Record<string, string>;
|
|
23
|
+
} {
|
|
24
|
+
const req = Readable.from([Buffer.from(JSON.stringify(body), "utf8")]) as Readable & {
|
|
25
|
+
url?: string;
|
|
26
|
+
method?: string;
|
|
27
|
+
headers: Record<string, string>;
|
|
28
|
+
};
|
|
29
|
+
req.url = path;
|
|
30
|
+
req.method = method;
|
|
31
|
+
req.headers = { "content-type": "application/json; charset=utf-8" };
|
|
32
|
+
return req;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function response() {
|
|
36
|
+
const res = {
|
|
37
|
+
statusCode: 0,
|
|
38
|
+
headers: {} as Record<string, string>,
|
|
39
|
+
body: "",
|
|
40
|
+
writeHead(status: number, headers: Record<string, string>) {
|
|
41
|
+
this.statusCode = status;
|
|
42
|
+
this.headers = headers;
|
|
43
|
+
return this;
|
|
44
|
+
},
|
|
45
|
+
end(chunk?: string) {
|
|
46
|
+
this.body += chunk ?? "";
|
|
47
|
+
return this;
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
return res;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function platform(kind: "feishu" | "wechat" = "feishu"): PlatformAdapter {
|
|
54
|
+
return {
|
|
55
|
+
kind,
|
|
56
|
+
sendText: vi.fn(async () => true),
|
|
57
|
+
sendCard: vi.fn(async () => true),
|
|
58
|
+
sendRawCard: vi.fn(async () => true),
|
|
59
|
+
createGroup: vi.fn(async () => "chat-id"),
|
|
60
|
+
updateChatInfo: vi.fn(async () => {}),
|
|
61
|
+
getChatInfo: vi.fn(async () => ({ name: "chat", description: "" })),
|
|
62
|
+
disbandChat: vi.fn(async () => {}),
|
|
63
|
+
setChatAvatar: vi.fn(async () => {}),
|
|
64
|
+
extractSessionInfo: vi.fn(() => null),
|
|
65
|
+
cardCreate: vi.fn(async () => "card-id"),
|
|
66
|
+
cardSend: vi.fn(async () => "message-id"),
|
|
67
|
+
cardUpdate: vi.fn(async () => {}),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
describe("agent delegate task RPC", () => {
|
|
72
|
+
beforeEach(() => {
|
|
73
|
+
mockDelegateAgentTask.mockReset();
|
|
74
|
+
mockDelegateAgentTask.mockResolvedValue({
|
|
75
|
+
chatId: "chat-new",
|
|
76
|
+
sessionId: "sid-new",
|
|
77
|
+
tool: "codex",
|
|
78
|
+
cwd: "F:\\repo",
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("delegates a task through the local API", async () => {
|
|
83
|
+
const req = request({
|
|
84
|
+
tool: "codex",
|
|
85
|
+
cwd: "F:\\repo",
|
|
86
|
+
open_id: "ou-user",
|
|
87
|
+
prompt: "帮我分析日志",
|
|
88
|
+
});
|
|
89
|
+
const res = response();
|
|
90
|
+
|
|
91
|
+
await handleAgentDelegateTaskRequest(req as never, res as never, platform());
|
|
92
|
+
|
|
93
|
+
expect(res.statusCode).toBe(200);
|
|
94
|
+
expect(JSON.parse(res.body)).toMatchObject({
|
|
95
|
+
ok: true,
|
|
96
|
+
chat_id: "chat-new",
|
|
97
|
+
session_id: "sid-new",
|
|
98
|
+
tool: "codex",
|
|
99
|
+
});
|
|
100
|
+
expect(mockDelegateAgentTask).toHaveBeenCalledWith(expect.objectContaining({
|
|
101
|
+
tool: "codex",
|
|
102
|
+
cwd: expect.stringContaining("repo"),
|
|
103
|
+
promptText: "帮我分析日志",
|
|
104
|
+
openIds: ["ou-user"],
|
|
105
|
+
chatNamePrefix: "帮我分析日志",
|
|
106
|
+
}));
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("applies the shared ABD prefix to API prompts", async () => {
|
|
110
|
+
const req = request({
|
|
111
|
+
tool: "claude",
|
|
112
|
+
cwd: "F:\\repo",
|
|
113
|
+
openIds: ["ou-user"],
|
|
114
|
+
prompt: "/abd帮我分析",
|
|
115
|
+
});
|
|
116
|
+
const res = response();
|
|
117
|
+
|
|
118
|
+
await handleAgentDelegateTaskRequest(req as never, res as never, platform());
|
|
119
|
+
|
|
120
|
+
expect(res.statusCode).toBe(200);
|
|
121
|
+
expect(mockDelegateAgentTask).toHaveBeenCalledWith(expect.objectContaining({
|
|
122
|
+
promptText: `帮我分析\n\n---\n${ABD_APPEND_PROMPT}`,
|
|
123
|
+
chatNamePrefix: "帮我分析",
|
|
124
|
+
}));
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("rejects non-Feishu platforms", async () => {
|
|
128
|
+
const req = request({
|
|
129
|
+
tool: "codex",
|
|
130
|
+
cwd: "F:\\repo",
|
|
131
|
+
open_id: "ou-user",
|
|
132
|
+
prompt: "task",
|
|
133
|
+
});
|
|
134
|
+
const res = response();
|
|
135
|
+
|
|
136
|
+
await handleAgentDelegateTaskRequest(req as never, res as never, platform("wechat"));
|
|
137
|
+
|
|
138
|
+
expect(res.statusCode).toBe(409);
|
|
139
|
+
expect(mockDelegateAgentTask).not.toHaveBeenCalled();
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("rejects missing required parameters", async () => {
|
|
143
|
+
const req = request({ tool: "codex", cwd: "F:\\repo", prompt: "task" });
|
|
144
|
+
const res = response();
|
|
145
|
+
|
|
146
|
+
await handleAgentDelegateTaskRequest(req as never, res as never, platform());
|
|
147
|
+
|
|
148
|
+
expect(res.statusCode).toBe(400);
|
|
149
|
+
expect(JSON.parse(res.body).error).toContain("open_id");
|
|
150
|
+
expect(mockDelegateAgentTask).not.toHaveBeenCalled();
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("builds prompt instructions for the delegate endpoint", () => {
|
|
154
|
+
const prompt = buildAgentDelegateTaskCapabilityPrompt({
|
|
155
|
+
url: `http://127.0.0.1:18080${AGENT_DELEGATE_TASK_PATH}`,
|
|
156
|
+
cwd: "F:/repo",
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
expect(prompt).toContain("POST http://127.0.0.1:18080/api/agent/delegate-task");
|
|
160
|
+
expect(prompt).toContain('"tool":"codex|claude|cursor"');
|
|
161
|
+
expect(prompt).toContain('"cwd":"absolute working directory"');
|
|
162
|
+
expect(prompt).toContain("project prompt injection and IM skills still apply");
|
|
163
|
+
expect(prompt).toContain("Current working directory: F:/repo");
|
|
164
|
+
});
|
|
165
|
+
});
|
|
@@ -1,146 +1,146 @@
|
|
|
1
|
-
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
_resetCodexResetRequestsForTest,
|
|
5
|
-
handleCodexResetCardAction,
|
|
6
|
-
type CodexResetActionDeps,
|
|
7
|
-
} from "../codex-reset-actions.ts";
|
|
8
|
-
|
|
9
|
-
function deps(): CodexResetActionDeps & {
|
|
10
|
-
getTenantAccessToken: ReturnType<typeof vi.fn>;
|
|
11
|
-
sendRawCard: ReturnType<typeof vi.fn>;
|
|
12
|
-
sendTextReply: ReturnType<typeof vi.fn>;
|
|
13
|
-
sendCardReply: ReturnType<typeof vi.fn>;
|
|
14
|
-
updateCardMessage: ReturnType<typeof vi.fn>;
|
|
15
|
-
recallMessage: ReturnType<typeof vi.fn>;
|
|
16
|
-
consumeCodexRateLimitResetCredit: ReturnType<typeof vi.fn>;
|
|
17
|
-
createRequestId: ReturnType<typeof vi.fn>;
|
|
18
|
-
} {
|
|
19
|
-
return {
|
|
20
|
-
getTenantAccessToken: vi.fn(async () => "tenant-token"),
|
|
21
|
-
sendRawCard: vi.fn(async () => true),
|
|
22
|
-
sendTextReply: vi.fn(async () => true),
|
|
23
|
-
sendCardReply: vi.fn(async () => true),
|
|
24
|
-
updateCardMessage: vi.fn(async () => true),
|
|
25
|
-
recallMessage: vi.fn(async () => true),
|
|
26
|
-
consumeCodexRateLimitResetCredit: vi.fn(async () => ({ code: "reset" as const, windowsReset: 2 })),
|
|
27
|
-
createRequestId: vi.fn(() => "request-1"),
|
|
28
|
-
};
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function event(value: Record<string, unknown>, messageId: string) {
|
|
32
|
-
return {
|
|
33
|
-
event: {
|
|
34
|
-
open_message_id: messageId,
|
|
35
|
-
open_chat_id: "chat-1",
|
|
36
|
-
operator: { open_id: "ou-user" },
|
|
37
|
-
action: { value },
|
|
38
|
-
},
|
|
39
|
-
};
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
describe("Codex reset card actions", () => {
|
|
43
|
-
beforeEach(() => {
|
|
44
|
-
_resetCodexResetRequestsForTest();
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
it("sends a confirmation card tied to the clicked usage card", async () => {
|
|
48
|
-
const d = deps();
|
|
49
|
-
|
|
50
|
-
await expect(handleCodexResetCardAction(
|
|
51
|
-
event({ action: "codex_reset_request" }, "usage-message"),
|
|
52
|
-
d,
|
|
53
|
-
)).resolves.toBe(true);
|
|
54
|
-
|
|
55
|
-
expect(d.sendRawCard).toHaveBeenCalledTimes(1);
|
|
56
|
-
expect(d.sendRawCard.mock.calls[0][1]).toBe("chat-1");
|
|
57
|
-
const confirmCard = JSON.parse(d.sendRawCard.mock.calls[0][2]);
|
|
58
|
-
const action = confirmCard.elements.find((element: any) => element.tag === "action");
|
|
59
|
-
expect(action.actions[0].value).toEqual({
|
|
60
|
-
action: "codex_reset_confirm",
|
|
61
|
-
decision: "yes",
|
|
62
|
-
parentMessageId: "usage-message",
|
|
63
|
-
requestId: "request-1",
|
|
64
|
-
});
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
it("consumes a reset credit, sends the result, and collapses the confirmation card when user confirms", async () => {
|
|
68
|
-
const d = deps();
|
|
69
|
-
await handleCodexResetCardAction(event({ action: "codex_reset_request" }, "usage-message"), d);
|
|
70
|
-
|
|
71
|
-
await expect(handleCodexResetCardAction(
|
|
72
|
-
event({
|
|
73
|
-
action: "codex_reset_confirm",
|
|
74
|
-
decision: "yes",
|
|
75
|
-
parentMessageId: "usage-message",
|
|
76
|
-
requestId: "request-1",
|
|
77
|
-
}, "confirm-message"),
|
|
78
|
-
d,
|
|
79
|
-
)).resolves.toBe(true);
|
|
80
|
-
|
|
81
|
-
expect(d.consumeCodexRateLimitResetCredit).toHaveBeenCalledWith("request-1");
|
|
82
|
-
expect(d.updateCardMessage).toHaveBeenCalledTimes(1);
|
|
83
|
-
expect(d.updateCardMessage.mock.calls[0][1]).toBe("confirm-message");
|
|
84
|
-
expect(JSON.parse(d.updateCardMessage.mock.calls[0][2]).elements).toEqual([
|
|
85
|
-
{ tag: "markdown", content: " " },
|
|
86
|
-
]);
|
|
87
|
-
expect(d.recallMessage).toHaveBeenCalledWith("tenant-token", "confirm-message");
|
|
88
|
-
expect(d.sendTextReply).toHaveBeenCalledWith(
|
|
89
|
-
"tenant-token",
|
|
90
|
-
"chat-1",
|
|
91
|
-
expect.stringContaining("重置成功"),
|
|
92
|
-
);
|
|
93
|
-
expect(d.sendCardReply).not.toHaveBeenCalled();
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
it("does not consume a reset credit and sends a cancellation message when user declines", async () => {
|
|
97
|
-
const d = deps();
|
|
98
|
-
await handleCodexResetCardAction(event({ action: "codex_reset_request" }, "usage-message"), d);
|
|
99
|
-
|
|
100
|
-
await expect(handleCodexResetCardAction(
|
|
101
|
-
event({
|
|
102
|
-
action: "codex_reset_confirm",
|
|
103
|
-
decision: "no",
|
|
104
|
-
parentMessageId: "usage-message",
|
|
105
|
-
requestId: "request-1",
|
|
106
|
-
}, "confirm-message"),
|
|
107
|
-
d,
|
|
108
|
-
)).resolves.toBe(true);
|
|
109
|
-
|
|
110
|
-
expect(d.consumeCodexRateLimitResetCredit).not.toHaveBeenCalled();
|
|
111
|
-
expect(d.updateCardMessage).toHaveBeenCalledTimes(1);
|
|
112
|
-
expect(d.recallMessage).toHaveBeenCalledWith("tenant-token", "confirm-message");
|
|
113
|
-
expect(d.sendTextReply).toHaveBeenCalledWith(
|
|
114
|
-
"tenant-token",
|
|
115
|
-
"chat-1",
|
|
116
|
-
"Codex 主动重置:用户取消了重置。",
|
|
117
|
-
);
|
|
118
|
-
expect(d.sendCardReply).not.toHaveBeenCalled();
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
it("can collapse a confirmation card using context.message_id fallback", async () => {
|
|
122
|
-
const d = deps();
|
|
123
|
-
await handleCodexResetCardAction(event({ action: "codex_reset_request" }, "usage-message"), d);
|
|
124
|
-
|
|
125
|
-
await expect(handleCodexResetCardAction({
|
|
126
|
-
event: {
|
|
127
|
-
context: {
|
|
128
|
-
message_id: "confirm-message-from-context",
|
|
129
|
-
open_chat_id: "chat-1",
|
|
130
|
-
},
|
|
131
|
-
action: {
|
|
132
|
-
value: {
|
|
133
|
-
action: "codex_reset_confirm",
|
|
134
|
-
decision: "no",
|
|
135
|
-
parentMessageId: "usage-message",
|
|
136
|
-
requestId: "request-1",
|
|
137
|
-
},
|
|
138
|
-
},
|
|
139
|
-
},
|
|
140
|
-
}, d)).resolves.toBe(true);
|
|
141
|
-
|
|
142
|
-
expect(d.updateCardMessage).toHaveBeenCalledTimes(1);
|
|
143
|
-
expect(d.updateCardMessage.mock.calls[0][1]).toBe("confirm-message-from-context");
|
|
144
|
-
expect(d.recallMessage).toHaveBeenCalledWith("tenant-token", "confirm-message-from-context");
|
|
145
|
-
});
|
|
146
|
-
});
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
_resetCodexResetRequestsForTest,
|
|
5
|
+
handleCodexResetCardAction,
|
|
6
|
+
type CodexResetActionDeps,
|
|
7
|
+
} from "../codex-reset-actions.ts";
|
|
8
|
+
|
|
9
|
+
function deps(): CodexResetActionDeps & {
|
|
10
|
+
getTenantAccessToken: ReturnType<typeof vi.fn>;
|
|
11
|
+
sendRawCard: ReturnType<typeof vi.fn>;
|
|
12
|
+
sendTextReply: ReturnType<typeof vi.fn>;
|
|
13
|
+
sendCardReply: ReturnType<typeof vi.fn>;
|
|
14
|
+
updateCardMessage: ReturnType<typeof vi.fn>;
|
|
15
|
+
recallMessage: ReturnType<typeof vi.fn>;
|
|
16
|
+
consumeCodexRateLimitResetCredit: ReturnType<typeof vi.fn>;
|
|
17
|
+
createRequestId: ReturnType<typeof vi.fn>;
|
|
18
|
+
} {
|
|
19
|
+
return {
|
|
20
|
+
getTenantAccessToken: vi.fn(async () => "tenant-token"),
|
|
21
|
+
sendRawCard: vi.fn(async () => true),
|
|
22
|
+
sendTextReply: vi.fn(async () => true),
|
|
23
|
+
sendCardReply: vi.fn(async () => true),
|
|
24
|
+
updateCardMessage: vi.fn(async () => true),
|
|
25
|
+
recallMessage: vi.fn(async () => true),
|
|
26
|
+
consumeCodexRateLimitResetCredit: vi.fn(async () => ({ code: "reset" as const, windowsReset: 2 })),
|
|
27
|
+
createRequestId: vi.fn(() => "request-1"),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function event(value: Record<string, unknown>, messageId: string) {
|
|
32
|
+
return {
|
|
33
|
+
event: {
|
|
34
|
+
open_message_id: messageId,
|
|
35
|
+
open_chat_id: "chat-1",
|
|
36
|
+
operator: { open_id: "ou-user" },
|
|
37
|
+
action: { value },
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
describe("Codex reset card actions", () => {
|
|
43
|
+
beforeEach(() => {
|
|
44
|
+
_resetCodexResetRequestsForTest();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("sends a confirmation card tied to the clicked usage card", async () => {
|
|
48
|
+
const d = deps();
|
|
49
|
+
|
|
50
|
+
await expect(handleCodexResetCardAction(
|
|
51
|
+
event({ action: "codex_reset_request" }, "usage-message"),
|
|
52
|
+
d,
|
|
53
|
+
)).resolves.toBe(true);
|
|
54
|
+
|
|
55
|
+
expect(d.sendRawCard).toHaveBeenCalledTimes(1);
|
|
56
|
+
expect(d.sendRawCard.mock.calls[0][1]).toBe("chat-1");
|
|
57
|
+
const confirmCard = JSON.parse(d.sendRawCard.mock.calls[0][2]);
|
|
58
|
+
const action = confirmCard.elements.find((element: any) => element.tag === "action");
|
|
59
|
+
expect(action.actions[0].value).toEqual({
|
|
60
|
+
action: "codex_reset_confirm",
|
|
61
|
+
decision: "yes",
|
|
62
|
+
parentMessageId: "usage-message",
|
|
63
|
+
requestId: "request-1",
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("consumes a reset credit, sends the result, and collapses the confirmation card when user confirms", async () => {
|
|
68
|
+
const d = deps();
|
|
69
|
+
await handleCodexResetCardAction(event({ action: "codex_reset_request" }, "usage-message"), d);
|
|
70
|
+
|
|
71
|
+
await expect(handleCodexResetCardAction(
|
|
72
|
+
event({
|
|
73
|
+
action: "codex_reset_confirm",
|
|
74
|
+
decision: "yes",
|
|
75
|
+
parentMessageId: "usage-message",
|
|
76
|
+
requestId: "request-1",
|
|
77
|
+
}, "confirm-message"),
|
|
78
|
+
d,
|
|
79
|
+
)).resolves.toBe(true);
|
|
80
|
+
|
|
81
|
+
expect(d.consumeCodexRateLimitResetCredit).toHaveBeenCalledWith("request-1");
|
|
82
|
+
expect(d.updateCardMessage).toHaveBeenCalledTimes(1);
|
|
83
|
+
expect(d.updateCardMessage.mock.calls[0][1]).toBe("confirm-message");
|
|
84
|
+
expect(JSON.parse(d.updateCardMessage.mock.calls[0][2]).elements).toEqual([
|
|
85
|
+
{ tag: "markdown", content: " " },
|
|
86
|
+
]);
|
|
87
|
+
expect(d.recallMessage).toHaveBeenCalledWith("tenant-token", "confirm-message");
|
|
88
|
+
expect(d.sendTextReply).toHaveBeenCalledWith(
|
|
89
|
+
"tenant-token",
|
|
90
|
+
"chat-1",
|
|
91
|
+
expect.stringContaining("重置成功"),
|
|
92
|
+
);
|
|
93
|
+
expect(d.sendCardReply).not.toHaveBeenCalled();
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("does not consume a reset credit and sends a cancellation message when user declines", async () => {
|
|
97
|
+
const d = deps();
|
|
98
|
+
await handleCodexResetCardAction(event({ action: "codex_reset_request" }, "usage-message"), d);
|
|
99
|
+
|
|
100
|
+
await expect(handleCodexResetCardAction(
|
|
101
|
+
event({
|
|
102
|
+
action: "codex_reset_confirm",
|
|
103
|
+
decision: "no",
|
|
104
|
+
parentMessageId: "usage-message",
|
|
105
|
+
requestId: "request-1",
|
|
106
|
+
}, "confirm-message"),
|
|
107
|
+
d,
|
|
108
|
+
)).resolves.toBe(true);
|
|
109
|
+
|
|
110
|
+
expect(d.consumeCodexRateLimitResetCredit).not.toHaveBeenCalled();
|
|
111
|
+
expect(d.updateCardMessage).toHaveBeenCalledTimes(1);
|
|
112
|
+
expect(d.recallMessage).toHaveBeenCalledWith("tenant-token", "confirm-message");
|
|
113
|
+
expect(d.sendTextReply).toHaveBeenCalledWith(
|
|
114
|
+
"tenant-token",
|
|
115
|
+
"chat-1",
|
|
116
|
+
"Codex 主动重置:用户取消了重置。",
|
|
117
|
+
);
|
|
118
|
+
expect(d.sendCardReply).not.toHaveBeenCalled();
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("can collapse a confirmation card using context.message_id fallback", async () => {
|
|
122
|
+
const d = deps();
|
|
123
|
+
await handleCodexResetCardAction(event({ action: "codex_reset_request" }, "usage-message"), d);
|
|
124
|
+
|
|
125
|
+
await expect(handleCodexResetCardAction({
|
|
126
|
+
event: {
|
|
127
|
+
context: {
|
|
128
|
+
message_id: "confirm-message-from-context",
|
|
129
|
+
open_chat_id: "chat-1",
|
|
130
|
+
},
|
|
131
|
+
action: {
|
|
132
|
+
value: {
|
|
133
|
+
action: "codex_reset_confirm",
|
|
134
|
+
decision: "no",
|
|
135
|
+
parentMessageId: "usage-message",
|
|
136
|
+
requestId: "request-1",
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
}, d)).resolves.toBe(true);
|
|
141
|
+
|
|
142
|
+
expect(d.updateCardMessage).toHaveBeenCalledTimes(1);
|
|
143
|
+
expect(d.updateCardMessage.mock.calls[0][1]).toBe("confirm-message-from-context");
|
|
144
|
+
expect(d.recallMessage).toHaveBeenCalledWith("tenant-token", "confirm-message-from-context");
|
|
145
|
+
});
|
|
146
|
+
});
|
|
@@ -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
|
+
});
|