@wu529778790/open-im 1.11.8-beta.2 → 1.11.8-beta.20
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/codebuddy-adapter.js +3 -17
- package/dist/adapters/codex-adapter.js +3 -16
- package/dist/adapters/opencode-adapter.d.ts +0 -5
- package/dist/adapters/opencode-adapter.js +10 -25
- package/dist/adapters/registry.js +10 -1
- package/dist/clawbot/client.js +72 -78
- package/dist/clawbot/qr-login.d.ts +1 -0
- package/dist/clawbot/qr-login.js +7 -0
- package/dist/codebuddy/cli-runner.d.ts +2 -22
- package/dist/codebuddy/cli-runner.js +9 -27
- package/dist/codex/cli-runner.d.ts +2 -22
- package/dist/codex/cli-runner.js +24 -75
- package/dist/commands/handler.d.ts +1 -0
- package/dist/commands/handler.js +34 -0
- package/dist/config/types.d.ts +23 -0
- package/dist/config-web-auth.d.ts +13 -0
- package/dist/config-web-auth.js +114 -0
- package/dist/config-web-browser.d.ts +4 -0
- package/dist/config-web-browser.js +48 -0
- package/dist/config-web-cors.d.ts +5 -0
- package/dist/config-web-cors.js +48 -0
- package/dist/config-web-health.d.ts +7 -0
- package/dist/config-web-health.js +65 -0
- package/dist/config-web-http.d.ts +3 -0
- package/dist/config-web-http.js +31 -0
- package/dist/config-web-page-i18n.d.ts +36 -2
- package/dist/config-web-page-i18n.js +36 -2
- package/dist/config-web-payload.d.ts +84 -0
- package/dist/config-web-payload.js +260 -0
- package/dist/config-web-probes.d.ts +2 -0
- package/dist/config-web-probes.js +271 -0
- package/dist/config-web.d.ts +3 -11
- package/dist/config-web.js +45 -815
- package/dist/config.js +22 -1
- package/dist/constants.d.ts +10 -0
- package/dist/constants.js +11 -0
- package/dist/opencode/cli-runner.d.ts +2 -22
- package/dist/opencode/cli-runner.js +47 -68
- package/dist/opencode/sdk-manager.d.ts +11 -0
- package/dist/opencode/sdk-manager.js +46 -0
- package/dist/opencode/sdk-runner.d.ts +7 -0
- package/dist/opencode/sdk-runner.js +225 -0
- package/dist/platform/handle-ai-request.js +18 -1
- package/dist/qq/client.js +45 -53
- package/dist/shared/ai-task.d.ts +15 -0
- package/dist/shared/ai-task.js +174 -2
- package/dist/shared/cli-runner-base.d.ts +39 -0
- package/dist/shared/cli-runner-base.js +108 -0
- package/dist/shared/connection-manager.d.ts +111 -0
- package/dist/shared/connection-manager.js +157 -0
- package/dist/shared/session-invalid-detector.d.ts +5 -0
- package/dist/shared/session-invalid-detector.js +20 -0
- package/dist/wework/client.js +79 -127
- package/dist/workbuddy/client.js +40 -74
- package/package.json +4 -1
- package/web/dist/assets/index-6e51Wtaa.css +1 -0
- package/web/dist/assets/index-D-gU5A1y.js +57 -0
- package/web/dist/index.html +2 -2
- package/web/dist/assets/index-XKVTb-0p.css +0 -1
- package/web/dist/assets/index-yDVGC_84.js +0 -57
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { runCodeBuddy } from '../codebuddy/cli-runner.js';
|
|
2
|
+
import { isSessionInvalidMessage } from '../shared/session-invalid-detector.js';
|
|
2
3
|
export class CodeBuddyAdapter {
|
|
3
4
|
cliPath;
|
|
4
5
|
toolId = 'codebuddy';
|
|
@@ -10,25 +11,10 @@ export class CodeBuddyAdapter {
|
|
|
10
11
|
onText: callbacks.onText,
|
|
11
12
|
onThinking: callbacks.onThinking,
|
|
12
13
|
onToolUse: callbacks.onToolUse,
|
|
13
|
-
onComplete:
|
|
14
|
-
const result = {
|
|
15
|
-
success: raw.success,
|
|
16
|
-
result: raw.result,
|
|
17
|
-
accumulated: raw.accumulated,
|
|
18
|
-
cost: raw.cost,
|
|
19
|
-
durationMs: raw.durationMs,
|
|
20
|
-
model: raw.model,
|
|
21
|
-
numTurns: raw.numTurns,
|
|
22
|
-
toolStats: raw.toolStats,
|
|
23
|
-
};
|
|
24
|
-
callbacks.onComplete(result);
|
|
25
|
-
},
|
|
14
|
+
onComplete: callbacks.onComplete,
|
|
26
15
|
onError: (err) => {
|
|
27
16
|
const msg = typeof err === 'string' ? err : String(err);
|
|
28
|
-
const friendly = msg
|
|
29
|
-
msg.includes('Session not found') ||
|
|
30
|
-
msg.includes('Invalid session') ||
|
|
31
|
-
msg.includes('Unable to resume')
|
|
17
|
+
const friendly = isSessionInvalidMessage(msg)
|
|
32
18
|
? 'CodeBuddy 会话已失效,旧 session 已清理。请直接重试当前请求。'
|
|
33
19
|
: msg;
|
|
34
20
|
callbacks.onError(friendly);
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Codex Adapter - run tasks through OpenAI Codex CLI (`codex exec`)
|
|
3
3
|
*/
|
|
4
4
|
import { runCodex } from "../codex/cli-runner.js";
|
|
5
|
+
import { isSessionInvalidMessage } from "../shared/session-invalid-detector.js";
|
|
5
6
|
export class CodexAdapter {
|
|
6
7
|
cliPath;
|
|
7
8
|
toolId = "codex";
|
|
@@ -21,24 +22,10 @@ export class CodexAdapter {
|
|
|
21
22
|
onText: callbacks.onText,
|
|
22
23
|
onThinking: callbacks.onThinking,
|
|
23
24
|
onToolUse: callbacks.onToolUse,
|
|
24
|
-
onComplete:
|
|
25
|
-
const result = {
|
|
26
|
-
success: raw.success,
|
|
27
|
-
result: raw.result,
|
|
28
|
-
accumulated: raw.accumulated,
|
|
29
|
-
cost: raw.cost,
|
|
30
|
-
durationMs: raw.durationMs,
|
|
31
|
-
model: raw.model,
|
|
32
|
-
numTurns: raw.numTurns,
|
|
33
|
-
toolStats: raw.toolStats,
|
|
34
|
-
};
|
|
35
|
-
callbacks.onComplete(result);
|
|
36
|
-
},
|
|
25
|
+
onComplete: callbacks.onComplete,
|
|
37
26
|
onError: (err) => {
|
|
38
27
|
const msg = typeof err === "string" ? err : String(err);
|
|
39
|
-
const friendly = msg
|
|
40
|
-
msg.includes("No conversation found") ||
|
|
41
|
-
msg.includes("Unable to find session")
|
|
28
|
+
const friendly = isSessionInvalidMessage(msg)
|
|
42
29
|
? "Codex 会话已失效,旧 session 已清理。请直接重试当前请求。"
|
|
43
30
|
: msg;
|
|
44
31
|
callbacks.onError(friendly);
|
|
@@ -1,10 +1,5 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* OpenCode Adapter — run tasks through OpenCode CLI (`opencode run`)
|
|
3
|
-
*/
|
|
4
1
|
import type { RunCallbacks, RunHandle, RunOptions, ToolAdapter } from './tool-adapter.interface.js';
|
|
5
2
|
export declare class OpenCodeAdapter implements ToolAdapter {
|
|
6
|
-
private cliPath;
|
|
7
3
|
readonly toolId = "opencode";
|
|
8
|
-
constructor(cliPath: string);
|
|
9
4
|
run(prompt: string, sessionId: string | undefined, workDir: string, callbacks: RunCallbacks, options?: RunOptions): RunHandle;
|
|
10
5
|
}
|
|
@@ -1,36 +1,16 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
*/
|
|
4
|
-
import { runOpenCode } from '../opencode/cli-runner.js';
|
|
1
|
+
import { runOpenCodeSdk } from '../opencode/sdk-runner.js';
|
|
2
|
+
import { isSessionInvalidMessage } from '../shared/session-invalid-detector.js';
|
|
5
3
|
export class OpenCodeAdapter {
|
|
6
|
-
cliPath;
|
|
7
4
|
toolId = 'opencode';
|
|
8
|
-
constructor(cliPath) {
|
|
9
|
-
this.cliPath = cliPath;
|
|
10
|
-
}
|
|
11
5
|
run(prompt, sessionId, workDir, callbacks, options) {
|
|
12
|
-
|
|
6
|
+
const handle = runOpenCodeSdk(prompt, sessionId, workDir, {
|
|
13
7
|
onText: callbacks.onText,
|
|
14
8
|
onThinking: callbacks.onThinking,
|
|
15
9
|
onToolUse: callbacks.onToolUse,
|
|
16
|
-
onComplete:
|
|
17
|
-
const result = {
|
|
18
|
-
success: raw.success,
|
|
19
|
-
result: raw.result,
|
|
20
|
-
accumulated: raw.accumulated,
|
|
21
|
-
cost: raw.cost,
|
|
22
|
-
durationMs: raw.durationMs,
|
|
23
|
-
model: raw.model,
|
|
24
|
-
numTurns: raw.numTurns,
|
|
25
|
-
toolStats: raw.toolStats,
|
|
26
|
-
};
|
|
27
|
-
callbacks.onComplete(result);
|
|
28
|
-
},
|
|
10
|
+
onComplete: callbacks.onComplete,
|
|
29
11
|
onError: (err) => {
|
|
30
12
|
const msg = typeof err === 'string' ? err : String(err);
|
|
31
|
-
const friendly = msg
|
|
32
|
-
msg.includes('Session not found') ||
|
|
33
|
-
msg.includes('no sessions found')
|
|
13
|
+
const friendly = isSessionInvalidMessage(msg)
|
|
34
14
|
? 'OpenCode 会话已失效,旧 session 已清理。请直接重试当前请求。'
|
|
35
15
|
: msg;
|
|
36
16
|
callbacks.onError(friendly);
|
|
@@ -41,5 +21,10 @@ export class OpenCodeAdapter {
|
|
|
41
21
|
skipPermissions: options?.skipPermissions,
|
|
42
22
|
model: options?.model,
|
|
43
23
|
});
|
|
24
|
+
return {
|
|
25
|
+
abort: () => {
|
|
26
|
+
handle.then(h => h.abort());
|
|
27
|
+
},
|
|
28
|
+
};
|
|
44
29
|
}
|
|
45
30
|
}
|
|
@@ -3,6 +3,7 @@ import { ClaudeSDKAdapter } from './claude-sdk-adapter.js';
|
|
|
3
3
|
import { CodexAdapter } from './codex-adapter.js';
|
|
4
4
|
import { CodeBuddyAdapter } from './codebuddy-adapter.js';
|
|
5
5
|
import { OpenCodeAdapter } from './opencode-adapter.js';
|
|
6
|
+
import { startOpencode, stopOpencode, isOpencodeRunning } from '../opencode/sdk-manager.js';
|
|
6
7
|
import { createLogger } from '../logger.js';
|
|
7
8
|
import { destroyAllLiveChildren } from '../shared/process-kill.js';
|
|
8
9
|
const log = createLogger('Registry');
|
|
@@ -15,7 +16,7 @@ const ADAPTER_FACTORIES = {
|
|
|
15
16
|
claude: () => new ClaudeSDKAdapter(),
|
|
16
17
|
codex: (c) => new CodexAdapter(c.codexCliPath),
|
|
17
18
|
codebuddy: (c) => new CodeBuddyAdapter(c.codebuddyCliPath),
|
|
18
|
-
opencode: (
|
|
19
|
+
opencode: () => new OpenCodeAdapter(),
|
|
19
20
|
};
|
|
20
21
|
export function initAdapters(config) {
|
|
21
22
|
adapters.clear();
|
|
@@ -27,6 +28,12 @@ export function initAdapters(config) {
|
|
|
27
28
|
}
|
|
28
29
|
log.info(`${aiCommand} adapter enabled`);
|
|
29
30
|
adapters.set(aiCommand, factory(config));
|
|
31
|
+
// SDK 工具需要懒启动 server,这里先触发预热
|
|
32
|
+
if (aiCommand === 'opencode' && !isOpencodeRunning()) {
|
|
33
|
+
startOpencode().catch((err) => {
|
|
34
|
+
log.warn(`OpenCode SDK server prewarm failed (will retry on first use): ${err}`);
|
|
35
|
+
});
|
|
36
|
+
}
|
|
30
37
|
}
|
|
31
38
|
}
|
|
32
39
|
export function getAdapter(aiCommand) {
|
|
@@ -34,6 +41,8 @@ export function getAdapter(aiCommand) {
|
|
|
34
41
|
}
|
|
35
42
|
export function cleanupAdapters() {
|
|
36
43
|
ClaudeSDKAdapter.destroy();
|
|
44
|
+
// 关闭 opencode SDK server
|
|
45
|
+
stopOpencode();
|
|
37
46
|
// 强制终止仍在运行的 CLI 子进程(Codex/CodeBuddy),避免僵尸 / 孤儿
|
|
38
47
|
destroyAllLiveChildren();
|
|
39
48
|
adapters.clear();
|
package/dist/clawbot/client.js
CHANGED
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { randomBytes } from 'node:crypto';
|
|
11
11
|
import { createLogger } from '../logger.js';
|
|
12
|
-
import {
|
|
12
|
+
import { isFatalReconnectError } from '../shared/reconnect.js';
|
|
13
|
+
import { ReconnectManager, HeartbeatMonitor, StateManager } from '../shared/connection-manager.js';
|
|
13
14
|
import { cacheContextToken } from './message-sender.js';
|
|
14
15
|
import { setClawbotContextToken, clearClawbotContextToken } from '../shared/active-chats.js';
|
|
15
16
|
import { createMediaTargetPath } from '../shared/media-storage.js';
|
|
@@ -19,27 +20,30 @@ const log = createLogger('ClawBot');
|
|
|
19
20
|
const RECONNECT_DELAYS_MS = [3000, 5000, 10000, 20000, 30000];
|
|
20
21
|
const BASE_INFO = { channel_version: '0.1.0' };
|
|
21
22
|
let pollController = null;
|
|
22
|
-
let channelState = 'disconnected';
|
|
23
23
|
let messageHandler = null;
|
|
24
|
-
let stateChangeHandler = null;
|
|
25
|
-
let reconnectTimer = null;
|
|
26
|
-
let watchdogTimer = null;
|
|
27
|
-
let reconnectAttempt = 0;
|
|
28
|
-
let fatal = false;
|
|
29
24
|
let stopped = false;
|
|
30
25
|
let apiUrl = 'https://ilinkai.weixin.qq.com';
|
|
31
26
|
let apiToken = '';
|
|
32
27
|
/** Opaque cursor for getupdates pagination (replaces numeric offset) */
|
|
33
28
|
let getUpdatesBuf = '';
|
|
34
|
-
/** Timestamp of last successful poll response (for watchdog) */
|
|
35
|
-
let lastResponseAt = 0;
|
|
36
29
|
/** Per-request timeout for long-polling (ms) */
|
|
37
30
|
const POLL_REQUEST_TIMEOUT_MS = 3 * 60 * 1000; // 3 minutes
|
|
38
31
|
/** Watchdog interval: force reconnect if no response for this long (ms) */
|
|
39
32
|
const WATCHDOG_INTERVAL_MS = 60_000; // check every 60s
|
|
40
33
|
const WATCHDOG_STALE_MS = 5 * 60 * 1000; // 5 minutes without response = stale
|
|
34
|
+
// Connection infrastructure (shared managers replace raw timers/counters)
|
|
35
|
+
const stateManager = new StateManager('disconnected');
|
|
36
|
+
const heartbeatMonitor = new HeartbeatMonitor();
|
|
37
|
+
const reconnectManager = new ReconnectManager({
|
|
38
|
+
name: 'ClawBot',
|
|
39
|
+
backoff: { mode: 'stepped', delays: RECONNECT_DELAYS_MS },
|
|
40
|
+
onReconnect: () => {
|
|
41
|
+
stateManager.set('connected');
|
|
42
|
+
startPolling();
|
|
43
|
+
},
|
|
44
|
+
});
|
|
41
45
|
export function getChannelState() {
|
|
42
|
-
return
|
|
46
|
+
return stateManager.current;
|
|
43
47
|
}
|
|
44
48
|
export async function initClawbot(config, eventHandler, onStateChange) {
|
|
45
49
|
const pc = config.platforms?.clawbot;
|
|
@@ -52,14 +56,14 @@ export async function initClawbot(config, eventHandler, onStateChange) {
|
|
|
52
56
|
apiUrl = pc.apiUrl ?? 'https://ilinkai.weixin.qq.com';
|
|
53
57
|
apiToken = pc.apiToken;
|
|
54
58
|
messageHandler = eventHandler;
|
|
55
|
-
|
|
59
|
+
stateManager.setOnChange(onStateChange);
|
|
56
60
|
stopped = false;
|
|
57
|
-
|
|
58
|
-
|
|
61
|
+
reconnectManager.reset();
|
|
62
|
+
reconnectManager.resume();
|
|
59
63
|
getUpdatesBuf = '';
|
|
60
64
|
// Start polling directly — no blocking connectivity check.
|
|
61
65
|
// The polling loop handles errors and reconnection internally.
|
|
62
|
-
|
|
66
|
+
stateManager.set('connected');
|
|
63
67
|
startPolling();
|
|
64
68
|
log.info('ClawBot client initialized');
|
|
65
69
|
}
|
|
@@ -82,18 +86,18 @@ function startPolling() {
|
|
|
82
86
|
base_info: BASE_INFO,
|
|
83
87
|
}, combinedSignal);
|
|
84
88
|
// Record successful response time for watchdog
|
|
85
|
-
|
|
89
|
+
heartbeatMonitor.recordResponse();
|
|
86
90
|
if (signal.aborted)
|
|
87
91
|
break;
|
|
88
92
|
if (!res.ok) {
|
|
89
93
|
// Detect fatal errors (e.g. errcode -14 "session timeout") — retrying won't help
|
|
90
94
|
if (res.errcode === -14 || isFatalReconnectError(res.error)) {
|
|
91
95
|
log.warn(`ClawBot fatal error (errcode=${res.errcode}), entering slow-probe mode`);
|
|
92
|
-
|
|
96
|
+
reconnectManager.setFatal(true);
|
|
93
97
|
getUpdatesBuf = ''; // session expired, cursor is stale
|
|
94
98
|
clearClawbotContextToken(); // session expired, token is stale
|
|
95
|
-
|
|
96
|
-
|
|
99
|
+
stateManager.set('error');
|
|
100
|
+
reconnectManager.schedule();
|
|
97
101
|
return;
|
|
98
102
|
}
|
|
99
103
|
log.warn(`ClawBot getupdates error: ${res.error ?? 'unknown'}`);
|
|
@@ -101,10 +105,9 @@ function startPolling() {
|
|
|
101
105
|
continue;
|
|
102
106
|
}
|
|
103
107
|
// Successful response — clear fatal mode and reset backoff
|
|
104
|
-
if (fatal ||
|
|
108
|
+
if (reconnectManager.fatal || reconnectManager.attemptCount > 0) {
|
|
105
109
|
log.info('ClawBot connection recovered');
|
|
106
|
-
|
|
107
|
-
reconnectAttempt = 0;
|
|
110
|
+
reconnectManager.reset();
|
|
108
111
|
}
|
|
109
112
|
// Update cursor for next poll
|
|
110
113
|
if (res.updatesBuf) {
|
|
@@ -122,6 +125,17 @@ function startPolling() {
|
|
|
122
125
|
const extracted = extractTextContent(msg);
|
|
123
126
|
if (!extracted)
|
|
124
127
|
continue;
|
|
128
|
+
// Skip echoed bot messages (iLink API echoes bot's own messages as USER type)
|
|
129
|
+
// 1) Messages sent by this bot have client_id starting with "open-im:"
|
|
130
|
+
if (msg.client_id?.startsWith('open-im:')) {
|
|
131
|
+
log.debug(`Skipping echoed bot message: client_id=${msg.client_id}`);
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
// 2) Bot lifecycle notifications and tool call notifications echoed back
|
|
135
|
+
if (isBotNotificationEcho(extracted)) {
|
|
136
|
+
log.debug(`Skipping bot notification echo: content="${extracted.substring(0, 80)}"`);
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
125
139
|
const chatId = msg.from_user_id ?? '';
|
|
126
140
|
const msgId = String(msg.message_id ?? msg.seq ?? '');
|
|
127
141
|
if (!chatId) {
|
|
@@ -178,69 +192,35 @@ function startPolling() {
|
|
|
178
192
|
if (err instanceof Error && err.name === 'AbortError')
|
|
179
193
|
break;
|
|
180
194
|
log.error('ClawBot polling error:', err);
|
|
181
|
-
|
|
182
|
-
|
|
195
|
+
stateManager.set('error');
|
|
196
|
+
reconnectManager.schedule();
|
|
183
197
|
return;
|
|
184
198
|
}
|
|
185
199
|
}
|
|
186
200
|
})();
|
|
187
201
|
}
|
|
188
|
-
function scheduleReconnect() {
|
|
189
|
-
if (stopped)
|
|
190
|
-
return;
|
|
191
|
-
if (reconnectTimer) {
|
|
192
|
-
clearTimeout(reconnectTimer);
|
|
193
|
-
reconnectTimer = null;
|
|
194
|
-
}
|
|
195
|
-
const baseDelay = RECONNECT_DELAYS_MS[Math.min(reconnectAttempt, RECONNECT_DELAYS_MS.length - 1)];
|
|
196
|
-
const delay = fatal ? jitteredDelay(SLOW_PROBE_MS) : jitteredDelay(baseDelay);
|
|
197
|
-
reconnectAttempt++;
|
|
198
|
-
if (fatal) {
|
|
199
|
-
log.warn(`ClawBot fatal error, slow-probe in ${Math.round(delay / 1000)}s (attempt ${reconnectAttempt})...`);
|
|
200
|
-
}
|
|
201
|
-
else {
|
|
202
|
-
log.info(`ClawBot reconnecting in ${delay}ms (attempt ${reconnectAttempt})...`);
|
|
203
|
-
}
|
|
204
|
-
reconnectTimer = setTimeout(() => {
|
|
205
|
-
reconnectTimer = null;
|
|
206
|
-
if (stopped)
|
|
207
|
-
return;
|
|
208
|
-
updateState('connected');
|
|
209
|
-
startPolling();
|
|
210
|
-
}, delay);
|
|
211
|
-
}
|
|
212
|
-
function updateState(state) {
|
|
213
|
-
channelState = state;
|
|
214
|
-
stateChangeHandler?.(state);
|
|
215
|
-
log.debug(`ClawBot state: ${state}`);
|
|
216
|
-
}
|
|
217
202
|
/**
|
|
218
203
|
* Watchdog: periodically check if the poll loop is alive.
|
|
219
204
|
* After Mac sleep or network drop, the fetch may hang without throwing.
|
|
220
205
|
* If no successful response for WATCHDOG_STALE_MS, force a reconnect.
|
|
221
206
|
*/
|
|
222
207
|
function startWatchdog() {
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
watchdogTimer = setInterval(() => {
|
|
226
|
-
if (stopped)
|
|
227
|
-
return;
|
|
228
|
-
const elapsed = Date.now() - lastResponseAt;
|
|
229
|
-
if (elapsed > WATCHDOG_STALE_MS) {
|
|
230
|
-
log.warn(`ClawBot watchdog: no response for ${Math.round(elapsed / 1000)}s, forcing reconnect`);
|
|
231
|
-
if (pollController) {
|
|
232
|
-
pollController.abort();
|
|
233
|
-
pollController = null;
|
|
234
|
-
}
|
|
235
|
-
updateState('error');
|
|
236
|
-
scheduleReconnect();
|
|
237
|
-
}
|
|
238
|
-
}, WATCHDOG_INTERVAL_MS);
|
|
208
|
+
heartbeatMonitor.recordResponse();
|
|
209
|
+
heartbeatMonitor.start(WATCHDOG_INTERVAL_MS, watchdogTick);
|
|
239
210
|
}
|
|
240
|
-
function
|
|
241
|
-
if (
|
|
242
|
-
|
|
243
|
-
|
|
211
|
+
function watchdogTick() {
|
|
212
|
+
if (stopped)
|
|
213
|
+
return;
|
|
214
|
+
if (heartbeatMonitor.isStale(WATCHDOG_STALE_MS)) {
|
|
215
|
+
const elapsed = Date.now() - heartbeatMonitor.lastResponseTime;
|
|
216
|
+
log.warn(`ClawBot watchdog: no response for ${Math.round(elapsed / 1000)}s, forcing reconnect`);
|
|
217
|
+
heartbeatMonitor.stop();
|
|
218
|
+
if (pollController) {
|
|
219
|
+
pollController.abort();
|
|
220
|
+
pollController = null;
|
|
221
|
+
}
|
|
222
|
+
stateManager.set('error');
|
|
223
|
+
reconnectManager.schedule();
|
|
244
224
|
}
|
|
245
225
|
}
|
|
246
226
|
/**
|
|
@@ -313,6 +293,23 @@ async function extractImages(msg) {
|
|
|
313
293
|
}
|
|
314
294
|
return paths;
|
|
315
295
|
}
|
|
296
|
+
/**
|
|
297
|
+
* Check if extracted text is a bot notification being echoed back by the iLink API.
|
|
298
|
+
* Bot notifications have distinctive emoji prefixes that are unlikely in real user messages.
|
|
299
|
+
*/
|
|
300
|
+
function isBotNotificationEcho(text) {
|
|
301
|
+
const firstLine = text.split('\n')[0]?.trim() ?? '';
|
|
302
|
+
// Lifecycle/AI info notifications
|
|
303
|
+
if (firstLine.startsWith('🤖 AI:'))
|
|
304
|
+
return true;
|
|
305
|
+
// Service status notifications
|
|
306
|
+
if (firstLine.startsWith('✅ ') || firstLine.startsWith('🔄 ') || firstLine.startsWith('ℹ️ '))
|
|
307
|
+
return true;
|
|
308
|
+
// Tool call notifications (sent by streamUpdate in event-handler)
|
|
309
|
+
if (firstLine.startsWith('⚙️ '))
|
|
310
|
+
return true;
|
|
311
|
+
return false;
|
|
312
|
+
}
|
|
316
313
|
/**
|
|
317
314
|
* Extract text content from an iLink message's item_list.
|
|
318
315
|
* Returns the first text item found, or a placeholder for media types.
|
|
@@ -413,13 +410,10 @@ export function stopClawbot() {
|
|
|
413
410
|
pollController.abort();
|
|
414
411
|
pollController = null;
|
|
415
412
|
}
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
reconnectTimer = null;
|
|
419
|
-
}
|
|
420
|
-
stopWatchdog();
|
|
413
|
+
reconnectManager.stop();
|
|
414
|
+
heartbeatMonitor.stop();
|
|
421
415
|
messageHandler = null;
|
|
422
416
|
// Don't clear context_token here — it's persisted for startup notifications
|
|
423
|
-
|
|
417
|
+
stateManager.set('disconnected');
|
|
424
418
|
log.info('ClawBot client stopped');
|
|
425
419
|
}
|
package/dist/clawbot/qr-login.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* Returns bot_token on success.
|
|
6
6
|
*/
|
|
7
7
|
import { randomUUID } from 'node:crypto';
|
|
8
|
+
import QRCode from 'qrcode';
|
|
8
9
|
import { createLogger } from '../logger.js';
|
|
9
10
|
const log = createLogger('ClawBotQR');
|
|
10
11
|
const ILINK_BASE_URL = 'https://ilinkai.weixin.qq.com';
|
|
@@ -64,10 +65,16 @@ export async function startQRLogin() {
|
|
|
64
65
|
log.info('Starting ClawBot QR login...');
|
|
65
66
|
const { qrcode, qrcodeUrl } = await fetchQRCode();
|
|
66
67
|
log.info(`QR code received, url=${qrcodeUrl}`);
|
|
68
|
+
// iLink 返回的 qrcodeUrl 是一个 HTML 中转页(X-Frame-Options: SAMEORIGIN,
|
|
69
|
+
// Content-Type: text/html),前端无法用 <img> 或 iframe 直接展示。
|
|
70
|
+
// 把这个 URL 本身编码成二维码 PNG——用户扫码后微信打开中转页完成绑定。
|
|
71
|
+
const qrcodeImage = await QRCode.toDataURL(qrcodeUrl, { width: 280, margin: 1 });
|
|
72
|
+
log.info('QR code PNG generated (base64)');
|
|
67
73
|
return {
|
|
68
74
|
sessionKey: randomUUID(),
|
|
69
75
|
qrcode,
|
|
70
76
|
qrcodeUrl,
|
|
77
|
+
qrcodeImage,
|
|
71
78
|
startedAt: Date.now(),
|
|
72
79
|
};
|
|
73
80
|
}
|
|
@@ -1,29 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
onText: (accumulated: string) => void;
|
|
3
|
-
onThinking?: (accumulated: string) => void;
|
|
4
|
-
onToolUse?: (toolName: string, toolInput?: Record<string, unknown>) => void;
|
|
5
|
-
onComplete: (result: {
|
|
6
|
-
success: boolean;
|
|
7
|
-
result: string;
|
|
8
|
-
accumulated: string;
|
|
9
|
-
cost: number;
|
|
10
|
-
durationMs: number;
|
|
11
|
-
model?: string;
|
|
12
|
-
numTurns: number;
|
|
13
|
-
toolStats: Record<string, number>;
|
|
14
|
-
}) => void;
|
|
15
|
-
onError: (error: string) => void;
|
|
16
|
-
onSessionId?: (sessionId: string) => void;
|
|
17
|
-
onSessionInvalid?: () => void;
|
|
18
|
-
}
|
|
1
|
+
import type { RunCallbacks, RunHandle } from '../adapters/tool-adapter.interface.js';
|
|
19
2
|
export interface CodeBuddyRunOptions {
|
|
20
3
|
skipPermissions?: boolean;
|
|
21
4
|
permissionMode?: 'default' | 'acceptEdits' | 'plan';
|
|
22
5
|
model?: string;
|
|
23
6
|
}
|
|
24
|
-
export interface CodeBuddyRunHandle {
|
|
25
|
-
abort: () => void;
|
|
26
|
-
}
|
|
27
7
|
export declare function buildCodeBuddyArgs(prompt: string, sessionId: string | undefined, options?: CodeBuddyRunOptions): string[];
|
|
28
8
|
export declare function extractBufferedPayloads(state: {
|
|
29
9
|
buffer: string;
|
|
@@ -41,4 +21,4 @@ export declare function flushBufferedPayloads(state: {
|
|
|
41
21
|
* - 其他情况 → 拼接(不丢内容)
|
|
42
22
|
*/
|
|
43
23
|
export declare function mergeAssistantReply(previous: string, incoming: string): string;
|
|
44
|
-
export declare function runCodeBuddy(cliPath: string, prompt: string, sessionId: string | undefined, workDir: string, callbacks:
|
|
24
|
+
export declare function runCodeBuddy(cliPath: string, prompt: string, sessionId: string | undefined, workDir: string, callbacks: RunCallbacks, options?: CodeBuddyRunOptions): RunHandle;
|
|
@@ -2,8 +2,9 @@ import { execFileSync, spawn } from 'node:child_process';
|
|
|
2
2
|
import { accessSync, constants } from 'node:fs';
|
|
3
3
|
import { isAbsolute, join } from 'node:path';
|
|
4
4
|
import { createLogger } from '../logger.js';
|
|
5
|
-
import { processEnvForNonClaudeCliChild } from '../config/file-io.js';
|
|
6
5
|
import { killProcessTree, trackChild } from '../shared/process-kill.js';
|
|
6
|
+
import { isSessionInvalidMessage } from '../shared/session-invalid-detector.js';
|
|
7
|
+
import { clearWallClockTimeout, buildBaseSpawnOptions } from '../shared/cli-runner-base.js';
|
|
7
8
|
const log = createLogger('CodeBuddyCli');
|
|
8
9
|
export function buildCodeBuddyArgs(prompt, sessionId, options) {
|
|
9
10
|
const args = ['--print', '--output-format', 'stream-json'];
|
|
@@ -190,24 +191,12 @@ export function runCodeBuddy(cliPath, prompt, sessionId, workDir, callbacks, opt
|
|
|
190
191
|
...options,
|
|
191
192
|
permissionMode: normalizePermissionMode(options?.permissionMode),
|
|
192
193
|
});
|
|
193
|
-
const env = processEnvForNonClaudeCliChild();
|
|
194
|
-
if (process.platform === 'win32') {
|
|
195
|
-
env.LANG = env.LANG || 'C.UTF-8';
|
|
196
|
-
env.LC_ALL = env.LC_ALL || 'C.UTF-8';
|
|
197
|
-
}
|
|
198
194
|
const isCmd = process.platform === 'win32' && (/\.(cmd|bat)$/i.test(normalizedCliPath) ||
|
|
199
195
|
normalizedCliPath === 'codebuddy');
|
|
200
196
|
const spawnCmd = isCmd ? 'cmd.exe' : normalizedCliPath;
|
|
201
197
|
const spawnArgs = isCmd ? ['/c', normalizedCliPath, ...args] : args;
|
|
202
198
|
log.info(`Spawning CodeBuddy CLI: path=${normalizedCliPath}, cwd=${workDir}, session=${sessionId ?? 'new'}, args=${args.join(' ')}`);
|
|
203
|
-
const child = spawn(spawnCmd, spawnArgs,
|
|
204
|
-
cwd: workDir,
|
|
205
|
-
// Unix 上放入独立进程组,使 abort/关停能用 process.kill(-pid) 杀掉整棵子树(含 MCP/shell 孙进程)
|
|
206
|
-
detached: process.platform !== 'win32',
|
|
207
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
208
|
-
env,
|
|
209
|
-
windowsHide: process.platform === 'win32',
|
|
210
|
-
});
|
|
199
|
+
const child = spawn(spawnCmd, spawnArgs, buildBaseSpawnOptions(workDir, ['ignore', 'pipe', 'pipe']));
|
|
211
200
|
trackChild(child);
|
|
212
201
|
let cliTimeoutHandle = null;
|
|
213
202
|
let accumulated = '';
|
|
@@ -222,7 +211,7 @@ export function runCodeBuddy(cliPath, prompt, sessionId, workDir, callbacks, opt
|
|
|
222
211
|
const MAX_STDERR = 8 * 1024;
|
|
223
212
|
let stderrText = '';
|
|
224
213
|
const handleErrorText = (message) => {
|
|
225
|
-
if (sessionId &&
|
|
214
|
+
if (sessionId && isSessionInvalidMessage(message)) {
|
|
226
215
|
callbacks.onSessionInvalid?.();
|
|
227
216
|
}
|
|
228
217
|
callbacks.onError(message);
|
|
@@ -317,10 +306,8 @@ export function runCodeBuddy(cliPath, prompt, sessionId, workDir, callbacks, opt
|
|
|
317
306
|
}
|
|
318
307
|
});
|
|
319
308
|
child.on('close', (code) => {
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
cliTimeoutHandle = null;
|
|
323
|
-
}
|
|
309
|
+
clearWallClockTimeout(cliTimeoutHandle);
|
|
310
|
+
cliTimeoutHandle = null;
|
|
324
311
|
if (completed)
|
|
325
312
|
return;
|
|
326
313
|
if (stdoutState.buffer.trim()) {
|
|
@@ -353,10 +340,8 @@ export function runCodeBuddy(cliPath, prompt, sessionId, workDir, callbacks, opt
|
|
|
353
340
|
});
|
|
354
341
|
});
|
|
355
342
|
child.on('error', (err) => {
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
cliTimeoutHandle = null;
|
|
359
|
-
}
|
|
343
|
+
clearWallClockTimeout(cliTimeoutHandle);
|
|
344
|
+
cliTimeoutHandle = null;
|
|
360
345
|
if (completed)
|
|
361
346
|
return;
|
|
362
347
|
completed = true;
|
|
@@ -378,10 +363,7 @@ export function runCodeBuddy(cliPath, prompt, sessionId, workDir, callbacks, opt
|
|
|
378
363
|
if (completed)
|
|
379
364
|
return;
|
|
380
365
|
completed = true;
|
|
381
|
-
|
|
382
|
-
clearTimeout(cliTimeoutHandle);
|
|
383
|
-
cliTimeoutHandle = null;
|
|
384
|
-
}
|
|
366
|
+
clearWallClockTimeout(cliTimeoutHandle);
|
|
385
367
|
killProcessTree(child);
|
|
386
368
|
},
|
|
387
369
|
};
|
|
@@ -1,24 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Codex CLI runner for `codex exec --json` JSONL output.
|
|
3
3
|
*/
|
|
4
|
-
|
|
5
|
-
onText: (accumulated: string) => void;
|
|
6
|
-
onThinking?: (accumulated: string) => void;
|
|
7
|
-
onToolUse?: (toolName: string, toolInput?: Record<string, unknown>) => void;
|
|
8
|
-
onComplete: (result: {
|
|
9
|
-
success: boolean;
|
|
10
|
-
result: string;
|
|
11
|
-
accumulated: string;
|
|
12
|
-
cost: number;
|
|
13
|
-
durationMs: number;
|
|
14
|
-
model?: string;
|
|
15
|
-
numTurns: number;
|
|
16
|
-
toolStats: Record<string, number>;
|
|
17
|
-
}) => void;
|
|
18
|
-
onError: (error: string) => void;
|
|
19
|
-
onSessionId?: (sessionId: string) => void;
|
|
20
|
-
onSessionInvalid?: () => void;
|
|
21
|
-
}
|
|
4
|
+
import type { RunCallbacks, RunHandle } from '../adapters/tool-adapter.interface.js';
|
|
22
5
|
export interface CodexRunOptions {
|
|
23
6
|
skipPermissions?: boolean;
|
|
24
7
|
permissionMode?: 'default' | 'acceptEdits' | 'plan';
|
|
@@ -27,9 +10,6 @@ export interface CodexRunOptions {
|
|
|
27
10
|
hookPort?: number;
|
|
28
11
|
proxy?: string;
|
|
29
12
|
}
|
|
30
|
-
export interface CodexRunHandle {
|
|
31
|
-
abort: () => void;
|
|
32
|
-
}
|
|
33
13
|
export declare function extractPromptImagePaths(prompt: string): string[];
|
|
34
14
|
export declare function buildCodexArgs(prompt: string, sessionId: string | undefined, workDir: string, options?: CodexRunOptions): string[];
|
|
35
|
-
export declare function runCodex(cliPath: string, prompt: string, sessionId: string | undefined, workDir: string, callbacks:
|
|
15
|
+
export declare function runCodex(cliPath: string, prompt: string, sessionId: string | undefined, workDir: string, callbacks: RunCallbacks, options?: CodexRunOptions): RunHandle;
|