@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.
- package/dist/autonomous/decisions.js +73 -0
- package/dist/autonomous/index.js +141 -0
- package/dist/cli.js +1 -10
- package/dist/core/config.js +1 -1
- package/dist/core/git-push.js +120 -0
- package/dist/core/notifications.js +2 -2
- package/dist/core/router.js +97 -305
- package/dist/feishu/bot.js +0 -2
- package/dist/feishu/commands.js +82 -417
- package/dist/feishu/handler.js +26 -217
- package/dist/opencode/client.js +167 -145
- package/dist/plugins/agents/claude-code/index.js +6 -2
- package/dist/plugins/agents/opencode/index.js +40 -10
- package/dist/telegram/adapter.js +3 -6
- package/dist/telegram/bot.js +1 -6
- package/dist/weixin/api.js +9 -2
- package/dist/weixin/bot.js +123 -69
- package/dist/weixin/commands.js +361 -584
- package/dist/weixin/handler.js +170 -422
- package/dist/weixin/user-adapter-map.js +1 -0
- package/package.json +1 -1
- 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);
|
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,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() {
|
|
52
|
-
|
|
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 (
|
|
55
|
-
|
|
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
|
-
|
|
60
|
-
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
if (!credentials) {
|
|
154
|
+
let credentialsList = loadAllCredentials();
|
|
155
|
+
if (credentialsList.length === 0) {
|
|
78
156
|
console.log('No saved credentials. Starting login...');
|
|
79
|
-
|
|
80
|
-
if (!
|
|
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
|
-
|
|
83
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
137
|
-
|
|
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
|
|
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
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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);
|