chatccc 0.2.183 → 0.2.185
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 +88 -69
- package/config.sample.json +5 -0
- package/im-skills/feishu-skill/skill.md +3 -2
- package/package.json +1 -1
- package/src/__tests__/agent-delegate-task-rpc.test.ts +165 -0
- package/src/__tests__/chatgpt-subscription-rpc.test.ts +89 -0
- package/src/__tests__/chatgpt-subscription.test.ts +135 -0
- package/src/__tests__/chrome-devtools-guard.test.ts +125 -0
- package/src/__tests__/codex-reset-actions.test.ts +146 -146
- package/src/__tests__/config-reload.test.ts +13 -0
- package/src/__tests__/config-sample.test.ts +11 -0
- package/src/__tests__/format-message.test.ts +47 -47
- package/src/__tests__/orchestrator.test.ts +162 -117
- package/src/__tests__/web-ui.test.ts +16 -0
- package/src/agent-delegate-task-rpc.ts +153 -0
- package/src/agent-delegate-task.ts +81 -0
- package/src/chatgpt-subscription-rpc.ts +27 -0
- package/src/chatgpt-subscription.ts +299 -0
- package/src/chrome-devtools-guard.ts +242 -0
- package/src/codex-reset-actions.ts +184 -184
- package/src/config.ts +21 -0
- package/src/format-message.ts +293 -293
- package/src/index.ts +19 -4
- package/src/orchestrator.ts +173 -237
- package/src/session-name.ts +8 -0
- package/src/session.ts +6 -5
- package/src/web-ui.ts +136 -6
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
ensureChromeCdpRunning,
|
|
6
|
+
isChromeCdpHealthy,
|
|
7
|
+
probeChromeCdp,
|
|
8
|
+
resolveChromeExecutable,
|
|
9
|
+
resolveChromeUserDataDir,
|
|
10
|
+
} from "../chrome-devtools-guard.ts";
|
|
11
|
+
|
|
12
|
+
describe("Chrome CDP guard", () => {
|
|
13
|
+
it("treats /json/version with Browser as healthy", async () => {
|
|
14
|
+
const fetchImpl = vi.fn(async () =>
|
|
15
|
+
new Response(JSON.stringify({ Browser: "Chrome/144.0.0.0" }), { status: 200 }),
|
|
16
|
+
) as unknown as typeof fetch;
|
|
17
|
+
|
|
18
|
+
await expect(isChromeCdpHealthy(15166, { fetchImpl })).resolves.toBe(true);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("does not spawn Chrome when the CDP endpoint is already healthy", async () => {
|
|
22
|
+
const fetchImpl = vi.fn(async () =>
|
|
23
|
+
new Response(JSON.stringify({ webSocketDebuggerUrl: "ws://127.0.0.1:15166/devtools/browser/1" }), { status: 200 }),
|
|
24
|
+
) as unknown as typeof fetch;
|
|
25
|
+
const spawnImpl = vi.fn() as unknown as typeof spawn;
|
|
26
|
+
|
|
27
|
+
await expect(
|
|
28
|
+
ensureChromeCdpRunning(
|
|
29
|
+
{ enabled: true, port: 15166, chromePath: "C:/Chrome/chrome.exe" },
|
|
30
|
+
{ fetchImpl, spawnImpl },
|
|
31
|
+
),
|
|
32
|
+
).resolves.toEqual({ ok: true, started: false, port: 15166 });
|
|
33
|
+
expect(spawnImpl).not.toHaveBeenCalled();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("spawns Chrome with a dedicated profile when the endpoint is unhealthy", async () => {
|
|
37
|
+
const fetchImpl = vi.fn()
|
|
38
|
+
.mockRejectedValueOnce(new Error("ECONNREFUSED"))
|
|
39
|
+
.mockResolvedValue(new Response(JSON.stringify({ Browser: "Chrome/144.0.0.0" }), { status: 200 })) as unknown as typeof fetch;
|
|
40
|
+
const unref = vi.fn();
|
|
41
|
+
const spawnImpl = vi.fn(() => ({ unref })) as unknown as typeof spawn;
|
|
42
|
+
const mkdirSyncImpl = vi.fn();
|
|
43
|
+
|
|
44
|
+
await expect(
|
|
45
|
+
ensureChromeCdpRunning(
|
|
46
|
+
{ enabled: true, port: 15166, chromePath: "C:/Chrome/chrome.exe" },
|
|
47
|
+
{
|
|
48
|
+
fetchImpl,
|
|
49
|
+
spawnImpl,
|
|
50
|
+
mkdirSyncImpl: mkdirSyncImpl as never,
|
|
51
|
+
existsSyncImpl: vi.fn(() => true),
|
|
52
|
+
env: { LOCALAPPDATA: "C:/Users/me/AppData/Local" },
|
|
53
|
+
},
|
|
54
|
+
),
|
|
55
|
+
).resolves.toEqual({ ok: true, started: true, port: 15166 });
|
|
56
|
+
|
|
57
|
+
expect(mkdirSyncImpl).toHaveBeenCalledWith("C:\\Users\\me\\AppData\\Local\\chatccc\\chrome-cdp-15166", { recursive: true });
|
|
58
|
+
expect(spawnImpl).toHaveBeenCalledWith(
|
|
59
|
+
"C:/Chrome/chrome.exe",
|
|
60
|
+
expect.arrayContaining([
|
|
61
|
+
"--remote-debugging-address=127.0.0.1",
|
|
62
|
+
"--remote-debugging-port=15166",
|
|
63
|
+
"--user-data-dir=C:\\Users\\me\\AppData\\Local\\chatccc\\chrome-cdp-15166",
|
|
64
|
+
"--new-window",
|
|
65
|
+
"about:blank",
|
|
66
|
+
]),
|
|
67
|
+
expect.objectContaining({ detached: true, stdio: "ignore", windowsHide: true }),
|
|
68
|
+
);
|
|
69
|
+
expect(unref).toHaveBeenCalled();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("does not spawn Chrome when the port is occupied by a non-CDP service", async () => {
|
|
73
|
+
const fetchImpl = vi.fn(async () => new Response("not found", { status: 404 })) as unknown as typeof fetch;
|
|
74
|
+
const spawnImpl = vi.fn() as unknown as typeof spawn;
|
|
75
|
+
|
|
76
|
+
await expect(probeChromeCdp(15166, { fetchImpl })).resolves.toBe("occupied");
|
|
77
|
+
await expect(
|
|
78
|
+
ensureChromeCdpRunning(
|
|
79
|
+
{ enabled: true, port: 15166, chromePath: "C:/Chrome/chrome.exe" },
|
|
80
|
+
{ fetchImpl, spawnImpl },
|
|
81
|
+
),
|
|
82
|
+
).resolves.toMatchObject({
|
|
83
|
+
ok: false,
|
|
84
|
+
started: false,
|
|
85
|
+
port: 15166,
|
|
86
|
+
error: expect.stringContaining("not a healthy Chrome CDP endpoint"),
|
|
87
|
+
});
|
|
88
|
+
expect(spawnImpl).not.toHaveBeenCalled();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("reports a clear error when Chrome cannot be found", async () => {
|
|
92
|
+
const fetchImpl = vi.fn(async () => { throw new Error("ECONNREFUSED"); }) as unknown as typeof fetch;
|
|
93
|
+
|
|
94
|
+
await expect(
|
|
95
|
+
ensureChromeCdpRunning(
|
|
96
|
+
{ enabled: true, port: 15166, chromePath: "" },
|
|
97
|
+
{
|
|
98
|
+
fetchImpl,
|
|
99
|
+
existsSyncImpl: vi.fn(() => false),
|
|
100
|
+
platform: "win32",
|
|
101
|
+
env: {},
|
|
102
|
+
},
|
|
103
|
+
),
|
|
104
|
+
).resolves.toMatchObject({
|
|
105
|
+
ok: false,
|
|
106
|
+
started: false,
|
|
107
|
+
port: 15166,
|
|
108
|
+
error: expect.stringContaining("Cannot find chrome executable"),
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("resolves explicit and default Chrome paths", () => {
|
|
113
|
+
expect(resolveChromeExecutable("C:/Chrome/chrome.exe", { existsSyncImpl: vi.fn(() => true) })).toBe("C:/Chrome/chrome.exe");
|
|
114
|
+
expect(resolveChromeExecutable("", {
|
|
115
|
+
existsSyncImpl: vi.fn((p: unknown) => String(p).endsWith("Google\\Chrome\\Application\\chrome.exe")),
|
|
116
|
+
platform: "win32",
|
|
117
|
+
env: { ProgramFiles: "C:/Program Files" },
|
|
118
|
+
})).toBe("C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe");
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("uses LocalAppData for the Chrome CDP user data directory", () => {
|
|
122
|
+
expect(resolveChromeUserDataDir(15166, { LOCALAPPDATA: "C:/Users/me/AppData/Local" }))
|
|
123
|
+
.toBe("C:\\Users\\me\\AppData\\Local\\chatccc\\chrome-cdp-15166");
|
|
124
|
+
});
|
|
125
|
+
});
|
|
@@ -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
|
+
});
|
|
@@ -40,6 +40,7 @@ import {
|
|
|
40
40
|
const baseAppConfig: AppConfig = {
|
|
41
41
|
feishu: { appId: "INITIAL_APP", appSecret: "INITIAL_SECRET" },
|
|
42
42
|
platforms: { feishu: { enabled: true }, ilink: { enabled: true } },
|
|
43
|
+
chromeDevtools: { enabled: false, port: 15166, chromePath: "" },
|
|
43
44
|
port: 18080,
|
|
44
45
|
gitTimeoutSeconds: 180,
|
|
45
46
|
allowInterrupt: false,
|
|
@@ -195,6 +196,18 @@ describe("applyLoadedConfig — config 对象引用契约", () => {
|
|
|
195
196
|
expect(config.codex.path).toBe("/refresh/codex");
|
|
196
197
|
});
|
|
197
198
|
|
|
199
|
+
it("刷新 chromeDevtools 配置但保持 config 引用", () => {
|
|
200
|
+
const refBefore = config;
|
|
201
|
+
|
|
202
|
+
applyLoadedConfig({
|
|
203
|
+
...structuredClone(baseAppConfig),
|
|
204
|
+
chromeDevtools: { enabled: true, port: 15166, chromePath: "C:/Chrome/chrome.exe" },
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
expect(config).toBe(refBefore);
|
|
208
|
+
expect(config.chromeDevtools).toEqual({ enabled: true, port: 15166, chromePath: "C:/Chrome/chrome.exe" });
|
|
209
|
+
});
|
|
210
|
+
|
|
198
211
|
it("空 feishu 凭证也能正确刷入(向导回滚到空的反向场景)", () => {
|
|
199
212
|
applyLoadedConfig({
|
|
200
213
|
...structuredClone(baseAppConfig),
|
|
@@ -27,4 +27,15 @@ describe("config.sample.json", () => {
|
|
|
27
27
|
expect(sample.claude?.model).toBe("");
|
|
28
28
|
expect(sample.claude?.subagentModel).toBe("");
|
|
29
29
|
});
|
|
30
|
+
|
|
31
|
+
it("keeps Chrome CDP guard disabled by default with port 15166", () => {
|
|
32
|
+
const configSamplePath = join(process.cwd(), "config.sample.json");
|
|
33
|
+
const sample = JSON.parse(readFileSync(configSamplePath, "utf8")) as {
|
|
34
|
+
chromeDevtools?: { enabled?: unknown; port?: unknown; chromePath?: unknown };
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
expect(sample.chromeDevtools?.enabled).toBe(false);
|
|
38
|
+
expect(sample.chromeDevtools?.port).toBe(15166);
|
|
39
|
+
expect(sample.chromeDevtools?.chromePath).toBe("");
|
|
40
|
+
});
|
|
30
41
|
});
|
|
@@ -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
|
+
});
|