@yvhitxcel/opencode-remote 0.16.2 → 0.17.0

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