chatccc 0.2.58 → 0.2.59

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.
@@ -1,168 +1,168 @@
1
- import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
- import { mkdtemp, rm } from "node:fs/promises";
3
- import { tmpdir } from "node:os";
4
- import { join } from "node:path";
5
-
6
- import type { PlatformAdapter } from "../platform-adapter.ts";
7
- import type { SessionInfo, ToolAdapter } from "../adapters/adapter-interface.ts";
8
-
9
- const mockStreamStates = new Map<string, { status: "running" | "done" | "stopped"; finalReply: string }>();
10
-
11
- vi.mock("../im-skills.ts", () => ({
12
- buildImSkillsPrompt: async () => "",
13
- exportSkillSubDocs: async () => {},
14
- }));
15
-
16
- vi.mock("../stream-state.ts", () => ({
17
- readStreamState: async (sessionId: string) => {
18
- const state = mockStreamStates.get(sessionId);
19
- if (!state) return null;
20
- return {
21
- sessionId,
22
- status: state.status,
23
- accumulatedContent: "",
24
- finalReply: state.finalReply,
25
- chunkCount: 0,
26
- turnCount: 1,
27
- contextTokens: 0,
28
- updatedAt: Date.now(),
29
- cwd: "F:\\repo",
30
- tool: "claude",
31
- };
32
- },
33
- writeStreamState: async (state: { sessionId: string; status: "running" | "done" | "stopped"; finalReply: string }) => {
34
- mockStreamStates.set(state.sessionId, {
35
- status: state.status,
36
- finalReply: state.finalReply,
37
- });
38
- },
39
- createEmptyStreamState: (sessionId: string, cwd: string, tool: string, turnCount: number) => ({
40
- sessionId,
41
- status: "running" as const,
42
- accumulatedContent: "",
43
- finalReply: "",
44
- chunkCount: 0,
45
- turnCount,
46
- contextTokens: 0,
47
- updatedAt: Date.now(),
48
- cwd,
49
- tool,
50
- }),
51
- fixStaleStreamStates: async () => {},
52
- }));
53
-
54
- import { handleCommand } from "../orchestrator.ts";
55
- import {
56
- _clearAdapterCacheForTest,
57
- _resetSessionRegistryFileForTest,
58
- _resetSessionToolsFileForTest,
59
- _setAdapterForToolForTest,
60
- _setSessionRegistryFileForTest,
61
- _setSessionToolsFileForTest,
62
- recordSessionRegistry,
63
- resetState,
64
- } from "../session.ts";
65
- import { activePrompts, resetBindingState } from "../session-chat-binding.ts";
66
-
67
- function mockPlatform(): PlatformAdapter {
68
- return {
69
- kind: "wechat",
70
- sendText: vi.fn(async () => true),
71
- sendCard: vi.fn(async () => true),
72
- sendRawCard: vi.fn(async () => true),
73
- createGroup: vi.fn(async () => "unused-group"),
74
- updateChatInfo: vi.fn(async () => {}),
75
- getChatInfo: vi.fn(async () => ({ name: "微信会话", description: "" })),
76
- disbandChat: vi.fn(async () => {}),
77
- setChatAvatar: vi.fn(async () => {}),
78
- extractSessionInfo: vi.fn(() => null),
79
- cardCreate: vi.fn(async () => "card-id"),
80
- cardSend: vi.fn(async () => "message-id"),
81
- cardUpdate: vi.fn(async () => {}),
82
- };
83
- }
84
-
85
- function mockAdapter(): ToolAdapter {
86
- return {
87
- displayName: "Claude",
88
- sessionDescPrefix: "Claude Session:",
89
- createSession: async () => ({ sessionId: "sid-wechat" }),
90
- prompt: async function* () {
91
- yield {
92
- type: "assistant",
93
- blocks: [{ type: "text", text: "done" }],
94
- };
95
- },
96
- getSessionInfo: async (sessionId: string): Promise<SessionInfo> => ({
97
- sessionId,
98
- cwd: "F:\\repo",
99
- }),
100
- closeSession: async () => {},
101
- };
102
- }
103
-
104
- describe("handleCommand WeChat processing ack", () => {
105
- let tempDir: string;
106
-
107
- beforeEach(async () => {
108
- vi.useFakeTimers();
109
- tempDir = await mkdtemp(join(tmpdir(), "chatccc-orchestrator-"));
110
- _setSessionRegistryFileForTest(join(tempDir, "session-registry.json"));
111
- _setSessionToolsFileForTest(join(tempDir, "sessions.json"));
112
- resetState();
113
- resetBindingState();
114
- mockStreamStates.clear();
115
- _setAdapterForToolForTest("claude", mockAdapter());
116
- });
117
-
118
- afterEach(async () => {
119
- resetState();
120
- resetBindingState();
121
- _clearAdapterCacheForTest();
122
- _resetSessionRegistryFileForTest();
123
- _resetSessionToolsFileForTest();
124
- vi.useRealTimers();
125
- await rm(tempDir, { recursive: true, force: true });
126
- });
127
-
128
- it("does not send the WeChat processing ack when the session is already running", async () => {
129
- const platform = mockPlatform();
130
- await recordSessionRegistry({
131
- chatId: "wx-chat",
132
- sessionId: "sid-wechat",
133
- tool: "claude",
134
- chatName: "busy-session",
135
- running: true,
136
- });
137
- activePrompts.set("sid-wechat", {
138
- controller: new AbortController(),
139
- stopped: false,
140
- startTime: Date.now(),
141
- });
142
-
143
- await handleCommand(platform, "继续说明", "wx-chat", "wx-user", Date.now(), "p2p");
144
-
145
- expect(platform.sendText).not.toHaveBeenCalledWith("wx-chat", "生成中...");
146
- expect(platform.sendCard).toHaveBeenCalledWith(
147
- "wx-chat",
148
- "生成中",
149
- "该会话正在生成回复中,请等待完成后再发送新消息。",
150
- "yellow",
151
- );
152
- });
153
-
154
- it("sends the WeChat processing ack after the busy check for normal prompts", async () => {
155
- const platform = mockPlatform();
156
- await recordSessionRegistry({
157
- chatId: "wx-chat",
158
- sessionId: "sid-wechat",
159
- tool: "claude",
160
- chatName: "ready-session",
161
- running: false,
162
- });
163
-
164
- await handleCommand(platform, "继续说明", "wx-chat", "wx-user", Date.now(), "p2p");
165
-
166
- expect(platform.sendText).toHaveBeenCalledWith("wx-chat", "生成中...");
167
- });
168
- });
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
+ import { mkdtemp, rm } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+
6
+ import type { PlatformAdapter } from "../platform-adapter.ts";
7
+ import type { SessionInfo, ToolAdapter } from "../adapters/adapter-interface.ts";
8
+
9
+ const mockStreamStates = new Map<string, { status: "running" | "done" | "stopped"; finalReply: string }>();
10
+
11
+ vi.mock("../im-skills.ts", () => ({
12
+ buildImSkillsPrompt: async () => "",
13
+ exportSkillSubDocs: async () => {},
14
+ }));
15
+
16
+ vi.mock("../stream-state.ts", () => ({
17
+ readStreamState: async (sessionId: string) => {
18
+ const state = mockStreamStates.get(sessionId);
19
+ if (!state) return null;
20
+ return {
21
+ sessionId,
22
+ status: state.status,
23
+ accumulatedContent: "",
24
+ finalReply: state.finalReply,
25
+ chunkCount: 0,
26
+ turnCount: 1,
27
+ contextTokens: 0,
28
+ updatedAt: Date.now(),
29
+ cwd: "F:\\repo",
30
+ tool: "claude",
31
+ };
32
+ },
33
+ writeStreamState: async (state: { sessionId: string; status: "running" | "done" | "stopped"; finalReply: string }) => {
34
+ mockStreamStates.set(state.sessionId, {
35
+ status: state.status,
36
+ finalReply: state.finalReply,
37
+ });
38
+ },
39
+ createEmptyStreamState: (sessionId: string, cwd: string, tool: string, turnCount: number) => ({
40
+ sessionId,
41
+ status: "running" as const,
42
+ accumulatedContent: "",
43
+ finalReply: "",
44
+ chunkCount: 0,
45
+ turnCount,
46
+ contextTokens: 0,
47
+ updatedAt: Date.now(),
48
+ cwd,
49
+ tool,
50
+ }),
51
+ fixStaleStreamStates: async () => {},
52
+ }));
53
+
54
+ import { handleCommand } from "../orchestrator.ts";
55
+ import {
56
+ _clearAdapterCacheForTest,
57
+ _resetSessionRegistryFileForTest,
58
+ _resetSessionToolsFileForTest,
59
+ _setAdapterForToolForTest,
60
+ _setSessionRegistryFileForTest,
61
+ _setSessionToolsFileForTest,
62
+ recordSessionRegistry,
63
+ resetState,
64
+ } from "../session.ts";
65
+ import { activePrompts, resetBindingState } from "../session-chat-binding.ts";
66
+
67
+ function mockPlatform(): PlatformAdapter {
68
+ return {
69
+ kind: "wechat",
70
+ sendText: vi.fn(async () => true),
71
+ sendCard: vi.fn(async () => true),
72
+ sendRawCard: vi.fn(async () => true),
73
+ createGroup: vi.fn(async () => "unused-group"),
74
+ updateChatInfo: vi.fn(async () => {}),
75
+ getChatInfo: vi.fn(async () => ({ name: "微信会话", description: "" })),
76
+ disbandChat: vi.fn(async () => {}),
77
+ setChatAvatar: vi.fn(async () => {}),
78
+ extractSessionInfo: vi.fn(() => null),
79
+ cardCreate: vi.fn(async () => "card-id"),
80
+ cardSend: vi.fn(async () => "message-id"),
81
+ cardUpdate: vi.fn(async () => {}),
82
+ };
83
+ }
84
+
85
+ function mockAdapter(): ToolAdapter {
86
+ return {
87
+ displayName: "Claude",
88
+ sessionDescPrefix: "Claude Session:",
89
+ createSession: async () => ({ sessionId: "sid-wechat" }),
90
+ prompt: async function* () {
91
+ yield {
92
+ type: "assistant",
93
+ blocks: [{ type: "text", text: "done" }],
94
+ };
95
+ },
96
+ getSessionInfo: async (sessionId: string): Promise<SessionInfo> => ({
97
+ sessionId,
98
+ cwd: "F:\\repo",
99
+ }),
100
+ closeSession: async () => {},
101
+ };
102
+ }
103
+
104
+ describe("handleCommand WeChat processing ack", () => {
105
+ let tempDir: string;
106
+
107
+ beforeEach(async () => {
108
+ vi.useFakeTimers();
109
+ tempDir = await mkdtemp(join(tmpdir(), "chatccc-orchestrator-"));
110
+ _setSessionRegistryFileForTest(join(tempDir, "session-registry.json"));
111
+ _setSessionToolsFileForTest(join(tempDir, "sessions.json"));
112
+ resetState();
113
+ resetBindingState();
114
+ mockStreamStates.clear();
115
+ _setAdapterForToolForTest("claude", mockAdapter());
116
+ });
117
+
118
+ afterEach(async () => {
119
+ resetState();
120
+ resetBindingState();
121
+ _clearAdapterCacheForTest();
122
+ _resetSessionRegistryFileForTest();
123
+ _resetSessionToolsFileForTest();
124
+ vi.useRealTimers();
125
+ await rm(tempDir, { recursive: true, force: true });
126
+ });
127
+
128
+ it("does not send the WeChat processing ack when the session is already running", async () => {
129
+ const platform = mockPlatform();
130
+ await recordSessionRegistry({
131
+ chatId: "wx-chat",
132
+ sessionId: "sid-wechat",
133
+ tool: "claude",
134
+ chatName: "busy-session",
135
+ running: true,
136
+ });
137
+ activePrompts.set("sid-wechat", {
138
+ controller: new AbortController(),
139
+ stopped: false,
140
+ startTime: Date.now(),
141
+ });
142
+
143
+ await handleCommand(platform, "继续说明", "wx-chat", "wx-user", Date.now(), "p2p");
144
+
145
+ expect(platform.sendText).not.toHaveBeenCalledWith("wx-chat", "生成中...");
146
+ expect(platform.sendCard).toHaveBeenCalledWith(
147
+ "wx-chat",
148
+ "生成中",
149
+ "该会话正在生成回复中,请等待完成后再发送新消息。",
150
+ "yellow",
151
+ );
152
+ });
153
+
154
+ it("sends the WeChat processing ack after the busy check for normal prompts", async () => {
155
+ const platform = mockPlatform();
156
+ await recordSessionRegistry({
157
+ chatId: "wx-chat",
158
+ sessionId: "sid-wechat",
159
+ tool: "claude",
160
+ chatName: "ready-session",
161
+ running: false,
162
+ });
163
+
164
+ await handleCommand(platform, "继续说明", "wx-chat", "wx-user", Date.now(), "p2p");
165
+
166
+ expect(platform.sendText).toHaveBeenCalledWith("wx-chat", "生成中...");
167
+ });
168
+ });
@@ -1,10 +1,10 @@
1
1
  import { describe, it, expect } from "vitest";
2
2
  import {
3
- applyClaudeApiMode,
4
- chooseStartPath,
5
- detectClaudeApiMode,
6
- unflattenConfig,
7
- } from "../web-ui.ts";
3
+ applyClaudeApiMode,
4
+ chooseStartPath,
5
+ detectClaudeApiMode,
6
+ unflattenConfig,
7
+ } from "../web-ui.ts";
8
8
 
9
9
  // ---------------------------------------------------------------------------
10
10
  // detectClaudeApiMode — 加载已有 config 时如何判定 UI 初始模式
@@ -57,7 +57,7 @@ describe("detectClaudeApiMode", () => {
57
57
  // - mode 未传 默认按 official 兌底(不保留可能误填的密钥)
58
58
  // ---------------------------------------------------------------------------
59
59
 
60
- describe("applyClaudeApiMode", () => {
60
+ describe("applyClaudeApiMode", () => {
61
61
  it("mode=official 时清空 CLAUDE_API_KEY / CLAUDE_BASE_URL(即使 vars 没传)", () => {
62
62
  const out = applyClaudeApiMode({ CHATCCC_APP_ID: "x" }, "official");
63
63
  expect(out).toEqual({
@@ -128,23 +128,23 @@ describe("applyClaudeApiMode", () => {
128
128
  expect(input).toEqual({ CLAUDE_API_KEY: "sk-x" }); // 原对象未变
129
129
  expect(out).not.toBe(input);
130
130
  });
131
- });
132
-
133
- describe("unflattenConfig", () => {
134
- it("maps Claude subagent model into claude.subagentModel", () => {
135
- expect(
136
- unflattenConfig({
137
- CHATCCC_ANTHROPIC_MODEL: "claude-sonnet-4-6",
138
- CHATCCC_ANTHROPIC_SUBAGENT_MODEL: "claude-haiku-4-5-20251001",
139
- }),
140
- ).toEqual({
141
- claude: {
142
- model: "claude-sonnet-4-6",
143
- subagentModel: "claude-haiku-4-5-20251001",
144
- },
145
- });
146
- });
147
- });
131
+ });
132
+
133
+ describe("unflattenConfig", () => {
134
+ it("maps Claude subagent model into claude.subagentModel", () => {
135
+ expect(
136
+ unflattenConfig({
137
+ CHATCCC_ANTHROPIC_MODEL: "claude-sonnet-4-6",
138
+ CHATCCC_ANTHROPIC_SUBAGENT_MODEL: "claude-haiku-4-5-20251001",
139
+ }),
140
+ ).toEqual({
141
+ claude: {
142
+ model: "claude-sonnet-4-6",
143
+ subagentModel: "claude-haiku-4-5-20251001",
144
+ },
145
+ });
146
+ });
147
+ });
148
148
 
149
149
  // ---------------------------------------------------------------------------
150
150
  // chooseStartPath — /api/start 的路径选择
@@ -1,9 +1,17 @@
1
- import { describe, expect, it, vi } from "vitest";
1
+ import { beforeEach, describe, expect, it, vi } from "vitest";
2
2
 
3
3
  import { buildHelpCard } from "../cards.ts";
4
- import { createWechatAdapter } from "../wechat-platform.ts";
4
+ import {
5
+ _flushPendingClawFinalTextForTest,
6
+ _resetWechatClawStateForTest,
7
+ createWechatAdapter,
8
+ } from "../wechat-platform.ts";
5
9
 
6
10
  describe("createWechatAdapter", () => {
11
+ beforeEach(() => {
12
+ _resetWechatClawStateForTest();
13
+ });
14
+
7
15
  it("degrades raw cards to plain text messages", async () => {
8
16
  const wire = {
9
17
  push: vi.fn(async (_chatId: string, _text: string) => "msg-id"),
@@ -54,4 +62,38 @@ describe("createWechatAdapter", () => {
54
62
  "[WECHAT] sendText skipped (claw limit): chatId=wx-chat-limit count=11",
55
63
  );
56
64
  });
65
+
66
+ it("queues final messages after the claw limit until the user wakes the chat", async () => {
67
+ const wire = {
68
+ push: vi.fn(async (_chatId: string, _text: string) => "msg-id"),
69
+ sendText: vi.fn(
70
+ async (_chatId: string, _text: string, _contextToken?: string) =>
71
+ "msg-id",
72
+ ),
73
+ };
74
+ const log = vi.fn();
75
+ const platform = createWechatAdapter({
76
+ getWire: () => wire,
77
+ log,
78
+ });
79
+ const chatId = "wx-chat-final-limit";
80
+ const finalText = "done\n━━━ 回答结束 ━━━";
81
+
82
+ for (let i = 0; i < 10; i++) {
83
+ await expect(platform.sendText(chatId, `chunk ${i}`)).resolves.toBe(true);
84
+ }
85
+
86
+ await expect(platform.sendText(chatId, finalText)).resolves.toBe(true);
87
+ expect(wire.push).toHaveBeenCalledTimes(10);
88
+ expect(log).toHaveBeenCalledWith(
89
+ `[WECHAT] final queued (claw limit): chatId=${chatId} count=11 len=${finalText.length}`,
90
+ );
91
+
92
+ await expect(_flushPendingClawFinalTextForTest(chatId, wire, log)).resolves.toBe(true);
93
+ expect(wire.push).toHaveBeenCalledTimes(11);
94
+ expect(wire.push).toHaveBeenLastCalledWith(chatId, finalText);
95
+ expect(log).toHaveBeenCalledWith(
96
+ `[WECHAT] pending final sent after claw wake: chatId=${chatId} len=${finalText.length}`,
97
+ );
98
+ });
57
99
  });
@@ -86,33 +86,33 @@ function buildSdkEnv(
86
86
  baseUrl: string | undefined,
87
87
  subagentModel: string | undefined,
88
88
  ): Record<string, string | undefined> | undefined {
89
- const apiKeyTrim = (apiKey ?? "").trim();
90
- const baseUrlTrim = (baseUrl ?? "").trim();
91
- const subagentModelTrim = (subagentModel ?? "").trim();
92
- const hasApiOverride = Boolean(apiKeyTrim || baseUrlTrim);
93
- if (!hasApiOverride) return undefined;
94
-
95
- const env: Record<string, string | undefined> = { ...process.env };
96
- // ChatCCC's third-party Claude API config is authoritative when present.
97
- // Remove Claude Code/user settings env that can silently override gateway/auth/model choice.
98
- delete env.ANTHROPIC_AUTH_TOKEN;
99
- delete env.CLAUDE_CODE_OAUTH_TOKEN;
100
- delete env.ANTHROPIC_MODEL;
101
- delete env.ANTHROPIC_DEFAULT_OPUS_MODEL;
102
- delete env.ANTHROPIC_DEFAULT_SONNET_MODEL;
103
- delete env.ANTHROPIC_DEFAULT_HAIKU_MODEL;
104
- delete env.CLAUDE_CODE_SUBAGENT_MODEL;
105
- delete env.CLAUDE_CODE_EFFORT_LEVEL;
106
-
107
- if (apiKeyTrim) env.ANTHROPIC_API_KEY = apiKeyTrim;
108
- if (baseUrlTrim) {
109
- env.ANTHROPIC_BASE_URL = baseUrlTrim;
110
- } else {
111
- delete env.ANTHROPIC_BASE_URL;
112
- }
113
- if (subagentModelTrim) env.CLAUDE_CODE_SUBAGENT_MODEL = subagentModelTrim;
114
- return env;
115
- }
89
+ const apiKeyTrim = (apiKey ?? "").trim();
90
+ const baseUrlTrim = (baseUrl ?? "").trim();
91
+ const subagentModelTrim = (subagentModel ?? "").trim();
92
+ const hasApiOverride = Boolean(apiKeyTrim || baseUrlTrim);
93
+ if (!hasApiOverride) return undefined;
94
+
95
+ const env: Record<string, string | undefined> = { ...process.env };
96
+ // ChatCCC's third-party Claude API config is authoritative when present.
97
+ // Remove Claude Code/user settings env that can silently override gateway/auth/model choice.
98
+ delete env.ANTHROPIC_AUTH_TOKEN;
99
+ delete env.CLAUDE_CODE_OAUTH_TOKEN;
100
+ delete env.ANTHROPIC_MODEL;
101
+ delete env.ANTHROPIC_DEFAULT_OPUS_MODEL;
102
+ delete env.ANTHROPIC_DEFAULT_SONNET_MODEL;
103
+ delete env.ANTHROPIC_DEFAULT_HAIKU_MODEL;
104
+ delete env.CLAUDE_CODE_SUBAGENT_MODEL;
105
+ delete env.CLAUDE_CODE_EFFORT_LEVEL;
106
+
107
+ if (apiKeyTrim) env.ANTHROPIC_API_KEY = apiKeyTrim;
108
+ if (baseUrlTrim) {
109
+ env.ANTHROPIC_BASE_URL = baseUrlTrim;
110
+ } else {
111
+ delete env.ANTHROPIC_BASE_URL;
112
+ }
113
+ if (subagentModelTrim) env.CLAUDE_CODE_SUBAGENT_MODEL = subagentModelTrim;
114
+ return env;
115
+ }
116
116
 
117
117
  function resolveSettingSources(
118
118
  _apiKey: string | undefined,
@@ -291,19 +291,19 @@ class ClaudeAdapter implements ToolAdapter {
291
291
  this.baseUrl,
292
292
  this.subagentModel,
293
293
  );
294
- const session = unstable_v2_resumeSession(
295
- sessionId,
296
- sessionOpts as any,
297
- );
298
-
299
- if (signal?.aborted) {
300
- session.close();
301
- return;
302
- }
303
- const onAbort = () => { session.close(); };
304
- signal?.addEventListener("abort", onAbort, { once: true });
305
-
306
- await session.send(userText);
294
+ const session = unstable_v2_resumeSession(
295
+ sessionId,
296
+ sessionOpts as any,
297
+ );
298
+
299
+ if (signal?.aborted) {
300
+ session.close();
301
+ return;
302
+ }
303
+ const onAbort = () => { session.close(); };
304
+ signal?.addEventListener("abort", onAbort, { once: true });
305
+
306
+ await session.send(userText);
307
307
 
308
308
  const stream = session.stream();
309
309
 
package/src/index.ts CHANGED
@@ -84,9 +84,9 @@ import {
84
84
  updateCardKitCard,
85
85
  } from "./cardkit.ts";
86
86
  import {
87
- MAX_PROCESSED,
88
- clearAdapterCache,
89
- loadSessionRegistryForBinding,
87
+ MAX_PROCESSED,
88
+ clearAdapterCache,
89
+ loadSessionRegistryForBinding,
90
90
  processedMessages,
91
91
  rebuildBindingsFromRegistry,
92
92
  resetState,
@@ -744,12 +744,12 @@ async function main(): Promise<void> {
744
744
  // "保存并启动" 时,web-ui 会调用本回调,把磁盘上刚保存的 config.json
745
745
  // 刷进进程内的 export let 常量(live binding 让 CLAUDE_MODEL 等下次创建
746
746
  // 会话时自动看到新值)。setup 首次激活走 onActivate 路径,不依赖此 hook。
747
- setReloadConfigHook(() => {
748
- reloadConfigFromDisk();
749
- clearAdapterCache();
750
- appendStartupTrace("reload-from-ui: config reloaded", {
751
- appIdMask: maskAppId(APP_ID),
752
- });
747
+ setReloadConfigHook(() => {
748
+ reloadConfigFromDisk();
749
+ clearAdapterCache();
750
+ appendStartupTrace("reload-from-ui: config reloaded", {
751
+ appIdMask: maskAppId(APP_ID),
752
+ });
753
753
  });
754
754
  setExtraApiHandler(async (req, res) => {
755
755
  return (await handleAgentImageRequest(req, res)) || (await handleAgentFileRequest(req, res));
@@ -769,12 +769,12 @@ async function main(): Promise<void> {
769
769
  // 凭证不全:进 setup 向导。注入 onActivate 回调让用户点"保存并启动"
770
770
  // 时,原地(同进程)调用 startBotService,复用 setup HTTP server。
771
771
  startSetupMode(CHATCCC_PORT, {
772
- onActivate: async (httpServer: Server) => {
773
- reloadConfigFromDisk();
774
- clearAdapterCache();
775
- appendStartupTrace("setup-activate: reloaded config from disk", {
776
- appIdMaskAfterReload: maskAppId(APP_ID),
777
- });
772
+ onActivate: async (httpServer: Server) => {
773
+ reloadConfigFromDisk();
774
+ clearAdapterCache();
775
+ appendStartupTrace("setup-activate: reloaded config from disk", {
776
+ appIdMaskAfterReload: maskAppId(APP_ID),
777
+ });
778
778
  try {
779
779
  await startBotService({ httpServer, port: CHATCCC_PORT });
780
780
  installShutdownHandlers(httpServer);
@@ -818,8 +818,8 @@ async function main(): Promise<void> {
818
818
  try {
819
819
  await startBotService({ httpServer, port: CHATCCC_PORT });
820
820
  } catch (err) {
821
- printServiceDidNotStart((err as Error).message);
822
- process.exit(1);
821
+ console.error(`\n[飞书] 启动失败: ${(err as Error).message}`);
822
+ console.error("[飞书] 微信等其他平台不受影响,将继续启动。\n");
823
823
  }
824
824
  }
825
825