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.
@@ -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
- 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
- });
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("**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
- });
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
 
@@ -33,6 +33,22 @@ describe("unflattenConfig", () => {
33
33
  },
34
34
  });
35
35
  });
36
+
37
+ it("maps Chrome CDP guard fields into chromeDevtools config", () => {
38
+ expect(
39
+ unflattenConfig({
40
+ CHATCCC_CHROME_DEVTOOLS_ENABLED: true,
41
+ CHATCCC_CHROME_DEVTOOLS_PORT: "15166",
42
+ CHATCCC_CHROME_DEVTOOLS_PATH: "C:/Program Files/Google/Chrome/Application/chrome.exe",
43
+ }),
44
+ ).toEqual({
45
+ chromeDevtools: {
46
+ enabled: true,
47
+ port: 15166,
48
+ chromePath: "C:/Program Files/Google/Chrome/Application/chrome.exe",
49
+ },
50
+ });
51
+ });
36
52
  });
37
53
 
38
54
  describe("dashboard edit modal", () => {
@@ -0,0 +1,153 @@
1
+ import type { IncomingMessage, ServerResponse } from "node:http";
2
+ import { resolve } from "node:path";
3
+
4
+ import { resolveDefaultAgentTool } from "./config.ts";
5
+ import { readUtf8JsonBody } from "./agent-rpc-body.ts";
6
+ import { delegateAgentTask } from "./agent-delegate-task.ts";
7
+ import type { PlatformAdapter } from "./platform-adapter.ts";
8
+ import { applySharedPrefix } from "./shared-prefix.ts";
9
+
10
+ export const AGENT_DELEGATE_TASK_PATH = "/api/agent/delegate-task";
11
+
12
+ const MAX_REQUEST_BYTES = 128 * 1024;
13
+ const VALID_TOOLS = new Set(["claude", "cursor", "codex"]);
14
+
15
+ interface AgentDelegateTaskPayload {
16
+ tool?: unknown;
17
+ cwd?: unknown;
18
+ prompt?: unknown;
19
+ text?: unknown;
20
+ message?: unknown;
21
+ open_id?: unknown;
22
+ open_ids?: unknown;
23
+ openIds?: unknown;
24
+ chat_name?: unknown;
25
+ }
26
+
27
+ function jsonReply(res: ServerResponse, status: number, data: unknown): void {
28
+ res.writeHead(status, { "Content-Type": "application/json; charset=utf-8" });
29
+ res.end(JSON.stringify(data));
30
+ }
31
+
32
+ function stringValue(value: unknown): string {
33
+ return typeof value === "string" ? value.trim() : "";
34
+ }
35
+
36
+ function normalizeOpenIds(payload: AgentDelegateTaskPayload): string[] {
37
+ const explicitOpenId = stringValue(payload.open_id);
38
+ if (explicitOpenId) return [explicitOpenId];
39
+
40
+ const rawOpenIds = Array.isArray(payload.open_ids) ? payload.open_ids : payload.openIds;
41
+ if (!Array.isArray(rawOpenIds)) return [];
42
+ return rawOpenIds
43
+ .filter((item): item is string => typeof item === "string")
44
+ .map((item) => item.trim())
45
+ .filter(Boolean);
46
+ }
47
+
48
+ function promptFromPayload(payload: AgentDelegateTaskPayload): string {
49
+ return stringValue(payload.prompt) || stringValue(payload.text) || stringValue(payload.message);
50
+ }
51
+
52
+ function validateTool(rawTool: unknown): string {
53
+ const tool = stringValue(rawTool).toLowerCase() || resolveDefaultAgentTool();
54
+ if (!VALID_TOOLS.has(tool)) throw new Error(`unsupported tool: ${tool}`);
55
+ return tool;
56
+ }
57
+
58
+ function validateCwd(rawCwd: unknown): string {
59
+ const cwd = stringValue(rawCwd);
60
+ if (!cwd) throw new Error("cwd must be a non-empty string");
61
+ return resolve(cwd);
62
+ }
63
+
64
+ export async function handleAgentDelegateTaskRequest(
65
+ req: IncomingMessage,
66
+ res: ServerResponse,
67
+ platform: PlatformAdapter,
68
+ ): Promise<boolean> {
69
+ const url = new URL(req.url ?? "/", "http://127.0.0.1");
70
+ if (url.pathname !== AGENT_DELEGATE_TASK_PATH) return false;
71
+
72
+ if (req.method !== "POST") {
73
+ jsonReply(res, 405, { ok: false, error: "Method not allowed" });
74
+ return true;
75
+ }
76
+
77
+ if (platform.kind !== "feishu") {
78
+ jsonReply(res, 409, { ok: false, error: "This endpoint currently only supports Feishu." });
79
+ return true;
80
+ }
81
+
82
+ let payload: AgentDelegateTaskPayload;
83
+ try {
84
+ payload = await readUtf8JsonBody(req, MAX_REQUEST_BYTES);
85
+ } catch (err) {
86
+ jsonReply(res, 400, { ok: false, error: (err as Error).message || "Invalid JSON" });
87
+ return true;
88
+ }
89
+
90
+ let tool: string;
91
+ let cwd: string;
92
+ let promptText: string;
93
+ let promptNamePrefix: string;
94
+ let openIds: string[];
95
+ try {
96
+ tool = validateTool(payload.tool);
97
+ cwd = validateCwd(payload.cwd);
98
+ const rawPrompt = promptFromPayload(payload);
99
+ if (!rawPrompt) throw new Error("prompt must be a non-empty string");
100
+ const sharedPrefix = applySharedPrefix(rawPrompt);
101
+ promptText = sharedPrefix.text;
102
+ promptNamePrefix = sharedPrefix.body || rawPrompt;
103
+ openIds = normalizeOpenIds(payload);
104
+ if (openIds.length === 0) throw new Error("open_id or openIds must include at least one user");
105
+ } catch (err) {
106
+ jsonReply(res, 400, { ok: false, error: (err as Error).message });
107
+ return true;
108
+ }
109
+
110
+ try {
111
+ const result = await delegateAgentTask({
112
+ platform,
113
+ tool,
114
+ cwd,
115
+ promptText,
116
+ openIds,
117
+ chatNamePrefix: stringValue(payload.chat_name) || promptNamePrefix.slice(0, 10),
118
+ });
119
+ jsonReply(res, 200, {
120
+ ok: true,
121
+ chat_id: result.chatId,
122
+ session_id: result.sessionId,
123
+ tool: result.tool,
124
+ cwd: result.cwd,
125
+ });
126
+ } catch (err) {
127
+ jsonReply(res, 500, { ok: false, error: (err as Error).message });
128
+ }
129
+ return true;
130
+ }
131
+
132
+ export function buildAgentDelegateTaskCapabilityPrompt(input: { url: string; cwd?: string }): string {
133
+ const lines = [
134
+ "[ChatCCC local capability: delegate task]",
135
+ "You can create a separate Feishu ChatCCC agent session and assign its first task by calling this local endpoint.",
136
+ "",
137
+ `POST ${input.url}`,
138
+ "Content-Type: application/json; charset=utf-8",
139
+ "",
140
+ 'Body: {"tool":"codex|claude|cursor","cwd":"absolute working directory","open_id":"Feishu open_id to invite","prompt":"first task text"}',
141
+ "",
142
+ "Rules:",
143
+ "- Use this only when the user asks you to start a separate delegated conversation/session.",
144
+ "- Pass cwd explicitly as an absolute local path.",
145
+ "- Pass tool explicitly when the user specified a target agent.",
146
+ "- Use open_id for one user or open_ids/openIds for multiple users.",
147
+ "- The prompt is sent through the normal ChatCCC prompt path, so project prompt injection and IM skills still apply.",
148
+ "- Request body must be UTF-8 encoded JSON bytes. Do not call Feishu Open Platform directly.",
149
+ "[/ChatCCC local capability: delegate task]",
150
+ ];
151
+ if (input.cwd) lines.splice(2, 0, `Current working directory: ${input.cwd}`);
152
+ return lines.join("\n");
153
+ }
@@ -0,0 +1,81 @@
1
+ import { resolve } from "node:path";
2
+
3
+ import { sessionPrefixForTool, toolDisplayName, ts } from "./config.ts";
4
+ import { setDefaultCwd } from "./config.ts";
5
+ import type { PlatformAdapter } from "./platform-adapter.ts";
6
+ import { initClaudeSession, recordSessionRegistry, resumeAndPrompt, saveSessionTool } from "./session.ts";
7
+ import { bindChatToSession } from "./session-chat-binding.ts";
8
+ import { sessionChatName } from "./session-name.ts";
9
+
10
+ export interface DelegateAgentTaskInput {
11
+ platform: PlatformAdapter;
12
+ tool: string;
13
+ cwd: string;
14
+ promptText: string;
15
+ openIds: string[];
16
+ chatNamePrefix?: string;
17
+ msgTimestamp?: number;
18
+ traceId?: string;
19
+ }
20
+
21
+ export interface DelegateAgentTaskResult {
22
+ chatId: string;
23
+ sessionId: string;
24
+ tool: string;
25
+ cwd: string;
26
+ }
27
+
28
+ export async function delegateAgentTask(input: DelegateAgentTaskInput): Promise<DelegateAgentTaskResult> {
29
+ const cwd = resolve(input.cwd);
30
+ const toolLabel = toolDisplayName(input.tool);
31
+ const init = await initClaudeSession(input.tool, cwd);
32
+ const sessionId = init.sessionId;
33
+ const chatNamePrefix = input.chatNamePrefix?.trim() || input.promptText.slice(0, 10) || "新会话";
34
+ const chatName = sessionChatName(chatNamePrefix, cwd);
35
+
36
+ let chatId: string;
37
+ try {
38
+ chatId = await input.platform.createGroup(chatName, input.openIds);
39
+ await input.platform.updateChatInfo(chatId, chatName, `${sessionPrefixForTool(input.tool)} ${sessionId}`);
40
+ await setDefaultCwd(cwd, chatId);
41
+ bindChatToSession(sessionId, chatId);
42
+ await recordSessionRegistry({
43
+ chatId,
44
+ sessionId,
45
+ tool: input.tool,
46
+ chatName,
47
+ turnCount: 0,
48
+ startTime: Date.now(),
49
+ running: false,
50
+ });
51
+ await saveSessionTool(sessionId, input.tool, chatName);
52
+ } catch (err) {
53
+ console.error(`[${ts()}] [AGENT-DELEGATE-TASK] create group failed: ${(err as Error).message}`);
54
+ throw err;
55
+ }
56
+
57
+ await input.platform.sendCard(
58
+ chatId,
59
+ `${toolLabel} Session Ready`,
60
+ `已创建 **${toolLabel}** 会话群。\n\n` +
61
+ `**Session ID:** ${sessionId}\n` +
62
+ `**工作目录:** \`${cwd}\`\n\n` +
63
+ `下面会自动把任务作为第一句话发送给 ${toolLabel}。`,
64
+ "green",
65
+ ).catch(() => {});
66
+ input.platform.setChatAvatar(chatId, input.tool, "new").catch(() => {});
67
+
68
+ await resumeAndPrompt(
69
+ sessionId,
70
+ input.promptText,
71
+ input.platform,
72
+ chatId,
73
+ input.msgTimestamp ?? Date.now(),
74
+ input.tool,
75
+ input.traceId,
76
+ );
77
+
78
+ console.log(`[${ts()}] [AGENT-DELEGATE-TASK] created ${toolLabel} session=${sessionId} chat=${chatId} cwd=${cwd}`);
79
+ return { chatId, sessionId, tool: input.tool, cwd };
80
+ }
81
+
@@ -0,0 +1,27 @@
1
+ import type { IncomingMessage, ServerResponse } from "node:http";
2
+
3
+ import { getChatGptSubscriptionStatus } from "./chatgpt-subscription.ts";
4
+
5
+ export const CHATGPT_SUBSCRIPTION_PATH = "/api/chatgpt/subscription";
6
+
7
+ function jsonReply(res: ServerResponse, code: number, data: unknown): void {
8
+ res.writeHead(code, { "Content-Type": "application/json; charset=utf-8" });
9
+ res.end(JSON.stringify(data));
10
+ }
11
+
12
+ export async function handleChatGptSubscriptionRequest(
13
+ req: IncomingMessage,
14
+ res: ServerResponse,
15
+ ): Promise<boolean> {
16
+ const method = req.method ?? "GET";
17
+ const url = new URL(req.url ?? "/", "http://127.0.0.1");
18
+ if (url.pathname !== CHATGPT_SUBSCRIPTION_PATH) return false;
19
+
20
+ if (method !== "GET") {
21
+ jsonReply(res, 405, { ok: false, code: "method_not_allowed", reason: "Use GET." });
22
+ return true;
23
+ }
24
+
25
+ jsonReply(res, 200, await getChatGptSubscriptionStatus());
26
+ return true;
27
+ }