@yvhitxcel/opencode-remote 0.16.3 → 0.18.0
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/LICENSE +21 -0
- package/README.md +70 -1
- package/dist/autonomous/decisions.js +73 -0
- package/dist/autonomous/index.js +141 -0
- package/dist/cli.js +121 -19
- package/dist/core/adapter.js +12 -0
- package/dist/core/agent-registry.js +77 -0
- package/dist/core/crypto.js +80 -0
- package/dist/core/git-push.js +143 -0
- package/dist/core/handler.js +293 -0
- package/dist/core/log.js +92 -0
- package/dist/core/lru.js +98 -0
- package/dist/core/notifications.js +2 -2
- package/dist/core/qiniu.js +2 -2
- package/dist/core/retry.js +46 -0
- package/dist/core/router.js +62 -296
- package/dist/core/state.js +190 -0
- package/dist/core/stats.js +115 -0
- package/dist/feishu/adapter.js +0 -1
- package/dist/feishu/bot.js +4 -4
- package/dist/feishu/commands.js +28 -397
- package/dist/feishu/handler.js +9 -369
- package/dist/opencode/client.js +172 -168
- package/dist/patch_spawn.js +1 -0
- package/dist/plugins/agents/claude-code/index.js +59 -47
- package/dist/plugins/agents/codex/index.js +32 -6
- package/dist/plugins/agents/copilot/index.js +32 -6
- package/dist/plugins/agents/opencode/index.js +38 -12
- package/dist/telegram/adapter.js +22 -9
- package/dist/telegram/bot.js +1 -6
- package/dist/weixin/adapter.js +37 -15
- package/dist/weixin/api.js +47 -19
- package/dist/weixin/bot.js +172 -83
- package/dist/weixin/commands.js +476 -597
- package/dist/weixin/handler.js +27 -541
- package/dist/weixin/user-adapter-map.js +12 -0
- package/package.json +5 -3
- package/dist/core/session.js +0 -403
package/dist/weixin/api.js
CHANGED
|
@@ -53,7 +53,14 @@ async function apiFetch(params) {
|
|
|
53
53
|
}
|
|
54
54
|
catch (err) {
|
|
55
55
|
clearTimeout(t);
|
|
56
|
-
|
|
56
|
+
const details = {
|
|
57
|
+
url: url.toString(),
|
|
58
|
+
label: params.label,
|
|
59
|
+
message: err.message,
|
|
60
|
+
cause: err.cause ? (err.cause.message || String(err.cause)) : undefined,
|
|
61
|
+
code: err.code,
|
|
62
|
+
};
|
|
63
|
+
console.error(`[apiFetch] ${params.label} error:`, JSON.stringify(details));
|
|
57
64
|
throw err;
|
|
58
65
|
}
|
|
59
66
|
}
|
|
@@ -61,7 +68,7 @@ async function apiFetch(params) {
|
|
|
61
68
|
// Login APIs
|
|
62
69
|
// ---------------------------------------------------------------------------
|
|
63
70
|
/**
|
|
64
|
-
* Fetch QR code for login
|
|
71
|
+
* Fetch QR code for login (admin auth)
|
|
65
72
|
*/
|
|
66
73
|
export async function fetchQRCode(baseUrl = DEFAULT_BASE_URL, botType = '3') {
|
|
67
74
|
const base = ensureTrailingSlash(baseUrl);
|
|
@@ -122,26 +129,47 @@ export async function getUpdates(params) {
|
|
|
122
129
|
}
|
|
123
130
|
}
|
|
124
131
|
/**
|
|
125
|
-
* Send a message
|
|
132
|
+
* Send a message with rate limit retry
|
|
126
133
|
*/
|
|
127
134
|
export async function sendMessage(params) {
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
135
|
+
const MAX_ATTEMPTS = 3;
|
|
136
|
+
let lastErr;
|
|
137
|
+
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
|
|
138
|
+
try {
|
|
139
|
+
const rawText = await apiFetch({
|
|
140
|
+
baseUrl: params.baseUrl,
|
|
141
|
+
endpoint: 'ilink/bot/sendmessage',
|
|
142
|
+
body: JSON.stringify({
|
|
143
|
+
...params.body,
|
|
144
|
+
base_info: { channel_version: '1.0.0' },
|
|
145
|
+
}),
|
|
146
|
+
token: params.token,
|
|
147
|
+
timeoutMs: params.timeoutMs ?? DEFAULT_API_TIMEOUT_MS,
|
|
148
|
+
label: 'sendMessage',
|
|
149
|
+
});
|
|
150
|
+
// 微信返回限流错误码时,rawText 含 errcode,重试
|
|
151
|
+
try {
|
|
152
|
+
const j = JSON.parse(rawText);
|
|
153
|
+
if (j && typeof j.errcode === 'number' && (j.errcode === -1001 || j.errcode === -1002 || j.errcode === 45009 || j.errcode === 45047)) {
|
|
154
|
+
const backoff = 1000 * attempt;
|
|
155
|
+
console.warn(`[sendMessage] rate-limited (errcode=${j.errcode}), retry in ${backoff}ms (${attempt}/${MAX_ATTEMPTS})`);
|
|
156
|
+
await new Promise(r => setTimeout(r, backoff));
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
} catch (e) { console.debug('[sendMessage] parse error:', e.message); }
|
|
160
|
+
return;
|
|
161
|
+
} catch (err) {
|
|
162
|
+
lastErr = err;
|
|
163
|
+
if (attempt < MAX_ATTEMPTS && /AbortError|fetch failed|ECONNRESET/i.test(err.message || '')) {
|
|
164
|
+
const backoff = 1000 * attempt;
|
|
165
|
+
console.warn(`[sendMessage] transient error, retry in ${backoff}ms: ${err.message}`);
|
|
166
|
+
await new Promise(r => setTimeout(r, backoff));
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
throw err;
|
|
170
|
+
}
|
|
144
171
|
}
|
|
172
|
+
throw lastErr;
|
|
145
173
|
}
|
|
146
174
|
/**
|
|
147
175
|
* Get bot config (includes typing_ticket)
|
package/dist/weixin/bot.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync, statSync, chmodSync, unlinkSync } from 'fs';
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, statSync, chmodSync, unlinkSync, readdirSync } from 'fs';
|
|
2
2
|
import { homedir } from 'os';
|
|
3
3
|
import { join } from 'path';
|
|
4
|
-
import { initSessionManager, loadSessionMapping, saveSessionMapping } from '../core/session.js';
|
|
5
4
|
import { initOpenCode, initFetchConfig } from '../opencode/client.js';
|
|
6
5
|
import { getAuthStatus } from '../core/auth.js';
|
|
7
6
|
import { registry } from '../core/registry.js';
|
|
@@ -9,15 +8,32 @@ import { fetchQRCode, pollQRStatus, getUpdates } from './api.js';
|
|
|
9
8
|
import { DEFAULT_BASE_URL } from './types.js';
|
|
10
9
|
import { createWeixinAdapter } from './adapter.js';
|
|
11
10
|
import { handleMessage } from './handler.js';
|
|
11
|
+
import { userAdapterMap } from './user-adapter-map.js';
|
|
12
|
+
import { initState } from '../core/state.js';
|
|
13
|
+
import { initLogger, cleanOldLogs, logger } from '../core/log.js';
|
|
14
|
+
import { LRUSessionMap } from '../core/lru.js';
|
|
15
|
+
import { encryptCredential, decryptCredential } from '../core/crypto.js';
|
|
12
16
|
export { COMMAND_ALIASES, detectCommand } from '../core/router.js';
|
|
13
17
|
|
|
18
|
+
let _initialized = false;
|
|
19
|
+
function initBot() {
|
|
20
|
+
if (_initialized) return;
|
|
21
|
+
_initialized = true;
|
|
22
|
+
initLogger();
|
|
23
|
+
cleanOldLogs();
|
|
24
|
+
initState();
|
|
25
|
+
logger.info('Bot starting', { ts: new Date().toISOString() });
|
|
26
|
+
}
|
|
27
|
+
|
|
14
28
|
const CONFIG_DIR = join(homedir(), '.opencode-remote');
|
|
15
29
|
const WEIXIN_DIR = join(CONFIG_DIR, 'weixin');
|
|
30
|
+
const CREDENTIALS_DIR = join(WEIXIN_DIR, 'credentials');
|
|
16
31
|
const INSTANCE_ID = process.env.OPENCODE_INSTANCE_ID || 'default';
|
|
17
32
|
const CREDENTIALS_FILE = INSTANCE_ID === 'default'
|
|
18
33
|
? join(WEIXIN_DIR, 'credentials.json')
|
|
19
34
|
: join(WEIXIN_DIR, `credentials-${INSTANCE_ID}.json`);
|
|
20
|
-
|
|
35
|
+
|
|
36
|
+
const botInstances = [];
|
|
21
37
|
|
|
22
38
|
export async function loginWithQR(baseUrl = DEFAULT_BASE_URL, onQRCode) {
|
|
23
39
|
console.log('Starting Weixin login...');
|
|
@@ -48,21 +64,112 @@ export async function loginWithQR(baseUrl = DEFAULT_BASE_URL, onQRCode) {
|
|
|
48
64
|
} catch (e) { console.error('Login error:', e); return null; }
|
|
49
65
|
}
|
|
50
66
|
|
|
51
|
-
function ensureDirs() {
|
|
52
|
-
|
|
67
|
+
function ensureDirs() {
|
|
68
|
+
if (!existsSync(WEIXIN_DIR)) mkdirSync(WEIXIN_DIR, { recursive: true });
|
|
69
|
+
if (!existsSync(CREDENTIALS_DIR)) mkdirSync(CREDENTIALS_DIR, { recursive: true });
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function loadAllCredentials() {
|
|
53
73
|
ensureDirs();
|
|
54
|
-
if (
|
|
55
|
-
|
|
74
|
+
if (existsSync(CREDENTIALS_DIR)) {
|
|
75
|
+
const files = readdirSync(CREDENTIALS_DIR).filter(f => f.endsWith('.json'));
|
|
76
|
+
if (files.length > 0) {
|
|
77
|
+
return files.map(f => {
|
|
78
|
+
try {
|
|
79
|
+
const raw = readFileSync(join(CREDENTIALS_DIR, f), 'utf-8');
|
|
80
|
+
const obj = JSON.parse(raw);
|
|
81
|
+
// 检测是否加密信封 → 解密
|
|
82
|
+
if (obj && obj.v === 1 && obj.enc) {
|
|
83
|
+
const decrypted = decryptCredential(obj.enc);
|
|
84
|
+
if (decrypted) return JSON.parse(decrypted);
|
|
85
|
+
}
|
|
86
|
+
return obj;
|
|
87
|
+
}
|
|
88
|
+
catch (e) { console.debug('[credentials] Failed to parse:', f, e.message); return null; }
|
|
89
|
+
}).filter(Boolean);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (existsSync(CREDENTIALS_FILE)) {
|
|
93
|
+
try { return [JSON.parse(readFileSync(CREDENTIALS_FILE, 'utf-8'))]; } catch (e) { console.debug('[credentials] Failed to parse legacy:', e.message); }
|
|
94
|
+
}
|
|
95
|
+
return [];
|
|
56
96
|
}
|
|
97
|
+
|
|
98
|
+
export function loadWeixinCredentials() {
|
|
99
|
+
const all = loadAllCredentials();
|
|
100
|
+
return all.length > 0 ? all[0] : null;
|
|
101
|
+
}
|
|
102
|
+
|
|
57
103
|
export function saveWeixinCredentials(creds) {
|
|
58
104
|
ensureDirs();
|
|
59
|
-
|
|
60
|
-
|
|
105
|
+
const filePath = join(CREDENTIALS_DIR, `credentials-${creds.accountId}.json`);
|
|
106
|
+
const plain = JSON.stringify({ ...creds, savedAt: new Date().toISOString() }, null, 2);
|
|
107
|
+
const enc = encryptCredential(plain);
|
|
108
|
+
const envelope = { v: 1, enc, savedAt: new Date().toISOString() };
|
|
109
|
+
writeFileSync(filePath, JSON.stringify(envelope, null, 2), 'utf-8');
|
|
110
|
+
try { const s = statSync(filePath); chmodSync(filePath, (s.mode & 0o777) | 0o600); } catch (e) { console.warn('[credentials] chmod failed:', e.message); }
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function saveCredential(creds) {
|
|
114
|
+
saveWeixinCredentials(creds);
|
|
61
115
|
}
|
|
62
116
|
|
|
63
117
|
let _restartCallback = null;
|
|
64
118
|
function setRestartCallback(fn) { _restartCallback = fn; }
|
|
119
|
+
|
|
120
|
+
async function runPollingLoop(adapter, baseUrl, token, openCodeSessions, signal) {
|
|
121
|
+
let buf = '';
|
|
122
|
+
let retryCount = 0;
|
|
123
|
+
while (!signal.aborted) {
|
|
124
|
+
try {
|
|
125
|
+
const resp = await getUpdates({ baseUrl, token, get_updates_buf: buf });
|
|
126
|
+
if (signal.aborted) break;
|
|
127
|
+
if (resp.get_updates_buf) buf = resp.get_updates_buf;
|
|
128
|
+
for (const msg of (resp.msgs || [])) {
|
|
129
|
+
if (msg.message_type !== 1) continue;
|
|
130
|
+
const textItem = msg.item_list?.find((i) => i.type === 1);
|
|
131
|
+
const text = textItem?.text_item?.text;
|
|
132
|
+
const fromUserId = msg.from_user_id;
|
|
133
|
+
if (!fromUserId || !text) continue;
|
|
134
|
+
userAdapterMap.set(fromUserId, adapter);
|
|
135
|
+
const messageId = msg.message_id?.toString();
|
|
136
|
+
if (adapter.isDuplicate(messageId, `${fromUserId}:${text}`)) continue;
|
|
137
|
+
if (msg.context_token) adapter.contextTokens.set(fromUserId, { value: msg.context_token, _ts: Date.now() });
|
|
138
|
+
try { await handleMessage(adapter, { platform: 'weixin', threadId: fromUserId, userId: fromUserId, messageId }, text, openCodeSessions); } catch (e) { console.error('Handle error:', e); }
|
|
139
|
+
}
|
|
140
|
+
} catch (e) {
|
|
141
|
+
if (signal.aborted) break;
|
|
142
|
+
const errMsg = e.message || '';
|
|
143
|
+
const isConnReset = errMsg.includes('ECONNRESET') || errMsg.includes('fetch failed');
|
|
144
|
+
if (isConnReset) {
|
|
145
|
+
retryCount++;
|
|
146
|
+
const delay = Math.min(2000 * retryCount, 15000);
|
|
147
|
+
console.error(`[bot] Connection error (${retryCount}), retry in ${delay}ms...`);
|
|
148
|
+
await new Promise(r => setTimeout(r, delay));
|
|
149
|
+
} else {
|
|
150
|
+
console.error('Polling error:', e);
|
|
151
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function addBotInstance(creds, openCodeSessions) {
|
|
158
|
+
const baseUrl = creds.baseUrl || DEFAULT_BASE_URL;
|
|
159
|
+
const token = creds.token;
|
|
160
|
+
const botId = creds.accountId;
|
|
161
|
+
const adapter = createWeixinAdapter(baseUrl, token, botId);
|
|
162
|
+
const abortController = new AbortController();
|
|
163
|
+
const instance = { adapter, abortController, creds };
|
|
164
|
+
botInstances.push(instance);
|
|
165
|
+
runPollingLoop(adapter, baseUrl, token, openCodeSessions, abortController.signal).catch(e => console.error('[bot] Polling loop ended:', e));
|
|
166
|
+
return instance;
|
|
167
|
+
}
|
|
168
|
+
|
|
65
169
|
export async function startWeixinBot(botConfig, restartFn) {
|
|
170
|
+
// 仅在子进程启动时初始化日志和状态 (不在父进程 import 时)
|
|
171
|
+
initBot();
|
|
172
|
+
process.on('unhandledRejection', (reason) => { console.error('[bot] Unhandled Rejection:', reason); });
|
|
66
173
|
if (restartFn) _restartCallback = restartFn;
|
|
67
174
|
console.log('');
|
|
68
175
|
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
@@ -72,78 +179,78 @@ export async function startWeixinBot(botConfig, restartFn) {
|
|
|
72
179
|
|
|
73
180
|
await registry.loadBuiltInPlugins();
|
|
74
181
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
if (!credentials) {
|
|
182
|
+
let credentialsList = loadAllCredentials();
|
|
183
|
+
if (credentialsList.length === 0) {
|
|
78
184
|
console.log('No saved credentials. Starting login...');
|
|
79
|
-
|
|
80
|
-
if (!
|
|
185
|
+
const creds = await loginWithQR(botConfig.weixinBaseUrl || DEFAULT_BASE_URL);
|
|
186
|
+
if (!creds) { console.error('Login failed'); process.exit(1); }
|
|
187
|
+
credentialsList = [creds];
|
|
81
188
|
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
try { await initOpenCode(); console.log('OpenCode ready'); } catch (e) { console.error('Failed to init OpenCode:', e); }
|
|
90
|
-
|
|
189
|
+
const firstCreds = credentialsList[0];
|
|
190
|
+
console.log(`Using account: ${firstCreds.accountId}${credentialsList.length > 1 ? ` (+${credentialsList.length - 1} more)` : ''}`);
|
|
191
|
+
const openCodeSessions = new LRUSessionMap({ maxSize: 100, ttlMs: 30 * 60 * 1000, name: 'opencode-sessions' });
|
|
192
|
+
// 定期清理过期 session (每 5 分钟)
|
|
193
|
+
setInterval(() => openCodeSessions.cleanup(), 5 * 60 * 1000).unref();
|
|
194
|
+
|
|
195
|
+
let opencodeServer = null;
|
|
91
196
|
try {
|
|
92
197
|
const opencode = await initOpenCode();
|
|
93
198
|
if (opencode) {
|
|
199
|
+
opencodeServer = opencode.server;
|
|
200
|
+
globalThis.__opencodeServer = opencode.server;
|
|
201
|
+
console.log('OpenCode ready');
|
|
94
202
|
const result = await opencode.client.session.list();
|
|
95
203
|
if (!result.error && result.data && result.data.length > 0) {
|
|
96
204
|
const sorted = result.data.sort((a, b) => (b.updated_at || 0) - (a.updated_at || 0));
|
|
97
205
|
const latest = sorted[0];
|
|
98
206
|
console.log(`Latest OpenCode session: ${latest.title || 'Untitled'} (${latest.id.slice(0, 8)}...)`);
|
|
99
|
-
globalThis.__latestOpenCodeSession = { id: latest.id, directory: latest.directory };
|
|
100
207
|
if (latest.directory) {
|
|
101
|
-
|
|
102
|
-
|
|
208
|
+
// resume 的目录可能指向 bot 自身而非 openchat 项目
|
|
209
|
+
// 如果 resume 目录没 lab.mjs,尝试 fallback 到 openchat 项目
|
|
210
|
+
const projectDir = existsSync(`${latest.directory}/bridge/bin/lab.mjs`) ? latest.directory : (existsSync('F:\\openchat\\bridge\\bin\\lab.mjs') ? 'F:\\openchat' : latest.directory);
|
|
211
|
+
console.log(`Project directory: ${projectDir}`);
|
|
212
|
+
globalThis.__autoProjectDir = projectDir;
|
|
103
213
|
}
|
|
104
214
|
}
|
|
105
215
|
}
|
|
106
|
-
} catch (e) { console.
|
|
107
|
-
|
|
216
|
+
} catch (e) { console.error('Failed to init OpenCode:', e); }
|
|
217
|
+
|
|
108
218
|
if (!getAuthStatus().weixin) {
|
|
109
219
|
console.log('\n🔒 Bot not secured! First user to send /start becomes owner.\n');
|
|
110
220
|
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
let running = true;
|
|
221
|
+
|
|
222
|
+
for (const creds of credentialsList) {
|
|
223
|
+
console.log(`Starting bot for account: ${creds.accountId}`);
|
|
224
|
+
addBotInstance(creds, openCodeSessions);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// IPC 心跳:每 30s 通知父进程还活着
|
|
228
|
+
const hbTimer = setInterval(() => { try { process.send?.({ type: 'heartbeat', ts: Date.now() }); } catch {} }, 30_000);
|
|
229
|
+
if (hbTimer.unref) hbTimer.unref();
|
|
230
|
+
|
|
123
231
|
let shouldRestart = false;
|
|
124
232
|
const shutdown = (restart = false) => {
|
|
125
233
|
console.log(restart ? '\nRestarting...' : '\nShutting down...');
|
|
126
|
-
saveSessionMapping();
|
|
127
|
-
running = false;
|
|
128
234
|
shouldRestart = restart;
|
|
129
|
-
for (const
|
|
235
|
+
for (const instance of botInstances) {
|
|
236
|
+
try { instance.abortController.abort(); } catch (e) { }
|
|
237
|
+
}
|
|
238
|
+
// 关掉 opencode server 进程,防重启后端口冲突
|
|
239
|
+
try { globalThis.__opencodeServer?.kill?.(); } catch (e) { console.warn('[shutdown] Server kill error:', e.message); }
|
|
130
240
|
openCodeSessions.clear();
|
|
131
241
|
};
|
|
132
|
-
|
|
133
|
-
globalThis.__weixinBotShutdown = (restart = false) => shutdown(restart);
|
|
134
|
-
globalThis.__weixinBotRunning = () => running;
|
|
135
242
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
console.log('Polling for messages...');
|
|
243
|
+
globalThis.__weixinBotShutdown = (restart = false) => shutdown(restart);
|
|
244
|
+
globalThis.__weixinBotRunning = () => botInstances.some(i => !i.abortController.signal.aborted);
|
|
139
245
|
|
|
140
246
|
if (process.env.OPENCODE_RESTART === '1') {
|
|
141
247
|
try {
|
|
248
|
+
const firstAdapter = botInstances[0]?.adapter;
|
|
142
249
|
const restartInfoPath = join(process.env.HOME || process.cwd(), '.opencode-remote', '.restart_user.json');
|
|
143
250
|
if (existsSync(restartInfoPath)) {
|
|
144
251
|
const restartInfo = JSON.parse(readFileSync(restartInfoPath, 'utf8'));
|
|
145
|
-
if (Date.now() - restartInfo.time < 60000) {
|
|
146
|
-
await
|
|
252
|
+
if (Date.now() - restartInfo.time < 60000 && firstAdapter) {
|
|
253
|
+
await firstAdapter.reply(restartInfo.threadId, '✅ Bot 重启完成!');
|
|
147
254
|
console.log('Sent restart notification to user');
|
|
148
255
|
}
|
|
149
256
|
unlinkSync(restartInfoPath);
|
|
@@ -153,38 +260,20 @@ export async function startWeixinBot(botConfig, restartFn) {
|
|
|
153
260
|
}
|
|
154
261
|
}
|
|
155
262
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
handleMessage(adapter, { platform: 'weixin', threadId: fromUserId, userId: fromUserId, messageId }, text, openCodeSessions).catch(e => console.error('Handle error:', e));
|
|
171
|
-
}
|
|
172
|
-
} catch (e) {
|
|
173
|
-
if (!running) break;
|
|
174
|
-
const errMsg = e.message || '';
|
|
175
|
-
const isConnReset = errMsg.includes('ECONNRESET') || errMsg.includes('fetch failed');
|
|
176
|
-
if (isConnReset) {
|
|
177
|
-
retryCount++;
|
|
178
|
-
const delay = Math.min(2000 * retryCount, 15000);
|
|
179
|
-
console.error(`[bot] Connection error (${retryCount}), retry in ${delay}ms...`);
|
|
180
|
-
await new Promise(r => setTimeout(r, delay));
|
|
181
|
-
} else {
|
|
182
|
-
console.error('Polling error:', e);
|
|
183
|
-
await new Promise(r => setTimeout(r, 2000));
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
|
|
263
|
+
console.log(`✅ ${botInstances.length} bot instance(s) running`);
|
|
264
|
+
console.log('📡 Listening for WeChat messages...');
|
|
265
|
+
|
|
266
|
+
await new Promise(resolve => {
|
|
267
|
+
globalThis.__weixinBotShutdownAndExit = (restart) => {
|
|
268
|
+
shutdown(restart);
|
|
269
|
+
resolve();
|
|
270
|
+
};
|
|
271
|
+
// 收到信号时清理 opencode server 后再退出
|
|
272
|
+
const handleSignal = () => { shutdown(false); resolve(); };
|
|
273
|
+
process.on('SIGINT', handleSignal);
|
|
274
|
+
process.on('SIGTERM', handleSignal);
|
|
275
|
+
});
|
|
276
|
+
|
|
188
277
|
if (shouldRestart) {
|
|
189
278
|
console.log('✅ Bot shutdown complete, exiting for restart...');
|
|
190
279
|
process.exit(0);
|