@yvhitxcel/opencode-remote 0.16.0 → 0.16.1

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.
@@ -101,6 +101,17 @@ export const TEMPLATES = {
101
101
  details: 'Changes were automatically rejected.',
102
102
  }),
103
103
  };
104
+ // Task completion notification with timing
105
+ export function formatTaskCompletion(taskName, startTime, extra) {
106
+ const elapsed = Date.now() - startTime;
107
+ const seconds = Math.floor(elapsed / 1000);
108
+ const timeStr = seconds >= 60 ? `${Math.floor(seconds / 60)}分${seconds % 60}秒` : `${seconds}秒`;
109
+ const lines = [`✅ 任务完成: ${taskName}`, '', `⏱️ 耗时: ${timeStr}`];
110
+ if (extra?.files && extra.files > 0) lines.push(`📄 修改文件: ${extra.files} 个`);
111
+ if (extra?.iterations && extra.iterations > 0) lines.push(`🔄 迭代次数: ${extra.iterations}`);
112
+ return lines.join('\n');
113
+ }
114
+
104
115
  // Split message for Telegram's 4096 char limit
105
116
  export function splitMessage(text, maxLength = 4000) {
106
117
  if (text.length <= maxLength) {
@@ -1,6 +1,18 @@
1
1
  // Message router - full command definitions shared across all platforms
2
2
  import { registry } from './registry.js';
3
3
  import { initOpenCode, listProviders, updateGlobalModel, checkConnection, resumeSession, shareSession } from '../opencode/client.js';
4
+ import { formatTaskCompletion } from './notifications.js';
5
+
6
+ const demoModeMap = new Map();
7
+
8
+ export function setDemoMode(threadId, enabled) {
9
+ if (enabled) demoModeMap.set(threadId, true);
10
+ else demoModeMap.delete(threadId);
11
+ }
12
+
13
+ export function isDemoMode(threadId) {
14
+ return demoModeMap.has(threadId);
15
+ }
4
16
 
5
17
  export const COMMAND_ALIASES = {
6
18
  start: ['start'],
@@ -25,6 +37,34 @@ export const COMMAND_ALIASES = {
25
37
  agents: ['agents'],
26
38
  model: ['model'],
27
39
  expert: ['expert', 'z', 'Z', 'review'],
40
+ tutorial: ['tutorial', 'guide', 'walkthrough'],
41
+ demo: ['demo', 'sandbox', 'preview'],
42
+ };
43
+
44
+ export const DEMO_RESPONSES = {
45
+ start: '🚀 准备就绪,发送消息给 OpenCode 开始工作\n\n💡 这是演示模式,所有命令显示模拟输出',
46
+ get help() { return getHelpText(); },
47
+ status: '✅ OpenCode 在线\n✅ 七牛云 已配置\n✅ 会话: abc12345\n📁 项目目录: /home/user/my-project',
48
+ reset: '🔄 会话已重置,下次发送消息将创建新会话',
49
+ restart: '🔄 重启信号已发送,bot 即将重启...',
50
+ sessions: '📂 最近会话:\n\n1. Telegram 会话 (2分钟前)\n2. 微信开发会话 (15分钟前)\n3. 专家评审 (1小时前)',
51
+ delsessions: '🗑️ 选择要删除的会话(回复编号):\n\n1. Telegram 会话\n2. 微信开发会话\n\n回复编号删除',
52
+ loop: '🔄 循环任务已启动\n指令: 智能模式\n限制: 最多10次迭代或30分钟\n\n发送 /loop off 停止',
53
+ diagnose: '🔍 诊断报告\n\nOpenCode: ✅\n七牛云: ✅\nTelegram: ✅\n飞书: ❌ 未配置\n会话: ✅',
54
+ refresh: '✅ 会话已刷新',
55
+ copy: '📋 最新回复:\n\n这是 AI 的示例回复内容,演示 /copy 命令的功能。',
56
+ revert: '↩️ 已撤销最近的消息\n\n发送 /revert undo 恢复',
57
+ upload: '⬆️ 用法: /upload <文件路径>\n\n当前项目构建产物:\n📦 build/app.apk (12.5 MB)',
58
+ delete: '🗑️ 用法: /delete <key>\n\n示例: /delete uploads/1234567890-app.apk',
59
+ model: '🧠 可用模型:\n\nOpenAI (openai):\n gpt-4o\n gpt-4o-mini\n o3-mini\n\nAnthropic (anthropic):\n claude-sonnet-4-20250514\n\n用法: /model <provider/model>',
60
+ agents: '🤖 可用 AI Agent:\n\n✅ opencode\n✅ claude-code\n✅ codex\n❌ copilot\n\n切换: /oc /cc /cx /copilot',
61
+ oc: '✅ 已切换到 OpenCode\n\n💬 发送消息给 OpenCode 开始工作',
62
+ cc: '✅ 已切换到 Claude Code',
63
+ cx: '✅ 已切换到 Codex',
64
+ copilot: '✅ 已切换到 GitHub Copilot',
65
+ edit: '✏️ 用法: /edit <消息编号>\n\n选择要修改的消息,然后发送修正后的内容。',
66
+ expert: '🧠 专家评审模式已启动\n\n14 位 AI 专家正在分析您的项目...\n\n架构师、安全研究员、测试工程师、VC/投资人等角色将依次给出评审意见。',
67
+ tutorial: '📚 教程已启动\n发送 /tutorial 1 开始第1步',
28
68
  };
29
69
 
30
70
  export const EXPERT_SYSTEM_PROMPT = `你是一个专家评审系统。用户消息含触发词(z/c/叫全部专家/专家点评)时启动评审,前后可带具体问题则聚焦该问题。
@@ -134,6 +174,8 @@ const COMMAND_HELP = {
134
174
  agents: '查看 Agent',
135
175
  model: '切换模型',
136
176
  expert: '专家评审(z/叫全部专家)',
177
+ tutorial: '交互式教程(step-by-step 上手)',
178
+ demo: '沙箱模式(无需配置体验全部命令)',
137
179
  };
138
180
 
139
181
  const COMMAND_MAP = {};
@@ -249,13 +291,107 @@ async function getSessionMessages(sessionId) {
249
291
  return result.data || [];
250
292
  }
251
293
 
294
+ export const TUTORIAL_STEPS = [
295
+ {
296
+ step: 1,
297
+ title: '💬 发送第一条消息',
298
+ desc: '直接发一条消息给 bot,比如:"帮我写一个 Hello World 程序"\nAI 会自动接收并在你的电脑上执行。',
299
+ action: '现在试试:输入 "你好" 或 "帮我写一个 Python 程序"',
300
+ },
301
+ {
302
+ step: 2,
303
+ title: '📊 查看状态',
304
+ desc: '发送 /status 查看 OpenCode 是否在线、当前会话信息、运行中的任务。',
305
+ action: '试试:发送 /status',
306
+ },
307
+ {
308
+ step: 3,
309
+ title: '📋 复制 AI 回复',
310
+ desc: 'AI 回复了长篇代码?用 /copy 一键复制最新 AI 回复的内容。',
311
+ action: '试试:发送 /copy',
312
+ },
313
+ {
314
+ step: 4,
315
+ title: '🤖 切换 AI 模型',
316
+ desc: '不同模型擅长不同任务。用 /model 查看可用模型,/model provider/model 切换。',
317
+ action: '试试:发送 /model 查看列表',
318
+ },
319
+ {
320
+ step: 5,
321
+ title: '🧠 召唤专家评审',
322
+ desc: '发送 /z 启动专家评审模式,14 位 AI 专家分析你的项目,自动出修复方案并执行。',
323
+ action: '试试:发送 /z,然后发送 z',
324
+ },
325
+ {
326
+ step: 6,
327
+ title: '🔄 循环任务',
328
+ desc: '让 AI 持续工作。发送 /loop 启动循环任务,AI 会反复推进项目。\n停止:/loop off',
329
+ action: '试试:发送 /loop 检查测试覆盖率',
330
+ },
331
+ {
332
+ step: 7,
333
+ title: '🔍 系统诊断',
334
+ desc: '出问题了?/diagnose 一键检查 OpenCode、七牛云、各平台连接状态。',
335
+ action: '试试:发送 /diagnose',
336
+ },
337
+ {
338
+ step: 8,
339
+ title: '🎉 全部搞定',
340
+ desc: '你已经掌握了所有核心功能!\n下一步建议:\n• /help 查看全部 22 条命令\n• 设置你的项目目录并开始真正的开发\n• 尝试多 Agent 切换:/cc 用 Claude Code,/cx 用 Codex',
341
+ action: '',
342
+ },
343
+ ];
344
+
345
+ function getTutorialText(step) {
346
+ const s = TUTORIAL_STEPS[step - 1];
347
+ if (!s) return getTutorialText(1);
348
+ let msg = `📚 教程 · 第 ${s.step}/${TUTORIAL_STEPS.length} 步\n━━━━━━━━━━━━━━━━\n\n${s.title}\n\n${s.desc}\n\n`;
349
+ if (s.action) msg += `👉 ${s.action}`;
350
+ msg += `\n\n回复 /tutorial${step < TUTORIAL_STEPS.length ? ` 继续第${step + 1}步\n发送 /tutorial ${step + 1}` : ''} 进入下一步`;
351
+ return msg;
352
+ }
353
+
252
354
  export async function routeMessage(parsed, ctx) {
355
+ const threadId = ctx.threadId;
356
+ if (demoModeMap.has(threadId) && parsed.type === 'command' && DEMO_RESPONSES[parsed.command]) {
357
+ return DEMO_RESPONSES[parsed.command];
358
+ }
253
359
  switch (parsed.type) {
254
360
  case 'command': {
255
361
  switch (parsed.command) {
256
362
  case 'help':
257
363
  return getHelpText();
258
364
 
365
+ case 'tutorial': {
366
+ const stepNum = parseInt(parsed.arg, 10);
367
+ const step = !isNaN(stepNum) && stepNum >= 1 && stepNum <= TUTORIAL_STEPS.length ? stepNum : 1;
368
+ return getTutorialText(step);
369
+ }
370
+
371
+ case 'demo': {
372
+ const arg = (parsed.arg || '').trim().toLowerCase();
373
+ if (arg === 'off' || arg === 'exit' || arg === 'stop') {
374
+ setDemoMode(threadId, false);
375
+ return '⏹️ 已退出沙箱模式\n\n现在所有命令将正常连接 OpenCode 执行。';
376
+ }
377
+ setDemoMode(threadId, true);
378
+ let msg = '🎮 **沙箱模式已启动**\n\n';
379
+ msg += '在此模式下,所有命令返回模拟输出,无需连接 OpenCode。\n\n';
380
+ msg += '可用命令:\n';
381
+ const groups = [
382
+ ['🟢 常用', ['/help', '/start', '/status', '/reset']],
383
+ ['🔄 任务', ['/loop', '/refresh', '/diagnose']],
384
+ ['🤖 AI', ['/model', '/agents', '/oc', '/cc']],
385
+ ['📂 会话', ['/sessions', '/delsessions', '/copy', '/revert']],
386
+ ['⬆️ 文件', ['/upload', '/delete']],
387
+ ];
388
+ for (const [title, cmds] of groups) {
389
+ msg += `\n${title}\n ${cmds.join(' ')}\n`;
390
+ }
391
+ msg += '\n试试发送上面的命令体验效果!\n发送 /demo off 退出沙箱模式';
392
+ return msg;
393
+ }
394
+
259
395
  case 'agents': {
260
396
  const agents = registry.listAgents();
261
397
  const lines = ['🤖 可用 AI Agent:'];
@@ -411,8 +547,10 @@ export async function routeMessage(parsed, ctx) {
411
547
  if (!agent) return '❌ OpenCode agent not found';
412
548
  const available = await agent.isAvailable().catch(() => false);
413
549
  if (!available) return '❌ OpenCode 不可用';
550
+ const taskStart = Date.now();
414
551
  const response = await agent.sendPrompt(ctx.threadId || 'expert-review', EXPERT_SYSTEM_PROMPT + '\n\n用户问题:' + (parsed.arg || '请评审当前项目'), []);
415
- return response || '无响应';
552
+ const notification = response ? '' : `\n\n${formatTaskCompletion('专家评审', taskStart)}`;
553
+ return (response || '无响应') + notification;
416
554
  }
417
555
 
418
556
  default:
@@ -3,7 +3,7 @@ import { splitMessage } from '../core/notifications.js';
3
3
  import { EMOJI } from '../core/types.js';
4
4
  import { initOpenCode, createSession, sendMessage, checkConnection, abortSession, resumeSession, revertSessionMessage, unrevertSession, listProviders, updateGlobalModel } from '../opencode/client.js';
5
5
  import { claimOwnership } from '../core/auth.js';
6
- import { COMMAND_ALIASES, detectCommand, getHelpText } from '../core/router.js';
6
+ import { COMMAND_ALIASES, detectCommand, getHelpText, DEMO_RESPONSES, setDemoMode, isDemoMode } from '../core/router.js';
7
7
  import { registry } from '../core/registry.js';
8
8
  import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
9
9
  import { join, basename } from 'path';
@@ -78,6 +78,18 @@ async function handleCommand(adapter, ctx, command, arg, openCodeSessions) {
78
78
  case 'help':
79
79
  await adapter.reply(ctx.threadId, getHelpText());
80
80
  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
+ }
81
93
  case 'agents': {
82
94
  const agents = registry.listAgents();
83
95
  const lines = ['🤖 可用 AI Agent:'];
@@ -548,6 +560,21 @@ async function handleCommand(adapter, ctx, command, arg, openCodeSessions) {
548
560
 
549
561
 
550
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
+
551
578
  case 'diagnose': {
552
579
  const { checkConnection } = await import('../opencode/client.js');
553
580
  const diag = ['🔍 诊断报告\n'];
@@ -398,6 +398,7 @@ export async function sendMessage(session, message, callbacks) {
398
398
  let responseText = '';
399
399
  let hasToolActivity = false;
400
400
  let idleSince = 0; // 最后一次收到新内容的时间戳
401
+ let lastStatus = '';
401
402
 
402
403
  while (Date.now() - startTime < TIMEOUT_MS) {
403
404
  await new Promise(r => setTimeout(r, POLL_INTERVAL));
@@ -454,6 +455,7 @@ export async function sendMessage(session, message, callbacks) {
454
455
  if (latestStatus === 'thinking' || latestStatus === 'pending_tool') {
455
456
  idleSince = Date.now();
456
457
  }
458
+ if (latestStatus) lastStatus = latestStatus;
457
459
  if (latestStatus && latestStatus !== lastReportedStatus) {
458
460
  lastReportedStatus = latestStatus;
459
461
  console.log(`[AI状态] ${latestStatus}`);
@@ -2,6 +2,13 @@
2
2
  import { spawn } from 'child_process';
3
3
  import { platform } from 'os';
4
4
 
5
+ const LIUV_CRASH_PATTERNS = [
6
+ 'Assertion failed',
7
+ 'UV_HANDLE_CLOSING',
8
+ 'src\\win\\async.c',
9
+ 'libuv',
10
+ ];
11
+
5
12
  export class ClaudeCodeAgentAdapter {
6
13
  name = 'claude-code';
7
14
  aliases = ['cc', 'claude'];
@@ -19,9 +26,7 @@ export class ClaudeCodeAgentAdapter {
19
26
  const projectDir = options.projectDir;
20
27
  const contextualPrompt = this.buildContextualPrompt(prompt, history);
21
28
 
22
- // 构建命令参数
23
29
  const args = ['--print', contextualPrompt];
24
- // `cwd` is set via spawn opts below, no need for --project flag
25
30
 
26
31
  return this.callClaude(args, projectDir);
27
32
  }
@@ -32,13 +37,40 @@ export class ClaudeCodeAgentAdapter {
32
37
  return `Previous:\n${historyText}\n\n${prompt}`;
33
38
  }
34
39
 
40
+ isCrashNoise(line) {
41
+ return LIUV_CRASH_PATTERNS.some(p => line.includes(p));
42
+ }
43
+
44
+ extractErrorMessage(stdout, stderr, code) {
45
+ // 先检查 stdout:--print 模式把错误也输出到 stdout
46
+ const stdoutLines = stdout.trim().split('\n').map(l => l.trim()).filter(Boolean);
47
+ const stdoutErrors = stdoutLines.filter(l => !this.isCrashNoise(l));
48
+ if (stdoutErrors.length > 0) {
49
+ return stdoutErrors.join('\n');
50
+ }
51
+
52
+ // 再检查 stderr
53
+ const stderrLines = stderr.trim().split('\n').map(l => l.trim()).filter(Boolean);
54
+ const stderrReal = stderrLines.filter(l => !this.isCrashNoise(l));
55
+ if (stderrReal.length > 0) {
56
+ return stderrReal.join('\n');
57
+ }
58
+
59
+ // 如果全是崩溃噪音,尝试从任一流中找 Error 关键词
60
+ const all = [...stdoutLines, ...stderrLines];
61
+ const firstRelevant = all.find(l => /Error|error|ERROR|^\d{3}/.test(l));
62
+ if (firstRelevant) return firstRelevant;
63
+
64
+ // 兜底
65
+ return `进程异常退出 (code: ${code})`;
66
+ }
67
+
35
68
  callClaude(args, projectDir) {
36
69
  return new Promise((resolve) => {
37
70
  const opts = {
38
71
  stdio: ['ignore', 'pipe', 'pipe'],
39
72
  shell: true,
40
73
  };
41
- // 如果指定了项目目录,在该目录下执行
42
74
  if (projectDir) {
43
75
  opts.cwd = projectDir;
44
76
  }
@@ -57,7 +89,17 @@ export class ClaudeCodeAgentAdapter {
57
89
  proc.on('close', (code) => {
58
90
  clearTimeout(timeout);
59
91
  console.log(`[claude-code] Process exited with code ${code}, stdout=${stdout.length} bytes, stderr=${stderr.length} bytes`);
60
- resolve(code === 0 ? stdout.trim() : `❌ Claude Code 错误: ${stderr}`);
92
+
93
+ if (code === 0) {
94
+ resolve(stdout.trim());
95
+ return;
96
+ }
97
+
98
+ const errorMsg = this.extractErrorMessage(stdout, stderr, code);
99
+ console.log(`[claude-code] Process failed, raw stderr:\n${stderr.trim().slice(-1000)}`);
100
+ console.log(`[claude-code] Error detail:\n${errorMsg}`);
101
+
102
+ resolve(`❌ Claude Code 错误 (exit code ${code}): ${errorMsg}`);
61
103
  });
62
104
  proc.on('error', (err) => {
63
105
  clearTimeout(timeout);
@@ -29,10 +29,30 @@ export class TelegramAdapter {
29
29
 
30
30
  onMessage(handler) { this.messageHandler = handler; }
31
31
 
32
- async sendMessage(threadId, text) {
32
+ async sendMessage(threadId, text, opts = {}) {
33
33
  if (!this.bot) throw new Error('Telegram adapter not started');
34
34
  const chunks = splitMessage(text, { maxLength: 4000, addContinuationMarker: false });
35
- for (const chunk of chunks) await this.bot.api.sendMessage(threadId, chunk, { parse_mode: 'Markdown' });
35
+ for (const chunk of chunks) await this.bot.api.sendMessage(threadId, chunk, { parse_mode: 'Markdown', ...opts });
36
+ }
37
+
38
+ async sendCommandMenu(threadId, title) {
39
+ if (!this.bot) return;
40
+ const groups = [
41
+ ['🟢 常用', ['/help', '/status', '/start', '/reset']],
42
+ ['🔄 任务', ['/loop', '/refresh', '/restart']],
43
+ ['🤖 AI', ['/model', '/agents', '/oc', '/cc']],
44
+ ['🧠 专家', ['/tutorial', '/z', '/diagnose']],
45
+ ['📂 会话', ['/sessions', '/delsessions', '/copy', '/revert']],
46
+ ['⬆️ 文件', ['/upload', '/delete']],
47
+ ];
48
+ const keyboard = [];
49
+ for (const [, cmds] of groups) {
50
+ const row = cmds.map(cmd => ({ text: cmd, callback_data: `cmd:${cmd.slice(1)}` }));
51
+ keyboard.push(row);
52
+ }
53
+ await this.bot.api.sendMessage(threadId, title || '📱 选择指令:', {
54
+ reply_markup: { inline_keyboard: keyboard },
55
+ });
36
56
  }
37
57
 
38
58
  async sendTyping(threadId, isTyping) {
@@ -44,6 +44,15 @@ export async function startBot() {
44
44
  }
45
45
  });
46
46
 
47
+ telegramAdapter.bot.on('callback_query:data', async (ctx) => {
48
+ if (!ctx.callbackQuery.data.startsWith('cmd:')) return;
49
+ const cmd = ctx.callbackQuery.data.slice(4);
50
+ try {
51
+ await ctx.answerCallbackQuery({ text: `执行: /${cmd}` });
52
+ const msg = await telegramAdapter.bot.api.sendMessage(ctx.chat.id, `/${cmd}`);
53
+ } catch (e) { console.error('[Telegram] callback error:', e.message); }
54
+ });
55
+
47
56
  telegramAdapter.bot.start().catch((err) => {
48
57
  if (telegramAdapter.isRunning) console.error('[Telegram] Polling error:', err);
49
58
  });
@@ -85,6 +94,7 @@ export async function startBot() {
85
94
  }
86
95
  opencodeSessionId = session.sessionId;
87
96
 
97
+ const taskStart = Date.now();
88
98
  const response = await sendToOpenCode(session, parsed.prompt, {
89
99
  onTextDelta: () => {},
90
100
  onEvent: (event) => {
@@ -101,6 +111,8 @@ export async function startBot() {
101
111
  if (chunk.trim()) await telegramAdapter.sendMessage(message.threadId, chunk);
102
112
  }
103
113
  }
114
+ const { formatTaskCompletion } = await import('../core/notifications.js');
115
+ await telegramAdapter.sendMessage(message.threadId, formatTaskCompletion('AI 任务', taskStart));
104
116
  return;
105
117
  }
106
118
 
@@ -111,7 +123,12 @@ export async function startBot() {
111
123
 
112
124
  await telegramAdapter.sendTyping(message.threadId, false);
113
125
  if (typeof result === 'string') {
114
- await telegramAdapter.sendMessage(message.threadId, result);
126
+ if (parsed.type === 'command' && (parsed.command === 'help' || parsed.command === 'start')) {
127
+ await telegramAdapter.sendMessage(message.threadId, result);
128
+ await telegramAdapter.sendCommandMenu(message.threadId, '📱 快速选择指令:');
129
+ } else {
130
+ await telegramAdapter.sendMessage(message.threadId, result);
131
+ }
115
132
  } else if (result) {
116
133
  let full = '';
117
134
  for await (const chunk of result) full += chunk;
@@ -1,4 +1,4 @@
1
- import { detectCommand, COMMAND_ALIASES, getHelpText } from '../core/router.js';
1
+ import { detectCommand, COMMAND_ALIASES, getHelpText, DEMO_RESPONSES, setDemoMode, isDemoMode } from '../core/router.js';
2
2
  import { getOrCreateSession, saveSessionMapping, sessionManager } from '../core/session.js';
3
3
  import { splitMessage } from '../core/notifications.js';
4
4
  import { initOpenCode, checkConnection, abortSession, resumeSession, revertSessionMessage, unrevertSession, listProviders, updateGlobalModel } from '../opencode/client.js';
@@ -93,6 +93,18 @@ async function handleCommand(adapter, ctx, command, arg, openCodeSessions) {
93
93
  case 'help':
94
94
  await adapter.reply(ctx.threadId, getHelpText());
95
95
  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
+ }
96
108
  case 'status': {
97
109
  const connected = await checkConnection();
98
110
  const running = session.taskStartTime ? Math.round((Date.now() - session.taskStartTime) / 1000) : 0;
@@ -698,6 +710,21 @@ async function handleCommand(adapter, ctx, command, arg, openCodeSessions) {
698
710
 
699
711
 
700
712
 
713
+ case 'demo': {
714
+ const argText = (arg || '').trim().toLowerCase();
715
+ if (argText === 'off' || argText === 'exit' || argText === 'stop') {
716
+ setDemoMode(ctx.threadId, false);
717
+ await adapter.reply(ctx.threadId, '⏹️ 已退出沙箱模式');
718
+ return true;
719
+ }
720
+ setDemoMode(ctx.threadId, true);
721
+ let msg = '🎮 沙箱模式已启动\n\n在此模式下所有命令返回模拟输出,无需连接 OpenCode。\n\n';
722
+ msg += '试试发送: /help /status /model /agents /loop /copy\n';
723
+ msg += '发送 /demo off 退出';
724
+ await adapter.reply(ctx.threadId, msg);
725
+ return true;
726
+ }
727
+
701
728
  case 'diagnose': {
702
729
  const { checkConnection } = await import('../opencode/client.js');
703
730
  const diag = ['🔍 诊断报告\n'];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yvhitxcel/opencode-remote",
3
- "version": "0.16.0",
3
+ "version": "0.16.1",
4
4
  "description": "🤖 AI 专家团队随时待命!只需输入 /z,自动分析项目、诊断问题、给出改进方案。支持微信/飞书/Telegram 远程控制 OpenCode、Claude Code、Codex、Copilot。手机也能搞开发。",
5
5
  "type": "module",
6
6
  "bin": {