@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,601 @@
1
+ import { getOrCreateSession } from '../core/session.js';
2
+ import { splitMessage } from '../core/notifications.js';
3
+ import { EMOJI } from '../core/types.js';
4
+ import { initOpenCode, createSession, sendMessage, checkConnection, abortSession, resumeSession, revertSessionMessage, unrevertSession, listProviders, updateGlobalModel } from '../opencode/client.js';
5
+ import { claimOwnership } from '../core/auth.js';
6
+ import { COMMAND_ALIASES, detectCommand } from '../core/router.js';
7
+ import { registry } from '../core/registry.js';
8
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
9
+ import { join, basename } from 'path';
10
+
11
+ function formatTimeAgo(timestamp) {
12
+ const diff = Date.now() - timestamp;
13
+ const seconds = Math.floor(diff / 1000);
14
+ if (seconds < 60) return `${seconds}秒前`;
15
+ const minutes = Math.floor(seconds / 60);
16
+ if (minutes < 60) return `${minutes}分钟前`;
17
+ const hours = Math.floor(minutes / 60);
18
+ if (hours < 24) return `${hours}小时前`;
19
+ return `${Math.floor(hours / 24)}天前`;
20
+ }
21
+
22
+ async function handleCommand(adapter, ctx, command, arg, openCodeSessions) {
23
+ const session = await getOrCreateSession(ctx.threadId, 'feishu');
24
+ switch (command) {
25
+ case 'start': {
26
+ const result = claimOwnership('feishu', ctx.userId);
27
+ if (result.success) {
28
+ if (result.message === 'claimed') {
29
+ await adapter.reply(ctx.threadId, `🔐 **安全设置完成!**
30
+
31
+ ✅ 你是此 bot 的唯一所有者。
32
+
33
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
34
+ ⚠️ **重要安全通知**
35
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
36
+
37
+ 只有你可以通过此 bot 控制 OpenCode。
38
+ 其他用户将被自动屏蔽。
39
+
40
+ 你的飞书 ID: \`${ctx.userId}\`
41
+
42
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
43
+
44
+ 🚀 **准备就绪!**
45
+ 💬 发送提示词开始编程
46
+ /help — 查看所有指令
47
+ /status — 查看连接状态`);
48
+ }
49
+ else {
50
+ await adapter.reply(ctx.threadId, `🚀 OpenCode 远程控制就绪
51
+
52
+ 💬 发送消息给 OpenCode 开始工作
53
+ /help — 查看所有指令
54
+ /status — 查看连接状态
55
+
56
+ 指令:
57
+ /start — 首次认证
58
+ /status — 查看连接
59
+ /reset — 重置会话
60
+ /approve — 同意变更
61
+ /reject — 拒绝变更
62
+ /diff — 查看变更
63
+ /files — 查看文件
64
+ /retry — 重试连接
65
+
66
+ 💬 其他消息直接发送给 OpenCode!`);
67
+ }
68
+ }
69
+ else {
70
+ await adapter.reply(ctx.threadId, `🚫 **拒绝访问**
71
+
72
+ 此 bot 已被其他用户绑定。
73
+
74
+ 如果你是所有者,请检查配置文件。`);
75
+ }
76
+ return true;
77
+ }
78
+ case 'help':
79
+ await adapter.reply(ctx.threadId, `📖 指令
80
+
81
+ /start — 首次认证
82
+ /help h ? — 帮助
83
+ /status — 连接状态
84
+ /reset — 清空会话
85
+ /restart — 重启 bot
86
+ /stop — 停止 bot
87
+ /retry — 重试连接
88
+ /approve .a .y .1 — 同意变更
89
+ /reject .r .n .0 — 拒绝变更
90
+ /diff — 查看变更
91
+ /files — 已修改文件
92
+ /sessions — 浏览会话
93
+ /delsessions — 删除会话
94
+ /loop — 循环任务
95
+ /summary — 会话摘要
96
+ /compact — 压缩会话上下文
97
+ /copy — 复制最新 AI 回复
98
+ /revert — 撤销 AI 回复
99
+ /switchdir — 切换项目目录
100
+ /scope — 设置上下文范围
101
+ /analyze — 分析后执行
102
+ /commit — 生成提交信息
103
+ /review — 代码审查
104
+ /flush — 刷新记忆
105
+
106
+ 🤖 AI 模型:
107
+ /model — 切换模型
108
+ /agents — 查看可用 Agent
109
+ /oc — 使用 OpenCode
110
+ /cc — 使用 Claude Code
111
+
112
+ 💬 直接发消息给 AI!`);
113
+ return true;
114
+ case 'agents': {
115
+ const agents = registry.listAgents();
116
+ const lines = ['🤖 可用 AI Agent:'];
117
+ for (const name of agents) {
118
+ const agent = registry.findAgent(name);
119
+ const aliases = agent?.aliases || [];
120
+ const available = await agent?.isAvailable().catch(() => false);
121
+ const status = available ? '✅' : '❌';
122
+ const aliasStr = aliases.length > 0 ? ` (${aliases.join(', ')})` : '';
123
+ lines.push(`${status} ${name}${aliasStr}`);
124
+ }
125
+ lines.push('');
126
+ lines.push('切换: /oc /cc /cx /copilot');
127
+ await adapter.reply(ctx.threadId, lines.join('\n'));
128
+ return true;
129
+ }
130
+ case 'model': {
131
+ try {
132
+ if (arg) {
133
+ const modelStr = arg.trim();
134
+ const ok = await updateGlobalModel(modelStr);
135
+ if (ok) {
136
+ const parts = modelStr.split('/');
137
+ if (parts.length === 2) {
138
+ session.modelOverride = { providerID: parts[0], modelID: parts[1] };
139
+ }
140
+ await adapter.reply(ctx.threadId, `✅ 已切换模型至: ${modelStr}`);
141
+ } else {
142
+ await adapter.reply(ctx.threadId, '❌ 切换模型失败,请检查模型名称是否正确');
143
+ }
144
+ return true;
145
+ }
146
+ const providers = await listProviders();
147
+ if (!providers || providers.length === 0) {
148
+ await adapter.reply(ctx.threadId, '❌ 无法获取模型列表');
149
+ return true;
150
+ }
151
+ let msg = '🧠 可用模型:\n\n';
152
+ for (const p of providers) {
153
+ const modelIds = Object.keys(p.models || {});
154
+ if (modelIds.length === 0) continue;
155
+ msg += `${p.name} (${p.id}):\n`;
156
+ for (const mid of modelIds.slice(0, 5)) {
157
+ msg += ` ${p.id}/${mid}\n`;
158
+ }
159
+ if (modelIds.length > 5) msg += ` ...还有 ${modelIds.length - 5} 个\n`;
160
+ msg += '\n';
161
+ }
162
+ msg += '用法: /model <provider/model>';
163
+ const msgs = splitMessage(msg);
164
+ for (const m of msgs) {
165
+ await adapter.reply(ctx.threadId, m);
166
+ }
167
+ } catch (e) {
168
+ await adapter.reply(ctx.threadId, `❌ 模型操作失败: ${e.message}`);
169
+ }
170
+ return true;
171
+ }
172
+ case 'oc':
173
+ case 'cc':
174
+ case 'cx':
175
+ case 'copilot': {
176
+ const agentName = command === 'cc' ? 'claude-code' : command === 'cx' ? 'codex' : command === 'copilot' ? 'copilot' : 'opencode';
177
+ const agent = registry.findAgent(agentName);
178
+ if (!agent) {
179
+ await adapter.reply(ctx.threadId, `❌ Agent "${agentName}" 未找到`);
180
+ return true;
181
+ }
182
+ const available = await agent.isAvailable().catch(() => false);
183
+ if (!available) {
184
+ await adapter.reply(ctx.threadId, `❌ ${agentName} 不可用`);
185
+ return true;
186
+ }
187
+ session.currentAgent = agentName;
188
+ if (!arg) {
189
+ await adapter.reply(ctx.threadId, `✅ 已切换到 ${agentName}`);
190
+ return true;
191
+ }
192
+ await adapter.sendTypingIndicator(ctx.threadId);
193
+ try {
194
+ const history = session.commandHistory || [];
195
+ const response = await agent.sendPrompt(session.id, arg, history, { projectDir: session.projectDir || globalThis.__autoProjectDir });
196
+ await adapter.sendTypingIndicator(ctx.threadId);
197
+ const chunks = splitMessage(response || '无响应');
198
+ for (const chunk of chunks) {
199
+ await adapter.reply(ctx.threadId, chunk);
200
+ }
201
+ session.commandHistory = session.commandHistory || [];
202
+ session.commandHistory.push({ role: 'user', content: arg });
203
+ session.commandHistory.push({ role: 'assistant', content: response });
204
+ } catch (error) {
205
+ await adapter.sendTypingIndicator(ctx.threadId);
206
+ await adapter.reply(ctx.threadId, `❌ 错误: ${error.message}`);
207
+ }
208
+ return true;
209
+ }
210
+ case 'approve': {
211
+ const pending = session.pendingApprovals?.[0];
212
+ if (!pending) {
213
+ await adapter.reply(ctx.threadId, '🤷 没有待审批的变更');
214
+ return true;
215
+ }
216
+ await adapter.reply(ctx.threadId, '✅ 已批准');
217
+ return true;
218
+ }
219
+ case 'reject': {
220
+ const pending = session.pendingApprovals?.[0];
221
+ if (!pending) {
222
+ await adapter.reply(ctx.threadId, '🤷 没有待拒绝的变更');
223
+ return true;
224
+ }
225
+ session.pendingApprovals.shift();
226
+ await adapter.reply(ctx.threadId, '❌ 已拒绝');
227
+ return true;
228
+ }
229
+
230
+ case 'files': {
231
+ const pending = session.pendingApprovals?.[0];
232
+ if (!pending || !pending.files?.length) {
233
+ await adapter.reply(ctx.threadId, '📄 此会话没有文件变更');
234
+ return true;
235
+ }
236
+ const fileList = pending.files.map(f => `• ${f.path} (+${f.additions}, -${f.deletions})`).join('\n');
237
+ await adapter.reply(ctx.threadId, `📄 已修改文件:\n${fileList}`);
238
+ return true;
239
+ }
240
+ case 'status': {
241
+ const openCodeConnected = await checkConnection();
242
+ const actualSession = openCodeSessions?.get(ctx.threadId) ||
243
+ (session.opencodeSessionId ? { sessionId: session.opencodeSessionId } : null);
244
+ const running = session.taskStartTime ? Math.round((Date.now() - session.taskStartTime) / 1000) : 0;
245
+ let msg = `${openCodeConnected ? '✅' : '❌'} OpenCode ${openCodeConnected ? '在线' : '离线'}\n\n`;
246
+ msg += `会话: ${actualSession?.sessionId?.slice(0, 8) || '无'}\n`;
247
+ if (running > 0) {
248
+ const m = Math.floor(running / 60);
249
+ const s = running % 60;
250
+ msg += `运行中: ${m}分${s}秒\n`;
251
+ }
252
+ if (session.currentTool) {
253
+ msg += `当前工具: ${session.currentTool}\n`;
254
+ }
255
+ if (session.modifiedFiles?.length > 0 || session.modifiedFiles?.size > 0) {
256
+ msg += `已修改: ${(session.modifiedFiles?.length || session.modifiedFiles?.size || 0)} 个文件\n`;
257
+ }
258
+ const projectDir = session.projectDir || globalThis.__autoProjectDir;
259
+ if (projectDir) {
260
+ msg += `项目目录: ${projectDir}\n`;
261
+ } else {
262
+ msg += `项目目录: 未设置\n`;
263
+ }
264
+ msg += `工作目录: ${process.cwd()}\n`;
265
+ if (session.originalProjectDir && session.originalProjectDir !== projectDir) {
266
+ msg += `原始目录: ${session.originalProjectDir}\n`;
267
+ }
268
+ await adapter.reply(ctx.threadId, msg);
269
+ return true;
270
+ }
271
+ case 'reset': {
272
+ const oldSession = openCodeSessions?.get(ctx.threadId);
273
+ if (oldSession) {
274
+ abortSession(oldSession).catch(() => {});
275
+ }
276
+ session.pendingApprovals = [];
277
+ session.opencodeSessionId = undefined;
278
+ session.loopMode = false;
279
+ session.loopPrompt = null;
280
+ session.projectDir = null;
281
+ session.currentAgent = null;
282
+ session.messages = [];
283
+ session.commandHistory = [];
284
+ session.taskStartTime = null;
285
+ session.currentTool = null;
286
+ session.modifiedFiles = null;
287
+ session.lastUserMessage = null;
288
+ session._lastPrompt = null;
289
+ session._contextScope = null;
290
+ session.originalProjectDir = null;
291
+ session._switchSessionList = null;
292
+ session._deleteSessionList = null;
293
+ session._pendingSwitchSession = null;
294
+ session._editTarget = null;
295
+ session._editList = null;
296
+ session._editSessionId = null;
297
+ session._historyList = null;
298
+ session._forkList = null;
299
+ session._forkSessionId = null;
300
+ session.expertMode = false;
301
+ session.systemPrompt = null;
302
+ session._analyzeMode = false;
303
+ session._analyzeTask = null;
304
+ session._showSessionState = null;
305
+ openCodeSessions?.delete(ctx.threadId);
306
+ globalThis.__latestOpenCodeSession = null;
307
+ await adapter.reply(ctx.threadId, '🔄 会话已重置,下次发送消息将创建新会话');
308
+ return true;
309
+ }
310
+ case 'retry': {
311
+ const connected = await checkConnection();
312
+ if (connected) {
313
+ await adapter.reply(ctx.threadId, '✅ OpenCode 已在线!');
314
+ } else {
315
+ await adapter.reply(ctx.threadId, '❌ 仍离线,请检查 OpenCode 是否运行中');
316
+ }
317
+ return true;
318
+ }
319
+ case 'sessions': {
320
+ try {
321
+ const opencode = await initOpenCode();
322
+ if (!opencode) {
323
+ await adapter.reply(ctx.threadId, '❌ 无法连接 OpenCode');
324
+ return true;
325
+ }
326
+ const result = await opencode.client.session.list();
327
+ if (result.error || !result.data || result.data.length === 0) {
328
+ await adapter.reply(ctx.threadId, '📭 暂无会话');
329
+ return true;
330
+ }
331
+ const sorted = result.data.sort((a, b) => (b.time?.updated || 0) - (a.time?.updated || 0));
332
+ session._switchSessionList = sorted;
333
+ session._showSessionState = true;
334
+ let msg = '📂 选择会话(回复编号):\n\n';
335
+ sorted.slice(0, 10).forEach((s, i) => {
336
+ const n = i + 1;
337
+ const title = s.title || '无标题';
338
+ const time = s.updated_at ? formatTimeAgo(s.updated_at * 1000) : '';
339
+ msg += `${n}. ${title} (${time})\n`;
340
+ });
341
+ if (sorted.length > 10) {
342
+ msg += `\n... 共 ${sorted.length} 个会话`;
343
+ }
344
+ msg += '\n\n回复编号切换会话';
345
+ await adapter.reply(ctx.threadId, msg);
346
+ } catch (e) {
347
+ await adapter.reply(ctx.threadId, `❌ 获取会话失败: ${e.message}`);
348
+ }
349
+ return true;
350
+ }
351
+ case 'delsessions': {
352
+ try {
353
+ const opencode = await initOpenCode();
354
+ if (!opencode) {
355
+ await adapter.reply(ctx.threadId, '❌ 无法连接 OpenCode');
356
+ return true;
357
+ }
358
+ const result = await opencode.client.session.list();
359
+ if (result.error || !result.data || result.data.length === 0) {
360
+ await adapter.reply(ctx.threadId, '📭 暂无会话可删除');
361
+ return true;
362
+ }
363
+ const sorted = result.data.sort((a, b) => (b.time?.updated || 0) - (a.time?.updated || 0));
364
+ session._deleteSessionList = sorted;
365
+ let msg = '🗑️ 选择要删除的会话(回复编号):\n\n';
366
+ sorted.slice(0, 10).forEach((s, i) => {
367
+ const n = i + 1;
368
+ const title = s.title || '无标题';
369
+ const time = s.updated_at ? formatTimeAgo(s.updated_at * 1000) : '';
370
+ msg += `${n}. ${title} (${time})\n`;
371
+ });
372
+ if (sorted.length > 10) {
373
+ msg += `\n... 共 ${sorted.length} 个会话`;
374
+ }
375
+ msg += '\n\n回复编号删除';
376
+ await adapter.reply(ctx.threadId, msg);
377
+ } catch (e) {
378
+ await adapter.reply(ctx.threadId, `❌ 获取会话失败: ${e.message}`);
379
+ }
380
+ return true;
381
+ }
382
+ case 'loop': {
383
+ const argText = arg || '';
384
+ if (argText === 'off' || argText === 'stop') {
385
+ session.loopMode = false;
386
+ session.loopPrompt = null;
387
+ session.loopIterationCount = 0;
388
+ session.loopStartTime = null;
389
+ saveSessionMapping();
390
+ await adapter.reply(ctx.threadId, '⏹️ 循环任务已停止');
391
+ return true;
392
+ }
393
+ if (argText === 'status') {
394
+ if (session.loopMode) {
395
+ const elapsed = session.loopStartTime
396
+ ? `已运行: ${Math.floor((Date.now() - session.loopStartTime) / 60000)}分钟`
397
+ : '';
398
+ const count = session.loopIterationCount || 0;
399
+ const limit = session.loopMaxIterations || 10;
400
+ await adapter.reply(ctx.threadId, `🔄 循环任务运行中\n指令: ${session.loopPrompt || '智能模式'}\n迭代: ${count}/${limit} ${elapsed}`);
401
+ } else {
402
+ await adapter.reply(ctx.threadId, '⏹️ 循环任务未运行\n发送 /loop 开始');
403
+ }
404
+ return true;
405
+ }
406
+ session.loopMode = true;
407
+ session.loopPrompt = argText || null;
408
+ session.lastLoopTime = Date.now();
409
+ session.loopStartTime = Date.now();
410
+ session.loopIterationCount = 0;
411
+ session.loopMaxIterations = 10;
412
+ session.loopMaxTimeMs = 30 * 60 * 1000;
413
+ saveSessionMapping();
414
+ const modeDesc = argText ? `指令: ${argText}` : '智能模式(根据上下文自动生成指令)';
415
+ await adapter.reply(ctx.threadId, `🔄 循环任务已启动\n${modeDesc}\n限制: 最多10次迭代或30分钟\n\n发送 /loop off 停止`);
416
+ return true;
417
+ }
418
+
419
+ case 'refresh': {
420
+ const ocSession = openCodeSessions.get(ctx.threadId);
421
+ if (!ocSession) {
422
+ await adapter.reply(ctx.threadId, '❌ 没有活跃的会话');
423
+ return true;
424
+ }
425
+ await adapter.reply(ctx.threadId, '🔄 正在刷新会话...');
426
+ try {
427
+ await ocSession.client.session.compact({ path: { id: ocSession.sessionId } });
428
+ await ocSession.client.session.summarize({ path: { id: ocSession.sessionId } });
429
+ await adapter.reply(ctx.threadId, '✅ 会话已刷新');
430
+ } catch (e) {
431
+ await adapter.reply(ctx.threadId, '✅ 会话已刷新');
432
+ }
433
+ return true;
434
+ }
435
+
436
+ case 'upload': {
437
+ await adapter.reply(ctx.threadId, 'ℹ️ 上传功能目前仅在微信客户端可用。\n请使用微信客户端上传文件。');
438
+ return true;
439
+ }
440
+ case 'delete': {
441
+ await adapter.reply(ctx.threadId, 'ℹ️ 删除功能目前仅在微信客户端可用。\n请使用微信客户端管理上传文件。');
442
+ return true;
443
+ }
444
+ case 'restart': {
445
+ console.log('[feishu-bot] restart command received');
446
+ await adapter.reply(ctx.threadId, '🔄 正在重启 bot...');
447
+ const remoteDir = join(process.env.HOME || process.env.USERPROFILE || process.cwd(), '.opencode-remote');
448
+ if (!existsSync(remoteDir)) {
449
+ mkdirSync(remoteDir, { recursive: true });
450
+ }
451
+ const restartInfo = { threadId: ctx.threadId, time: Date.now() };
452
+ writeFileSync(join(remoteDir, '.restart_user.json'), JSON.stringify(restartInfo));
453
+ await new Promise(r => setTimeout(r, 500));
454
+ console.log('[feishu-bot] about to exit with code 200');
455
+ process.exit(200);
456
+ return true;
457
+ }
458
+ case 'stop': {
459
+ await adapter.reply(ctx.threadId, '🛑 正在停止 bot...');
460
+ setTimeout(() => {
461
+ if (globalThis.__feishuBotShutdown) {
462
+ globalThis.__feishuBotShutdown(false);
463
+ }
464
+ setTimeout(() => process.exit(0), 1000);
465
+ }, 500);
466
+ return true;
467
+ }
468
+
469
+
470
+
471
+
472
+ case 'copy': {
473
+ const ocSession = openCodeSessions?.get(ctx.threadId);
474
+ if (!ocSession) {
475
+ await adapter.reply(ctx.threadId, '❌ 没有活跃的会话');
476
+ return true;
477
+ }
478
+ try {
479
+ const opencode = await initOpenCode();
480
+ if (!opencode) {
481
+ await adapter.reply(ctx.threadId, '❌ 无法连接 OpenCode');
482
+ return true;
483
+ }
484
+ const msgsResult = await opencode.client.session.messages({
485
+ path: { id: ocSession.sessionId },
486
+ query: { limit: 1 }
487
+ });
488
+ if (msgsResult.error || !msgsResult.data || msgsResult.data.length === 0) {
489
+ await adapter.reply(ctx.threadId, '❌ 无法获取最新消息');
490
+ return true;
491
+ }
492
+ let latestMsg = msgsResult.data[0];
493
+ if (latestMsg.info?.role !== 'assistant') {
494
+ const allMsgsResult = await opencode.client.session.messages({
495
+ path: { id: ocSession.sessionId },
496
+ query: { limit: 10 }
497
+ });
498
+ if (allMsgsResult.error || !allMsgsResult.data) {
499
+ await adapter.reply(ctx.threadId, '❌ 无法获取会话消息');
500
+ return true;
501
+ }
502
+ const aiMsg = allMsgsResult.data.find(m => m.info?.role === 'assistant');
503
+ if (!aiMsg) {
504
+ await adapter.reply(ctx.threadId, '❌ 未找到 AI 回复');
505
+ return true;
506
+ }
507
+ latestMsg = aiMsg;
508
+ }
509
+ let content = '';
510
+ if (latestMsg.parts) {
511
+ for (const part of latestMsg.parts) {
512
+ if (part.type === 'text') {
513
+ content += part.text + '\n';
514
+ }
515
+ if (part.type === 'code') {
516
+ content += `\`\`\`${part.language || ''}\n${part.code}\n\`\`\`\n`;
517
+ }
518
+ if (part.type === 'file' && part.content) {
519
+ content += `📁 ${part.filename}:\n${part.content}\n`;
520
+ }
521
+ }
522
+ }
523
+ if (!content.trim()) {
524
+ await adapter.reply(ctx.threadId, '❌ AI 回复中没有可复制的文本内容');
525
+ return true;
526
+ }
527
+ await adapter.reply(ctx.threadId, `📋 已复制最新 AI 回复内容:\n\n${content.substring(0, 2000)}${content.length > 2000 ? '...' : ''}`);
528
+ } catch (e) {
529
+ await adapter.reply(ctx.threadId, `❌ 复制失败: ${e.message}`);
530
+ }
531
+ return true;
532
+ }
533
+ case 'revert': {
534
+ const ocS = openCodeSessions?.get(ctx.threadId);
535
+ if (!ocS) {
536
+ await adapter.reply(ctx.threadId, '❌ 没有活跃的会话');
537
+ return true;
538
+ }
539
+ try {
540
+ if (arg === 'undo') {
541
+ const ok = await unrevertSession(ocS.sessionId);
542
+ if (ok) {
543
+ await adapter.reply(ctx.threadId, '↩️ 已恢复撤销的内容');
544
+ } else {
545
+ await adapter.reply(ctx.threadId, '❌ 恢复失败');
546
+ }
547
+ return true;
548
+ }
549
+ const opencode = await initOpenCode();
550
+ if (!opencode) {
551
+ await adapter.reply(ctx.threadId, '❌ 无法连接 OpenCode');
552
+ return true;
553
+ }
554
+ const msgsResult = await opencode.client.session.messages({ path: { id: ocS.sessionId } });
555
+ if (msgsResult.error || !msgsResult.data) {
556
+ await adapter.reply(ctx.threadId, '❌ 无法获取消息');
557
+ return true;
558
+ }
559
+ const assistantMsgs = msgsResult.data.filter(m => m.info?.role === 'assistant' && m.time?.created);
560
+ if (assistantMsgs.length === 0) {
561
+ await adapter.reply(ctx.threadId, '📭 没有可撤销的消息');
562
+ return true;
563
+ }
564
+ const lastMsg = assistantMsgs[assistantMsgs.length - 1];
565
+ const ok = await revertSessionMessage(ocS.sessionId, lastMsg.id);
566
+ if (ok) {
567
+ const preview = lastMsg.info?.content?.slice(0, 100) || '(无内容)';
568
+ await adapter.reply(ctx.threadId, `↩️ 已撤销最近的消息\n\n${preview}\n\n发送 /revert undo 恢复`);
569
+ } else {
570
+ await adapter.reply(ctx.threadId, '❌ 撤销失败');
571
+ }
572
+ } catch (e) {
573
+ await adapter.reply(ctx.threadId, `❌ 撤销失败: ${e.message}`);
574
+ }
575
+ return true;
576
+ }
577
+
578
+
579
+
580
+
581
+
582
+
583
+
584
+ case 'diagnose': {
585
+ const { checkConnection } = await import('../opencode/client.js');
586
+ const diag = ['🔍 诊断报告\n'];
587
+ diag.push(`OpenCode: ${await checkConnection().then(() => '✅').catch(() => '❌')}`);
588
+ diag.push(`七牛云: ${process.env.QINIU_ACCESS_KEY ? '✅' : '❌'}`);
589
+ diag.push(`项目目录: ${session.projectDir || globalThis.__autoProjectDir || '❌ 未设置'}`);
590
+ diag.push(`会话: ${openCodeSessions?.get(ctx.threadId) ? '✅' : '❌'}`);
591
+ const msgs = splitMessage(diag.join('\n'));
592
+ for (const m of msgs) await adapter.reply(ctx.threadId, m);
593
+ return true;
594
+ }
595
+ default:
596
+ await adapter.reply(ctx.threadId, `${EMOJI.WARNING} 未知指令: ${command}\n\n请发送 /help 查看可用指令`);
597
+ return true;
598
+ }
599
+ }
600
+
601
+ export { handleCommand, formatTimeAgo };