chatccc 0.2.184 → 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 -3
- package/package.json +1 -1
- package/src/__tests__/agent-delegate-task-rpc.test.ts +165 -165
- 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__/config-reload.test.ts +13 -0
- package/src/__tests__/config-sample.test.ts +11 -0
- 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 -153
- package/src/agent-delegate-task.ts +81 -81
- 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/config.ts +21 -0
- package/src/index.ts +12 -4
- package/src/orchestrator.ts +41 -4
- package/src/session-name.ts +8 -8
- package/src/session.ts +6 -6
- package/src/web-ui.ts +136 -6
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { config } from "../config.ts";
|
|
4
|
+
import { getChatGptSubscriptionStatus } from "../chatgpt-subscription.ts";
|
|
5
|
+
|
|
6
|
+
describe("ChatGPT subscription lookup", () => {
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
config.chromeDevtools = { enabled: true, port: 15166, chromePath: "" };
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it("returns disabled without probing the port when Chrome CDP is off", async () => {
|
|
12
|
+
config.chromeDevtools = { enabled: false, port: 15166, chromePath: "" };
|
|
13
|
+
const probeChromeCdpImpl = vi.fn();
|
|
14
|
+
const fetchImpl = vi.fn() as unknown as typeof fetch;
|
|
15
|
+
|
|
16
|
+
await expect(getChatGptSubscriptionStatus({ probeChromeCdpImpl, fetchImpl })).resolves.toMatchObject({
|
|
17
|
+
ok: false,
|
|
18
|
+
code: "chrome_cdp_disabled",
|
|
19
|
+
chromeCdp: { enabled: false, port: 15166, status: "skipped" },
|
|
20
|
+
});
|
|
21
|
+
expect(probeChromeCdpImpl).not.toHaveBeenCalled();
|
|
22
|
+
expect(fetchImpl).not.toHaveBeenCalled();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("returns occupied when the configured port is not a healthy Chrome CDP endpoint", async () => {
|
|
26
|
+
await expect(getChatGptSubscriptionStatus({
|
|
27
|
+
probeChromeCdpImpl: vi.fn(async () => "occupied" as const),
|
|
28
|
+
})).resolves.toMatchObject({
|
|
29
|
+
ok: false,
|
|
30
|
+
code: "chrome_cdp_occupied",
|
|
31
|
+
reason: expect.stringContaining("not a healthy Chrome CDP endpoint"),
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("returns page missing when CDP has no ChatGPT page and cannot create one", async () => {
|
|
36
|
+
const fetchImpl = vi.fn(async (url: string | URL | Request) => {
|
|
37
|
+
const text = String(url);
|
|
38
|
+
if (text.endsWith("/json/list")) return new Response(JSON.stringify([]), { status: 200 });
|
|
39
|
+
if (text.includes("/json/new?")) return new Response("nope", { status: 500 });
|
|
40
|
+
throw new Error(`unexpected fetch: ${text}`);
|
|
41
|
+
}) as unknown as typeof fetch;
|
|
42
|
+
|
|
43
|
+
await expect(getChatGptSubscriptionStatus({
|
|
44
|
+
fetchImpl,
|
|
45
|
+
probeChromeCdpImpl: vi.fn(async () => "healthy" as const),
|
|
46
|
+
})).resolves.toMatchObject({
|
|
47
|
+
ok: false,
|
|
48
|
+
code: "chatgpt_page_missing",
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("closes only the temporary ChatGPT page created for this lookup", async () => {
|
|
53
|
+
const closed: string[] = [];
|
|
54
|
+
const fetchImpl = vi.fn(async (url: string | URL | Request) => {
|
|
55
|
+
const text = String(url);
|
|
56
|
+
if (text.endsWith("/json/list")) return new Response(JSON.stringify([]), { status: 200 });
|
|
57
|
+
if (text.includes("/json/new?")) {
|
|
58
|
+
return new Response(JSON.stringify({
|
|
59
|
+
id: "temp-page",
|
|
60
|
+
type: "page",
|
|
61
|
+
url: "https://chatgpt.com/",
|
|
62
|
+
webSocketDebuggerUrl: "ws://cdp/temp-page",
|
|
63
|
+
}), { status: 200 });
|
|
64
|
+
}
|
|
65
|
+
if (text.endsWith("/json/close/temp-page")) {
|
|
66
|
+
closed.push("temp-page");
|
|
67
|
+
return new Response("Target is closing", { status: 200 });
|
|
68
|
+
}
|
|
69
|
+
throw new Error(`unexpected fetch: ${text}`);
|
|
70
|
+
}) as unknown as typeof fetch;
|
|
71
|
+
|
|
72
|
+
await expect(getChatGptSubscriptionStatus({
|
|
73
|
+
fetchImpl,
|
|
74
|
+
probeChromeCdpImpl: vi.fn(async () => "healthy" as const),
|
|
75
|
+
evaluateInPage: vi.fn(async () => ({
|
|
76
|
+
sessionOk: true,
|
|
77
|
+
hasAccessToken: true,
|
|
78
|
+
maskedEmail: "gg***@gmail.com",
|
|
79
|
+
sessionExpires: "2026-09-20T09:30:07.340Z",
|
|
80
|
+
account: {
|
|
81
|
+
status: 200,
|
|
82
|
+
ok: true,
|
|
83
|
+
entitlement: {
|
|
84
|
+
has_active_subscription: true,
|
|
85
|
+
subscription_plan: "chatgptprolite",
|
|
86
|
+
expires_at: "2026-07-12T10:20:11+00:00",
|
|
87
|
+
},
|
|
88
|
+
last_active_subscription: {
|
|
89
|
+
will_renew: false,
|
|
90
|
+
purchase_origin_platform: "chatgpt_web",
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
})),
|
|
94
|
+
now: () => new Date("2026-06-22T00:00:00+08:00"),
|
|
95
|
+
})).resolves.toMatchObject({
|
|
96
|
+
ok: true,
|
|
97
|
+
code: "ok",
|
|
98
|
+
subscription: {
|
|
99
|
+
plan: "chatgptprolite",
|
|
100
|
+
expiresAt: "2026-07-12T10:20:11+00:00",
|
|
101
|
+
willRenew: false,
|
|
102
|
+
remainingDays: 21,
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
expect(closed).toEqual(["temp-page"]);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("does not close an existing ChatGPT page", async () => {
|
|
109
|
+
const fetchImpl = vi.fn(async (url: string | URL | Request) => {
|
|
110
|
+
const text = String(url);
|
|
111
|
+
if (text.endsWith("/json/list")) {
|
|
112
|
+
return new Response(JSON.stringify([{
|
|
113
|
+
id: "existing-page",
|
|
114
|
+
type: "page",
|
|
115
|
+
url: "https://chatgpt.com/",
|
|
116
|
+
webSocketDebuggerUrl: "ws://cdp/existing-page",
|
|
117
|
+
}]), { status: 200 });
|
|
118
|
+
}
|
|
119
|
+
if (text.includes("/json/close/")) throw new Error("should not close existing page");
|
|
120
|
+
throw new Error(`unexpected fetch: ${text}`);
|
|
121
|
+
}) as unknown as typeof fetch;
|
|
122
|
+
|
|
123
|
+
await expect(getChatGptSubscriptionStatus({
|
|
124
|
+
fetchImpl,
|
|
125
|
+
probeChromeCdpImpl: vi.fn(async () => "healthy" as const),
|
|
126
|
+
evaluateInPage: vi.fn(async () => ({
|
|
127
|
+
sessionOk: true,
|
|
128
|
+
hasAccessToken: false,
|
|
129
|
+
})),
|
|
130
|
+
})).resolves.toMatchObject({
|
|
131
|
+
ok: false,
|
|
132
|
+
code: "chatgpt_session_missing",
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
});
|
|
@@ -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
|
+
});
|
|
@@ -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
|
});
|
|
@@ -9,6 +9,7 @@ import type { SessionInfo, ToolAdapter } from "../adapters/adapter-interface.ts"
|
|
|
9
9
|
const mockStreamStates = new Map<string, { status: "running" | "done" | "stopped"; finalReply: string }>();
|
|
10
10
|
const mockGetCodexUsageSummary = vi.hoisted(() => vi.fn());
|
|
11
11
|
const mockGetCursorUsageSummary = vi.hoisted(() => vi.fn());
|
|
12
|
+
const mockGetChatGptSubscriptionStatus = vi.hoisted(() => vi.fn());
|
|
12
13
|
|
|
13
14
|
vi.mock("../im-skills.ts", () => ({
|
|
14
15
|
buildImSkillsPrompt: async () => "",
|
|
@@ -64,9 +65,13 @@ vi.mock("../cursor-usage.ts", () => ({
|
|
|
64
65
|
getCursorUsageSummary: mockGetCursorUsageSummary,
|
|
65
66
|
}));
|
|
66
67
|
|
|
68
|
+
vi.mock("../chatgpt-subscription.ts", () => ({
|
|
69
|
+
getChatGptSubscriptionStatus: mockGetChatGptSubscriptionStatus,
|
|
70
|
+
}));
|
|
71
|
+
|
|
67
72
|
import { handleCommand } from "../orchestrator.ts";
|
|
68
|
-
import {
|
|
69
|
-
_clearAdapterCacheForTest,
|
|
73
|
+
import {
|
|
74
|
+
_clearAdapterCacheForTest,
|
|
70
75
|
_resetSessionRegistryFileForTest,
|
|
71
76
|
_resetSessionToolsFileForTest,
|
|
72
77
|
_setAdapterForToolForTest,
|
|
@@ -74,10 +79,10 @@ import {
|
|
|
74
79
|
_setSessionToolsFileForTest,
|
|
75
80
|
loadSessionRegistryForBinding,
|
|
76
81
|
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";
|
|
82
|
+
resetState,
|
|
83
|
+
} from "../session.ts";
|
|
84
|
+
import { activePrompts, resetBindingState } from "../session-chat-binding.ts";
|
|
85
|
+
import { ABD_APPEND_PROMPT } from "../shared-prefix.ts";
|
|
81
86
|
|
|
82
87
|
function mockPlatform(kind: "wechat" | "feishu" = "wechat"): PlatformAdapter {
|
|
83
88
|
return {
|
|
@@ -129,12 +134,13 @@ describe("handleCommand WeChat processing ack", () => {
|
|
|
129
134
|
mockStreamStates.clear();
|
|
130
135
|
mockGetCodexUsageSummary.mockReset();
|
|
131
136
|
mockGetCursorUsageSummary.mockReset();
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
137
|
+
mockGetChatGptSubscriptionStatus.mockReset();
|
|
138
|
+
mockGetCodexUsageSummary.mockResolvedValue({
|
|
139
|
+
fiveHour: { usedPercent: 0, remainingPercent: 100, resetAtEpochSeconds: null, resetAfterSeconds: null },
|
|
140
|
+
weekly: { usedPercent: 0, remainingPercent: 100, resetAtEpochSeconds: null, resetAfterSeconds: null },
|
|
141
|
+
rateLimitResetCreditsAvailable: null,
|
|
142
|
+
rateLimitResetCredits: null,
|
|
143
|
+
});
|
|
138
144
|
mockGetCursorUsageSummary.mockResolvedValue({
|
|
139
145
|
billingCycleStart: "1779357999000",
|
|
140
146
|
billingCycleEnd: "1782036399000",
|
|
@@ -160,6 +166,12 @@ describe("handleCommand WeChat processing ack", () => {
|
|
|
160
166
|
displayMessage: "You've hit your usage limit",
|
|
161
167
|
autoBucketModels: ["default"],
|
|
162
168
|
});
|
|
169
|
+
mockGetChatGptSubscriptionStatus.mockResolvedValue({
|
|
170
|
+
ok: false,
|
|
171
|
+
code: "chrome_cdp_disabled",
|
|
172
|
+
reason: "Chrome CDP guard is disabled in ChatCCC config.",
|
|
173
|
+
chromeCdp: { enabled: false, port: 15166, status: "skipped" },
|
|
174
|
+
});
|
|
163
175
|
_setAdapterForToolForTest("claude", mockAdapter());
|
|
164
176
|
});
|
|
165
177
|
|
|
@@ -200,8 +212,8 @@ describe("handleCommand WeChat processing ack", () => {
|
|
|
200
212
|
expect(platform.sendCard).not.toHaveBeenCalled();
|
|
201
213
|
});
|
|
202
214
|
|
|
203
|
-
it("sends the WeChat processing ack after the busy check for normal prompts", async () => {
|
|
204
|
-
const platform = mockPlatform();
|
|
215
|
+
it("sends the WeChat processing ack after the busy check for normal prompts", async () => {
|
|
216
|
+
const platform = mockPlatform();
|
|
205
217
|
await recordSessionRegistry({
|
|
206
218
|
chatId: "wx-chat",
|
|
207
219
|
sessionId: "sid-wechat",
|
|
@@ -211,44 +223,44 @@ describe("handleCommand WeChat processing ack", () => {
|
|
|
211
223
|
});
|
|
212
224
|
|
|
213
225
|
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
|
-
});
|
|
226
|
+
|
|
227
|
+
expect(platform.sendText).toHaveBeenCalledWith("wx-chat", "生成中...");
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it("treats /abd as a shared prompt prefix in an existing session", async () => {
|
|
231
|
+
const platform = mockPlatform();
|
|
232
|
+
const prompt = vi.fn(async function* (_sessionId: string, userText: string) {
|
|
233
|
+
yield {
|
|
234
|
+
type: "assistant" as const,
|
|
235
|
+
blocks: [{ type: "text" as const, text: "done" }],
|
|
236
|
+
};
|
|
237
|
+
});
|
|
238
|
+
_setAdapterForToolForTest("claude", {
|
|
239
|
+
displayName: "Claude",
|
|
240
|
+
sessionDescPrefix: "Claude Session:",
|
|
241
|
+
createSession: vi.fn(async () => ({ sessionId: "sid-wechat" })),
|
|
242
|
+
prompt,
|
|
243
|
+
getSessionInfo: async (sessionId: string): Promise<SessionInfo> => ({
|
|
244
|
+
sessionId,
|
|
245
|
+
cwd: "F:\\repo",
|
|
246
|
+
}),
|
|
247
|
+
closeSession: async () => {},
|
|
248
|
+
});
|
|
249
|
+
await recordSessionRegistry({
|
|
250
|
+
chatId: "wx-chat",
|
|
251
|
+
sessionId: "sid-wechat",
|
|
252
|
+
tool: "claude",
|
|
253
|
+
chatName: "ready-session",
|
|
254
|
+
running: false,
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
await handleCommand(platform, "/abd帮我分析", "wx-chat", "wx-user", Date.now(), "p2p");
|
|
258
|
+
|
|
259
|
+
expect(platform.sendText).toHaveBeenCalledWith("wx-chat", "生成中...");
|
|
260
|
+
const userText = prompt.mock.calls[0][1];
|
|
261
|
+
expect(userText).toContain(`[User message]\n帮我分析\n\n---\n${ABD_APPEND_PROMPT}\n[/User message]`);
|
|
262
|
+
expect(userText).not.toContain("/abd");
|
|
263
|
+
});
|
|
252
264
|
|
|
253
265
|
it("does not send the stopped success text until the running prompt really exits", async () => {
|
|
254
266
|
const platform = mockPlatform();
|
|
@@ -270,7 +282,7 @@ describe("handleCommand WeChat processing ack", () => {
|
|
|
270
282
|
expect(platform.sendText).not.toHaveBeenCalledWith("wx-chat", "会话已停止。");
|
|
271
283
|
});
|
|
272
284
|
|
|
273
|
-
it("cleans stale Feishu p2p binding, creates a group, and sends the private message as first prompt", async () => {
|
|
285
|
+
it("cleans stale Feishu p2p binding, creates a group, and sends the private message as first prompt", async () => {
|
|
274
286
|
const platform = mockPlatform("feishu");
|
|
275
287
|
const prompt = vi.fn(async function* (_sessionId: string, userText: string) {
|
|
276
288
|
yield {
|
|
@@ -315,40 +327,40 @@ describe("handleCommand WeChat processing ack", () => {
|
|
|
315
327
|
|
|
316
328
|
const registry = await loadSessionRegistryForBinding();
|
|
317
329
|
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
|
-
});
|
|
330
|
+
expect(registry["feishu-group"]?.sessionId).toBe("sid-feishu-new");
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
it("auto-creates a Feishu group for /abd private messages and sends the transformed prompt", async () => {
|
|
334
|
+
const platform = mockPlatform("feishu");
|
|
335
|
+
const prompt = vi.fn(async function* (_sessionId: string, userText: string) {
|
|
336
|
+
yield {
|
|
337
|
+
type: "assistant" as const,
|
|
338
|
+
blocks: [{ type: "text" as const, text: "done" }],
|
|
339
|
+
};
|
|
340
|
+
});
|
|
341
|
+
_setAdapterForToolForTest("claude", {
|
|
342
|
+
displayName: "Claude",
|
|
343
|
+
sessionDescPrefix: "Claude Session:",
|
|
344
|
+
createSession: vi.fn(async () => ({ sessionId: "sid-feishu-abd" })),
|
|
345
|
+
prompt,
|
|
346
|
+
getSessionInfo: async (sessionId: string): Promise<SessionInfo> => ({
|
|
347
|
+
sessionId,
|
|
348
|
+
cwd: "F:\\repo",
|
|
349
|
+
}),
|
|
350
|
+
closeSession: async () => {},
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
await handleCommand(platform, "/abd帮我看一下日志", "feishu-p2p", "ou-user", Date.now(), "p2p");
|
|
354
|
+
|
|
355
|
+
expect(platform.createGroup).toHaveBeenCalledWith(expect.stringContaining("帮我看一下日志"), ["ou-user"]);
|
|
356
|
+
expect(platform.createGroup).not.toHaveBeenCalledWith(expect.stringContaining("---"), expect.anything());
|
|
357
|
+
const updateCall = vi.mocked(platform.updateChatInfo).mock.calls[0];
|
|
358
|
+
expect(updateCall[1]).not.toContain("---");
|
|
359
|
+
expect(updateCall[1]).not.toContain(ABD_APPEND_PROMPT);
|
|
360
|
+
const userText = prompt.mock.calls[0][1];
|
|
361
|
+
expect(userText).toContain(`[User message]\n帮我看一下日志\n\n---\n${ABD_APPEND_PROMPT}\n[/User message]`);
|
|
362
|
+
expect(userText).not.toContain("/abd");
|
|
363
|
+
});
|
|
352
364
|
|
|
353
365
|
it("cleans stale Feishu p2p binding but keeps valid commands from auto-creating a group", async () => {
|
|
354
366
|
const platform = mockPlatform("feishu");
|
|
@@ -370,35 +382,68 @@ describe("handleCommand WeChat processing ack", () => {
|
|
|
370
382
|
|
|
371
383
|
it("handles /usage without creating a new Feishu group", async () => {
|
|
372
384
|
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("
|
|
394
|
-
expect(card.elements[0].text.content).toContain("
|
|
395
|
-
expect(card.elements[0].text.content).toContain("
|
|
396
|
-
expect(card.elements[0].text.content).toContain("
|
|
397
|
-
expect(card.elements[0].text.content).toContain("
|
|
398
|
-
expect(card.elements[0].text.content).toContain("
|
|
399
|
-
expect(card.elements[
|
|
400
|
-
expect(card.elements[2].actions[0].
|
|
401
|
-
|
|
385
|
+
mockGetCodexUsageSummary.mockResolvedValue({
|
|
386
|
+
fiveHour: { usedPercent: 37, remainingPercent: 63, resetAtEpochSeconds: 1781528212, resetAfterSeconds: 10349 },
|
|
387
|
+
weekly: { usedPercent: 12, remainingPercent: 88, resetAtEpochSeconds: 1781842926, resetAfterSeconds: 325063 },
|
|
388
|
+
rateLimitResetCreditsAvailable: 2,
|
|
389
|
+
rateLimitResetCredits: [
|
|
390
|
+
{ grantedAt: "2026-06-12T04:01:47.770016Z", expiresAt: "2026-07-12T04:01:47.770016Z" },
|
|
391
|
+
{ grantedAt: "2026-06-18T00:44:23.904386Z", expiresAt: "2026-07-18T00:44:23.904386Z" },
|
|
392
|
+
],
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
await handleCommand(platform, "/usage", "feishu-p2p", "ou-user", Date.now(), "p2p");
|
|
396
|
+
|
|
397
|
+
expect(platform.createGroup).not.toHaveBeenCalled();
|
|
398
|
+
expect(platform.sendCard).not.toHaveBeenCalled();
|
|
399
|
+
expect(platform.sendRawCard).toHaveBeenCalledTimes(1);
|
|
400
|
+
const card = JSON.parse(vi.mocked(platform.sendRawCard).mock.calls[0][1]);
|
|
401
|
+
expect(card.header.title.content).toBe("Codex Usage");
|
|
402
|
+
expect(card.elements[0].text.content).toContain("2026-07-12");
|
|
403
|
+
expect(card.elements[0].text.content).toContain("2026-07-18");
|
|
404
|
+
expect(card.elements[0].text.content).toContain("**主动重置:** 剩余 2 次");
|
|
405
|
+
expect(card.elements[0].text.content).not.toContain("ChatGPT 订阅");
|
|
406
|
+
expect(card.elements[0].text.content).toContain("**5h:** 已用 37%,剩余 63%,重置:");
|
|
407
|
+
expect(card.elements[0].text.content).toContain("约 2小时52分钟后");
|
|
408
|
+
expect(card.elements[0].text.content).toContain("[███████░░░░░░░░░░░░░]");
|
|
409
|
+
expect(card.elements[0].text.content).toContain("**周:** 已用 12%,剩余 88%,重置:");
|
|
410
|
+
expect(card.elements[0].text.content).toContain("约 3天18小时17分钟后");
|
|
411
|
+
expect(card.elements[0].text.content).toContain("[██░░░░░░░░░░░░░░░░░░]");
|
|
412
|
+
expect(card.elements[2].actions[0].text.content).toBe("发起重置");
|
|
413
|
+
expect(card.elements[2].actions[0].value).toEqual({ action: "codex_reset_request", availableCount: 2 });
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
it("adds ChatGPT subscription expiry to Codex /usage when CDP lookup succeeds", async () => {
|
|
417
|
+
const platform = mockPlatform("feishu");
|
|
418
|
+
mockGetCodexUsageSummary.mockResolvedValue({
|
|
419
|
+
fiveHour: { usedPercent: 37, remainingPercent: 63, resetAtEpochSeconds: 1781528212, resetAfterSeconds: 10349 },
|
|
420
|
+
weekly: { usedPercent: 12, remainingPercent: 88, resetAtEpochSeconds: 1781842926, resetAfterSeconds: 325063 },
|
|
421
|
+
rateLimitResetCreditsAvailable: 0,
|
|
422
|
+
rateLimitResetCredits: [],
|
|
423
|
+
});
|
|
424
|
+
mockGetChatGptSubscriptionStatus.mockResolvedValue({
|
|
425
|
+
ok: true,
|
|
426
|
+
code: "ok",
|
|
427
|
+
chromeCdp: { enabled: true, port: 15166, status: "healthy" },
|
|
428
|
+
chatgpt: { sessionOk: true, maskedEmail: "gg***@gmail.com", sessionExpiresAt: "2026-09-20T09:30:07.340Z" },
|
|
429
|
+
subscription: {
|
|
430
|
+
active: true,
|
|
431
|
+
plan: "chatgptprolite",
|
|
432
|
+
expiresAt: "2026-07-12T10:20:11+00:00",
|
|
433
|
+
willRenew: false,
|
|
434
|
+
purchaseOriginPlatform: "chatgpt_web",
|
|
435
|
+
remainingDays: 20,
|
|
436
|
+
},
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
await handleCommand(platform, "/usage", "feishu-p2p", "ou-user", Date.now(), "p2p");
|
|
440
|
+
|
|
441
|
+
const card = JSON.parse(vi.mocked(platform.sendRawCard).mock.calls[0][1]);
|
|
442
|
+
expect(card.elements[0].text.content).toContain("**ChatGPT 订阅:**");
|
|
443
|
+
expect(card.elements[0].text.content).toContain("- 套餐: chatgptprolite");
|
|
444
|
+
expect(card.elements[0].text.content).toContain("剩余 20 天");
|
|
445
|
+
expect(card.elements[0].text.content).toContain("- 自动续费: 否");
|
|
446
|
+
});
|
|
402
447
|
|
|
403
448
|
it("handles /usage as Cursor usage in Cursor chats", async () => {
|
|
404
449
|
const platform = mockPlatform("feishu");
|
|
@@ -438,7 +483,7 @@ describe("handleCommand WeChat processing ack", () => {
|
|
|
438
483
|
expect(codexPlatform.sendCard).toHaveBeenCalledWith(
|
|
439
484
|
"feishu-group",
|
|
440
485
|
"Codex Session Ready",
|
|
441
|
-
expect.stringContaining("发送 **/usage** 查看 Codex 5h/周用量,以及查询/使用主动重置卡。"),
|
|
486
|
+
expect.stringContaining("发送 **/usage** 查看 Codex 5h/周用量,以及查询/使用主动重置卡。"),
|
|
442
487
|
"green",
|
|
443
488
|
);
|
|
444
489
|
|