@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.
- package/dist/adapters/registry.d.ts +1 -1
- package/dist/adapters/registry.js +23 -18
- package/dist/commands/handler.d.ts +1 -1
- package/dist/commands/handler.js +10 -8
- package/dist/config-web-page-i18n.d.ts +4 -0
- package/dist/config-web-page-i18n.js +4 -0
- package/dist/config-web-page-script.js +16 -5
- package/dist/config-web-page-template.js +5 -0
- package/dist/config-web.d.ts +7 -0
- package/dist/config-web.js +60 -2
- package/dist/config-web.test.js +9 -1
- package/dist/config.d.ts +14 -0
- package/dist/config.js +29 -1
- package/dist/dingtalk/event-handler.d.ts +1 -1
- package/dist/dingtalk/event-handler.js +7 -5
- package/dist/feishu/event-handler.d.ts +1 -1
- package/dist/feishu/event-handler.js +8 -6
- package/dist/hook/permission-server.d.ts +1 -0
- package/dist/hook/permission-server.js +13 -7
- package/dist/hook/permission-server.test.d.ts +1 -0
- package/dist/hook/permission-server.test.js +12 -0
- package/dist/index.js +7 -10
- package/dist/manager.js +2 -1
- package/dist/qq/event-handler.d.ts +1 -1
- package/dist/qq/event-handler.js +6 -4
- package/dist/qq/event-handler.test.js +7 -0
- package/dist/service-control.d.ts +1 -0
- package/dist/service-control.js +26 -7
- package/dist/service-control.test.d.ts +1 -0
- package/dist/service-control.test.js +36 -0
- package/dist/shared/ai-task.js +128 -104
- package/dist/shared/ai-task.test.d.ts +1 -0
- package/dist/shared/ai-task.test.js +69 -0
- package/dist/telegram/event-handler.d.ts +1 -1
- package/dist/telegram/event-handler.js +8 -6
- package/dist/wechat/event-handler.d.ts +1 -1
- package/dist/wechat/event-handler.js +8 -6
- package/dist/wework/event-handler.d.ts +1 -1
- package/dist/wework/event-handler.js +8 -6
- 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
|
|
179
|
+
const aiCommand = resolvePlatformAiCommand(config, 'dingtalk');
|
|
180
|
+
const toolAdapter = getAdapter(aiCommand);
|
|
179
181
|
if (!toolAdapter) {
|
|
180
|
-
await sendTextReply(chatId, `AI tool is not configured: ${
|
|
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,
|
|
186
|
+
? sessionManager.getSessionIdForConv(userId, convId, aiCommand)
|
|
185
187
|
: undefined;
|
|
186
|
-
log.info(`[AI_REQUEST] Running ${
|
|
187
|
-
const toolId =
|
|
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,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
|
|
95
|
+
const aiCommand = resolvePlatformAiCommand(config, 'feishu');
|
|
96
|
+
const toolAdapter = getAdapter(aiCommand);
|
|
95
97
|
if (!toolAdapter) {
|
|
96
|
-
log.error(`[handleAIRequest] No adapter found for: ${
|
|
97
|
-
await sendTextReply(chatId, `未配置 AI 工具: ${
|
|
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,
|
|
102
|
-
log.info(`[handleAIRequest] Running ${
|
|
103
|
-
const toolId =
|
|
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 = (
|
|
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(
|
|
167
|
-
res.writeHead(
|
|
168
|
-
res.end(JSON.stringify({
|
|
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,
|
|
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
|
-
`-
|
|
95
|
-
`-
|
|
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.
|
|
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
|
-
|
|
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
|
|
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));
|
package/dist/qq/event-handler.js
CHANGED
|
@@ -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
|
|
139
|
+
const aiCommand = resolvePlatformAiCommand(config, "qq");
|
|
140
|
+
const toolAdapter = getAdapter(aiCommand);
|
|
139
141
|
if (!toolAdapter) {
|
|
140
|
-
await sendTextReply(chatId, `AI tool is not configured: ${
|
|
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,
|
|
146
|
+
? sessionManager.getSessionIdForConv(userId, convId, aiCommand)
|
|
145
147
|
: undefined;
|
|
146
|
-
const toolId =
|
|
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;
|
package/dist/service-control.js
CHANGED
|
@@ -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
|
-
|
|
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
|
+
});
|
package/dist/shared/ai-task.js
CHANGED
|
@@ -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
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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 {};
|