@yvhitxcel/opencode-remote 0.15.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.
Files changed (102) hide show
  1. package/README.md +82 -0
  2. package/bin/opencode-remote.js +70 -0
  3. package/bin/opencode-weixin.js +10 -0
  4. package/dist/AGENTS.md +20 -0
  5. package/dist/MEMORY.md +21 -0
  6. package/dist/bot-runner.js +180 -0
  7. package/dist/cli.js +256 -0
  8. package/dist/core/approval.js +95 -0
  9. package/dist/core/auth.js +119 -0
  10. package/dist/core/config.js +61 -0
  11. package/dist/core/notifications.js +134 -0
  12. package/dist/core/qiniu.js +267 -0
  13. package/dist/core/registry.js +86 -0
  14. package/dist/core/router.js +344 -0
  15. package/dist/core/session.js +403 -0
  16. package/dist/core/setup.js +418 -0
  17. package/dist/core/types.js +16 -0
  18. package/dist/feishu/adapter.js +72 -0
  19. package/dist/feishu/bot.js +168 -0
  20. package/dist/feishu/commands.js +601 -0
  21. package/dist/feishu/handler.js +380 -0
  22. package/dist/index.js +60 -0
  23. package/dist/opencode/client.js +823 -0
  24. package/dist/package-lock.json +762 -0
  25. package/dist/patch_spawn.js +28 -0
  26. package/dist/plugins/agents/acp/acp-adapter.js +42 -0
  27. package/dist/plugins/agents/claude-code/index.js +69 -0
  28. package/dist/plugins/agents/codex/index.js +44 -0
  29. package/dist/plugins/agents/copilot/index.js +44 -0
  30. package/dist/plugins/agents/opencode/index.js +66 -0
  31. package/dist/telegram/bot.js +288 -0
  32. package/dist/utils/message-split.js +38 -0
  33. package/dist/web/code-viewer.js +266 -0
  34. package/dist/weixin/adapter.js +135 -0
  35. package/dist/weixin/api.js +179 -0
  36. package/dist/weixin/bot.js +183 -0
  37. package/dist/weixin/commands.js +758 -0
  38. package/dist/weixin/handler.js +577 -0
  39. package/dist/weixin/node_modules/encodeurl/LICENSE +22 -0
  40. package/dist/weixin/node_modules/encodeurl/README.md +109 -0
  41. package/dist/weixin/node_modules/encodeurl/index.js +60 -0
  42. package/dist/weixin/node_modules/encodeurl/package.json +40 -0
  43. package/dist/weixin/node_modules/qiniu/.claude/settings.local.json +7 -0
  44. package/dist/weixin/node_modules/qiniu/.github/workflows/ci-test.yml +36 -0
  45. package/dist/weixin/node_modules/qiniu/.github/workflows/npm-publish.yml +20 -0
  46. package/dist/weixin/node_modules/qiniu/.github/workflows/version-check.yml +19 -0
  47. package/dist/weixin/node_modules/qiniu/.idea/MarsCodeWorkspaceAppSettings.xml +7 -0
  48. package/dist/weixin/node_modules/qiniu/.idea/codeStyles/Project.xml +44 -0
  49. package/dist/weixin/node_modules/qiniu/.idea/codeStyles/codeStyleConfig.xml +5 -0
  50. package/dist/weixin/node_modules/qiniu/.idea/git_toolbox_blame.xml +6 -0
  51. package/dist/weixin/node_modules/qiniu/.idea/inspectionProfiles/Project_Default.xml +6 -0
  52. package/dist/weixin/node_modules/qiniu/.idea/jsLibraryMappings.xml +6 -0
  53. package/dist/weixin/node_modules/qiniu/.idea/modules.xml +8 -0
  54. package/dist/weixin/node_modules/qiniu/.idea/nodejs-sdk.iml +12 -0
  55. package/dist/weixin/node_modules/qiniu/.idea/vcs.xml +6 -0
  56. package/dist/weixin/node_modules/qiniu/CHANGELOG.md +292 -0
  57. package/dist/weixin/node_modules/qiniu/README.md +56 -0
  58. package/dist/weixin/node_modules/qiniu/StorageResponseInterface.d.ts +239 -0
  59. package/dist/weixin/node_modules/qiniu/codecov.yml +28 -0
  60. package/dist/weixin/node_modules/qiniu/index.d.ts +1995 -0
  61. package/dist/weixin/node_modules/qiniu/index.js +32 -0
  62. package/dist/weixin/node_modules/qiniu/node_modules/encodeurl/HISTORY.md +14 -0
  63. package/dist/weixin/node_modules/qiniu/node_modules/encodeurl/LICENSE +22 -0
  64. package/dist/weixin/node_modules/qiniu/node_modules/encodeurl/README.md +128 -0
  65. package/dist/weixin/node_modules/qiniu/node_modules/encodeurl/index.js +60 -0
  66. package/dist/weixin/node_modules/qiniu/node_modules/encodeurl/package.json +40 -0
  67. package/dist/weixin/node_modules/qiniu/package.json +80 -0
  68. package/dist/weixin/node_modules/qiniu/qiniu/auth/digest.js +13 -0
  69. package/dist/weixin/node_modules/qiniu/qiniu/cdn.js +149 -0
  70. package/dist/weixin/node_modules/qiniu/qiniu/conf.js +254 -0
  71. package/dist/weixin/node_modules/qiniu/qiniu/fop.js +112 -0
  72. package/dist/weixin/node_modules/qiniu/qiniu/httpc/client.js +253 -0
  73. package/dist/weixin/node_modules/qiniu/qiniu/httpc/endpoint.js +66 -0
  74. package/dist/weixin/node_modules/qiniu/qiniu/httpc/endpointsProvider.js +27 -0
  75. package/dist/weixin/node_modules/qiniu/qiniu/httpc/endpointsRetryPolicy.js +76 -0
  76. package/dist/weixin/node_modules/qiniu/qiniu/httpc/middleware/base.js +31 -0
  77. package/dist/weixin/node_modules/qiniu/qiniu/httpc/middleware/index.js +9 -0
  78. package/dist/weixin/node_modules/qiniu/qiniu/httpc/middleware/qiniuAuth.js +53 -0
  79. package/dist/weixin/node_modules/qiniu/qiniu/httpc/middleware/retryDomains.js +101 -0
  80. package/dist/weixin/node_modules/qiniu/qiniu/httpc/middleware/ua.js +36 -0
  81. package/dist/weixin/node_modules/qiniu/qiniu/httpc/region.js +349 -0
  82. package/dist/weixin/node_modules/qiniu/qiniu/httpc/regionsProvider.js +788 -0
  83. package/dist/weixin/node_modules/qiniu/qiniu/httpc/regionsRetryPolicy.js +242 -0
  84. package/dist/weixin/node_modules/qiniu/qiniu/httpc/responseWrapper.js +40 -0
  85. package/dist/weixin/node_modules/qiniu/qiniu/retry/index.js +4 -0
  86. package/dist/weixin/node_modules/qiniu/qiniu/retry/retrier.js +99 -0
  87. package/dist/weixin/node_modules/qiniu/qiniu/retry/retryPolicy.js +55 -0
  88. package/dist/weixin/node_modules/qiniu/qiniu/rpc.js +237 -0
  89. package/dist/weixin/node_modules/qiniu/qiniu/rtc/app.js +123 -0
  90. package/dist/weixin/node_modules/qiniu/qiniu/rtc/credentials.js +57 -0
  91. package/dist/weixin/node_modules/qiniu/qiniu/rtc/room.js +118 -0
  92. package/dist/weixin/node_modules/qiniu/qiniu/rtc/util.js +16 -0
  93. package/dist/weixin/node_modules/qiniu/qiniu/sms/message.js +58 -0
  94. package/dist/weixin/node_modules/qiniu/qiniu/storage/form.js +442 -0
  95. package/dist/weixin/node_modules/qiniu/qiniu/storage/internal.js +214 -0
  96. package/dist/weixin/node_modules/qiniu/qiniu/storage/resume.js +1272 -0
  97. package/dist/weixin/node_modules/qiniu/qiniu/storage/rs.js +1764 -0
  98. package/dist/weixin/node_modules/qiniu/qiniu/util.js +382 -0
  99. package/dist/weixin/node_modules/qiniu/qiniu/zone.js +230 -0
  100. package/dist/weixin/node_modules/qiniu/tsconfig.json +112 -0
  101. package/dist/weixin/types.js +25 -0
  102. package/package.json +56 -0
@@ -0,0 +1,758 @@
1
+ import { detectCommand, COMMAND_ALIASES } from '../core/router.js';
2
+ import { getOrCreateSession, saveSessionMapping, sessionManager } from '../core/session.js';
3
+ import { splitMessage } from '../core/notifications.js';
4
+ import { initOpenCode, checkConnection, abortSession, resumeSession, revertSessionMessage, unrevertSession, listProviders, updateGlobalModel } from '../opencode/client.js';
5
+ import { claimOwnership } from '../core/auth.js';
6
+ import { registry } from '../core/registry.js';
7
+ import { uploadToQiniu, findBuildOutputs, formatSize, deleteFromQiniu } from '../core/qiniu.js';
8
+ import { existsSync } from 'fs';
9
+ import { join, basename } from 'path';
10
+
11
+ export let _startLoopCycle = null;
12
+ export function _registerStartLoopCycle(fn) { _startLoopCycle = fn; }
13
+
14
+ async function handleAgentSwitch(adapter, ctx, agentName, prompt) {
15
+ const agent = registry.findAgent(agentName);
16
+
17
+ if (!agent) {
18
+ await adapter.reply(ctx.threadId, `❌ Agent "${agentName}" 未找到`);
19
+ return true;
20
+ }
21
+
22
+ const available = await agent.isAvailable().catch(() => false);
23
+ if (!available) {
24
+ await adapter.reply(ctx.threadId, `❌ ${agentName} 不可用`);
25
+ return true;
26
+ }
27
+
28
+ const session = await getOrCreateSession(ctx.threadId, 'weixin');
29
+ session.currentAgent = agentName;
30
+
31
+ if (!prompt) {
32
+ try {
33
+ await adapter.reply(ctx.threadId, `✅ 已切换到 ${agentName}`);
34
+ } catch (e) {
35
+ console.error(`[handleAgentSwitch] reply failed: ${e.message}`);
36
+ }
37
+ saveSessionMapping();
38
+ return true;
39
+ }
40
+
41
+ await adapter.sendTyping?.(ctx.threadId, true);
42
+
43
+ try {
44
+ const history = session.commandHistory || [];
45
+ const response = await agent.sendPrompt(session.id, prompt, history, { projectDir: session.projectDir || globalThis.__autoProjectDir });
46
+
47
+ await adapter.sendTyping?.(ctx.threadId, false);
48
+
49
+ const chunks = splitMessage(response || '无响应');
50
+ for (const chunk of chunks) {
51
+ await adapter.reply(ctx.threadId, chunk);
52
+ }
53
+
54
+ session.commandHistory = session.commandHistory || [];
55
+ session.commandHistory.push(prompt);
56
+ saveSessionMapping();
57
+
58
+ } catch (error) {
59
+ await adapter.sendTyping?.(ctx.threadId, false);
60
+ await adapter.reply(ctx.threadId, `❌ 错误: ${error.message}`);
61
+ }
62
+
63
+ return true;
64
+ }
65
+
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
+ async function handleCommand(adapter, ctx, command, arg, openCodeSessions) {
78
+ const session = await getOrCreateSession(ctx.threadId, 'weixin');
79
+ switch (command) {
80
+ case 'start': {
81
+ const result = claimOwnership('weixin', ctx.userId);
82
+ if (result.success) {
83
+ if (result.message === 'claimed') {
84
+ await adapter.reply(ctx.threadId, `🔐 安全设置完成!你是此 bot 的唯一所有者。\n\n发送消息给 OpenCode 开始工作\n/help 查看指令\n/status 查看状态`);
85
+ } else {
86
+ await adapter.reply(ctx.threadId, `🚀 准备就绪\n\n发送消息给 OpenCode 开始工作\n/help 查看指令`);
87
+ }
88
+ } else {
89
+ await adapter.reply(ctx.threadId, '🚫 你无权使用此 bot');
90
+ }
91
+ return true;
92
+ }
93
+ case 'help':
94
+ await adapter.reply(ctx.threadId, `📖 指令
95
+
96
+ 🟢 常用:
97
+ /start — 首次认证
98
+ /help — 帮助
99
+ /status — 查看状态
100
+ /reset — 重置会话
101
+ /copy — 复制回复
102
+ /revert — 撤销消息
103
+
104
+ 🔄 任务:
105
+ /loop — 循环执行
106
+ /refresh — 刷新上下文
107
+ /restart — 重启 bot
108
+ /stop — 停止 bot
109
+
110
+ 📂 会话:
111
+ /sessions — 浏览会话
112
+ /delsessions — 删除会话
113
+
114
+ 🤖 AI 模型:
115
+ /model — 切换模型
116
+ /agents — 查看可用 Agent
117
+ /oc — 使用 OpenCode
118
+ /cc — 使用 Claude Code
119
+
120
+ ⬆️ 文件:
121
+ /upload — 上传构建产物
122
+
123
+ 💬 直接发消息给 AI!`);
124
+ return true;
125
+ case 'status': {
126
+ const connected = await checkConnection();
127
+ const running = session.taskStartTime ? Math.round((Date.now() - session.taskStartTime) / 1000) : 0;
128
+
129
+ let msg = `${connected ? '✅' : '❌'} OpenCode ${connected ? '在线' : '离线'}\n\n`;
130
+
131
+ const actualSession = openCodeSessions?.get(ctx.threadId) ||
132
+ (session.opencodeSessionId ? { sessionId: session.opencodeSessionId } : null);
133
+
134
+ msg += `会话: ${actualSession?.sessionId?.slice(0, 8) || '无'}\n`;
135
+
136
+ if (running > 0) {
137
+ const m = Math.floor(running / 60);
138
+ const s = running % 60;
139
+ msg += `运行中: ${m}分${s}秒\n`;
140
+ }
141
+ if (session.currentTool) {
142
+ msg += `当前: ${session.currentTool}\n`;
143
+ }
144
+ if (session.modifiedFiles?.length > 0 || session.modifiedFiles?.size > 0) {
145
+ msg += `已修改: ${(session.modifiedFiles?.length || session.modifiedFiles?.size || 0)} 个文件\n`;
146
+ }
147
+
148
+ const projectDir = session.projectDir || globalThis.__autoProjectDir;
149
+ if (projectDir) {
150
+ msg += `项目目录: ${projectDir}\n`;
151
+ } else {
152
+ msg += `项目目录: 未设置\n`;
153
+ }
154
+
155
+ const workDir = process.cwd();
156
+ msg += `工作目录: ${workDir}\n`;
157
+
158
+ if (session.originalProjectDir && session.originalProjectDir !== projectDir) {
159
+ msg += `原始目录: ${session.originalProjectDir}\n`;
160
+ }
161
+
162
+ await adapter.reply(ctx.threadId, msg);
163
+ return true;
164
+ }
165
+
166
+ case 'sessions': {
167
+ try {
168
+ const opencode = await initOpenCode();
169
+ if (!opencode) {
170
+ await adapter.reply(ctx.threadId, '❌ 无法连接 OpenCode');
171
+ return true;
172
+ }
173
+ const result = await opencode.client.session.list();
174
+ if (result.error || !result.data || result.data.length === 0) {
175
+ await adapter.reply(ctx.threadId, '📭 暂无会话');
176
+ return true;
177
+ }
178
+ const sorted = result.data.sort((a, b) => (b.time.updated || 0) - (a.time.updated || 0));
179
+ session._switchSessionList = sorted;
180
+ session._showSessionState = true;
181
+ let msg = '📂 选择会话(回复编号):\n\n';
182
+ sorted.slice(0, 10).forEach((s, i) => {
183
+ const n = i + 1;
184
+ const title = s.title || '无标题';
185
+ const time = s.updated_at ? formatTimeAgo(s.updated_at * 1000) : '';
186
+ msg += `${n}. ${title} (${time})\n`;
187
+ });
188
+ if (sorted.length > 10) {
189
+ msg += `\n... 共 ${sorted.length} 个会话`;
190
+ }
191
+ msg += '\n\n回复编号切换会话';
192
+ await adapter.reply(ctx.threadId, msg);
193
+ } catch (e) {
194
+ await adapter.reply(ctx.threadId, `❌ 获取会话失败: ${e.message}`);
195
+ }
196
+ return true;
197
+ }
198
+
199
+ case 'delsessions': {
200
+ try {
201
+ const opencode = await initOpenCode();
202
+ if (!opencode) {
203
+ await adapter.reply(ctx.threadId, '❌ 无法连接 OpenCode');
204
+ return true;
205
+ }
206
+ const result = await opencode.client.session.list();
207
+ if (result.error || !result.data || result.data.length === 0) {
208
+ await adapter.reply(ctx.threadId, '📭 暂无会话可删除');
209
+ return true;
210
+ }
211
+ const sorted = result.data.sort((a, b) => (b.time.updated || 0) - (a.time.updated || 0));
212
+ session._deleteSessionList = sorted;
213
+ let msg = '🗑️ 选择要删除的会话(回复编号):\n\n';
214
+ sorted.slice(0, 10).forEach((s, i) => {
215
+ const n = i + 1;
216
+ const title = s.title || '无标题';
217
+ const time = s.updated_at ? formatTimeAgo(s.updated_at * 1000) : '';
218
+ msg += `${n}. ${title} (${time})\n`;
219
+ });
220
+ if (sorted.length > 10) {
221
+ msg += `\n... 共 ${sorted.length} 个会话`;
222
+ }
223
+ msg += '\n\n回复编号删除';
224
+ await adapter.reply(ctx.threadId, msg);
225
+ } catch (e) {
226
+ await adapter.reply(ctx.threadId, `❌ 获取会话失败: ${e.message}`);
227
+ }
228
+ return true;
229
+ }
230
+
231
+
232
+ case 'copy': {
233
+ const ocSession = openCodeSessions.get(ctx.threadId);
234
+ if (!ocSession) {
235
+ await adapter.reply(ctx.threadId, '❌ 没有活跃的会话');
236
+ return true;
237
+ }
238
+
239
+ const msgsResult = await ocSession.client.session.messages({
240
+ path: { id: ocSession.sessionId },
241
+ query: { limit: 1 }
242
+ });
243
+
244
+ if (msgsResult.error || !msgsResult.data || msgsResult.data.length === 0) {
245
+ await adapter.reply(ctx.threadId, '❌ 无法获取最新消息');
246
+ return true;
247
+ }
248
+
249
+ let latestMsg = msgsResult.data[0];
250
+ if (latestMsg.info.role !== 'assistant') {
251
+ await adapter.reply(ctx.threadId, 'ℹ️ 最新消息不是 AI 回复,正在获取上一条 AI 消息...');
252
+
253
+ const allMsgsResult = await ocSession.client.session.messages({
254
+ path: { id: ocSession.sessionId },
255
+ query: { limit: 10 }
256
+ });
257
+
258
+ if (allMsgsResult.error || !allMsgsResult.data) {
259
+ await adapter.reply(ctx.threadId, '❌ 无法获取会话消息');
260
+ return true;
261
+ }
262
+
263
+ const aiMsg = allMsgsResult.data.find(m => m.info.role === 'assistant');
264
+ if (!aiMsg) {
265
+ await adapter.reply(ctx.threadId, '❌ 未找到 AI 回复');
266
+ return true;
267
+ }
268
+ latestMsg = aiMsg;
269
+ }
270
+
271
+ let content = '';
272
+ if (latestMsg.parts) {
273
+ for (const part of latestMsg.parts) {
274
+ if (part.type === 'text') {
275
+ content += part.text + '\n';
276
+ }
277
+ if (part.type === 'code') {
278
+ content += `\`\`\`${part.language || ''}\n${part.code}\n\`\`\`\n`;
279
+ }
280
+ if (part.type === 'file' && part.content) {
281
+ content += `📁 ${part.filename}:\n${part.content}\n`;
282
+ }
283
+ }
284
+ }
285
+
286
+ if (!content.trim()) {
287
+ await adapter.reply(ctx.threadId, '❌ AI 回复中没有可复制的文本内容');
288
+ return true;
289
+ }
290
+
291
+ await adapter.reply(ctx.threadId, `📋 已复制最新 AI 回复内容:\n\n${content.substring(0, 2000)}${content.length > 2000 ? '...' : ''}`);
292
+ return true;
293
+ }
294
+
295
+ case 'resume': {
296
+ try {
297
+ const opencode = await initOpenCode();
298
+ const result = await opencode.client.session.list();
299
+ if (!result.data || result.data.length === 0) {
300
+ await adapter.reply(ctx.threadId, '❌ 没有找到会话');
301
+ return true;
302
+ }
303
+
304
+ const sorted = result.data.sort((a, b) => (b.time.updated || 0) - (a.time.updated || 0));
305
+ const latest = sorted[0];
306
+
307
+ const resumed = await resumeSession(latest.id);
308
+ if (!resumed) {
309
+ await adapter.reply(ctx.threadId, '❌ 恢复会话失败');
310
+ return true;
311
+ }
312
+
313
+ openCodeSessions.set(ctx.threadId, resumed);
314
+ session.opencodeSessionId = resumed.sessionId;
315
+ const key = `weixin:${ctx.userId}:${ctx.threadId}`;
316
+ sessionManager.saveSession(key, session).catch(() => {});
317
+ saveSessionMapping();
318
+
319
+ if (latest.directory) {
320
+ session.projectDir = latest.directory;
321
+ globalThis.__autoProjectDir = latest.directory;
322
+ }
323
+
324
+ await adapter.reply(ctx.threadId, `✅ 已恢复最近会话\n\n会话: ${latest.title || 'Untitled'}\n📁 目录: ${latest.directory || 'N/A'}\n📝 更新: ${new Date(latest.time.updated).toLocaleString()}`);
325
+ } catch (e) {
326
+ await adapter.reply(ctx.threadId, `❌ 恢复失败: ${e.message}`);
327
+ }
328
+ return true;
329
+ }
330
+ case 'edit': {
331
+ const ocSession = openCodeSessions.get(ctx.threadId);
332
+ if (!ocSession) {
333
+ await adapter.reply(ctx.threadId, '❌ 没有活跃的会话');
334
+ return true;
335
+ }
336
+ if (arg) {
337
+ const num = parseInt(arg, 10);
338
+ if (num >= 1) {
339
+ try {
340
+ const opencode = await initOpenCode();
341
+ const msgsResult = await opencode.client.session.messages({ path: { id: ocSession.sessionId } });
342
+ if (!msgsResult.error && msgsResult.data) {
343
+ const userMsgs = msgsResult.data.filter(m => m.info?.role === 'user');
344
+ if (num <= userMsgs.length) {
345
+ const targetMsg = userMsgs[num - 1];
346
+ const preview = targetMsg.info?.content?.slice(0, 80) || '(空)';
347
+ session._editTarget = { sessionId: ocSession.sessionId, messageID: targetMsg.id, num };
348
+ await adapter.reply(ctx.threadId, `✏️ 选择修改消息 #${num}:\n\n${preview}\n\n请发送修正后的内容,将从该消息之前创建新分支`);
349
+ return true;
350
+ }
351
+ }
352
+ } catch (e) {
353
+ await adapter.reply(ctx.threadId, `❌ 操作失败: ${e.message}`);
354
+ return true;
355
+ }
356
+ }
357
+ await adapter.reply(ctx.threadId, `❌ 无效编号`);
358
+ return true;
359
+ }
360
+ const opencode = await initOpenCode();
361
+ if (!opencode) {
362
+ await adapter.reply(ctx.threadId, '❌ 无法获取消息');
363
+ return true;
364
+ }
365
+ const msgsResult = await opencode.client.session.messages({ path: { id: ocSession.sessionId } });
366
+ if (msgsResult.error || !msgsResult.data) {
367
+ await adapter.reply(ctx.threadId, '❌ 无法获取消息');
368
+ return true;
369
+ }
370
+ const userMsgs = msgsResult.data.filter(m => m.info?.role === 'user');
371
+ if (userMsgs.length === 0) {
372
+ await adapter.reply(ctx.threadId, '📭 没有用户消息可编辑');
373
+ return true;
374
+ }
375
+ let msg = '✏️ 选择要修改的消息(回复编号):\n\n';
376
+ const showCount = Math.min(userMsgs.length, 15);
377
+ const startIdx = userMsgs.length - showCount;
378
+ for (let i = startIdx; i < userMsgs.length; i++) {
379
+ const m = userMsgs[i];
380
+ const num = i + 1;
381
+ const preview = m.info?.content?.slice(0, 60) || '(空)';
382
+ msg += `${num}. ${preview}\n`;
383
+ }
384
+ if (userMsgs.length > 15) {
385
+ msg += `\n... 共 ${userMsgs.length} 条消息`;
386
+ }
387
+ session._editList = userMsgs;
388
+ session._editSessionId = ocSession.sessionId;
389
+ const msgs = splitMessage(msg);
390
+ for (const m of msgs) {
391
+ await adapter.reply(ctx.threadId, m);
392
+ }
393
+ return true;
394
+ }
395
+ case 'revert': {
396
+ const ocS = openCodeSessions.get(ctx.threadId);
397
+ if (!ocS) {
398
+ await adapter.reply(ctx.threadId, '❌ 没有活跃的会话');
399
+ return true;
400
+ }
401
+ if (arg === 'undo') {
402
+ const ok = await unrevertSession(ocS.sessionId);
403
+ if (ok) {
404
+ await adapter.reply(ctx.threadId, '↩️ 已恢复撤销的内容');
405
+ } else {
406
+ await adapter.reply(ctx.threadId, '❌ 恢复失败');
407
+ }
408
+ return true;
409
+ }
410
+ const opencode = await initOpenCode();
411
+ if (!opencode) {
412
+ await adapter.reply(ctx.threadId, '❌ 无法获取消息');
413
+ return true;
414
+ }
415
+ const msgsResult = await opencode.client.session.messages({ path: { id: ocS.sessionId } });
416
+ if (msgsResult.error || !msgsResult.data) {
417
+ await adapter.reply(ctx.threadId, '❌ 无法获取消息');
418
+ return true;
419
+ }
420
+ const assistantMsgs = msgsResult.data.filter(m => m.info?.role === 'assistant' && m.time?.created);
421
+ if (assistantMsgs.length === 0) {
422
+ await adapter.reply(ctx.threadId, '📭 没有可撤销的消息');
423
+ return true;
424
+ }
425
+ const lastMsg = assistantMsgs[assistantMsgs.length - 1];
426
+ const ok = await revertSessionMessage(ocS.sessionId, lastMsg.id);
427
+ if (ok) {
428
+ const preview = lastMsg.info?.content?.slice(0, 100) || '(无内容)';
429
+ await adapter.reply(ctx.threadId, `↩️ 已撤销最近的消息\n\n${preview}\n\n发送 /revert undo 恢复`);
430
+ } else {
431
+ await adapter.reply(ctx.threadId, '❌ 撤销失败');
432
+ }
433
+ return true;
434
+ }
435
+ case 'loop': {
436
+ const argText = arg || '';
437
+ if (argText === 'off' || argText === 'stop') {
438
+ session.loopMode = false;
439
+ session.loopPrompt = null;
440
+ session.loopIterationCount = 0;
441
+ session.loopStartTime = null;
442
+ saveSessionMapping();
443
+ await adapter.reply(ctx.threadId, '⏹️ 循环任务已停止');
444
+ return true;
445
+ }
446
+ if (argText === 'status') {
447
+ if (session.loopMode) {
448
+ const elapsed = session.loopStartTime
449
+ ? `已运行: ${Math.floor((Date.now() - session.loopStartTime) / 60000)}分钟`
450
+ : '';
451
+ const count = session.loopIterationCount || 0;
452
+ const limit = session.loopMaxIterations || 10;
453
+ await adapter.reply(ctx.threadId, `🔄 循环任务运行中\n指令: ${session.loopPrompt || '智能模式'}\n迭代: ${count}/${limit} ${elapsed}`);
454
+ } else {
455
+ await adapter.reply(ctx.threadId, '⏹️ 循环任务未运行\n发送 /loop 开始');
456
+ }
457
+ return true;
458
+ }
459
+ session.loopMode = true;
460
+ session.loopPrompt = argText || null;
461
+ session.lastLoopTime = Date.now();
462
+ session.loopStartTime = Date.now();
463
+ session.loopIterationCount = 0;
464
+ session.loopMaxIterations = 10;
465
+ session.loopMaxTimeMs = 30 * 60 * 1000;
466
+ saveSessionMapping();
467
+ const modeDesc = argText ? `指令: ${argText}` : '智能模式(根据上下文自动生成指令)';
468
+ await adapter.reply(ctx.threadId, `🔄 循环任务已启动\n${modeDesc}\n限制: 最多10次迭代或30分钟\n\n发送 /loop off 停止`);
469
+ if (_startLoopCycle) {
470
+ _startLoopCycle(adapter, ctx, openCodeSessions, session);
471
+ }
472
+ return true;
473
+ }
474
+
475
+ case 'restart': {
476
+ console.log('[bot] restart command received');
477
+ await adapter.reply(ctx.threadId, '🔄 正在重启 bot...');
478
+ const fs = await import('fs');
479
+ const remoteDir = join(process.env.HOME || process.cwd(), '.opencode-remote');
480
+ if (!fs.existsSync(remoteDir)) {
481
+ fs.mkdirSync(remoteDir, { recursive: true });
482
+ }
483
+ const restartInfo = { threadId: ctx.threadId, time: Date.now() };
484
+ fs.writeFileSync(join(remoteDir, '.restart_user.json'), JSON.stringify(restartInfo));
485
+ console.log('[bot] reply sent, waiting...');
486
+ await new Promise(r => setTimeout(r, 500));
487
+ console.log('[bot] about to exit with code 200');
488
+ process.exit(200);
489
+ return true;
490
+ }
491
+
492
+ case 'stop': {
493
+ await adapter.reply(ctx.threadId, '🛑 正在停止 bot...');
494
+ setTimeout(() => {
495
+ if (globalThis.__weixinBotShutdown) {
496
+ globalThis.__weixinBotShutdown(false);
497
+ }
498
+ setTimeout(() => process.exit(0), 1000);
499
+ }, 500);
500
+ return true;
501
+ }
502
+
503
+ case 'upload': {
504
+ const projectDir = session.projectDir || globalThis.__autoProjectDir;
505
+
506
+ if (arg && arg.trim()) {
507
+ const filePath = arg.trim();
508
+
509
+ let fullPath = filePath;
510
+ if (!existsSync(fullPath) && projectDir) {
511
+ fullPath = join(projectDir, filePath);
512
+ }
513
+
514
+ if (!existsSync(fullPath)) {
515
+ await adapter.reply(ctx.threadId, `❌ 文件不存在: ${filePath}`);
516
+ return true;
517
+ }
518
+
519
+ await adapter.reply(ctx.threadId, `⬆️ 正在上传: ${basename(fullPath)}...`);
520
+
521
+ try {
522
+ const result = await uploadToQiniu(fullPath);
523
+ if (result.skipped) {
524
+ await adapter.reply(ctx.threadId, `⏭️ 文件已存在,不需要重复上传,你是要删除吗?\n/delete ${result.key}`);
525
+ } else {
526
+ await adapter.reply(ctx.threadId, result.url);
527
+ await adapter.reply(ctx.threadId, `/delete ${result.key}`);
528
+ }
529
+ } catch (e) {
530
+ await adapter.reply(ctx.threadId, `❌ 上传失败: ${e.message}`);
531
+ }
532
+ return true;
533
+ }
534
+
535
+ if (!projectDir) {
536
+ await adapter.reply(ctx.threadId, '❌ 未设置项目目录,请先设置项目目录或指定完整文件路径\n\n用法:\n/upload <文件路径>');
537
+ return true;
538
+ }
539
+
540
+ await adapter.reply(ctx.threadId, '🔍 正在搜索构建产物...');
541
+
542
+ const files = findBuildOutputs(projectDir);
543
+
544
+ if (files.length === 0) {
545
+ await adapter.reply(ctx.threadId, '❌ 未找到任何构建产物\n\n请指定完整文件路径,例如: /upload build/app.apk');
546
+ return true;
547
+ }
548
+
549
+ const displayFiles = files.slice(0, 10);
550
+ let listMsg = `📦 找到 ${files.length} 个构建产物:\n\n`;
551
+ for (let i = 0; i < displayFiles.length; i++) {
552
+ const f = displayFiles[i];
553
+ listMsg += `${i + 1}. ${f.name}\n`;
554
+ listMsg += ` 📍 ${f.relativePath}\n`;
555
+ listMsg += ` 📊 ${formatSize(f.size)}\n\n`;
556
+ }
557
+ if (files.length > 10) {
558
+ listMsg += `...还有 ${files.length - 10} 个文件`;
559
+ }
560
+ listMsg += `\n正在上传最新的: ${files[0].name}`;
561
+ await adapter.reply(ctx.threadId, listMsg);
562
+
563
+ const targetFile = files[0];
564
+
565
+ try {
566
+ const result = await uploadToQiniu(targetFile.path);
567
+ if (result.skipped) {
568
+ await adapter.reply(ctx.threadId, `⏭️ 文件已存在,不需要重复上传,你是要删除吗?\n/delete ${result.key}`);
569
+ } else {
570
+ await adapter.reply(ctx.threadId, result.url);
571
+ await adapter.reply(ctx.threadId, `/delete ${result.key}`);
572
+ }
573
+ } catch (e) {
574
+ await adapter.reply(ctx.threadId, `❌ 上传失败: ${e.message}`);
575
+ }
576
+ return true;
577
+ }
578
+
579
+ case 'reset': {
580
+ const oldSession = openCodeSessions?.get(ctx.threadId);
581
+ if (oldSession) {
582
+ abortSession(oldSession).catch(() => {});
583
+ }
584
+ session.pendingApprovals = [];
585
+ session.opencodeSessionId = null;
586
+ session.loopMode = false;
587
+ session.loopPrompt = null;
588
+ session.projectDir = null;
589
+ session.currentAgent = null;
590
+ session.messages = [];
591
+ session.commandHistory = [];
592
+ session.taskStartTime = null;
593
+ session.currentTool = null;
594
+ session.modifiedFiles = null;
595
+ session.lastUserMessage = null;
596
+ session._lastPrompt = null;
597
+ session._contextScope = null;
598
+ session.originalProjectDir = null;
599
+ session._switchSessionList = null;
600
+ session._deleteSessionList = null;
601
+ session._pendingSwitchSession = null;
602
+ session._editTarget = null;
603
+ session._editList = null;
604
+ session._editSessionId = null;
605
+ session._historyList = null;
606
+ session._forkList = null;
607
+ session._forkSessionId = null;
608
+ session.expertMode = false;
609
+ session.systemPrompt = null;
610
+ session._analyzeMode = false;
611
+ session._analyzeTask = null;
612
+ session._showSessionState = null;
613
+ session.id = `${Date.now()}-${ctx.threadId}-reset`;
614
+ openCodeSessions?.delete(ctx.threadId);
615
+ globalThis.__latestOpenCodeSession = null;
616
+ saveSessionMapping();
617
+ await adapter.reply(ctx.threadId, '🔄 会话已重置,下次发送消息将创建新会话');
618
+ return true;
619
+ }
620
+
621
+ case 'refresh': {
622
+ const ocSession = openCodeSessions.get(ctx.threadId);
623
+ if (!ocSession) {
624
+ await adapter.reply(ctx.threadId, '❌ 没有活跃的会话');
625
+ return true;
626
+ }
627
+ await adapter.reply(ctx.threadId, '🔄 正在刷新会话...');
628
+ try {
629
+ await ocSession.client.session.compact({ path: { id: ocSession.sessionId } });
630
+ await ocSession.client.session.summarize({ path: { id: ocSession.sessionId } });
631
+ await adapter.reply(ctx.threadId, '✅ 会话已刷新');
632
+ } catch (e) {
633
+ await adapter.reply(ctx.threadId, '✅ 会话已刷新');
634
+ }
635
+ return true;
636
+ }
637
+
638
+ case 'delete': {
639
+ const keyToDelete = arg ? arg.trim() : null;
640
+
641
+ if (!keyToDelete) {
642
+ await adapter.reply(ctx.threadId, '❌ 请指定要删除的文件key\n\n用法: /delete <file-key>\n\n文件key是上传后URL中的路径,如:\n/uploads/1234567890-app.apk');
643
+ return true;
644
+ }
645
+
646
+ const cleanKey = keyToDelete.replace(/^\//, '');
647
+ if (!cleanKey.startsWith('uploads/')) {
648
+ await adapter.reply(ctx.threadId, '❌ 无效的文件key,应以 uploads/ 开头');
649
+ return true;
650
+ }
651
+
652
+ await adapter.reply(ctx.threadId, `🗑️ 正在删除: ${cleanKey}...`);
653
+
654
+ try {
655
+ await deleteFromQiniu(cleanKey);
656
+ await adapter.reply(ctx.threadId, `✅ 已删除: ${cleanKey}`);
657
+ } catch (e) {
658
+ await adapter.reply(ctx.threadId, `❌ 删除失败: ${e.message}`);
659
+ }
660
+ return true;
661
+ }
662
+
663
+ case 'oc':
664
+ case 'cc':
665
+ case 'cx':
666
+ case 'copilot': {
667
+ const agentName = command === 'cc' ? 'claude-code' :
668
+ command === 'cx' ? 'codex' :
669
+ command === 'copilot' ? 'copilot' : 'opencode';
670
+ const result = await handleAgentSwitch(adapter, ctx, agentName, arg);
671
+ return result;
672
+ }
673
+
674
+ case 'agents': {
675
+ const agents = registry.listAgents();
676
+ const lines = ['🤖 可用 AI Agent:'];
677
+ for (const a of agents) {
678
+ const agent = registry.findAgent(a);
679
+ const available = await agent?.isAvailable().catch(() => false);
680
+ lines.push(`${available ? '✅' : '❌'} ${a}`);
681
+ }
682
+ lines.push('', '切换: /oc /cc /cx /copilot');
683
+ await adapter.reply(ctx.threadId, lines.join('\n'));
684
+ return true;
685
+ }
686
+
687
+ case 'model': {
688
+ try {
689
+ const opencode = await initOpenCode();
690
+ if (!opencode) {
691
+ await adapter.reply(ctx.threadId, '❌ 无法连接 OpenCode');
692
+ return true;
693
+ }
694
+ if (arg) {
695
+ const modelStr = arg.trim();
696
+ const ok = await updateGlobalModel(modelStr);
697
+ if (ok) {
698
+ const parts = modelStr.split('/');
699
+ if (parts.length === 2) {
700
+ session.modelOverride = { providerID: parts[0], modelID: parts[1] };
701
+ }
702
+ await adapter.reply(ctx.threadId, `✅ 已切换模型至: ${modelStr}`);
703
+ } else {
704
+ await adapter.reply(ctx.threadId, `❌ 切换模型失败,请检查模型名称是否正确`);
705
+ }
706
+ return true;
707
+ }
708
+ const providers = await listProviders();
709
+ if (!providers || providers.length === 0) {
710
+ await adapter.reply(ctx.threadId, '❌ 无法获取模型列表');
711
+ return true;
712
+ }
713
+ let msg = '🧠 可用模型:\n\n';
714
+ for (const p of providers) {
715
+ const modelIds = Object.keys(p.models || {});
716
+ if (modelIds.length === 0) continue;
717
+ msg += `${p.name} (${p.id}):\n`;
718
+ for (const mid of modelIds.slice(0, 8)) {
719
+ const m = p.models[mid];
720
+ const tags = [];
721
+ if (m.reasoning) tags.push('推理');
722
+ if (m.attachment) tags.push('附件');
723
+ msg += ` ${p.id}/${mid}${tags.length ? ` (${tags.join(', ')})` : ''}\n`;
724
+ }
725
+ if (modelIds.length > 8) msg += ` ...还有 ${modelIds.length - 8} 个\n`;
726
+ msg += '\n';
727
+ }
728
+ msg += '用法: /model <provider/model>';
729
+ const msgs = splitMessage(msg);
730
+ for (const m of msgs) {
731
+ await adapter.reply(ctx.threadId, m);
732
+ }
733
+ } catch (e) {
734
+ await adapter.reply(ctx.threadId, `❌ 模型操作失败: ${e.message}`);
735
+ }
736
+ return true;
737
+ }
738
+
739
+
740
+
741
+ case 'diagnose': {
742
+ const { checkConnection } = await import('../opencode/client.js');
743
+ const diag = ['🔍 诊断报告\n'];
744
+ diag.push(`OpenCode: ${await checkConnection().then(() => '✅').catch(() => '❌')}`);
745
+ diag.push(`七牛云: ${process.env.QINIU_ACCESS_KEY ? '✅' : '❌'}`);
746
+ diag.push(`项目目录: ${session.projectDir || globalThis.__autoProjectDir || '❌ 未设置'}`);
747
+ diag.push(`会话: ${openCodeSessions?.get(ctx.threadId) ? '✅' : '❌'}`);
748
+ const msgs = splitMessage(diag.join('\n'));
749
+ for (const m of msgs) await adapter.reply(ctx.threadId, m);
750
+ return true;
751
+ }
752
+
753
+ default:
754
+ return false;
755
+ }
756
+ }
757
+
758
+ export { handleAgentSwitch, handleCommand, formatTimeAgo };