@yvhitxcel/opencode-remote 0.16.2 → 0.17.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.
@@ -53,7 +53,14 @@ async function apiFetch(params) {
53
53
  }
54
54
  catch (err) {
55
55
  clearTimeout(t);
56
- console.error(`[apiFetch] ${params.label} error:`, err.message);
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);
@@ -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,16 +8,20 @@ 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
12
  export { COMMAND_ALIASES, detectCommand } from '../core/router.js';
13
13
 
14
14
  const CONFIG_DIR = join(homedir(), '.opencode-remote');
15
15
  const WEIXIN_DIR = join(CONFIG_DIR, 'weixin');
16
+ const CREDENTIALS_DIR = join(WEIXIN_DIR, 'credentials');
16
17
  const INSTANCE_ID = process.env.OPENCODE_INSTANCE_ID || 'default';
17
18
  const CREDENTIALS_FILE = INSTANCE_ID === 'default'
18
19
  ? join(WEIXIN_DIR, 'credentials.json')
19
20
  : join(WEIXIN_DIR, `credentials-${INSTANCE_ID}.json`);
20
21
  const RESTART_NOTIFY_FILE = join(WEIXIN_DIR, 'restart-notify.json');
21
22
 
23
+ const botInstances = [];
24
+
22
25
  export async function loginWithQR(baseUrl = DEFAULT_BASE_URL, onQRCode) {
23
26
  console.log('Starting Weixin login...');
24
27
  try {
@@ -48,20 +51,96 @@ export async function loginWithQR(baseUrl = DEFAULT_BASE_URL, onQRCode) {
48
51
  } catch (e) { console.error('Login error:', e); return null; }
49
52
  }
50
53
 
51
- function ensureDirs() { if (!existsSync(WEIXIN_DIR)) mkdirSync(WEIXIN_DIR, { recursive: true }); }
52
- export function loadWeixinCredentials() {
54
+ function ensureDirs() {
55
+ if (!existsSync(WEIXIN_DIR)) mkdirSync(WEIXIN_DIR, { recursive: true });
56
+ if (!existsSync(CREDENTIALS_DIR)) mkdirSync(CREDENTIALS_DIR, { recursive: true });
57
+ }
58
+
59
+ export function loadAllCredentials() {
53
60
  ensureDirs();
54
- if (!existsSync(CREDENTIALS_FILE)) return null;
55
- try { return JSON.parse(readFileSync(CREDENTIALS_FILE, 'utf-8')); } catch (e) { console.debug('[credentials] Failed to parse:', e.message); return null; }
61
+ if (existsSync(CREDENTIALS_DIR)) {
62
+ const files = readdirSync(CREDENTIALS_DIR).filter(f => f.endsWith('.json'));
63
+ if (files.length > 0) {
64
+ return files.map(f => {
65
+ try { return JSON.parse(readFileSync(join(CREDENTIALS_DIR, f), 'utf-8')); }
66
+ catch (e) { console.debug('[credentials] Failed to parse:', f, e.message); return null; }
67
+ }).filter(Boolean);
68
+ }
69
+ }
70
+ if (existsSync(CREDENTIALS_FILE)) {
71
+ try { return [JSON.parse(readFileSync(CREDENTIALS_FILE, 'utf-8'))]; } catch (e) { console.debug('[credentials] Failed to parse legacy:', e.message); }
72
+ }
73
+ return [];
74
+ }
75
+
76
+ export function loadWeixinCredentials() {
77
+ const all = loadAllCredentials();
78
+ return all.length > 0 ? all[0] : null;
56
79
  }
80
+
57
81
  export function saveWeixinCredentials(creds) {
58
82
  ensureDirs();
59
- writeFileSync(CREDENTIALS_FILE, JSON.stringify({ ...creds, savedAt: new Date().toISOString() }, null, 2), 'utf-8');
60
- try { const s = statSync(CREDENTIALS_FILE); chmodSync(CREDENTIALS_FILE, (s.mode & 0o777) | 0o600); } catch (e) { console.warn('[credentials] chmod failed:', e.message); }
83
+ const filePath = join(CREDENTIALS_DIR, `credentials-${creds.accountId}.json`);
84
+ writeFileSync(filePath, JSON.stringify({ ...creds, savedAt: new Date().toISOString() }, null, 2), 'utf-8');
85
+ try { const s = statSync(filePath); chmodSync(filePath, (s.mode & 0o777) | 0o600); } catch (e) { console.warn('[credentials] chmod failed:', e.message); }
86
+ }
87
+
88
+ export function saveCredential(creds) {
89
+ saveWeixinCredentials(creds);
61
90
  }
62
91
 
63
92
  let _restartCallback = null;
64
93
  function setRestartCallback(fn) { _restartCallback = fn; }
94
+
95
+ async function runPollingLoop(adapter, baseUrl, token, openCodeSessions, signal) {
96
+ let buf = '';
97
+ let retryCount = 0;
98
+ while (!signal.aborted) {
99
+ try {
100
+ const resp = await getUpdates({ baseUrl, token, get_updates_buf: buf });
101
+ if (signal.aborted) break;
102
+ if (resp.get_updates_buf) buf = resp.get_updates_buf;
103
+ for (const msg of (resp.msgs || [])) {
104
+ if (msg.message_type !== 1) continue;
105
+ const textItem = msg.item_list?.find((i) => i.type === 1);
106
+ const text = textItem?.text_item?.text;
107
+ const fromUserId = msg.from_user_id;
108
+ if (!fromUserId || !text) continue;
109
+ userAdapterMap.set(fromUserId, adapter);
110
+ const messageId = msg.message_id?.toString();
111
+ if (adapter.isDuplicate(messageId)) continue;
112
+ if (msg.context_token) adapter.contextTokens.set(fromUserId, msg.context_token);
113
+ handleMessage(adapter, { platform: 'weixin', threadId: fromUserId, userId: fromUserId, messageId }, text, openCodeSessions).catch(e => console.error('Handle error:', e));
114
+ }
115
+ } catch (e) {
116
+ if (signal.aborted) break;
117
+ const errMsg = e.message || '';
118
+ const isConnReset = errMsg.includes('ECONNRESET') || errMsg.includes('fetch failed');
119
+ if (isConnReset) {
120
+ retryCount++;
121
+ const delay = Math.min(2000 * retryCount, 15000);
122
+ console.error(`[bot] Connection error (${retryCount}), retry in ${delay}ms...`);
123
+ await new Promise(r => setTimeout(r, delay));
124
+ } else {
125
+ console.error('Polling error:', e);
126
+ await new Promise(r => setTimeout(r, 2000));
127
+ }
128
+ }
129
+ }
130
+ }
131
+
132
+ export function addBotInstance(creds, openCodeSessions) {
133
+ const baseUrl = creds.baseUrl || DEFAULT_BASE_URL;
134
+ const token = creds.token;
135
+ const botId = creds.accountId;
136
+ const adapter = createWeixinAdapter(baseUrl, token, botId);
137
+ const abortController = new AbortController();
138
+ const instance = { adapter, abortController, creds };
139
+ botInstances.push(instance);
140
+ runPollingLoop(adapter, baseUrl, token, openCodeSessions, abortController.signal).catch(e => console.error('[bot] Polling loop ended:', e));
141
+ return instance;
142
+ }
143
+
65
144
  export async function startWeixinBot(botConfig, restartFn) {
66
145
  if (restartFn) _restartCallback = restartFn;
67
146
  console.log('');
@@ -72,22 +151,19 @@ export async function startWeixinBot(botConfig, restartFn) {
72
151
 
73
152
  await registry.loadBuiltInPlugins();
74
153
 
75
- try { await initFetchConfig(); } catch (e) { console.warn('⚠️ Fetch config failed:', e); }
76
- let credentials = loadWeixinCredentials();
77
- if (!credentials) {
154
+ let credentialsList = loadAllCredentials();
155
+ if (credentialsList.length === 0) {
78
156
  console.log('No saved credentials. Starting login...');
79
- credentials = await loginWithQR(botConfig.weixinBaseUrl || DEFAULT_BASE_URL);
80
- if (!credentials) { console.error('Login failed'); process.exit(1); }
157
+ const creds = await loginWithQR(botConfig.weixinBaseUrl || DEFAULT_BASE_URL);
158
+ if (!creds) { console.error('Login failed'); process.exit(1); }
159
+ credentialsList = [creds];
81
160
  }
82
- console.log(`Using account: ${credentials.accountId}`);
83
- const baseUrl = credentials.baseUrl || DEFAULT_BASE_URL;
84
- const token = credentials.token;
85
- const botId = credentials.accountId;
86
- initSessionManager(botConfig);
161
+ const firstCreds = credentialsList[0];
162
+ console.log(`Using account: ${firstCreds.accountId}${credentialsList.length > 1 ? ` (+${credentialsList.length - 1} more)` : ''}`);
87
163
  const openCodeSessions = new Map();
88
- const adapter = createWeixinAdapter(baseUrl, token, botId);
164
+
89
165
  try { await initOpenCode(); console.log('OpenCode ready'); } catch (e) { console.error('Failed to init OpenCode:', e); }
90
-
166
+
91
167
  try {
92
168
  const opencode = await initOpenCode();
93
169
  if (opencode) {
@@ -96,7 +172,6 @@ export async function startWeixinBot(botConfig, restartFn) {
96
172
  const sorted = result.data.sort((a, b) => (b.updated_at || 0) - (a.updated_at || 0));
97
173
  const latest = sorted[0];
98
174
  console.log(`Latest OpenCode session: ${latest.title || 'Untitled'} (${latest.id.slice(0, 8)}...)`);
99
- globalThis.__latestOpenCodeSession = { id: latest.id, directory: latest.directory };
100
175
  if (latest.directory) {
101
176
  console.log(`Project directory: ${latest.directory}`);
102
177
  globalThis.__autoProjectDir = latest.directory;
@@ -104,46 +179,48 @@ export async function startWeixinBot(botConfig, restartFn) {
104
179
  }
105
180
  }
106
181
  } catch (e) { console.warn('⚠️ Auto-resume failed:', e.message); }
107
-
182
+
108
183
  if (!getAuthStatus().weixin) {
109
184
  console.log('\n🔒 Bot not secured! First user to send /start becomes owner.\n');
110
185
  }
111
-
186
+
187
+ for (const creds of credentialsList) {
188
+ console.log(`Starting bot for account: ${creds.accountId}`);
189
+ addBotInstance(creds, openCodeSessions);
190
+ }
191
+
192
+ const firstAdapter = botInstances[0]?.adapter;
112
193
  try {
113
194
  if (existsSync(RESTART_NOTIFY_FILE)) {
114
195
  const data = JSON.parse(readFileSync(RESTART_NOTIFY_FILE, 'utf8'));
115
- if (data.threadId && Date.now() - data.time < 60000) {
116
- await adapter.reply(data.threadId, '✅ Bot 重启完成!');
196
+ if (data.threadId && Date.now() - data.time < 60000 && firstAdapter) {
197
+ await firstAdapter.reply(data.threadId, '✅ Bot 重启完成!');
117
198
  }
118
199
  unlinkSync(RESTART_NOTIFY_FILE);
119
200
  }
120
201
  } catch (e) { console.warn('[restart-notify] Failed to read restart file:', e.message); }
121
-
122
- let running = true;
202
+
123
203
  let shouldRestart = false;
124
204
  const shutdown = (restart = false) => {
125
205
  console.log(restart ? '\nRestarting...' : '\nShutting down...');
126
- saveSessionMapping();
127
- running = false;
128
206
  shouldRestart = restart;
207
+ for (const instance of botInstances) {
208
+ try { instance.abortController.abort(); } catch (e) { }
209
+ }
129
210
  for (const [, s] of openCodeSessions.entries()) { try { s.server?.shutdown?.(); } catch (e) { console.warn('[shutdown] Server shutdown error:', e.message); } }
130
211
  openCodeSessions.clear();
131
212
  };
132
-
133
- globalThis.__weixinBotShutdown = (restart = false) => shutdown(restart);
134
- globalThis.__weixinBotRunning = () => running;
135
213
 
136
- let buf = '';
137
- let retryCount = 0;
138
- console.log('Polling for messages...');
214
+ globalThis.__weixinBotShutdown = (restart = false) => shutdown(restart);
215
+ globalThis.__weixinBotRunning = () => botInstances.some(i => !i.abortController.signal.aborted);
139
216
 
140
217
  if (process.env.OPENCODE_RESTART === '1') {
141
218
  try {
142
219
  const restartInfoPath = join(process.env.HOME || process.cwd(), '.opencode-remote', '.restart_user.json');
143
220
  if (existsSync(restartInfoPath)) {
144
221
  const restartInfo = JSON.parse(readFileSync(restartInfoPath, 'utf8'));
145
- if (Date.now() - restartInfo.time < 60000) {
146
- await adapter.reply(restartInfo.threadId, '✅ Bot 重启完成!');
222
+ if (Date.now() - restartInfo.time < 60000 && firstAdapter) {
223
+ await firstAdapter.reply(restartInfo.threadId, '✅ Bot 重启完成!');
147
224
  console.log('Sent restart notification to user');
148
225
  }
149
226
  unlinkSync(restartInfoPath);
@@ -153,38 +230,15 @@ export async function startWeixinBot(botConfig, restartFn) {
153
230
  }
154
231
  }
155
232
 
156
- while (running) {
157
- try {
158
- const resp = await getUpdates({ baseUrl, token, get_updates_buf: buf });
159
- if (!running) break;
160
- if (resp.get_updates_buf) buf = resp.get_updates_buf;
161
- for (const msg of (resp.msgs || [])) {
162
- if (msg.message_type !== 1) continue;
163
- const textItem = msg.item_list?.find((i) => i.type === 1);
164
- const text = textItem?.text_item?.text;
165
- const fromUserId = msg.from_user_id;
166
- if (!fromUserId || !text) continue;
167
- const messageId = msg.message_id?.toString();
168
- if (adapter.isDuplicate(messageId)) continue;
169
- if (msg.context_token) adapter.contextTokens.set(fromUserId, msg.context_token);
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
-
233
+ console.log(`✅ ${botInstances.length} bot instance(s) running`);
234
+
235
+ await new Promise(resolve => {
236
+ globalThis.__weixinBotShutdownAndExit = (restart) => {
237
+ shutdown(restart);
238
+ resolve();
239
+ };
240
+ });
241
+
188
242
  if (shouldRestart) {
189
243
  console.log('✅ Bot shutdown complete, exiting for restart...');
190
244
  process.exit(0);