@wu529778790/open-im 1.6.2 → 1.6.3-beta.2

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.
Files changed (40) hide show
  1. package/dist/adapters/registry.d.ts +1 -1
  2. package/dist/adapters/registry.js +23 -18
  3. package/dist/commands/handler.d.ts +1 -1
  4. package/dist/commands/handler.js +10 -8
  5. package/dist/config-web-page-i18n.d.ts +4 -0
  6. package/dist/config-web-page-i18n.js +4 -0
  7. package/dist/config-web-page-script.js +16 -5
  8. package/dist/config-web-page-template.js +5 -0
  9. package/dist/config-web.d.ts +7 -0
  10. package/dist/config-web.js +60 -2
  11. package/dist/config-web.test.js +9 -1
  12. package/dist/config.d.ts +14 -0
  13. package/dist/config.js +29 -1
  14. package/dist/dingtalk/event-handler.d.ts +1 -1
  15. package/dist/dingtalk/event-handler.js +7 -5
  16. package/dist/feishu/event-handler.d.ts +1 -1
  17. package/dist/feishu/event-handler.js +8 -6
  18. package/dist/hook/permission-server.d.ts +1 -0
  19. package/dist/hook/permission-server.js +13 -7
  20. package/dist/hook/permission-server.test.d.ts +1 -0
  21. package/dist/hook/permission-server.test.js +12 -0
  22. package/dist/index.js +7 -10
  23. package/dist/manager.js +2 -1
  24. package/dist/qq/event-handler.d.ts +1 -1
  25. package/dist/qq/event-handler.js +6 -4
  26. package/dist/qq/event-handler.test.js +7 -0
  27. package/dist/service-control.d.ts +1 -0
  28. package/dist/service-control.js +26 -7
  29. package/dist/service-control.test.d.ts +1 -0
  30. package/dist/service-control.test.js +36 -0
  31. package/dist/shared/ai-task.js +128 -104
  32. package/dist/shared/ai-task.test.d.ts +1 -0
  33. package/dist/shared/ai-task.test.js +69 -0
  34. package/dist/telegram/event-handler.d.ts +1 -1
  35. package/dist/telegram/event-handler.js +8 -6
  36. package/dist/wechat/event-handler.d.ts +1 -1
  37. package/dist/wechat/event-handler.js +8 -6
  38. package/dist/wework/event-handler.d.ts +1 -1
  39. package/dist/wework/event-handler.js +8 -6
  40. package/package.json +1 -1
@@ -1,3 +1,4 @@
1
+ import { resolvePlatformAiCommand } from '../config.js';
1
2
  import { AccessControl } from '../access/access-control.js';
2
3
  import { RequestQueue } from '../queue/request-queue.js';
3
4
  import { configureDingTalkMessageSender, sendThinkingMessage, updateMessage, sendFinalMessages, sendErrorMessage, sendTextReply, sendImageReply, startTypingLoop, sendPermissionCard, sendModeCard, sendDirectorySelection, } from './message-sender.js';
@@ -175,16 +176,17 @@ export function setupDingTalkHandlers(config, sessionManager) {
175
176
  }
176
177
  async function handleAIRequest(userId, chatId, prompt, workDir, convId, _threadCtx, replyToMessageId, dingtalkTarget) {
177
178
  log.info(`[AI_REQUEST] userId=${userId}, chatId=${chatId}, promptLength=${prompt.length}`);
178
- const toolAdapter = getAdapter(config.aiCommand);
179
+ const aiCommand = resolvePlatformAiCommand(config, 'dingtalk');
180
+ const toolAdapter = getAdapter(aiCommand);
179
181
  if (!toolAdapter) {
180
- await sendTextReply(chatId, `AI tool is not configured: ${config.aiCommand}`);
182
+ await sendTextReply(chatId, `AI tool is not configured: ${aiCommand}`);
181
183
  return;
182
184
  }
183
185
  const sessionId = convId
184
- ? sessionManager.getSessionIdForConv(userId, convId, config.aiCommand)
186
+ ? sessionManager.getSessionIdForConv(userId, convId, aiCommand)
185
187
  : undefined;
186
- log.info(`[AI_REQUEST] Running ${config.aiCommand} for user ${userId}, sessionId=${sessionId ?? 'new'}`);
187
- const toolId = config.aiCommand;
188
+ log.info(`[AI_REQUEST] Running ${aiCommand} for user ${userId}, sessionId=${sessionId ?? 'new'}`);
189
+ const toolId = aiCommand;
188
190
  const msgId = await sendThinkingMessage(chatId, replyToMessageId, toolId, dingtalkTarget);
189
191
  const stopTyping = startTypingLoop(chatId);
190
192
  const taskKey = `${userId}:${msgId}`;
@@ -1,4 +1,4 @@
1
- import type { Config } from '../config.js';
1
+ import { type Config } from '../config.js';
2
2
  import type { SessionManager } from '../session/session-manager.js';
3
3
  export interface FeishuEventHandlerHandle {
4
4
  stop: () => void;
@@ -1,3 +1,4 @@
1
+ import { resolvePlatformAiCommand } from '../config.js';
1
2
  import { AccessControl } from '../access/access-control.js';
2
3
  import { RequestQueue } from '../queue/request-queue.js';
3
4
  import { sendTextReply, sendTextReplyByOpenId, startTypingLoop, sendImageReply, createFeishuButtonCard, sendModeCard, createFeishuModeCardReadOnly, delayUpdateCard, sendThinkingCard, streamContentUpdate, sendFinalCards, sendErrorCard, } from './message-sender.js';
@@ -91,16 +92,17 @@ export function setupFeishuHandlers(config, sessionManager) {
91
92
  async function handleAIRequest(userId, chatId, prompt, workDir, convId, _threadCtx, replyToMessageId) {
92
93
  log.info(`[AI_REQUEST] userId=${userId}, chatId=${chatId}, promptLength=${prompt.length}`);
93
94
  log.info(`[AI_REQUEST] Full prompt: "${prompt}"`);
94
- const toolAdapter = getAdapter(config.aiCommand);
95
+ const aiCommand = resolvePlatformAiCommand(config, 'feishu');
96
+ const toolAdapter = getAdapter(aiCommand);
95
97
  if (!toolAdapter) {
96
- log.error(`[handleAIRequest] No adapter found for: ${config.aiCommand}`);
97
- await sendTextReply(chatId, `未配置 AI 工具: ${config.aiCommand}`);
98
+ log.error(`[handleAIRequest] No adapter found for: ${aiCommand}`);
99
+ await sendTextReply(chatId, `未配置 AI 工具: ${aiCommand}`);
98
100
  return;
99
101
  }
100
102
  log.info(`[handleAIRequest] Adapter found, getting session...`);
101
- const sessionId = convId ? sessionManager.getSessionIdForConv(userId, convId, config.aiCommand) : undefined;
102
- log.info(`[handleAIRequest] Running ${config.aiCommand} for user ${userId}, sessionId=${sessionId ?? 'new'}`);
103
- const toolId = config.aiCommand;
103
+ const sessionId = convId ? sessionManager.getSessionIdForConv(userId, convId, aiCommand) : undefined;
104
+ log.info(`[handleAIRequest] Running ${aiCommand} for user ${userId}, sessionId=${sessionId ?? 'new'}`);
105
+ const toolId = aiCommand;
104
106
  // 使用 CardKit 打字机效果(80ms 节流,约 12 次/秒,比 patch 5 QPS 更流畅)
105
107
  let cardHandle;
106
108
  try {
@@ -9,6 +9,7 @@ interface MessageSender {
9
9
  sendTextReply(chatId: string, text: string): Promise<void>;
10
10
  sendPermissionCard?(chatId: string, requestId: string, toolName: string, toolInput: string): Promise<void>;
11
11
  }
12
+ export declare function resolvePermissionChatId(data: Record<string, unknown>): string | undefined;
12
13
  /**
13
14
  * Start the permission HTTP server
14
15
  */
@@ -11,6 +11,15 @@ import { getPlatformByChatId } from '../shared/chat-user-map.js';
11
11
  const log = createLogger('PermissionServer');
12
12
  const PERMISSION_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes default timeout
13
13
  const DEFAULT_PORT = 35801;
14
+ export function resolvePermissionChatId(data) {
15
+ if (typeof data.chatId === 'string' && data.chatId.trim()) {
16
+ return data.chatId;
17
+ }
18
+ if (typeof data.chat_id === 'string' && data.chat_id.trim()) {
19
+ return data.chat_id;
20
+ }
21
+ return undefined;
22
+ }
14
23
  // Global state
15
24
  let server = null;
16
25
  let serverPort = DEFAULT_PORT;
@@ -153,19 +162,16 @@ function handlePermissionRequest(req, res) {
153
162
  const requestId = String(data.requestId ?? '');
154
163
  const toolName = String(data.toolName ?? '');
155
164
  const toolInput = data.toolInput;
156
- const chatId = (typeof data.chatId === 'string' ? data.chatId : null) ??
157
- (typeof data.chat_id === 'string' ? data.chat_id : null) ??
158
- process.env.CC_IM_CHAT_ID ??
159
- undefined;
165
+ const chatId = resolvePermissionChatId(data);
160
166
  if (!requestId || !toolName) {
161
167
  res.writeHead(400, { 'Content-Type': 'application/json' });
162
168
  res.end(JSON.stringify({ error: 'Missing required fields' }));
163
169
  return;
164
170
  }
165
171
  if (!chatId) {
166
- log.warn('No chatId, auto-allowing permission');
167
- res.writeHead(200, { 'Content-Type': 'application/json' });
168
- res.end(JSON.stringify({ allowed: true }));
172
+ log.warn(`Permission request ${requestId} missing chatId`);
173
+ res.writeHead(400, { 'Content-Type': 'application/json' });
174
+ res.end(JSON.stringify({ error: 'Missing chatId' }));
169
175
  return;
170
176
  }
171
177
  if (process.env.CC_SKIP_PERMISSIONS === 'true') {
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,12 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { resolvePermissionChatId } from "./permission-server.js";
3
+ describe("resolvePermissionChatId", () => {
4
+ it("prefers explicit chatId fields from the request payload", () => {
5
+ expect(resolvePermissionChatId({ chatId: "chat-1" })).toBe("chat-1");
6
+ expect(resolvePermissionChatId({ chat_id: "chat-2" })).toBe("chat-2");
7
+ });
8
+ it("does not fall back to process-global state", () => {
9
+ process.env.CC_IM_CHAT_ID = "leaked-chat";
10
+ expect(resolvePermissionChatId({})).toBeUndefined();
11
+ });
12
+ });
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { createServer } from "node:http";
2
2
  import { writeFileSync, existsSync, mkdirSync, unlinkSync } from "node:fs";
3
3
  import { dirname, join } from "node:path";
4
- import { loadConfig, needsSetup } from "./config.js";
4
+ import { getConfiguredAiCommands, loadConfig, needsSetup, resolvePlatformAiCommand } from "./config.js";
5
5
  import { runInteractiveSetup, runClaudeApiSetup } from "./setup.js";
6
6
  import { runWebConfigFlow } from "./config-web.js";
7
7
  // 导出供 cli.ts 使用
@@ -77,7 +77,7 @@ async function sendLifecycleNotification(platform, message) {
77
77
  }
78
78
  await Promise.all(sendPromises);
79
79
  }
80
- function buildStartupMessage(platform, appVersion, aiCommand, defaultWorkDir, successfulPlatforms, sessionManager) {
80
+ function buildStartupMessage(platform, appVersion, aiCommand, defaultWorkDir, sessionManager) {
81
81
  let sessionDir;
82
82
  // Telegram 私聊、企业微信当前实现里,活跃 chatId 可直接对应到 session userId。
83
83
  if (platform === "telegram" || platform === "wework") {
@@ -86,13 +86,12 @@ function buildStartupMessage(platform, appVersion, aiCommand, defaultWorkDir, su
86
86
  sessionDir = sessionManager.getWorkDir(activeChatId);
87
87
  }
88
88
  }
89
- const platformList = successfulPlatforms.map((item) => `\`${item}\``).join("、");
90
89
  const lines = [
91
90
  `**服务已启动**`,
92
91
  "",
93
92
  `- 版本: \`open-im v${appVersion}\``,
94
- `- AI 工具: \`${aiCommand}\``,
95
- `- 成功启动平台: ${platformList}`,
93
+ `- 当前渠道: \`${platform}\``,
94
+ `- 当前渠道 CLI: \`${aiCommand}\``,
96
95
  ];
97
96
  if (sessionDir) {
98
97
  lines.push(`- 会话目录: ${escapePathForMarkdown(sessionDir)}`);
@@ -151,15 +150,13 @@ export async function main() {
151
150
  const actualPort = startPermissionServer(config.hookPort);
152
151
  log.info(`Permission server listening on port ${actualPort}`);
153
152
  log.info("Starting open-im bridge...");
154
- log.info(`AI 工具: ${config.aiCommand}`);
153
+ log.info(`AI 工具: ${getConfiguredAiCommands(config).join(", ")}`);
155
154
  log.info(`默认会话目录: ${config.claudeWorkDir}`);
156
155
  log.info(`启用平台: ${config.enabledPlatforms.join(", ")}`);
157
156
  const sessionManager = new SessionManager(config.claudeWorkDir);
158
157
  // CLI 工具(Cursor/Codex)的 session 是进程级别的,服务重启后一定无效。
159
158
  // 启动时仅清除 CLI 工具自己的 sessionId,保留 Claude 的持久上下文。
160
- if (config.aiCommand !== 'claude') {
161
- sessionManager.clearAllCliSessionIds();
162
- }
159
+ sessionManager.clearAllCliSessionIds();
163
160
  let telegramHandle = null;
164
161
  let feishuHandle = null;
165
162
  let qqHandle = null;
@@ -237,7 +234,7 @@ export async function main() {
237
234
  log.info(`Successfully initialized platforms: ${successfulPlatforms.join(", ")}`);
238
235
  // Send notification only to successfully initialized platforms
239
236
  for (const platform of successfulPlatforms) {
240
- const startupMsg = buildStartupMessage(platform, APP_VERSION, config.aiCommand, config.claudeWorkDir, successfulPlatforms, sessionManager);
237
+ const startupMsg = buildStartupMessage(platform, APP_VERSION, resolvePlatformAiCommand(config, platform), config.claudeWorkDir, sessionManager);
241
238
  await sendLifecycleNotification(platform, startupMsg).catch((err) => {
242
239
  log.warn(`Failed to send startup notification to ${platform}:`, err);
243
240
  });
package/dist/manager.js CHANGED
@@ -1,9 +1,10 @@
1
1
  import { startWebConfigServer } from "./config-web.js";
2
2
  import { removeManagerPid, removeManagerReady, writeManagerReady } from "./manager-control.js";
3
- import { startBackgroundService, stopBackgroundService } from "./service-control.js";
3
+ import { startBackgroundService, stopBackgroundService, waitForBackgroundServiceReady } from "./service-control.js";
4
4
  async function main() {
5
5
  const web = await startWebConfigServer({ mode: "start", cwd: process.cwd(), persistent: true });
6
6
  startBackgroundService(process.cwd());
7
+ await waitForBackgroundServiceReady();
7
8
  writeManagerReady();
8
9
  const shutdown = async () => {
9
10
  await web.close().catch((err) => console.warn("[manager] Failed to close web server:", err));
@@ -1,4 +1,4 @@
1
- import type { Config } from "../config.js";
1
+ import { type Config } from "../config.js";
2
2
  import type { SessionManager } from "../session/session-manager.js";
3
3
  import type { QQMessageEvent } from "./types.js";
4
4
  export interface QQEventHandlerHandle {
@@ -1,3 +1,4 @@
1
+ import { resolvePlatformAiCommand } from "../config.js";
1
2
  import { AccessControl } from "../access/access-control.js";
2
3
  import { RequestQueue } from "../queue/request-queue.js";
3
4
  import { sendThinkingMessage, updateMessage, sendFinalMessages, sendErrorMessage, sendTextReply, sendImageReply, sendModeKeyboard, sendDirectorySelection, startTypingLoop, } from "./message-sender.js";
@@ -135,15 +136,16 @@ export function setupQQHandlers(config, sessionManager) {
135
136
  });
136
137
  }
137
138
  async function handleAIRequest(userId, chatId, prompt, workDir, convId, _threadCtx, replyToMessageId) {
138
- const toolAdapter = getAdapter(config.aiCommand);
139
+ const aiCommand = resolvePlatformAiCommand(config, "qq");
140
+ const toolAdapter = getAdapter(aiCommand);
139
141
  if (!toolAdapter) {
140
- await sendTextReply(chatId, `AI tool is not configured: ${config.aiCommand}`);
142
+ await sendTextReply(chatId, `AI tool is not configured: ${aiCommand}`);
141
143
  return;
142
144
  }
143
145
  const sessionId = convId
144
- ? sessionManager.getSessionIdForConv(userId, convId, config.aiCommand)
146
+ ? sessionManager.getSessionIdForConv(userId, convId, aiCommand)
145
147
  : undefined;
146
- const toolId = config.aiCommand;
148
+ const toolId = aiCommand;
147
149
  const msgId = await sendThinkingMessage(chatId, replyToMessageId, toolId);
148
150
  const stopTyping = startTypingLoop();
149
151
  const taskKey = `${userId}:${msgId}`;
@@ -46,6 +46,13 @@ describe("QQ event handler", () => {
46
46
  aiCommand: "codex",
47
47
  qqAllowedUserIds: ["user-1"],
48
48
  defaultPermissionMode: "ask",
49
+ platforms: {
50
+ qq: {
51
+ enabled: true,
52
+ aiCommand: "codex",
53
+ allowedUserIds: ["user-1"],
54
+ },
55
+ },
49
56
  };
50
57
  const sessionManager = {
51
58
  getWorkDir: vi.fn(() => "D:\\coding\\open-im"),
@@ -9,6 +9,7 @@ export declare function getServiceStatus(): {
9
9
  export declare function startBackgroundService(cwd: string): {
10
10
  pid: number;
11
11
  };
12
+ export declare function waitForBackgroundServiceReady(timeoutMs?: number, pollIntervalMs?: number): Promise<void>;
12
13
  export declare function stopBackgroundService(): Promise<{
13
14
  pid: number | null;
14
15
  stopped: boolean;
@@ -6,6 +6,15 @@ import { APP_HOME, SHUTDOWN_PORT } from "./constants.js";
6
6
  const __dirname = dirname(fileURLToPath(import.meta.url));
7
7
  const PID_FILE = join(APP_HOME, "open-im-worker.pid");
8
8
  const PORT_FILE = join(APP_HOME, "open-im.port");
9
+ function removePortFile() {
10
+ try {
11
+ if (existsSync(PORT_FILE))
12
+ unlinkSync(PORT_FILE);
13
+ }
14
+ catch {
15
+ /* ignore */
16
+ }
17
+ }
9
18
  function getServiceEntry() {
10
19
  const extension = extname(fileURLToPath(import.meta.url));
11
20
  if (extension === ".ts") {
@@ -64,6 +73,7 @@ export function getServiceStatus() {
64
73
  return { running: false, pid: null };
65
74
  if (!isRunning(pid)) {
66
75
  removePid();
76
+ removePortFile();
67
77
  return { running: false, pid: null };
68
78
  }
69
79
  return { running: true, pid };
@@ -74,6 +84,7 @@ export function startBackgroundService(cwd) {
74
84
  return { pid: current.pid };
75
85
  }
76
86
  removePid();
87
+ removePortFile();
77
88
  const entry = getServiceEntry();
78
89
  const child = spawn(entry.command, entry.args, {
79
90
  detached: true,
@@ -89,6 +100,20 @@ export function startBackgroundService(cwd) {
89
100
  writePid(child.pid);
90
101
  return { pid: child.pid };
91
102
  }
103
+ export async function waitForBackgroundServiceReady(timeoutMs = 8000, pollIntervalMs = 100) {
104
+ const startedAt = Date.now();
105
+ while (Date.now() - startedAt < timeoutMs) {
106
+ const status = getServiceStatus();
107
+ if (!status.running || !status.pid) {
108
+ throw new Error("Background service exited before becoming ready.");
109
+ }
110
+ if (existsSync(PORT_FILE)) {
111
+ return;
112
+ }
113
+ await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
114
+ }
115
+ throw new Error("Background service did not become ready in time.");
116
+ }
92
117
  export async function stopBackgroundService() {
93
118
  const pid = getPid();
94
119
  if (!pid)
@@ -120,12 +145,6 @@ export async function stopBackgroundService() {
120
145
  process.kill(pid, "SIGKILL");
121
146
  }
122
147
  removePid();
123
- try {
124
- if (existsSync(PORT_FILE))
125
- unlinkSync(PORT_FILE);
126
- }
127
- catch {
128
- /* ignore */
129
- }
148
+ removePortFile();
130
149
  return { pid, stopped: true };
131
150
  }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,36 @@
1
+ import { beforeEach, describe, expect, it, vi } from "vitest";
2
+ const { execFileSyncMock, existsSyncMock, readFileSyncMock, unlinkSyncMock, writeFileSyncMock, spawnMock } = vi.hoisted(() => ({
3
+ execFileSyncMock: vi.fn(),
4
+ existsSyncMock: vi.fn(),
5
+ readFileSyncMock: vi.fn(),
6
+ unlinkSyncMock: vi.fn(),
7
+ writeFileSyncMock: vi.fn(),
8
+ spawnMock: vi.fn(),
9
+ }));
10
+ vi.mock("node:child_process", () => ({
11
+ execFileSync: execFileSyncMock,
12
+ spawn: spawnMock,
13
+ }));
14
+ vi.mock("node:fs", () => ({
15
+ existsSync: existsSyncMock,
16
+ readFileSync: readFileSyncMock,
17
+ unlinkSync: unlinkSyncMock,
18
+ writeFileSync: writeFileSyncMock,
19
+ }));
20
+ import { waitForBackgroundServiceReady } from "./service-control.js";
21
+ describe("waitForBackgroundServiceReady", () => {
22
+ beforeEach(() => {
23
+ vi.clearAllMocks();
24
+ execFileSyncMock.mockReturnValue(Buffer.from("node.exe 123 Console 1 10,000 K"));
25
+ });
26
+ it("returns once the worker pid is running and the port file appears", async () => {
27
+ existsSyncMock.mockImplementation((target) => target.includes("worker.pid") || target.includes("open-im.port"));
28
+ readFileSyncMock.mockImplementation((target) => (target.includes("worker.pid") ? "123" : "39281"));
29
+ await expect(waitForBackgroundServiceReady(20, 0)).resolves.toBeUndefined();
30
+ });
31
+ it("fails if the worker never becomes ready", async () => {
32
+ existsSyncMock.mockImplementation((target) => target.includes("worker.pid"));
33
+ readFileSyncMock.mockReturnValue("123");
34
+ await expect(waitForBackgroundServiceReady(10, 0)).rejects.toThrow("Background service did not become ready in time.");
35
+ });
36
+ });
@@ -5,6 +5,9 @@ import { getPermissionMode } from '../permission-mode/session-mode.js';
5
5
  import { formatToolStats, formatToolCallNotification, getContextWarning, getAIToolDisplayName, } from './utils.js';
6
6
  import { createLogger } from '../logger.js';
7
7
  const log = createLogger('AITask');
8
+ function isUsageLimitError(error) {
9
+ return /usage limit/i.test(error) || /try again at\s+\d{1,2}:\d{2}\s*(AM|PM)/i.test(error);
10
+ }
8
11
  function buildCompletionNote(result, sessionManager, ctx, mode) {
9
12
  const toolInfo = formatToolStats(result.toolStats, result.numTurns);
10
13
  const parts = [];
@@ -34,6 +37,8 @@ export function runAITask(deps, ctx, prompt, toolAdapter, platformAdapter) {
34
37
  let firstContentLogged = false;
35
38
  let wasThinking = false;
36
39
  let thinkingText = '';
40
+ let currentSessionId = ctx.sessionId;
41
+ let activeHandle = null;
37
42
  const toolLines = [];
38
43
  const minDelta = platformAdapter.minContentDeltaChars ?? 0;
39
44
  const cleanup = () => {
@@ -79,7 +84,6 @@ export function runAITask(deps, ctx, prompt, toolAdapter, platformAdapter) {
79
84
  }
80
85
  };
81
86
  const mode = getPermissionMode(ctx.userId, config.defaultPermissionMode);
82
- process.env.CC_IM_CHAT_ID = ctx.chatId;
83
87
  let skipPermissions;
84
88
  let permissionMode;
85
89
  if (mode === 'plan') {
@@ -99,115 +103,135 @@ export function runAITask(deps, ctx, prompt, toolAdapter, platformAdapter) {
99
103
  const timeoutMs = config.aiCommand === 'codex'
100
104
  ? config.codexTimeoutMs
101
105
  : config.claudeTimeoutMs;
102
- const handle = toolAdapter.run(prompt, ctx.sessionId, ctx.workDir, {
103
- onSessionId: (id) => {
104
- if (ctx.threadId)
105
- sessionManager.setSessionIdForThread(ctx.userId, ctx.threadId, config.aiCommand, id);
106
- else if (ctx.convId)
107
- sessionManager.setSessionIdForConv(ctx.userId, ctx.convId, config.aiCommand, id);
108
- },
109
- onSessionInvalid: () => {
110
- if (ctx.convId)
111
- sessionManager.clearSessionForConv(ctx.userId, ctx.convId, config.aiCommand);
112
- },
113
- onThinking: (t) => {
114
- if (!firstContentLogged) {
115
- firstContentLogged = true;
116
- platformAdapter.onFirstContent?.();
117
- }
118
- wasThinking = true;
119
- thinkingText = t;
120
- throttledUpdate(`💭 **${getAIToolDisplayName(config.aiCommand)} 思考中...**\n\n${t}`);
121
- },
122
- onText: (accumulated) => {
123
- if (!firstContentLogged) {
124
- firstContentLogged = true;
125
- platformAdapter.onFirstContent?.();
126
- }
127
- if (wasThinking && platformAdapter.onThinkingToText) {
106
+ const startRun = () => {
107
+ activeHandle = toolAdapter.run(prompt, currentSessionId, ctx.workDir, {
108
+ onSessionId: (id) => {
109
+ currentSessionId = id;
110
+ if (ctx.threadId)
111
+ sessionManager.setSessionIdForThread(ctx.userId, ctx.threadId, config.aiCommand, id);
112
+ else if (ctx.convId)
113
+ sessionManager.setSessionIdForConv(ctx.userId, ctx.convId, config.aiCommand, id);
114
+ },
115
+ onSessionInvalid: () => {
116
+ if (ctx.convId)
117
+ sessionManager.clearSessionForConv(ctx.userId, ctx.convId, config.aiCommand);
118
+ },
119
+ onThinking: (t) => {
120
+ if (!firstContentLogged) {
121
+ firstContentLogged = true;
122
+ platformAdapter.onFirstContent?.();
123
+ }
124
+ wasThinking = true;
125
+ thinkingText = t;
126
+ throttledUpdate(`💭 **${getAIToolDisplayName(config.aiCommand)} 思考中...**\n\n${t}`);
127
+ },
128
+ onText: (accumulated) => {
129
+ if (!firstContentLogged) {
130
+ firstContentLogged = true;
131
+ platformAdapter.onFirstContent?.();
132
+ }
133
+ if (wasThinking && platformAdapter.onThinkingToText) {
134
+ wasThinking = false;
135
+ if (pendingUpdate) {
136
+ clearTimeout(pendingUpdate);
137
+ pendingUpdate = null;
138
+ }
139
+ lastUpdateTime = Date.now();
140
+ taskState.latestContent = accumulated;
141
+ platformAdapter.onThinkingToText(accumulated);
142
+ return;
143
+ }
128
144
  wasThinking = false;
145
+ throttledUpdate(accumulated);
146
+ },
147
+ onToolUse: (toolName, toolInput) => {
148
+ const notification = formatToolCallNotification(toolName, toolInput);
149
+ toolLines.push(notification);
150
+ if (toolLines.length > 5)
151
+ toolLines.shift();
152
+ throttledUpdate(taskState.latestContent, true);
153
+ },
154
+ onComplete: async (result) => {
155
+ if (settled)
156
+ return;
157
+ settled = true;
129
158
  if (pendingUpdate) {
130
159
  clearTimeout(pendingUpdate);
131
160
  pendingUpdate = null;
132
161
  }
133
- lastUpdateTime = Date.now();
134
- taskState.latestContent = accumulated;
135
- platformAdapter.onThinkingToText(accumulated);
136
- return;
137
- }
138
- wasThinking = false;
139
- throttledUpdate(accumulated);
140
- },
141
- onToolUse: (toolName, toolInput) => {
142
- const notification = formatToolCallNotification(toolName, toolInput);
143
- toolLines.push(notification);
144
- if (toolLines.length > 5)
145
- toolLines.shift();
146
- throttledUpdate(taskState.latestContent, true);
147
- },
148
- onComplete: async (result) => {
149
- if (settled)
150
- return;
151
- settled = true;
152
- if (pendingUpdate) {
153
- clearTimeout(pendingUpdate);
154
- pendingUpdate = null;
155
- }
156
- const note = buildCompletionNote(result, sessionManager, ctx, mode);
157
- const output = result.accumulated ||
158
- result.result ||
159
- taskState.latestContent ||
160
- '(无输出)';
161
- if (!result.accumulated && !result.result && taskState.latestContent) {
162
- log.warn(`Empty AI output from adapter but had streamed content (${taskState.latestContent.length} chars), using latestContent. platform=${ctx.platform}, taskKey=${ctx.taskKey}`);
163
- }
164
- else if (!output || output === '(无输出)') {
165
- log.warn(`Empty AI output for user ${ctx.userId}, platform=${ctx.platform}, taskKey=${ctx.taskKey}`);
166
- }
167
- try {
168
- await platformAdapter.sendComplete(output, note, thinkingText || undefined);
169
- }
170
- catch (err) {
171
- log.error('Failed to send complete:', err);
172
- }
173
- cleanup();
174
- resolve();
175
- },
176
- onError: async (error) => {
177
- if (settled)
178
- return;
179
- settled = true;
180
- if (pendingUpdate) {
181
- clearTimeout(pendingUpdate);
182
- pendingUpdate = null;
183
- }
184
- log.error(`Task error for user ${ctx.userId}: ${error}`);
185
- if (config.aiCommand !== 'claude') {
186
- if (ctx.convId)
187
- sessionManager.clearSessionForConv(ctx.userId, ctx.convId, config.aiCommand);
188
- else
189
- sessionManager.clearActiveToolSession(ctx.userId, config.aiCommand);
190
- log.info(`Session reset for user ${ctx.userId} due to ${config.aiCommand} task error`);
191
- }
192
- try {
193
- await platformAdapter.sendError(error);
194
- }
195
- catch (err) {
196
- log.error('Failed to send error:', err);
197
- }
198
- cleanup();
199
- resolve();
162
+ const note = buildCompletionNote(result, sessionManager, ctx, mode);
163
+ const output = result.accumulated ||
164
+ result.result ||
165
+ taskState.latestContent ||
166
+ '(无输出)';
167
+ if (!result.accumulated && !result.result && taskState.latestContent) {
168
+ log.warn(`Empty AI output from adapter but had streamed content (${taskState.latestContent.length} chars), using latestContent. platform=${ctx.platform}, taskKey=${ctx.taskKey}`);
169
+ }
170
+ else if (!output || output === '(无输出)') {
171
+ log.warn(`Empty AI output for user ${ctx.userId}, platform=${ctx.platform}, taskKey=${ctx.taskKey}`);
172
+ }
173
+ try {
174
+ await platformAdapter.sendComplete(output, note, thinkingText || undefined);
175
+ }
176
+ catch (err) {
177
+ log.error('Failed to send complete:', err);
178
+ }
179
+ cleanup();
180
+ resolve();
181
+ },
182
+ onError: async (error) => {
183
+ if (settled)
184
+ return;
185
+ if (pendingUpdate) {
186
+ clearTimeout(pendingUpdate);
187
+ pendingUpdate = null;
188
+ }
189
+ settled = true;
190
+ log.error(`Task error for user ${ctx.userId}: ${error}`);
191
+ if (config.aiCommand !== 'claude' && !isUsageLimitError(error)) {
192
+ if (ctx.convId)
193
+ sessionManager.clearSessionForConv(ctx.userId, ctx.convId, config.aiCommand);
194
+ else
195
+ sessionManager.clearActiveToolSession(ctx.userId, config.aiCommand);
196
+ log.info(`Session reset for user ${ctx.userId} due to ${config.aiCommand} task error`);
197
+ }
198
+ else if (config.aiCommand === 'codex' && isUsageLimitError(error)) {
199
+ log.info(`Keeping codex session for user ${ctx.userId} after usage limit error`);
200
+ }
201
+ try {
202
+ await platformAdapter.sendError(error);
203
+ }
204
+ catch (err) {
205
+ log.error('Failed to send error:', err);
206
+ }
207
+ cleanup();
208
+ resolve();
209
+ },
210
+ }, {
211
+ skipPermissions,
212
+ permissionMode,
213
+ timeoutMs,
214
+ model: sessionManager.getModel(ctx.userId, ctx.threadId) ?? config.claudeModel,
215
+ chatId: ctx.chatId,
216
+ ...(config.useSdkMode ? {} : { hookPort: config.hookPort }),
217
+ ...(config.aiCommand === 'codex' && config.codexProxy ? { proxy: config.codexProxy } : {}),
218
+ });
219
+ return activeHandle;
220
+ };
221
+ taskState = {
222
+ handle: {
223
+ abort: () => {
224
+ activeHandle?.abort();
225
+ cleanup();
226
+ settle();
227
+ },
200
228
  },
201
- }, {
202
- skipPermissions,
203
- permissionMode,
204
- timeoutMs,
205
- model: sessionManager.getModel(ctx.userId, ctx.threadId) ?? config.claudeModel,
206
- chatId: ctx.chatId,
207
- ...(config.useSdkMode ? {} : { hookPort: config.hookPort }),
208
- ...(config.aiCommand === 'codex' && config.codexProxy ? { proxy: config.codexProxy } : {}),
209
- });
210
- taskState = { handle, latestContent: '', settle, startedAt: Date.now(), toolId: config.aiCommand };
229
+ latestContent: '',
230
+ settle,
231
+ startedAt: Date.now(),
232
+ toolId: config.aiCommand,
233
+ };
234
+ startRun();
211
235
  platformAdapter.onTaskReady(taskState);
212
236
  });
213
237
  }
@@ -0,0 +1 @@
1
+ export {};