@yvhitxcel/opencode-remote 0.15.1 → 0.16.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.
package/README.md CHANGED
@@ -2,12 +2,28 @@
2
2
 
3
3
  通过微信、Telegram、飞书等平台随时随地控制 OpenCode。
4
4
 
5
+ ## 为什么这么爽?
6
+
7
+ **输入一个字母 `z`,14 位 AI 专家帮你审项目。** 架构师、安全研究员、测试工程师、运维……每人给出犀利点评,技术经理汇总 P0-P2 任务清单。你在手机上躺着看就行。
8
+
9
+ ```bash
10
+ # 安装
11
+ npm install -g @yvhitxcel/opencode-remote
12
+
13
+ # 开干
14
+ opencode-remote telegram
15
+ # → 输入 /z 叫专家团队
16
+ # → 输入 z 让专家分析项目
17
+ # → AI 自动干活,你喝茶
18
+ ```
19
+
5
20
  ## 功能特性
6
21
 
7
- - **多平台支持**微信、飞书、Telegram
8
- - **多 AI Agent** OpenCode、Claude Code、Codex、GitHub Copilot
9
- - **会话管理**多会话并行,自动保存与恢复
10
- - **智能循环任务**长时间任务自动循环执行
22
+ - **🤖 专家评审系统** `/z` 一键召唤 14 位 AI 专家,自动分析、投票、出方案
23
+ - **📱 多平台支持**微信、飞书、Telegram
24
+ - **🧠 多 AI Agent** OpenCode、Claude Code、Codex、GitHub Copilot
25
+ - **🔄 循环任务** `/loop` 让 AI 持续干活
26
+ - **🔍 一键诊断** — `/diagnose` 检查各组件状态
11
27
 
12
28
  ## 安装
13
29
 
@@ -17,15 +33,42 @@ npm install -g opencode-remote
17
33
 
18
34
  ## 快速开始
19
35
 
36
+ 推荐路径:**Telegram(5分钟)→ 微信(10分钟)→ 飞书(30分钟)**
37
+
20
38
  ```bash
21
- # 微信
39
+ # 1. 安装
40
+ npm install -g @yvhitxcel/opencode-remote
41
+
42
+ # 2. 启动 Telegram(最快上手,无需配置)
43
+ opencode-remote telegram
44
+ # 在 Telegram 里搜索你的 bot,发送 /start
45
+
46
+ # 3. 微信(需要 iLink 平台账号)
22
47
  opencode-remote weixin
48
+ # 扫码登录后即可使用
23
49
 
24
- # 飞书
50
+ # 4. 飞书(需要企业版账号)
25
51
  opencode-remote feishu
52
+ ```
26
53
 
27
- # Telegram
28
- opencode-remote telegram
54
+ ## 首次使用
55
+
56
+ 1. 安装后运行 `opencode-remote telegram`
57
+ 2. 在 Telegram 里找到你的 bot,发送 `/start`
58
+ 3. 发送 `/help` 查看所有命令
59
+ 4. 发送一条消息给 AI,比如"你好"
60
+ 5. **发送 `/z` 启动专家模式,然后发送 `z` —— 14 位 AI 专家开始分析你的项目**
61
+
62
+ > 💡 所有核心功能(对话、会话管理、AI 模型切换、专家评审)无需任何配置。只有 `/upload` 上传才需要七牛云。
63
+
64
+ ## 手机开发工作流
65
+
66
+ 把 `weixin.bat` 复制到项目根目录,双击运行,扫码登录后即可在手机上通过微信开发该项目。
67
+
68
+ ```bash
69
+ # 或者手动指定目录
70
+ cd 你的项目
71
+ opencode-remote
29
72
  ```
30
73
 
31
74
  ## 平台兼容性
@@ -90,10 +133,37 @@ opencode-remote
90
133
 
91
134
  **七牛云是可选的**,不配也能用全部核心功能。只有上传构建产物才需要配置。
92
135
 
136
+ ## 常见问题
137
+
138
+ **Q: 为什么有些命令不能用?**
139
+ A: 先运行 `/diagnose` 检查各组件状态。Telegram 功能最全,微信和飞书部分命令需要额外配置。
140
+
141
+ **Q: 微信怎么登录?**
142
+ A: 运行 `opencode-remote weixin`,终端会显示二维码,用微信扫码即可。
143
+
144
+ **Q: 专家评审怎么用?**
145
+ A: 发 `z` 或 `叫全部专家`,AI 自动扫项目、组队评审、出 P0/P1 修复方案,**然后自动修代码**。评审过程包括三级质量保障:脑内路径追踪、服务端模拟验证、对抗评审。
146
+
147
+ **Q: `/z` 什么时候用?**
148
+ A: 任何时候。发一个 `z` 让 AI 评审当前项目,发 `z 帮我看看这个bug` 聚焦具体问题。先 `/z` 设置自定义 prompt 再发问题也行。
149
+
150
+ **Q: `/z` 自动修代码会改坏吗?**
151
+ A: **强烈建议在 git 仓库中使用。** 如果改坏了可以 `git checkout .` 回滚。没有 git 的项目,AI 改完不可逆。不确定的话先发 `/z off` 关闭自动执行,只看报告不改代码。
152
+
153
+ **Q: 需要自己的服务器吗?**
154
+ A: 需要一台电脑运行 bot,手机上通过 IM 控制。OpenCode 也运行在这台电脑上。
155
+
156
+ **Q: 如何更新?**
157
+ A: `npm update -g @yvhitxcel/opencode-remote`
158
+
93
159
  ## 系统要求
94
160
 
95
161
  - Node.js >= 18.0.0
96
162
 
163
+ ## 致谢
164
+
165
+ 本项目基于 [opencode-remote-control](https://github.com/ceociocto/opencode-remote-control) 开发。
166
+
97
167
  ## 许可证
98
168
 
99
169
  MIT License
package/dist/core/auth.js CHANGED
@@ -1,119 +1,52 @@
1
- // Authorization management for OpenCode Remote Control
2
- // First user to send /start becomes the owner automatically
3
- const authState = {
4
- telegramOwner: null,
5
- feishuOwner: null,
6
- weixinOwner: null,
7
- };
8
- // Auth file path for persistence
9
1
  import { homedir } from 'os';
10
2
  import { join } from 'path';
11
3
  import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
12
- const AUTH_DIR = join(homedir(), '.opencode-remote');
13
- const AUTH_FILE = join(AUTH_DIR, 'auth.json');
14
- function ensureAuthDir() {
15
- if (!existsSync(AUTH_DIR)) {
16
- mkdirSync(AUTH_DIR, { recursive: true });
17
- }
18
- }
19
- function loadAuth() {
20
- try {
21
- if (existsSync(AUTH_FILE)) {
22
- const data = JSON.parse(readFileSync(AUTH_FILE, 'utf-8'));
23
- authState.telegramOwner = data.telegramOwner || null;
24
- authState.feishuOwner = data.feishuOwner || null;
25
- authState.weixinOwner = data.weixinOwner || null;
26
- }
27
- }
28
- catch (error) {
29
- console.warn('Failed to load auth state, starting fresh:', error);
30
- }
31
- }
32
- function saveAuth() {
33
- try {
34
- ensureAuthDir();
35
- writeFileSync(AUTH_FILE, JSON.stringify(authState, null, 2));
36
- }
37
- catch (error) {
38
- console.error('Failed to save auth state:', error);
39
- }
40
- }
41
- // Initialize on module load
42
- loadAuth();
4
+
5
+ const OWNER_KEY = { telegram: 'telegramOwner', feishu: 'feishuOwner', weixin: 'weixinOwner' };
6
+ const AUTH_FILE = join(homedir(), '.opencode-remote', 'auth.json');
7
+
8
+ const state = { telegramOwner: null, feishuOwner: null, weixinOwner: null };
9
+
10
+ function load() {
11
+ try {
12
+ if (existsSync(AUTH_FILE)) {
13
+ const d = JSON.parse(readFileSync(AUTH_FILE, 'utf-8'));
14
+ for (const k of Object.keys(OWNER_KEY)) state[OWNER_KEY[k]] = d[OWNER_KEY[k]] || null;
15
+ }
16
+ } catch (e) { console.warn('[auth] load failed:', e.message); }
17
+ }
18
+ function save() {
19
+ try {
20
+ const dir = join(homedir(), '.opencode-remote');
21
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
22
+ writeFileSync(AUTH_FILE, JSON.stringify(state, null, 2));
23
+ } catch (e) { console.error('[auth] save failed:', e.message); }
24
+ }
25
+ load();
26
+
43
27
  export function isAuthorized(platform, userId) {
44
- if (platform === 'telegram') {
45
- return authState.telegramOwner === userId;
46
- }
47
- else if (platform === 'feishu') {
48
- return authState.feishuOwner === userId;
49
- }
50
- else {
51
- return authState.weixinOwner === userId;
52
- }
28
+ const key = OWNER_KEY[platform];
29
+ return key ? state[key] === userId : false;
53
30
  }
54
31
  export function hasOwner(platform) {
55
- if (platform === 'telegram') {
56
- return authState.telegramOwner !== null;
57
- }
58
- else if (platform === 'feishu') {
59
- return authState.feishuOwner !== null;
60
- }
61
- else {
62
- return authState.weixinOwner !== null;
63
- }
64
- }
65
- export function claimOwnership(platform, userId) {
66
- if (platform === 'telegram') {
67
- if (authState.telegramOwner) {
68
- if (authState.telegramOwner === userId) {
69
- return { success: true, message: 'already_owner' };
70
- }
71
- return { success: false, message: 'already_claimed' };
72
- }
73
- authState.telegramOwner = userId;
74
- saveAuth();
75
- return { success: true, message: 'claimed' };
76
- }
77
- else if (platform === 'feishu') {
78
- if (authState.feishuOwner) {
79
- if (authState.feishuOwner === userId) {
80
- return { success: true, message: 'already_owner' };
81
- }
82
- return { success: false, message: 'already_claimed' };
83
- }
84
- authState.feishuOwner = userId;
85
- saveAuth();
86
- return { success: true, message: 'claimed' };
87
- }
88
- else {
89
- // weixin
90
- if (authState.weixinOwner) {
91
- if (authState.weixinOwner === userId) {
92
- return { success: true, message: 'already_owner' };
93
- }
94
- return { success: false, message: 'already_claimed' };
95
- }
96
- authState.weixinOwner = userId;
97
- saveAuth();
98
- return { success: true, message: 'claimed' };
99
- }
32
+ const key = OWNER_KEY[platform];
33
+ return key ? state[key] !== null : false;
100
34
  }
101
35
  export function getOwner(platform) {
102
- if (platform === 'telegram') {
103
- return authState.telegramOwner;
104
- }
105
- else if (platform === 'feishu') {
106
- return authState.feishuOwner;
107
- }
108
- else {
109
- return authState.weixinOwner;
110
- }
36
+ const key = OWNER_KEY[platform];
37
+ return key ? state[key] : null;
38
+ }
39
+ export function claimOwnership(platform, userId) {
40
+ const key = OWNER_KEY[platform];
41
+ if (!key) return { success: false, message: 'unknown_platform' };
42
+ if (state[key]) {
43
+ if (state[key] === userId) return { success: true, message: 'already_owner' };
44
+ return { success: false, message: 'already_claimed' };
45
+ }
46
+ state[key] = userId;
47
+ save();
48
+ return { success: true, message: 'claimed' };
111
49
  }
112
- // For debugging/display
113
50
  export function getAuthStatus() {
114
- return {
115
- telegram: authState.telegramOwner !== null,
116
- feishu: authState.feishuOwner !== null,
117
- weixin: authState.weixinOwner !== null,
118
- };
51
+ return { telegram: !!state.telegramOwner, feishu: !!state.feishuOwner, weixin: !!state.weixinOwner };
119
52
  }
@@ -7,7 +7,6 @@ export const COMMAND_ALIASES = {
7
7
  help: ['help', 'h', '?'],
8
8
  status: ['status'],
9
9
  reset: ['reset'],
10
- stop: ['stop'],
11
10
  restart: ['restart'],
12
11
  sessions: ['sessions', 'sw'],
13
12
  delsessions: ['delsessions', 'del'],
@@ -25,7 +24,116 @@ export const COMMAND_ALIASES = {
25
24
  copilot: ['copilot'],
26
25
  agents: ['agents'],
27
26
  model: ['model'],
28
- expert: ['expert', 'z', 'review'],
27
+ expert: ['expert', 'z', 'Z', 'review'],
28
+ };
29
+
30
+ export const EXPERT_SYSTEM_PROMPT = `你是一个专家评审系统。用户消息含触发词(z/c/叫全部专家/专家点评)时启动评审,前后可带具体问题则聚焦该问题。
31
+
32
+ ## 角色(14 位)
33
+ 1. 架构师 — 代码架构、模块划分、依赖管理
34
+ 2. 后端工程师 — 稳定性、错误处理、性能
35
+ 3. 测试工程师 — 测试覆盖、可测性
36
+ 4. VC / 投资人 — 值不值得投
37
+ 5. 开源社区经理 — 新人能不能上车
38
+ 6. Flutter 开发者 — 移动端好不好用
39
+ 7. SRE / 运维 — 能不能上线
40
+ 8. 安全研究员 — 有没有洞
41
+ 9. AI 研究员 — agent loop 质量
42
+ 10. 用户支持 — 用户卡在哪
43
+ 11. 技术写作者 — 文档好不好写
44
+ 12. 竞品分析师 — 市场定位
45
+ 13. Git 专家 — commit 质量、分支管理、历史整洁度、回滚安全
46
+ 14. **技术经理(最后出场)** — 汇总以上 13 位意见,给出 P0-P2 分级的可执行任务清单
47
+
48
+ ## 执行流程
49
+
50
+ ### 第 1 步:1-13 号专家点评
51
+ 每人最多 200 字。**言辞必须苛刻、犀利、严谨。不讨好,不委婉。不投票。**
52
+ - 如果某条意见在上轮已提过但未修复,必须指出"上轮已提过,未落实"
53
+ - 格式: \`[意见] 内容 / [上轮已提: N]\`
54
+
55
+ ### 第 2 步:技术经理提出 15 条问题
56
+ 基于以上 13 位意见,输出 1) 2) ... 15) 清单。
57
+ - **1-5 为 P0(红色🔴)**,6-10 为 P1(黄色🟡),11-15 为 P2(蓝色🔵)
58
+ - 每条包含一句话描述(做什么),不展开
59
+ - 小 bug 合并同类项,一条 = 一个可执行动作
60
+ - **每条必须标注对应的专家意见编号**(格式: \`1) xxx [来源: 3,7,11]\`)
61
+ - **不含投票结果。不含投票结果。不含投票结果。**
62
+
63
+ ### 第 3 步:1-13 号专家投票
64
+ 每人从 15 条中选 3 条最关键的。格式:\`-> 三票:1, 7, 12\`
65
+ - **投票时必须检查自己上轮的意见是否已落实,如未落实则优先投票给相关条目**
66
+
67
+ ### 第 4 步:技术经理公布结果
68
+ 得票统计,汇入 P0-P2,标记得票数。
69
+ - **P0 🔴 1) xxx ( 票)[来源: 3,7] — 说明**
70
+ - **P1 🟡 7) xxx ( 票)[来源: 1,5] — 说明**
71
+
72
+ ### 第 5 步:自动执行
73
+ 对 P0(票数 ≥ 3)按得票从高到低逐个自动执行修复。
74
+
75
+ ## 三级质量保障(嵌入评审全程)
76
+
77
+ ### 1. 脑内执行路径追踪
78
+ 在评审和修复代码时,**在脑中逐条执行关键路径**:变量怎么赋值、条件怎么分支、循环怎么迭代、异常怎么传播。不只看代码静态结构,要模拟运行时行为。发现逻辑断点、边界遗漏、状态覆盖不全立即提出。
79
+
80
+ ### 2. 服务端模拟验证
81
+ 对评审中发现的 P0/P1 问题,在修复后**自动执行模拟验证**:
82
+ - 跑 \`npm run lint\` 检查语法和模块图
83
+ - 跑 \`node --test\` 验证单元测试
84
+ - 对修改的文件做 \`node --check\` 语法校验
85
+ - 如果项目有 CI 脚本,触发本地等效检查
86
+ - 验证结果写入执行总结
87
+
88
+ ### 3. LLM 对抗评审
89
+ 修复完成后,**以对抗视角重新审视自己的修改**:"这段修改有没有引入新 bug?有没有遗漏边界情况?变更是否最小?会不会破坏现有功能?"
90
+ - 如果发现自己的修改有问题→回退重做
91
+ - 如果确认无误→在总结中标注"已通过对抗评审"
92
+
93
+ ## 核心三要素
94
+ 1. **吃透代码再动手** — 不靠猜测修复 bug
95
+ 2. **追到根因** — 用户反馈的现象要追到代码根因
96
+ 3. **改完必须验证** — 先跑测试跑 lint 再交付
97
+
98
+ ## 四项基本原则(硬约束)
99
+
100
+ 1. **一次性做好,不重复返工** — 同一个模块的同类问题最多修两轮。第三轮还提同类问题说明方案不对,技术经理必须输出"换方案"而非"继续修"。
101
+
102
+ 2. **以用户价值为导向** — P0 排序:用户能不能跑起来 > 会不会崩 > 好不好用。内部代码质量默认不进 P0。
103
+
104
+ 3. **对用户友好** — 每次评审必须包含"首次使用视角"。P0 必须至少有一条直接回应首次使用视角。**若没有,整轮评审无效。**
105
+
106
+ 4. **快速迭代,先上线** — R4 开始技术经理必须回答"是否可以发布?最短路径是什么?" 是则只保留阻塞发布的问题。
107
+
108
+ ## 规则
109
+ - 言辞苛刻犀利,不讨好,不委婉
110
+ - 节约 token:摘要不超过 400 字,不贴源码
111
+ - 上轮已提过且已修的问题本轮不得再提(除非验收不合格)
112
+ - 技术经理的 15 条必须标注来源,**没有对应来源的问题不得出现**`;
113
+
114
+ const COMMAND_HELP = {
115
+ start: '认领所有权',
116
+ help: '显示帮助',
117
+ status: '连接状态',
118
+ reset: '重置会话',
119
+ restart: '重启 Bot',
120
+ sessions: '浏览会话',
121
+ delsessions: '删除会话',
122
+ loop: '循环任务',
123
+ edit: '编辑消息',
124
+ diagnose: '系统诊断',
125
+ refresh: '刷新上下文',
126
+ copy: '复制回复',
127
+ revert: '撤销消息',
128
+ upload: '上传文件',
129
+ delete: '删除上传文件',
130
+ oc: '使用 OpenCode',
131
+ cc: '使用 Claude Code',
132
+ cx: '使用 Codex',
133
+ copilot: '使用 Copilot',
134
+ agents: '查看 Agent',
135
+ model: '切换模型',
136
+ expert: '专家评审(z/叫全部专家)',
29
137
  };
30
138
 
31
139
  const COMMAND_MAP = {};
@@ -35,6 +143,46 @@ for (const [cmd, aliases] of Object.entries(COMMAND_ALIASES)) {
35
143
  }
36
144
  }
37
145
 
146
+ /**
147
+ * 保持 typing 指示器常亮。每 8 秒刷新一次,30 秒无活动自熄。
148
+ * 调用 poke 刷新计时,done 手动关闭。
149
+ * 放在 core 层避免被各平台 handler 误删。
150
+ */
151
+ export function startTypingPing(adapter, threadId) {
152
+ let lastActivity = Date.now();
153
+ const timer = setInterval(() => {
154
+ adapter.sendTypingIndicator(threadId).catch(() => {});
155
+ if (Date.now() - lastActivity > 30000) clearInterval(timer);
156
+ }, 3000);
157
+ return {
158
+ poke: () => { lastActivity = Date.now(); },
159
+ done: () => { clearInterval(timer); },
160
+ };
161
+ }
162
+
163
+ export function getHelpText() {
164
+ const lines = ['📖 指令\n'];
165
+ const groups = [
166
+ ['🟢 常用', ['start', 'help', 'status', 'reset', 'copy', 'revert', 'diagnose']],
167
+ ['🔄 任务', ['loop', 'refresh', 'restart']],
168
+ ['📂 会话', ['sessions', 'delsessions']],
169
+ ['🤖 AI', ['model', 'agents', 'oc', 'cc']],
170
+ ['⬆️ 文件', ['upload', 'delete']],
171
+ ['🧠 专家', ['expert']],
172
+ ];
173
+ for (const [title, cmds] of groups) {
174
+ lines.push(title);
175
+ for (const cmd of cmds) {
176
+ const aliases = COMMAND_ALIASES[cmd];
177
+ const aliasStr = aliases.length > 1 ? ` (${aliases.slice(1).join(', ')})` : '';
178
+ lines.push(` /${cmd}${aliasStr} — ${COMMAND_HELP[cmd] || cmd}`);
179
+ }
180
+ lines.push('');
181
+ }
182
+ lines.push('💬 直接发消息给 AI!');
183
+ return lines.join('\n');
184
+ }
185
+
38
186
  export function detectCommand(text) {
39
187
  const trimmed = text.trim();
40
188
  if (trimmed === 'h' || trimmed === '?') {
@@ -106,33 +254,7 @@ export async function routeMessage(parsed, ctx) {
106
254
  case 'command': {
107
255
  switch (parsed.command) {
108
256
  case 'help':
109
- return `📖 指令
110
-
111
- 🟢 常用:
112
- /start — 首次认证
113
- /help — 帮助
114
- /status — 连接状态
115
- /reset — 清空会话
116
- /copy — 复制回复
117
- /revert — 撤销消息
118
-
119
- 🔄 任务:
120
- /loop — 循环执行
121
- /refresh — 刷新上下文
122
- /restart — 重启 bot
123
- /stop — 停止 bot
124
-
125
- 📂 会话:
126
- /sessions — 浏览会话
127
- /delsessions — 删除会话
128
-
129
- 🤖 AI 模型:
130
- /model — 切换模型
131
- /agents — 查看可用 Agent
132
- /oc — 使用 OpenCode
133
- /cc — 使用 Claude Code
134
-
135
- 💬 直接发消息给 AI!`;
257
+ return getHelpText();
136
258
 
137
259
  case 'agents': {
138
260
  const agents = registry.listAgents();
@@ -165,9 +287,6 @@ export async function routeMessage(parsed, ctx) {
165
287
  case 'restart':
166
288
  return '🔄 重启信号已发送,bot 即将重启...';
167
289
 
168
- case 'stop':
169
- return '🛑 停止信号已发送';
170
-
171
290
  case 'sessions': {
172
291
  const sessions = await getSessionsList();
173
292
  if (!sessions || sessions.length === 0) return '📭 暂无会话';
@@ -288,38 +407,11 @@ export async function routeMessage(parsed, ctx) {
288
407
  return ctx.opencodeSessionId ? '✏️ 用法: /edit <消息编号>' : '❌ 没有活跃的会话';
289
408
 
290
409
  case 'expert': {
291
- const { execSync } = await import('child_process');
292
- const { existsSync, readFileSync } = await import('fs');
293
- const { homedir } = await import('os');
294
- const { join } = await import('path');
295
- const projectRoot = process.cwd();
296
- let gitStatus = '', recentCommits = '', dirTree = '';
297
- try { gitStatus = execSync('git status --short', { cwd: projectRoot, timeout: 5000, encoding: 'utf-8' }); } catch { gitStatus = '(not a git repo)'; }
298
- try { recentCommits = execSync('git log --oneline -5', { cwd: projectRoot, timeout: 5000, encoding: 'utf-8' }); } catch { recentCommits = '(no commits)'; }
299
- try { dirTree = execSync('cmd /c "tree /F /A"', { cwd: projectRoot, timeout: 5000, encoding: 'utf-8' }); } catch { dirTree = '(failed to get tree)'; }
300
- const customPromptPath = join(homedir(), '.opencode-remote', 'expert-prompt.md');
301
- let promptTemplate = '';
302
- if (existsSync(customPromptPath)) {
303
- promptTemplate = readFileSync(customPromptPath, 'utf-8');
304
- } else {
305
- promptTemplate = `你是一个软件工程专家团队。请按以下流程执行:
306
-
307
- ## 项目上下文
308
- {git_status}
309
- {recent_commits}
310
- {directory_tree}
311
-
312
- ## 要求
313
- 1. 分析项目当前状态
314
- 2. 找出问题
315
- 3. 给出改进建议`;
316
- }
317
- const prompt = promptTemplate.replace('{git_status}', gitStatus.trim()).replace('{recent_commits}', recentCommits.trim()).replace('{directory_tree}', dirTree.trim());
318
410
  const agent = registry.findAgent('opencode');
319
411
  if (!agent) return '❌ OpenCode agent not found';
320
412
  const available = await agent.isAvailable().catch(() => false);
321
413
  if (!available) return '❌ OpenCode 不可用';
322
- const response = await agent.sendPrompt(ctx.threadId || 'expert-review', prompt, []);
414
+ const response = await agent.sendPrompt(ctx.threadId || 'expert-review', EXPERT_SYSTEM_PROMPT + '\n\n用户问题:' + (parsed.arg || '请评审当前项目'), []);
323
415
  return response || '无响应';
324
416
  }
325
417
 
@@ -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 } from '../core/router.js';
6
+ import { COMMAND_ALIASES, detectCommand, getHelpText } 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';
@@ -76,40 +76,7 @@ async function handleCommand(adapter, ctx, command, arg, openCodeSessions) {
76
76
  return true;
77
77
  }
78
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!`);
79
+ await adapter.reply(ctx.threadId, getHelpText());
113
80
  return true;
114
81
  case 'agents': {
115
82
  const agents = registry.listAgents();
@@ -2,46 +2,36 @@ import { getOrCreateSession } from '../core/session.js';
2
2
  import { splitMessage } from '../core/notifications.js';
3
3
  import { initOpenCode, createSession, sendMessage, checkConnection, resumeSession, shareSession } from '../opencode/client.js';
4
4
  import { isAuthorized, hasOwner } from '../core/auth.js';
5
- import { detectCommand } from '../core/router.js';
5
+ import { detectCommand, EXPERT_SYSTEM_PROMPT } from '../core/router.js';
6
6
  import { handleCommand, formatTimeAgo } from './commands.js';
7
7
  import { existsSync, readFileSync } from 'fs';
8
8
  import { join } from 'path';
9
9
 
10
- const EXPERT_SYSTEM_PROMPT = `你是一个专家角色扮演系统,严格按照 AGENTS.md 中的"专家点评系统"流程执行。
11
-
12
- 当用户输入包含触发词(z / 叫全部专家 / 叫所有专家 / 呼叫专家点评 / 专家点评 / 专家意见 / call all experts / expert review)时,启动专家评审。
13
-
14
- ## 规则
15
- - 严格遵循 AGENTS.md 中定义的 13 位角色和点评流程
16
- - 言辞必须苛刻犀利,不讨好不委婉
17
- - 不说客套话
18
- - 直接指出问题`;
19
-
20
10
  async function handleMessage(adapter, ctx, text, openCodeSessions) {
21
11
  const session = await getOrCreateSession(ctx.threadId, 'feishu');
22
12
 
13
+ const expertTriggers = ['z', 'Z', '叫全部专家', '叫所有专家', '呼叫专家点评', '专家点评', '专家意见', 'call all experts', 'expert review', '专家会诊', '团队评审', '代码审查', '全员review', 'review all', '请专家', '叫专家', '找专家'];
14
+ let expertPrompt = null;
15
+
23
16
  if (text.startsWith('/z')) {
24
17
  const arg = text.slice(2).trim();
25
18
  if (arg === 'off' || arg === 'reset' || arg === '关闭') {
26
- session.expertMode = false;
27
- session.systemPrompt = null;
28
- await adapter.reply(ctx.threadId, '⏹️ 专家模式已关闭');
19
+ await adapter.reply(ctx.threadId, '⏹️ 自定义 prompt 已清除');
29
20
  return;
30
21
  }
31
22
  if (arg) {
32
- session.expertMode = true;
33
- session.systemPrompt = arg;
34
- await adapter.reply(ctx.threadId, `✅ 自定义专家 prompt 已设置 (${arg.length}字)`);
35
- return;
36
- }
37
- if (!session.expertMode) {
38
- session.expertMode = true;
39
- session.systemPrompt = EXPERT_SYSTEM_PROMPT;
23
+ expertPrompt = arg;
24
+ } else {
25
+ expertPrompt = EXPERT_SYSTEM_PROMPT;
40
26
  }
41
- await adapter.reply(ctx.threadId, '✅ 专家模式已启动,直接发送你的问题\n/z off — 关闭\n/z <内容> — 自定义 prompt');
27
+ await forwardToOpenCode(adapter, ctx, text, openCodeSessions, session, expertPrompt);
42
28
  return;
43
29
  }
44
30
 
31
+ if (expertTriggers.some(t => text.trim().toLowerCase().includes(t))) {
32
+ expertPrompt = EXPERT_SYSTEM_PROMPT;
33
+ }
34
+
45
35
  const parsed = detectCommand(text);
46
36
  if (parsed) {
47
37
  await handleCommand(adapter, ctx, parsed.name, parsed.arg, openCodeSessions);
@@ -222,7 +212,7 @@ async function handleMessage(adapter, ctx, text, openCodeSessions) {
222
212
  await forwardToOpenCode(adapter, ctx, text, openCodeSessions, session);
223
213
  }
224
214
 
225
- async function forwardToOpenCode(adapter, ctx, text, openCodeSessions, session) {
215
+ async function forwardToOpenCode(adapter, ctx, text, openCodeSessions, session, expertPrompt) {
226
216
  await adapter.sendTypingIndicator(ctx.threadId);
227
217
  let openCodeSession = openCodeSessions.get(ctx.threadId);
228
218
  if (!openCodeSession) {
@@ -258,8 +248,8 @@ async function forwardToOpenCode(adapter, ctx, text, openCodeSessions, session)
258
248
  }
259
249
  }
260
250
 
261
- if (session.expertMode && session.systemPrompt) {
262
- scopedText = `${session.systemPrompt}\n\n${scopedText}`;
251
+ if (expertPrompt) {
252
+ scopedText = `${expertPrompt}\n\n${scopedText}`;
263
253
  }
264
254
 
265
255
  console.log(`📤 Forwarding to OpenCode: ${text.substring(0, 80)}...`);
@@ -268,6 +258,7 @@ async function forwardToOpenCode(adapter, ctx, text, openCodeSessions, session)
268
258
  let hasToolActivity = false;
269
259
  let toolCallCount = 0;
270
260
  let response = await sendMessage(openCodeSession, scopedText, {
261
+ idleThreshold: expertPrompt ? 30 : 10,
271
262
  onEvent: (event) => {
272
263
  if (event.type === 'tool.call') {
273
264
  const props = event.properties || {};