@yvhitxcel/opencode-remote 0.15.1 → 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.
- package/README.md +78 -8
- package/dist/core/auth.js +41 -108
- package/dist/core/notifications.js +11 -0
- package/dist/core/router.js +291 -61
- package/dist/feishu/commands.js +29 -35
- package/dist/feishu/handler.js +17 -26
- package/dist/opencode/client.js +48 -77
- package/dist/plugins/agents/claude-code/index.js +46 -4
- package/dist/telegram/adapter.js +75 -0
- package/dist/telegram/bot.js +66 -208
- package/dist/weixin/bot.js +12 -2
- package/dist/weixin/commands.js +29 -42
- package/dist/weixin/handler.js +80 -107
- package/package.json +2 -3
package/dist/core/router.js
CHANGED
|
@@ -1,13 +1,24 @@
|
|
|
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'],
|
|
7
19
|
help: ['help', 'h', '?'],
|
|
8
20
|
status: ['status'],
|
|
9
21
|
reset: ['reset'],
|
|
10
|
-
stop: ['stop'],
|
|
11
22
|
restart: ['restart'],
|
|
12
23
|
sessions: ['sessions', 'sw'],
|
|
13
24
|
delsessions: ['delsessions', 'del'],
|
|
@@ -25,7 +36,146 @@ export const COMMAND_ALIASES = {
|
|
|
25
36
|
copilot: ['copilot'],
|
|
26
37
|
agents: ['agents'],
|
|
27
38
|
model: ['model'],
|
|
28
|
-
expert: ['expert', 'z', 'review'],
|
|
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步',
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export const EXPERT_SYSTEM_PROMPT = `你是一个专家评审系统。用户消息含触发词(z/c/叫全部专家/专家点评)时启动评审,前后可带具体问题则聚焦该问题。
|
|
71
|
+
|
|
72
|
+
## 角色(14 位)
|
|
73
|
+
1. 架构师 — 代码架构、模块划分、依赖管理
|
|
74
|
+
2. 后端工程师 — 稳定性、错误处理、性能
|
|
75
|
+
3. 测试工程师 — 测试覆盖、可测性
|
|
76
|
+
4. VC / 投资人 — 值不值得投
|
|
77
|
+
5. 开源社区经理 — 新人能不能上车
|
|
78
|
+
6. Flutter 开发者 — 移动端好不好用
|
|
79
|
+
7. SRE / 运维 — 能不能上线
|
|
80
|
+
8. 安全研究员 — 有没有洞
|
|
81
|
+
9. AI 研究员 — agent loop 质量
|
|
82
|
+
10. 用户支持 — 用户卡在哪
|
|
83
|
+
11. 技术写作者 — 文档好不好写
|
|
84
|
+
12. 竞品分析师 — 市场定位
|
|
85
|
+
13. Git 专家 — commit 质量、分支管理、历史整洁度、回滚安全
|
|
86
|
+
14. **技术经理(最后出场)** — 汇总以上 13 位意见,给出 P0-P2 分级的可执行任务清单
|
|
87
|
+
|
|
88
|
+
## 执行流程
|
|
89
|
+
|
|
90
|
+
### 第 1 步:1-13 号专家点评
|
|
91
|
+
每人最多 200 字。**言辞必须苛刻、犀利、严谨。不讨好,不委婉。不投票。**
|
|
92
|
+
- 如果某条意见在上轮已提过但未修复,必须指出"上轮已提过,未落实"
|
|
93
|
+
- 格式: \`[意见] 内容 / [上轮已提: N]\`
|
|
94
|
+
|
|
95
|
+
### 第 2 步:技术经理提出 15 条问题
|
|
96
|
+
基于以上 13 位意见,输出 1) 2) ... 15) 清单。
|
|
97
|
+
- **1-5 为 P0(红色🔴)**,6-10 为 P1(黄色🟡),11-15 为 P2(蓝色🔵)
|
|
98
|
+
- 每条包含一句话描述(做什么),不展开
|
|
99
|
+
- 小 bug 合并同类项,一条 = 一个可执行动作
|
|
100
|
+
- **每条必须标注对应的专家意见编号**(格式: \`1) xxx [来源: 3,7,11]\`)
|
|
101
|
+
- **不含投票结果。不含投票结果。不含投票结果。**
|
|
102
|
+
|
|
103
|
+
### 第 3 步:1-13 号专家投票
|
|
104
|
+
每人从 15 条中选 3 条最关键的。格式:\`-> 三票:1, 7, 12\`
|
|
105
|
+
- **投票时必须检查自己上轮的意见是否已落实,如未落实则优先投票给相关条目**
|
|
106
|
+
|
|
107
|
+
### 第 4 步:技术经理公布结果
|
|
108
|
+
得票统计,汇入 P0-P2,标记得票数。
|
|
109
|
+
- **P0 🔴 1) xxx ( 票)[来源: 3,7] — 说明**
|
|
110
|
+
- **P1 🟡 7) xxx ( 票)[来源: 1,5] — 说明**
|
|
111
|
+
|
|
112
|
+
### 第 5 步:自动执行
|
|
113
|
+
对 P0(票数 ≥ 3)按得票从高到低逐个自动执行修复。
|
|
114
|
+
|
|
115
|
+
## 三级质量保障(嵌入评审全程)
|
|
116
|
+
|
|
117
|
+
### 1. 脑内执行路径追踪
|
|
118
|
+
在评审和修复代码时,**在脑中逐条执行关键路径**:变量怎么赋值、条件怎么分支、循环怎么迭代、异常怎么传播。不只看代码静态结构,要模拟运行时行为。发现逻辑断点、边界遗漏、状态覆盖不全立即提出。
|
|
119
|
+
|
|
120
|
+
### 2. 服务端模拟验证
|
|
121
|
+
对评审中发现的 P0/P1 问题,在修复后**自动执行模拟验证**:
|
|
122
|
+
- 跑 \`npm run lint\` 检查语法和模块图
|
|
123
|
+
- 跑 \`node --test\` 验证单元测试
|
|
124
|
+
- 对修改的文件做 \`node --check\` 语法校验
|
|
125
|
+
- 如果项目有 CI 脚本,触发本地等效检查
|
|
126
|
+
- 验证结果写入执行总结
|
|
127
|
+
|
|
128
|
+
### 3. LLM 对抗评审
|
|
129
|
+
修复完成后,**以对抗视角重新审视自己的修改**:"这段修改有没有引入新 bug?有没有遗漏边界情况?变更是否最小?会不会破坏现有功能?"
|
|
130
|
+
- 如果发现自己的修改有问题→回退重做
|
|
131
|
+
- 如果确认无误→在总结中标注"已通过对抗评审"
|
|
132
|
+
|
|
133
|
+
## 核心三要素
|
|
134
|
+
1. **吃透代码再动手** — 不靠猜测修复 bug
|
|
135
|
+
2. **追到根因** — 用户反馈的现象要追到代码根因
|
|
136
|
+
3. **改完必须验证** — 先跑测试跑 lint 再交付
|
|
137
|
+
|
|
138
|
+
## 四项基本原则(硬约束)
|
|
139
|
+
|
|
140
|
+
1. **一次性做好,不重复返工** — 同一个模块的同类问题最多修两轮。第三轮还提同类问题说明方案不对,技术经理必须输出"换方案"而非"继续修"。
|
|
141
|
+
|
|
142
|
+
2. **以用户价值为导向** — P0 排序:用户能不能跑起来 > 会不会崩 > 好不好用。内部代码质量默认不进 P0。
|
|
143
|
+
|
|
144
|
+
3. **对用户友好** — 每次评审必须包含"首次使用视角"。P0 必须至少有一条直接回应首次使用视角。**若没有,整轮评审无效。**
|
|
145
|
+
|
|
146
|
+
4. **快速迭代,先上线** — R4 开始技术经理必须回答"是否可以发布?最短路径是什么?" 是则只保留阻塞发布的问题。
|
|
147
|
+
|
|
148
|
+
## 规则
|
|
149
|
+
- 言辞苛刻犀利,不讨好,不委婉
|
|
150
|
+
- 节约 token:摘要不超过 400 字,不贴源码
|
|
151
|
+
- 上轮已提过且已修的问题本轮不得再提(除非验收不合格)
|
|
152
|
+
- 技术经理的 15 条必须标注来源,**没有对应来源的问题不得出现**`;
|
|
153
|
+
|
|
154
|
+
const COMMAND_HELP = {
|
|
155
|
+
start: '认领所有权',
|
|
156
|
+
help: '显示帮助',
|
|
157
|
+
status: '连接状态',
|
|
158
|
+
reset: '重置会话',
|
|
159
|
+
restart: '重启 Bot',
|
|
160
|
+
sessions: '浏览会话',
|
|
161
|
+
delsessions: '删除会话',
|
|
162
|
+
loop: '循环任务',
|
|
163
|
+
edit: '编辑消息',
|
|
164
|
+
diagnose: '系统诊断',
|
|
165
|
+
refresh: '刷新上下文',
|
|
166
|
+
copy: '复制回复',
|
|
167
|
+
revert: '撤销消息',
|
|
168
|
+
upload: '上传文件',
|
|
169
|
+
delete: '删除上传文件',
|
|
170
|
+
oc: '使用 OpenCode',
|
|
171
|
+
cc: '使用 Claude Code',
|
|
172
|
+
cx: '使用 Codex',
|
|
173
|
+
copilot: '使用 Copilot',
|
|
174
|
+
agents: '查看 Agent',
|
|
175
|
+
model: '切换模型',
|
|
176
|
+
expert: '专家评审(z/叫全部专家)',
|
|
177
|
+
tutorial: '交互式教程(step-by-step 上手)',
|
|
178
|
+
demo: '沙箱模式(无需配置体验全部命令)',
|
|
29
179
|
};
|
|
30
180
|
|
|
31
181
|
const COMMAND_MAP = {};
|
|
@@ -35,6 +185,46 @@ for (const [cmd, aliases] of Object.entries(COMMAND_ALIASES)) {
|
|
|
35
185
|
}
|
|
36
186
|
}
|
|
37
187
|
|
|
188
|
+
/**
|
|
189
|
+
* 保持 typing 指示器常亮。每 8 秒刷新一次,30 秒无活动自熄。
|
|
190
|
+
* 调用 poke 刷新计时,done 手动关闭。
|
|
191
|
+
* 放在 core 层避免被各平台 handler 误删。
|
|
192
|
+
*/
|
|
193
|
+
export function startTypingPing(adapter, threadId) {
|
|
194
|
+
let lastActivity = Date.now();
|
|
195
|
+
const timer = setInterval(() => {
|
|
196
|
+
adapter.sendTypingIndicator(threadId).catch(() => {});
|
|
197
|
+
if (Date.now() - lastActivity > 30000) clearInterval(timer);
|
|
198
|
+
}, 3000);
|
|
199
|
+
return {
|
|
200
|
+
poke: () => { lastActivity = Date.now(); },
|
|
201
|
+
done: () => { clearInterval(timer); },
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export function getHelpText() {
|
|
206
|
+
const lines = ['📖 指令\n'];
|
|
207
|
+
const groups = [
|
|
208
|
+
['🟢 常用', ['start', 'help', 'status', 'reset', 'copy', 'revert', 'diagnose']],
|
|
209
|
+
['🔄 任务', ['loop', 'refresh', 'restart']],
|
|
210
|
+
['📂 会话', ['sessions', 'delsessions']],
|
|
211
|
+
['🤖 AI', ['model', 'agents', 'oc', 'cc']],
|
|
212
|
+
['⬆️ 文件', ['upload', 'delete']],
|
|
213
|
+
['🧠 专家', ['expert']],
|
|
214
|
+
];
|
|
215
|
+
for (const [title, cmds] of groups) {
|
|
216
|
+
lines.push(title);
|
|
217
|
+
for (const cmd of cmds) {
|
|
218
|
+
const aliases = COMMAND_ALIASES[cmd];
|
|
219
|
+
const aliasStr = aliases.length > 1 ? ` (${aliases.slice(1).join(', ')})` : '';
|
|
220
|
+
lines.push(` /${cmd}${aliasStr} — ${COMMAND_HELP[cmd] || cmd}`);
|
|
221
|
+
}
|
|
222
|
+
lines.push('');
|
|
223
|
+
}
|
|
224
|
+
lines.push('💬 直接发消息给 AI!');
|
|
225
|
+
return lines.join('\n');
|
|
226
|
+
}
|
|
227
|
+
|
|
38
228
|
export function detectCommand(text) {
|
|
39
229
|
const trimmed = text.trim();
|
|
40
230
|
if (trimmed === 'h' || trimmed === '?') {
|
|
@@ -101,38 +291,106 @@ async function getSessionMessages(sessionId) {
|
|
|
101
291
|
return result.data || [];
|
|
102
292
|
}
|
|
103
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
|
+
|
|
104
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
|
+
}
|
|
105
359
|
switch (parsed.type) {
|
|
106
360
|
case 'command': {
|
|
107
361
|
switch (parsed.command) {
|
|
108
362
|
case 'help':
|
|
109
|
-
return
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
/model
|
|
131
|
-
/
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
363
|
+
return getHelpText();
|
|
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
|
+
}
|
|
136
394
|
|
|
137
395
|
case 'agents': {
|
|
138
396
|
const agents = registry.listAgents();
|
|
@@ -165,9 +423,6 @@ export async function routeMessage(parsed, ctx) {
|
|
|
165
423
|
case 'restart':
|
|
166
424
|
return '🔄 重启信号已发送,bot 即将重启...';
|
|
167
425
|
|
|
168
|
-
case 'stop':
|
|
169
|
-
return '🛑 停止信号已发送';
|
|
170
|
-
|
|
171
426
|
case 'sessions': {
|
|
172
427
|
const sessions = await getSessionsList();
|
|
173
428
|
if (!sessions || sessions.length === 0) return '📭 暂无会话';
|
|
@@ -288,39 +543,14 @@ export async function routeMessage(parsed, ctx) {
|
|
|
288
543
|
return ctx.opencodeSessionId ? '✏️ 用法: /edit <消息编号>' : '❌ 没有活跃的会话';
|
|
289
544
|
|
|
290
545
|
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
546
|
const agent = registry.findAgent('opencode');
|
|
319
547
|
if (!agent) return '❌ OpenCode agent not found';
|
|
320
548
|
const available = await agent.isAvailable().catch(() => false);
|
|
321
549
|
if (!available) return '❌ OpenCode 不可用';
|
|
322
|
-
const
|
|
323
|
-
|
|
550
|
+
const taskStart = Date.now();
|
|
551
|
+
const response = await agent.sendPrompt(ctx.threadId || 'expert-review', EXPERT_SYSTEM_PROMPT + '\n\n用户问题:' + (parsed.arg || '请评审当前项目'), []);
|
|
552
|
+
const notification = response ? '' : `\n\n${formatTaskCompletion('专家评审', taskStart)}`;
|
|
553
|
+
return (response || '无响应') + notification;
|
|
324
554
|
}
|
|
325
555
|
|
|
326
556
|
default:
|
package/dist/feishu/commands.js
CHANGED
|
@@ -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, 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';
|
|
@@ -76,41 +76,20 @@ 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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
/
|
|
89
|
-
|
|
90
|
-
|
|
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());
|
|
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);
|
|
113
91
|
return true;
|
|
92
|
+
}
|
|
114
93
|
case 'agents': {
|
|
115
94
|
const agents = registry.listAgents();
|
|
116
95
|
const lines = ['🤖 可用 AI Agent:'];
|
|
@@ -581,6 +560,21 @@ async function handleCommand(adapter, ctx, command, arg, openCodeSessions) {
|
|
|
581
560
|
|
|
582
561
|
|
|
583
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
|
+
|
|
584
578
|
case 'diagnose': {
|
|
585
579
|
const { checkConnection } = await import('../opencode/client.js');
|
|
586
580
|
const diag = ['🔍 诊断报告\n'];
|
package/dist/feishu/handler.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
|
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 (
|
|
262
|
-
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 || {};
|