@yvhitxcel/opencode-remote 0.16.3 → 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.
@@ -1,15 +1,33 @@
1
- import { detectCommand, COMMAND_ALIASES, getHelpText, DEMO_RESPONSES, setDemoMode, isDemoMode } from '../core/router.js';
2
- import { getOrCreateSession, saveSessionMapping, sessionManager } from '../core/session.js';
1
+ import { getHelpText } from '../core/router.js';
3
2
  import { splitMessage } from '../core/notifications.js';
4
- import { initOpenCode, checkConnection, abortSession, resumeSession, revertSessionMessage, unrevertSession, setThreadModel, getThreadModel, getRecentModels } from '../opencode/client.js';
5
- import { claimOwnership } from '../core/auth.js';
3
+ import { abortSession, initOpenCode, listProviders, getThreadModel, setThreadModel, getRecentModels, setRawDebug, isRawDebug, createSession } from '../opencode/client.js';
4
+ import { claimOwnership, hasOwner } from '../core/auth.js';
6
5
  import { registry } from '../core/registry.js';
7
- import { uploadToQiniu, findBuildOutputs, formatSize, deleteFromQiniu } from '../core/qiniu.js';
6
+ import { deleteFromQiniu } from '../core/qiniu.js';
7
+ import { join } from 'path';
8
8
  import { existsSync } from 'fs';
9
- import { join, basename } from 'path';
9
+ import { homedir } from 'os';
10
+ import { DEFAULT_BASE_URL } from './types.js';
11
+ import { userAdapterMap } from './user-adapter-map.js';
12
+
13
+ // 共享会话
14
+ export const sharedRoom = {
15
+ session: null,
16
+ members: new Set(),
17
+ busy: false,
18
+ };
19
+ export function isSharedMember(threadId) {
20
+ return sharedRoom.members.has(threadId);
21
+ }
22
+ export function addSharedMember(threadId) {
23
+ sharedRoom.members.add(threadId);
24
+ }
25
+ export function removeSharedMember(threadId) {
26
+ sharedRoom.members.delete(threadId);
27
+ }
10
28
 
11
- export let _startLoopCycle = null;
12
- export function _registerStartLoopCycle(fn) { _startLoopCycle = fn; }
29
+ // 线程级活跃 agent 追踪
30
+ export const threadAgent = new Map();
13
31
 
14
32
  async function handleAgentSwitch(adapter, ctx, agentName, prompt) {
15
33
  const agent = registry.findAgent(agentName);
@@ -25,24 +43,32 @@ async function handleAgentSwitch(adapter, ctx, agentName, prompt) {
25
43
  return true;
26
44
  }
27
45
 
28
- const session = await getOrCreateSession(ctx.threadId, 'weixin');
29
- session.currentAgent = agentName;
30
-
31
46
  if (!prompt) {
32
47
  try {
33
- await adapter.reply(ctx.threadId, `✅ 已切换到 ${agentName}`);
48
+ if (agentName === 'opencode') {
49
+ threadAgent.delete(ctx.threadId);
50
+ await adapter.reply(ctx.threadId, `✅ 已切换回 OpenCode`);
51
+ } else {
52
+ threadAgent.set(ctx.threadId, agentName);
53
+ await adapter.reply(ctx.threadId, `✅ 已切换到 ${agentName},后续消息将路由至 ${agentName}`);
54
+ }
34
55
  } catch (e) {
35
56
  console.error(`[handleAgentSwitch] reply failed: ${e.message}`);
36
57
  }
37
- saveSessionMapping();
38
58
  return true;
39
59
  }
40
60
 
61
+ // 有 prompt 时也设置活跃 agent
62
+ if (agentName === 'opencode') {
63
+ threadAgent.delete(ctx.threadId);
64
+ } else {
65
+ threadAgent.set(ctx.threadId, agentName);
66
+ }
67
+
41
68
  await adapter.sendTyping?.(ctx.threadId, true);
42
69
 
43
70
  try {
44
- const history = session.commandHistory || [];
45
- const response = await agent.sendPrompt(session.id, prompt, history, { projectDir: session.projectDir || globalThis.__autoProjectDir });
71
+ const response = await agent.sendPrompt(agentName, prompt, [], { projectDir: globalThis.__autoProjectDir });
46
72
 
47
73
  await adapter.sendTyping?.(ctx.threadId, false);
48
74
 
@@ -51,10 +77,6 @@ async function handleAgentSwitch(adapter, ctx, agentName, prompt) {
51
77
  await adapter.reply(ctx.threadId, chunk);
52
78
  }
53
79
 
54
- session.commandHistory = session.commandHistory || [];
55
- session.commandHistory.push(prompt);
56
- saveSessionMapping();
57
-
58
80
  } catch (error) {
59
81
  await adapter.sendTyping?.(ctx.threadId, false);
60
82
  await adapter.reply(ctx.threadId, `❌ 错误: ${error.message}`);
@@ -63,25 +85,14 @@ async function handleAgentSwitch(adapter, ctx, agentName, prompt) {
63
85
  return true;
64
86
  }
65
87
 
66
- function formatTimeAgo(timestamp) {
67
- const diff = Date.now() - timestamp;
68
- const seconds = Math.floor(diff / 1000);
69
- if (seconds < 60) return `${seconds}秒前`;
70
- const minutes = Math.floor(seconds / 60);
71
- if (minutes < 60) return `${minutes}分钟前`;
72
- const hours = Math.floor(minutes / 60);
73
- if (hours < 24) return `${hours}小时前`;
74
- return `${Math.floor(hours / 24)}天前`;
75
- }
76
-
77
88
  async function handleCommand(adapter, ctx, command, arg, openCodeSessions) {
78
- const session = await getOrCreateSession(ctx.threadId, 'weixin');
89
+ const session = {};
79
90
  switch (command) {
80
91
  case 'start': {
81
92
  const result = claimOwnership('weixin', ctx.userId);
82
93
  if (result.success) {
83
94
  if (result.message === 'claimed') {
84
- await adapter.reply(ctx.threadId, `🔐 安全设置完成!你是此 bot 的唯一所有者。\n\n发送消息给 OpenCode 开始工作\n/help 查看指令\n/status 查看状态`);
95
+ await adapter.reply(ctx.threadId, `🔐 安全设置完成!你是此 bot 的唯一所有者。\n\n发送消息给 OpenCode 开始工作\n/help 查看指令`);
85
96
  } else {
86
97
  await adapter.reply(ctx.threadId, `🚀 准备就绪\n\n发送消息给 OpenCode 开始工作\n/help 查看指令`);
87
98
  }
@@ -93,368 +104,6 @@ async function handleCommand(adapter, ctx, command, arg, openCodeSessions) {
93
104
  case 'help':
94
105
  await adapter.reply(ctx.threadId, getHelpText());
95
106
  return true;
96
- case 'tutorial': {
97
- const { TUTORIAL_STEPS } = await import('../core/router.js');
98
- const stepNum = parseInt(arg, 10);
99
- const step = !isNaN(stepNum) && stepNum >= 1 && stepNum <= TUTORIAL_STEPS.length ? stepNum : 1;
100
- const s = TUTORIAL_STEPS[step - 1];
101
- let msg = `📚 教程 · 第 ${s.step}/${TUTORIAL_STEPS.length} 步\n━━━━━━━━━━━━━━━━\n\n${s.title}\n\n${s.desc}\n\n`;
102
- if (s.action) msg += `👉 ${s.action}`;
103
- msg += `\n\n回复 /tutorial${step < TUTORIAL_STEPS.length ? ` 继续第${step + 1}步` : ''} 进入下一步`;
104
- const msgs = splitMessage(msg);
105
- for (const m of msgs) await adapter.reply(ctx.threadId, m);
106
- return true;
107
- }
108
- case 'status': {
109
- const connected = await checkConnection();
110
- const running = session.taskStartTime ? Math.round((Date.now() - session.taskStartTime) / 1000) : 0;
111
-
112
- let msg = `${connected ? '✅' : '❌'} OpenCode ${connected ? '在线' : '离线'}\n\n`;
113
-
114
- const actualSession = openCodeSessions?.get(ctx.threadId) ||
115
- (session.opencodeSessionId ? { sessionId: session.opencodeSessionId } : null);
116
-
117
- msg += `会话: ${actualSession?.sessionId?.slice(0, 8) || '无'}\n`;
118
-
119
- if (running > 0) {
120
- const m = Math.floor(running / 60);
121
- const s = running % 60;
122
- msg += `运行中: ${m}分${s}秒\n`;
123
- }
124
- if (session.currentTool) {
125
- msg += `当前: ${session.currentTool}\n`;
126
- }
127
- if (session.modifiedFiles?.length > 0 || session.modifiedFiles?.size > 0) {
128
- msg += `已修改: ${(session.modifiedFiles?.length || session.modifiedFiles?.size || 0)} 个文件\n`;
129
- }
130
-
131
- const projectDir = session.projectDir || globalThis.__autoProjectDir;
132
- if (projectDir) {
133
- msg += `项目目录: ${projectDir}\n`;
134
- } else {
135
- msg += `项目目录: 未设置\n`;
136
- }
137
-
138
- const workDir = process.cwd();
139
- msg += `工作目录: ${workDir}\n`;
140
-
141
- if (session.originalProjectDir && session.originalProjectDir !== projectDir) {
142
- msg += `原始目录: ${session.originalProjectDir}\n`;
143
- }
144
-
145
- await adapter.reply(ctx.threadId, msg);
146
- return true;
147
- }
148
-
149
- case 'sessions': {
150
- try {
151
- const opencode = await initOpenCode();
152
- if (!opencode) {
153
- await adapter.reply(ctx.threadId, '❌ 无法连接 OpenCode');
154
- return true;
155
- }
156
- const result = await opencode.client.session.list();
157
- if (result.error || !result.data || result.data.length === 0) {
158
- await adapter.reply(ctx.threadId, '📭 暂无会话');
159
- return true;
160
- }
161
- const sorted = result.data.sort((a, b) => (b.time.updated || 0) - (a.time.updated || 0));
162
- session._switchSessionList = sorted;
163
- session._showSessionState = true;
164
- let msg = '📂 选择会话(回复编号):\n\n';
165
- sorted.slice(0, 10).forEach((s, i) => {
166
- const n = i + 1;
167
- const title = s.title || '无标题';
168
- const time = s.updated_at ? formatTimeAgo(s.updated_at * 1000) : '';
169
- msg += `${n}. ${title} (${time})\n`;
170
- });
171
- if (sorted.length > 10) {
172
- msg += `\n... 共 ${sorted.length} 个会话`;
173
- }
174
- msg += '\n\n回复编号切换会话';
175
- await adapter.reply(ctx.threadId, msg);
176
- } catch (e) {
177
- await adapter.reply(ctx.threadId, `❌ 获取会话失败: ${e.message}`);
178
- }
179
- return true;
180
- }
181
-
182
- case 'delsessions': {
183
- try {
184
- const opencode = await initOpenCode();
185
- if (!opencode) {
186
- await adapter.reply(ctx.threadId, '❌ 无法连接 OpenCode');
187
- return true;
188
- }
189
- const result = await opencode.client.session.list();
190
- if (result.error || !result.data || result.data.length === 0) {
191
- await adapter.reply(ctx.threadId, '📭 暂无会话可删除');
192
- return true;
193
- }
194
- const sorted = result.data.sort((a, b) => (b.time.updated || 0) - (a.time.updated || 0));
195
- session._deleteSessionList = sorted;
196
- let msg = '🗑️ 选择要删除的会话(回复编号):\n\n';
197
- sorted.slice(0, 10).forEach((s, i) => {
198
- const n = i + 1;
199
- const title = s.title || '无标题';
200
- const time = s.updated_at ? formatTimeAgo(s.updated_at * 1000) : '';
201
- msg += `${n}. ${title} (${time})\n`;
202
- });
203
- if (sorted.length > 10) {
204
- msg += `\n... 共 ${sorted.length} 个会话`;
205
- }
206
- msg += '\n\n回复编号删除';
207
- await adapter.reply(ctx.threadId, msg);
208
- } catch (e) {
209
- await adapter.reply(ctx.threadId, `❌ 获取会话失败: ${e.message}`);
210
- }
211
- return true;
212
- }
213
-
214
-
215
- case 'copy': {
216
- const ocSession = openCodeSessions.get(ctx.threadId);
217
- if (!ocSession) {
218
- await adapter.reply(ctx.threadId, '❌ 没有活跃的会话');
219
- return true;
220
- }
221
-
222
- const msgsResult = await ocSession.client.session.messages({
223
- path: { id: ocSession.sessionId },
224
- query: { limit: 1 }
225
- });
226
-
227
- if (msgsResult.error || !msgsResult.data || msgsResult.data.length === 0) {
228
- await adapter.reply(ctx.threadId, '❌ 无法获取最新消息');
229
- return true;
230
- }
231
-
232
- let latestMsg = msgsResult.data[0];
233
- if (latestMsg.info.role !== 'assistant') {
234
- await adapter.reply(ctx.threadId, 'ℹ️ 最新消息不是 AI 回复,正在获取上一条 AI 消息...');
235
-
236
- const allMsgsResult = await ocSession.client.session.messages({
237
- path: { id: ocSession.sessionId },
238
- query: { limit: 10 }
239
- });
240
-
241
- if (allMsgsResult.error || !allMsgsResult.data) {
242
- await adapter.reply(ctx.threadId, '❌ 无法获取会话消息');
243
- return true;
244
- }
245
-
246
- const aiMsg = allMsgsResult.data.find(m => m.info.role === 'assistant');
247
- if (!aiMsg) {
248
- await adapter.reply(ctx.threadId, '❌ 未找到 AI 回复');
249
- return true;
250
- }
251
- latestMsg = aiMsg;
252
- }
253
-
254
- let content = '';
255
- if (latestMsg.parts) {
256
- for (const part of latestMsg.parts) {
257
- if (part.type === 'text') {
258
- content += part.text + '\n';
259
- }
260
- if (part.type === 'code') {
261
- content += `\`\`\`${part.language || ''}\n${part.code}\n\`\`\`\n`;
262
- }
263
- if (part.type === 'file' && part.content) {
264
- content += `📁 ${part.filename}:\n${part.content}\n`;
265
- }
266
- }
267
- }
268
-
269
- if (!content.trim()) {
270
- await adapter.reply(ctx.threadId, '❌ AI 回复中没有可复制的文本内容');
271
- return true;
272
- }
273
-
274
- await adapter.reply(ctx.threadId, `📋 已复制最新 AI 回复内容:\n\n${content.substring(0, 2000)}${content.length > 2000 ? '...' : ''}`);
275
- return true;
276
- }
277
-
278
- case 'resume': {
279
- try {
280
- const opencode = await initOpenCode();
281
- const result = await opencode.client.session.list();
282
- if (!result.data || result.data.length === 0) {
283
- await adapter.reply(ctx.threadId, '❌ 没有找到会话');
284
- return true;
285
- }
286
-
287
- const sorted = result.data.sort((a, b) => (b.time.updated || 0) - (a.time.updated || 0));
288
- const latest = sorted[0];
289
-
290
- const resumed = await resumeSession(latest.id);
291
- if (!resumed) {
292
- await adapter.reply(ctx.threadId, '❌ 恢复会话失败');
293
- return true;
294
- }
295
-
296
- openCodeSessions.set(ctx.threadId, resumed);
297
- session.opencodeSessionId = resumed.sessionId;
298
- const key = `weixin:${ctx.userId}:${ctx.threadId}`;
299
- sessionManager.saveSession(key, session).catch(() => {});
300
- saveSessionMapping();
301
-
302
- if (latest.directory) {
303
- session.projectDir = latest.directory;
304
- globalThis.__autoProjectDir = latest.directory;
305
- }
306
-
307
- await adapter.reply(ctx.threadId, `✅ 已恢复最近会话\n\n会话: ${latest.title || 'Untitled'}\n📁 目录: ${latest.directory || 'N/A'}\n📝 更新: ${new Date(latest.time.updated).toLocaleString()}`);
308
- } catch (e) {
309
- await adapter.reply(ctx.threadId, `❌ 恢复失败: ${e.message}`);
310
- }
311
- return true;
312
- }
313
- case 'edit': {
314
- const ocSession = openCodeSessions.get(ctx.threadId);
315
- if (!ocSession) {
316
- await adapter.reply(ctx.threadId, '❌ 没有活跃的会话');
317
- return true;
318
- }
319
- if (arg) {
320
- const num = parseInt(arg, 10);
321
- if (num >= 1) {
322
- try {
323
- const opencode = await initOpenCode();
324
- const msgsResult = await opencode.client.session.messages({ path: { id: ocSession.sessionId } });
325
- if (!msgsResult.error && msgsResult.data) {
326
- const userMsgs = msgsResult.data.filter(m => m.info?.role === 'user');
327
- if (num <= userMsgs.length) {
328
- const targetMsg = userMsgs[num - 1];
329
- const preview = targetMsg.info?.content?.slice(0, 80) || '(空)';
330
- session._editTarget = { sessionId: ocSession.sessionId, messageID: targetMsg.id, num };
331
- await adapter.reply(ctx.threadId, `✏️ 选择修改消息 #${num}:\n\n${preview}\n\n请发送修正后的内容,将从该消息之前创建新分支`);
332
- return true;
333
- }
334
- }
335
- } catch (e) {
336
- await adapter.reply(ctx.threadId, `❌ 操作失败: ${e.message}`);
337
- return true;
338
- }
339
- }
340
- await adapter.reply(ctx.threadId, `❌ 无效编号`);
341
- return true;
342
- }
343
- const opencode = await initOpenCode();
344
- if (!opencode) {
345
- await adapter.reply(ctx.threadId, '❌ 无法获取消息');
346
- return true;
347
- }
348
- const msgsResult = await opencode.client.session.messages({ path: { id: ocSession.sessionId } });
349
- if (msgsResult.error || !msgsResult.data) {
350
- await adapter.reply(ctx.threadId, '❌ 无法获取消息');
351
- return true;
352
- }
353
- const userMsgs = msgsResult.data.filter(m => m.info?.role === 'user');
354
- if (userMsgs.length === 0) {
355
- await adapter.reply(ctx.threadId, '📭 没有用户消息可编辑');
356
- return true;
357
- }
358
- let msg = '✏️ 选择要修改的消息(回复编号):\n\n';
359
- const showCount = Math.min(userMsgs.length, 15);
360
- const startIdx = userMsgs.length - showCount;
361
- for (let i = startIdx; i < userMsgs.length; i++) {
362
- const m = userMsgs[i];
363
- const num = i + 1;
364
- const preview = m.info?.content?.slice(0, 60) || '(空)';
365
- msg += `${num}. ${preview}\n`;
366
- }
367
- if (userMsgs.length > 15) {
368
- msg += `\n... 共 ${userMsgs.length} 条消息`;
369
- }
370
- session._editList = userMsgs;
371
- session._editSessionId = ocSession.sessionId;
372
- const msgs = splitMessage(msg);
373
- for (const m of msgs) {
374
- await adapter.reply(ctx.threadId, m);
375
- }
376
- return true;
377
- }
378
- case 'revert': {
379
- const ocS = openCodeSessions.get(ctx.threadId);
380
- if (!ocS) {
381
- await adapter.reply(ctx.threadId, '❌ 没有活跃的会话');
382
- return true;
383
- }
384
- if (arg === 'undo') {
385
- const ok = await unrevertSession(ocS.sessionId);
386
- if (ok) {
387
- await adapter.reply(ctx.threadId, '↩️ 已恢复撤销的内容');
388
- } else {
389
- await adapter.reply(ctx.threadId, '❌ 恢复失败');
390
- }
391
- return true;
392
- }
393
- const opencode = await initOpenCode();
394
- if (!opencode) {
395
- await adapter.reply(ctx.threadId, '❌ 无法获取消息');
396
- return true;
397
- }
398
- const msgsResult = await opencode.client.session.messages({ path: { id: ocS.sessionId } });
399
- if (msgsResult.error || !msgsResult.data) {
400
- await adapter.reply(ctx.threadId, '❌ 无法获取消息');
401
- return true;
402
- }
403
- const assistantMsgs = msgsResult.data.filter(m => m.info?.role === 'assistant' && m.time?.created);
404
- if (assistantMsgs.length === 0) {
405
- await adapter.reply(ctx.threadId, '📭 没有可撤销的消息');
406
- return true;
407
- }
408
- const lastMsg = assistantMsgs[assistantMsgs.length - 1];
409
- const ok = await revertSessionMessage(ocS.sessionId, lastMsg.id);
410
- if (ok) {
411
- const preview = lastMsg.info?.content?.slice(0, 100) || '(无内容)';
412
- await adapter.reply(ctx.threadId, `↩️ 已撤销最近的消息\n\n${preview}\n\n发送 /revert undo 恢复`);
413
- } else {
414
- await adapter.reply(ctx.threadId, '❌ 撤销失败');
415
- }
416
- return true;
417
- }
418
- case 'loop': {
419
- const argText = arg || '';
420
- if (argText === 'off' || argText === 'stop') {
421
- session.loopMode = false;
422
- session.loopPrompt = null;
423
- session.loopIterationCount = 0;
424
- session.loopStartTime = null;
425
- saveSessionMapping();
426
- await adapter.reply(ctx.threadId, '⏹️ 循环任务已停止');
427
- return true;
428
- }
429
- if (argText === 'status') {
430
- if (session.loopMode) {
431
- const elapsed = session.loopStartTime
432
- ? `已运行: ${Math.floor((Date.now() - session.loopStartTime) / 60000)}分钟`
433
- : '';
434
- const count = session.loopIterationCount || 0;
435
- const limit = session.loopMaxIterations || 10;
436
- await adapter.reply(ctx.threadId, `🔄 循环任务运行中\n指令: ${session.loopPrompt || '智能模式'}\n迭代: ${count}/${limit} ${elapsed}`);
437
- } else {
438
- await adapter.reply(ctx.threadId, '⏹️ 循环任务未运行\n发送 /loop 开始');
439
- }
440
- return true;
441
- }
442
- session.loopMode = true;
443
- session.loopPrompt = argText || null;
444
- session.lastLoopTime = Date.now();
445
- session.loopStartTime = Date.now();
446
- session.loopIterationCount = 0;
447
- session.loopMaxIterations = 10;
448
- session.loopMaxTimeMs = 30 * 60 * 1000;
449
- saveSessionMapping();
450
- const modeDesc = argText ? `指令: ${argText}` : '智能模式(根据上下文自动生成指令)';
451
- await adapter.reply(ctx.threadId, `🔄 循环任务已启动\n${modeDesc}\n限制: 最多10次迭代或30分钟\n\n发送 /loop off 停止`);
452
- if (_startLoopCycle) {
453
- _startLoopCycle(adapter, ctx, openCodeSessions, session);
454
- }
455
- return true;
456
- }
457
-
458
107
  case 'restart': {
459
108
  console.log('[bot] restart command received');
460
109
  await adapter.reply(ctx.threadId, '🔄 正在重启 bot...');
@@ -472,141 +121,16 @@ async function handleCommand(adapter, ctx, command, arg, openCodeSessions) {
472
121
  return true;
473
122
  }
474
123
 
475
- case 'upload': {
476
- const projectDir = session.projectDir || globalThis.__autoProjectDir;
477
-
478
- if (arg && arg.trim()) {
479
- const filePath = arg.trim();
480
-
481
- let fullPath = filePath;
482
- if (!existsSync(fullPath) && projectDir) {
483
- fullPath = join(projectDir, filePath);
484
- }
485
-
486
- if (!existsSync(fullPath)) {
487
- await adapter.reply(ctx.threadId, `❌ 文件不存在: ${filePath}`);
488
- return true;
489
- }
490
-
491
- await adapter.reply(ctx.threadId, `⬆️ 正在上传: ${basename(fullPath)}...`);
492
-
493
- try {
494
- const result = await uploadToQiniu(fullPath);
495
- if (result.skipped) {
496
- await adapter.reply(ctx.threadId, `⏭️ 文件已存在,不需要重复上传,你是要删除吗?\n/delete ${result.key}`);
497
- } else {
498
- await adapter.reply(ctx.threadId, result.url);
499
- await adapter.reply(ctx.threadId, `/delete ${result.key}`);
500
- }
501
- } catch (e) {
502
- await adapter.reply(ctx.threadId, `❌ 上传失败: ${e.message}`);
503
- }
504
- return true;
505
- }
506
-
507
- if (!projectDir) {
508
- await adapter.reply(ctx.threadId, '❌ 未设置项目目录,请先设置项目目录或指定完整文件路径\n\n用法:\n/upload <文件路径>');
509
- return true;
510
- }
511
-
512
- await adapter.reply(ctx.threadId, '🔍 正在搜索构建产物...');
513
-
514
- const files = findBuildOutputs(projectDir);
515
-
516
- if (files.length === 0) {
517
- await adapter.reply(ctx.threadId, '❌ 未找到任何构建产物\n\n请指定完整文件路径,例如: /upload build/app.apk');
518
- return true;
519
- }
520
-
521
- const displayFiles = files.slice(0, 10);
522
- let listMsg = `📦 找到 ${files.length} 个构建产物:\n\n`;
523
- for (let i = 0; i < displayFiles.length; i++) {
524
- const f = displayFiles[i];
525
- listMsg += `${i + 1}. ${f.name}\n`;
526
- listMsg += ` 📍 ${f.relativePath}\n`;
527
- listMsg += ` 📊 ${formatSize(f.size)}\n\n`;
528
- }
529
- if (files.length > 10) {
530
- listMsg += `...还有 ${files.length - 10} 个文件`;
531
- }
532
- listMsg += `\n正在上传最新的: ${files[0].name}`;
533
- await adapter.reply(ctx.threadId, listMsg);
534
-
535
- const targetFile = files[0];
536
-
537
- try {
538
- const result = await uploadToQiniu(targetFile.path);
539
- if (result.skipped) {
540
- await adapter.reply(ctx.threadId, `⏭️ 文件已存在,不需要重复上传,你是要删除吗?\n/delete ${result.key}`);
541
- } else {
542
- await adapter.reply(ctx.threadId, result.url);
543
- await adapter.reply(ctx.threadId, `/delete ${result.key}`);
544
- }
545
- } catch (e) {
546
- await adapter.reply(ctx.threadId, `❌ 上传失败: ${e.message}`);
547
- }
548
- return true;
549
- }
550
-
551
124
  case 'reset': {
552
125
  const oldSession = openCodeSessions?.get(ctx.threadId);
553
126
  if (oldSession) {
554
127
  abortSession(oldSession).catch(() => {});
555
128
  }
556
- session.pendingApprovals = [];
557
- session.opencodeSessionId = null;
558
- session.loopMode = false;
559
- session.loopPrompt = null;
560
- session.projectDir = null;
561
- session.currentAgent = null;
562
- session.messages = [];
563
- session.commandHistory = [];
564
- session.taskStartTime = null;
565
- session.currentTool = null;
566
- session.modifiedFiles = null;
567
- session.lastUserMessage = null;
568
- session._lastPrompt = null;
569
- session._contextScope = null;
570
- session.originalProjectDir = null;
571
- session._switchSessionList = null;
572
- session._deleteSessionList = null;
573
- session._pendingSwitchSession = null;
574
- session._editTarget = null;
575
- session._editList = null;
576
- session._editSessionId = null;
577
- session._historyList = null;
578
- session._forkList = null;
579
- session._forkSessionId = null;
580
- session.expertMode = false;
581
- session.systemPrompt = null;
582
- session._analyzeMode = false;
583
- session._analyzeTask = null;
584
- session._showSessionState = null;
585
- session.id = `${Date.now()}-${ctx.threadId}-reset`;
586
129
  openCodeSessions?.delete(ctx.threadId);
587
- globalThis.__latestOpenCodeSession = null;
588
- saveSessionMapping();
589
130
  await adapter.reply(ctx.threadId, '🔄 会话已重置,下次发送消息将创建新会话');
590
131
  return true;
591
132
  }
592
133
 
593
- case 'refresh': {
594
- const ocSession = openCodeSessions.get(ctx.threadId);
595
- if (!ocSession) {
596
- await adapter.reply(ctx.threadId, '❌ 没有活跃的会话');
597
- return true;
598
- }
599
- await adapter.reply(ctx.threadId, '🔄 正在刷新会话...');
600
- try {
601
- await ocSession.client.session.compact({ path: { id: ocSession.sessionId } });
602
- await ocSession.client.session.summarize({ path: { id: ocSession.sessionId } });
603
- await adapter.reply(ctx.threadId, '✅ 会话已刷新');
604
- } catch (e) {
605
- await adapter.reply(ctx.threadId, '✅ 会话已刷新');
606
- }
607
- return true;
608
- }
609
-
610
134
  case 'delete': {
611
135
  const keyToDelete = arg ? arg.trim() : null;
612
136
 
@@ -643,89 +167,123 @@ async function handleCommand(adapter, ctx, command, arg, openCodeSessions) {
643
167
  return result;
644
168
  }
645
169
 
646
- case 'agents': {
647
- const agents = registry.listAgents();
648
- const lines = ['🤖 可用 AI Agent:'];
649
- for (const a of agents) {
650
- const agent = registry.findAgent(a);
651
- const available = await agent?.isAvailable().catch(() => false);
652
- lines.push(`${available ? '✅' : '❌'} ${a}`);
653
- }
654
- lines.push('', '切换: /oc /cc /cx /copilot');
655
- await adapter.reply(ctx.threadId, lines.join('\n'));
656
- return true;
657
- }
658
-
659
170
  case 'model': {
660
171
  try {
661
- if (arg) {
662
- const modelStr = arg.trim();
663
-
664
- // Search mode: /model <keyword>
665
- if (!modelStr.includes('/')) {
666
- const opencode = await initOpenCode();
667
- if (!opencode) {
668
- await adapter.reply(ctx.threadId, '❌ OpenCode 不可用');
669
- return true;
670
- }
671
- const result = await opencode.client.provider.list();
672
- if (result.error || !result.data?.all) {
673
- await adapter.reply(ctx.threadId, '❌ 无法获取模型列表');
674
- return true;
675
- }
676
- const q = modelStr.toLowerCase();
677
- const matches = [];
678
- for (const p of result.data.all) {
679
- for (const mid of Object.keys(p.models || {})) {
680
- if (`${p.id}/${mid}`.toLowerCase().includes(q)) {
681
- matches.push(`${p.id}/${mid}`);
682
- }
683
- }
684
- }
685
- if (matches.length === 0) {
686
- await adapter.reply(ctx.threadId, `🔍 未找到包含 "${modelStr}" 的模型`);
687
- return true;
172
+ const current = getThreadModel(ctx.threadId);
173
+ const recent = getRecentModels();
174
+
175
+ if (!arg) {
176
+ let msg = current
177
+ ? `🧠 当前: ${current.providerID}/${current.modelID}\n\n`
178
+ : '';
179
+ if (recent.length > 0) {
180
+ msg += '最近使用:\n';
181
+ for (const r of recent) {
182
+ const mark = (current && r.providerID === current.providerID && r.modelID === current.modelID) ? ' ←' : '';
183
+ msg += ` ${r.providerID}/${r.modelID}${mark}\n`;
688
184
  }
689
- matches.sort();
690
- let msg = `🔍 搜索 "${modelStr}" (${matches.length} 个):\n`;
691
- for (const m of matches.slice(0, 30)) {
692
- msg += ` ${m}\n`;
185
+ msg += '\n';
186
+ }
187
+ msg += '用法:\n /model list 显示全部模型\n /model 关键词 — 搜索\n /model <provider>/<id> — 切换';
188
+ const msgs = splitMessage(msg);
189
+ for (const m of msgs) await adapter.reply(ctx.threadId, m);
190
+ return true;
191
+ }
192
+
193
+ // Numbered selection: /model 3
194
+ if (/^\d+$/.test(arg.trim())) {
195
+ const idx = parseInt(arg.trim(), 10);
196
+ const providers = await listProviders();
197
+ if (!providers) {
198
+ await adapter.reply(ctx.threadId, '❌ 无法获取模型列表');
199
+ return true;
200
+ }
201
+ const allModels = [];
202
+ for (const p of providers) {
203
+ for (const mid of Object.keys(p.models || {})) {
204
+ allModels.push(`${p.id}/${mid}`);
693
205
  }
694
- msg += '\n切换: /model <provider>/<modelID>';
695
- const msgs = splitMessage(msg);
696
- for (const m of msgs) await adapter.reply(ctx.threadId, m);
206
+ }
207
+ if (idx < 1 || idx > allModels.length) {
208
+ await adapter.reply(ctx.threadId, `❌ 序号 ${idx} 超出范围 (1-${allModels.length})`);
697
209
  return true;
698
210
  }
699
-
700
- const entry = setThreadModel(ctx.threadId, modelStr);
211
+ const selected = allModels[idx - 1];
212
+ const entry = setThreadModel(ctx.threadId, selected);
701
213
  if (entry) {
702
- await adapter.reply(ctx.threadId, `✅ 已切换模型至: ${entry.providerID}/${entry.modelID}`);
703
- } else {
704
- await adapter.reply(ctx.threadId, '❌ 格式错误,请使用: /model <provider>/<modelID>');
214
+ await adapter.reply(ctx.threadId, `✅ 已切换至 #${idx}: ${entry.providerID}/${entry.modelID}`);
705
215
  }
706
216
  return true;
707
217
  }
708
- const current = getThreadModel(ctx.threadId);
709
- let msg = current
710
- ? `🧠 当前模型: ${current.providerID}/${current.modelID}\n\n`
711
- : '';
712
218
 
713
- const recent = getRecentModels();
714
- if (recent.length > 0) {
715
- msg += '最近使用:\n';
716
- for (const r of recent) {
717
- const mark = (current && r.providerID === current.providerID && r.modelID === current.modelID) ? ' ' : '';
718
- msg += ` ${r.providerID}/${r.modelID}${mark}\n`;
219
+ // /model list — show all models with numbers
220
+ if (arg.trim().toLowerCase() === 'list') {
221
+ const providers = await listProviders();
222
+ if (!providers) {
223
+ await adapter.reply(ctx.threadId, ' 无法获取模型列表');
224
+ return true;
225
+ }
226
+ const lines = [];
227
+ let n = 0;
228
+ for (const p of providers) {
229
+ const mids = Object.keys(p.models || {});
230
+ if (mids.length === 0) continue;
231
+ lines.push(`\n【${p.id}】`);
232
+ for (const mid of mids) {
233
+ n++;
234
+ const mark = (current && current.providerID === p.id && current.modelID === mid) ? ' ←' : '';
235
+ lines.push(` ${n}. ${p.id}/${mid}${mark}`);
236
+ }
719
237
  }
720
- msg += '\n';
238
+ if (n === 0) {
239
+ await adapter.reply(ctx.threadId, '❌ 没有可用模型');
240
+ return true;
241
+ }
242
+ lines.push(`\n切换: /model <序号>`);
243
+ const msgs = splitMessage(lines.join('\n'));
244
+ for (const m of msgs) await adapter.reply(ctx.threadId, m);
245
+ return true;
721
246
  }
722
- if (!current) {
723
- msg += '提示: 用 /model <关键词> 搜索模型,/model <provider>/<modelID> 切换\n';
247
+
248
+ // Search: /model <keyword>
249
+ if (!arg.includes('/')) {
250
+ const providers = await listProviders();
251
+ if (!providers) {
252
+ await adapter.reply(ctx.threadId, '❌ 无法获取模型列表');
253
+ return true;
254
+ }
255
+ const q = arg.trim().toLowerCase();
256
+ const matches = [];
257
+ for (const p of providers) {
258
+ for (const mid of Object.keys(p.models || {})) {
259
+ const name = `${p.id}/${mid}`;
260
+ if (name.toLowerCase().includes(q)) {
261
+ matches.push(name);
262
+ }
263
+ }
264
+ }
265
+ if (matches.length === 0) {
266
+ await adapter.reply(ctx.threadId, `🔍 未找到包含 "${arg.trim()}" 的模型`);
267
+ return true;
268
+ }
269
+ matches.sort();
270
+ let msg = `🔍 "${arg.trim()}" (${matches.length}):\n`;
271
+ for (const m of matches.slice(0, 30)) {
272
+ msg += ` ${m}\n`;
273
+ }
274
+ msg += '\n切换: /model <provider>/<modelID>';
275
+ const msgs = splitMessage(msg);
276
+ for (const m of msgs) await adapter.reply(ctx.threadId, m);
277
+ return true;
278
+ }
279
+
280
+ // Direct switch: /model <provider>/<modelID>
281
+ const entry = setThreadModel(ctx.threadId, arg.trim());
282
+ if (entry) {
283
+ await adapter.reply(ctx.threadId, `✅ 已切换至: ${entry.providerID}/${entry.modelID}`);
724
284
  } else {
725
- msg += '用法: /model <关键词> — 搜索\n /model <provider>/<modelID> — 切换';
285
+ await adapter.reply(ctx.threadId, ' 格式错误,使用: /model <provider>/<modelID>');
726
286
  }
727
- const msgs = splitMessage(msg);
728
- for (const m of msgs) await adapter.reply(ctx.threadId, m);
729
287
  return true;
730
288
  } catch (e) {
731
289
  await adapter.reply(ctx.threadId, `❌ 模型操作失败: ${e.message}`);
@@ -734,36 +292,231 @@ async function handleCommand(adapter, ctx, command, arg, openCodeSessions) {
734
292
  }
735
293
 
736
294
 
737
- case 'demo': {
738
- const argText = (arg || '').trim().toLowerCase();
739
- if (argText === 'off' || argText === 'exit' || argText === 'stop') {
740
- setDemoMode(ctx.threadId, false);
741
- await adapter.reply(ctx.threadId, '⏹️ 已退出沙箱模式');
742
- return true;
743
- }
744
- setDemoMode(ctx.threadId, true);
745
- let msg = '🎮 沙箱模式已启动\n\n在此模式下所有命令返回模拟输出,无需连接 OpenCode。\n\n';
746
- msg += '试试发送: /help /status /model /agents /loop /copy\n';
747
- msg += '发送 /demo off 退出';
748
- await adapter.reply(ctx.threadId, msg);
749
- return true;
750
- }
751
-
752
295
  case 'diagnose': {
753
296
  const { checkConnection } = await import('../opencode/client.js');
754
297
  const diag = ['🔍 诊断报告\n'];
755
298
  diag.push(`OpenCode: ${await checkConnection().then(() => '✅').catch(() => '❌')}`);
756
299
  diag.push(`七牛云: ${process.env.QINIU_ACCESS_KEY ? '✅' : '❌'}`);
757
- diag.push(`项目目录: ${session.projectDir || globalThis.__autoProjectDir || '❌ 未设置'}`);
300
+ diag.push(`项目目录: ${globalThis.__autoProjectDir || '❌ 未设置'}`);
758
301
  diag.push(`会话: ${openCodeSessions?.get(ctx.threadId) ? '✅' : '❌'}`);
759
302
  const msgs = splitMessage(diag.join('\n'));
760
303
  for (const m of msgs) await adapter.reply(ctx.threadId, m);
761
304
  return true;
762
305
  }
763
306
 
307
+ case 'raw': {
308
+ const val = arg?.trim().toLowerCase();
309
+ if (val === 'on' || val === '1' || val === 'true') {
310
+ setRawDebug(true);
311
+ await adapter.reply(ctx.threadId, '📄 RAW 输出已开启');
312
+ } else if (val === 'off' || val === '0' || val === 'false') {
313
+ setRawDebug(false);
314
+ await adapter.reply(ctx.threadId, '📄 RAW 输出已关闭');
315
+ } else {
316
+ await adapter.reply(ctx.threadId, `📄 RAW 输出当前: ${isRawDebug() ? '🟢 ON' : '🔴 OFF'}\n用法: /raw on 或 /raw off`);
317
+ }
318
+ return true;
319
+ }
320
+
321
+ case 'think': {
322
+ const { setThinkVisible, isThinkVisible } = await import('../opencode/client.js');
323
+ const val = arg?.trim().toLowerCase();
324
+ if (val === 'on' || val === '1' || val === 'true') {
325
+ setThinkVisible(true);
326
+ await adapter.reply(ctx.threadId, '🤔 思考过程已开启');
327
+ } else if (val === 'off' || val === '0' || val === 'false') {
328
+ setThinkVisible(false);
329
+ await adapter.reply(ctx.threadId, '🤔 思考过程已关闭');
330
+ } else {
331
+ await adapter.reply(ctx.threadId, `🤔 思考过程当前: ${isThinkVisible() ? '🟢 ON' : '🔴 OFF'}\n用法: /think on 或 /think off`);
332
+ }
333
+ return true;
334
+ }
335
+
336
+ case 'share': {
337
+ const val = arg?.trim().toLowerCase();
338
+ const isOwner = hasOwner('weixin') && claimOwnership('weixin', ctx.userId);
339
+
340
+ if (!val || val === 'status') {
341
+ const members = [...sharedRoom.members].join(', ') || '无';
342
+ await adapter.reply(ctx.threadId, `👥 共享会话\n成员: ${members}\n${sharedRoom.busy ? '⏳ 处理中' : '✅ 空闲'}\n\n/share join — 加入共享\n/share leave — 离开`);
343
+ return true;
344
+ }
345
+ if (val === 'join') {
346
+ if (sharedRoom.members.size === 0) {
347
+ await adapter.reply(ctx.threadId, '👥 暂无共享会话,你就是第一个!');
348
+ }
349
+ addSharedMember(ctx.threadId);
350
+ await adapter.reply(ctx.threadId, '✅ 你已加入共享会话,所有消息将共享给其他成员');
351
+ return true;
352
+ }
353
+ if (val === 'leave') {
354
+ removeSharedMember(ctx.threadId);
355
+ await adapter.reply(ctx.threadId, '✅ 你已离开共享会话');
356
+ return true;
357
+ }
358
+ await adapter.reply(ctx.threadId, '❌ 用法: /share — 查看状态\n/share join — 加入\n/share leave — 离开');
359
+ return true;
360
+ }
361
+
362
+ case 'bind': {
363
+ const { fetchQRCode, pollQRStatus } = await import('./api.js');
364
+ const { addBotInstance, saveCredential } = await import('./bot.js');
365
+
366
+ const baseUrl = DEFAULT_BASE_URL;
367
+ await adapter.reply(ctx.threadId, '📱 正在获取二维码...');
368
+ const qrResp = await fetchQRCode(baseUrl);
369
+ if (!qrResp.qrcode_img_content) {
370
+ await adapter.reply(ctx.threadId, '❌ 获取二维码失败');
371
+ return true;
372
+ }
373
+ await adapter.reply(ctx.threadId, `📱 扫码绑定新 Bot:\n${qrResp.qrcode_img_content}`);
374
+
375
+ (async () => {
376
+ const startTime = Date.now();
377
+ const timeout = 8 * 60 * 1000;
378
+ let notifiedScanned = false;
379
+ while (Date.now() - startTime < timeout) {
380
+ try {
381
+ const status = await pollQRStatus(baseUrl, qrResp.qrcode);
382
+ switch (status.status) {
383
+ case 'wait':
384
+ break;
385
+ case 'scaned':
386
+ if (!notifiedScanned) {
387
+ await adapter.reply(ctx.threadId, '📱 已扫码,请在手机上确认...');
388
+ notifiedScanned = true;
389
+ }
390
+ break;
391
+ case 'expired':
392
+ await adapter.reply(ctx.threadId, '⌛ 二维码已过期,请重新 /bind');
393
+ return;
394
+ case 'confirmed':
395
+ if (!status.bot_token || !status.ilink_bot_id) {
396
+ await adapter.reply(ctx.threadId, '❌ 绑定失败:未收到 Bot Token');
397
+ return;
398
+ }
399
+ const creds = {
400
+ token: status.bot_token,
401
+ baseUrl: status.baseurl || baseUrl,
402
+ accountId: status.ilink_bot_id,
403
+ userId: status.ilink_user_id,
404
+ };
405
+ saveCredential(creds);
406
+ addBotInstance(creds, openCodeSessions);
407
+ await adapter.reply(ctx.threadId, `✅ 新 Bot 绑定成功!账号: ${creds.accountId}`);
408
+ return;
409
+ }
410
+ } catch (e) {
411
+ console.error('[bind] poll error:', e);
412
+ }
413
+ await new Promise(r => setTimeout(r, 1000));
414
+ }
415
+ await adapter.reply(ctx.threadId, '⌛ 绑定超时,请重新 /bind');
416
+ })();
417
+
418
+ return true;
419
+ }
420
+
421
+ case 'who': {
422
+ const others = [];
423
+ for (const [uid] of userAdapterMap) {
424
+ if (uid !== ctx.threadId) {
425
+ others.push(uid);
426
+ }
427
+ }
428
+ if (others.length === 0) {
429
+ await adapter.reply(ctx.threadId, '👤 只有你一个人在线');
430
+ return true;
431
+ }
432
+ let msg = '👥 在线用户:\n';
433
+ others.forEach((uid, i) => { msg += ` ${i + 1}. ${uid}\n`; });
434
+ await adapter.reply(ctx.threadId, msg);
435
+ return true;
436
+ }
437
+
438
+ case 'push': {
439
+ const others = [];
440
+ for (const [uid] of userAdapterMap) {
441
+ if (uid !== ctx.threadId) {
442
+ others.push(uid);
443
+ }
444
+ }
445
+ if (others.length === 0) {
446
+ await adapter.reply(ctx.threadId, '❌ 没有其他 Bot 用户可推送');
447
+ return true;
448
+ }
449
+ const targetMatch = ctx.arg?.match(/^@(\d+)\s+(.+)/);
450
+ let msg;
451
+ let targets;
452
+ if (targetMatch) {
453
+ const idx = parseInt(targetMatch[1], 10) - 1;
454
+ const target = others[idx];
455
+ if (!target) {
456
+ await adapter.reply(ctx.threadId, `❌ 没有序号 ${targetMatch[1]} 的用户,先用 /who 查看`);
457
+ return true;
458
+ }
459
+ targets = [target];
460
+ msg = targetMatch[2];
461
+ } else {
462
+ targets = others;
463
+ msg = ctx.arg || '📢 请到项目上处理一下';
464
+ }
465
+ let sent = 0;
466
+ for (const uid of targets) {
467
+ const targetAdapter = userAdapterMap.get(uid);
468
+ if (targetAdapter) {
469
+ try {
470
+ await targetAdapter.reply(uid, msg);
471
+ sent++;
472
+ } catch (e) {
473
+ console.error('[push] send failed:', e.message);
474
+ }
475
+ }
476
+ }
477
+ await adapter.reply(ctx.threadId, `✅ 已推送给 ${sent}/${targets.length} 个用户`);
478
+ return true;
479
+ }
480
+
481
+ case 'auto': {
482
+ const { startAutoLoop, stopAutoLoop, isAutoRunning } = await import('../autonomous/index.js');
483
+ const arg = (ctx.arg || '').trim().toLowerCase();
484
+ if (arg === 'off' || arg === 'stop') {
485
+ stopAutoLoop();
486
+ await adapter.reply(ctx.threadId, '⏹ 自主开发已停止');
487
+ return true;
488
+ }
489
+ if (arg === 'status' || arg === '') {
490
+ const running = isAutoRunning();
491
+ await adapter.reply(ctx.threadId, running ? '🤖 自主开发运行中' : '⏸ 自主开发未启动');
492
+ return true;
493
+ }
494
+ if (isAutoRunning()) {
495
+ await adapter.reply(ctx.threadId, '⏳ 已有自主开发任务运行中,先 /auto off 再启动新的');
496
+ return true;
497
+ }
498
+ const goal = ctx.arg || '审查项目代码,找出最需要改进的地方并实施';
499
+ const autoBroadcast = isSharedMember(ctx.threadId) ? [...sharedRoom.members].filter(tid => tid !== ctx.threadId) : [];
500
+ startAutoLoop({ adapter, threadId: ctx.threadId, goal, openCodeSessions, broadcastTo: autoBroadcast });
501
+ return true;
502
+ }
503
+
504
+ case 'deploy': {
505
+ const { gitPush } = await import('../core/git-push.js');
506
+ await adapter.reply(ctx.threadId, '📤 正在推送代码...');
507
+ const result = gitPush({ message: ctx.arg || undefined });
508
+ if (result.ok) {
509
+ await adapter.reply(ctx.threadId, `✅ 推送成功: ${result.successUrl}`);
510
+ } else {
511
+ const details = result.results.map(r => `${r.ok ? '✅' : '❌'} ${r.url}${r.error ? ': ' + r.error : ''}`).join('\n');
512
+ await adapter.reply(ctx.threadId, `❌ 推送失败:\n${details}`);
513
+ }
514
+ return true;
515
+ }
516
+
764
517
  default:
765
518
  return false;
766
519
  }
767
520
  }
768
521
 
769
- export { handleAgentSwitch, handleCommand, formatTimeAgo };
522
+ export { handleAgentSwitch, handleCommand };