@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/weixin/handler.js
CHANGED
|
@@ -5,19 +5,9 @@ import { isAuthorized, hasOwner } from '../core/auth.js';
|
|
|
5
5
|
import { registry } from '../core/registry.js';
|
|
6
6
|
import { sendMessage as sendWeixinMessage } from './api.js';
|
|
7
7
|
import { randomBytes } from 'crypto';
|
|
8
|
-
import { detectCommand } from '../core/router.js';
|
|
8
|
+
import { detectCommand, EXPERT_SYSTEM_PROMPT, startTypingPing } from '../core/router.js';
|
|
9
9
|
import { handleCommand, formatTimeAgo, _registerStartLoopCycle } from './commands.js';
|
|
10
10
|
|
|
11
|
-
const EXPERT_SYSTEM_PROMPT = `你是一个专家角色扮演系统,严格按照 AGENTS.md 中的"专家点评系统"流程执行。
|
|
12
|
-
|
|
13
|
-
当用户输入包含触发词(z / 叫全部专家 / 叫所有专家 / 呼叫专家点评 / 专家点评 / 专家意见 / call all experts / expert review)时,启动专家评审。
|
|
14
|
-
|
|
15
|
-
## 规则
|
|
16
|
-
- 严格遵循 AGENTS.md 中定义的 13 位角色和点评流程
|
|
17
|
-
- 言辞必须苛刻犀利,不讨好不委婉
|
|
18
|
-
- 不说客套话
|
|
19
|
-
- 直接指出问题`;
|
|
20
|
-
|
|
21
11
|
async function startLoopCycle(adapter, ctx, openCodeSessions, session) {
|
|
22
12
|
if (!session.loopMode) return;
|
|
23
13
|
|
|
@@ -61,92 +51,84 @@ async function startLoopCycle(adapter, ctx, openCodeSessions, session) {
|
|
|
61
51
|
|
|
62
52
|
_registerStartLoopCycle(startLoopCycle);
|
|
63
53
|
|
|
64
|
-
async function forwardToOpenCode(adapter, ctx, text, openCodeSessions, session) {
|
|
54
|
+
async function forwardToOpenCode(adapter, ctx, text, openCodeSessions, session, expertPrompt) {
|
|
65
55
|
adapter.sendTypingIndicator(ctx.threadId).catch(() => {});
|
|
66
|
-
let openCodeSession =
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
}
|
|
71
|
-
if (!openCodeSession && globalThis.__latestOpenCodeSession?.id) {
|
|
72
|
-
console.log('Connecting to latest OpenCode session...');
|
|
73
|
-
openCodeSession = await resumeSession(globalThis.__latestOpenCodeSession.id);
|
|
74
|
-
}
|
|
56
|
+
let openCodeSession = null;
|
|
57
|
+
|
|
58
|
+
// 专家评审每次开独立会话,避免旧历史干扰
|
|
59
|
+
if (expertPrompt) {
|
|
60
|
+
openCodeSession = await createSession(`expert-${Date.now()}`, `专家评审 ${Date.now()}`);
|
|
75
61
|
if (!openCodeSession) {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
if (saved?.opencodeSessionId) {
|
|
79
|
-
openCodeSession = await resumeSession(saved.opencodeSessionId);
|
|
80
|
-
}
|
|
62
|
+
await adapter.reply(ctx.threadId, '❌ 无法创建评审会话');
|
|
63
|
+
return;
|
|
81
64
|
}
|
|
65
|
+
console.log(`✅ 新建评审会话: ${openCodeSession.sessionId.slice(0, 8)}`);
|
|
66
|
+
} else {
|
|
67
|
+
openCodeSession = openCodeSessions.get(ctx.threadId);
|
|
82
68
|
if (!openCodeSession) {
|
|
83
|
-
|
|
84
|
-
openCodeSession = await
|
|
69
|
+
if (session.opencodeSessionId) openCodeSession = await resumeSession(session.opencodeSessionId);
|
|
70
|
+
if (!openCodeSession && globalThis.__latestOpenCodeSession?.id) openCodeSession = await resumeSession(globalThis.__latestOpenCodeSession.id);
|
|
85
71
|
if (!openCodeSession) {
|
|
86
|
-
|
|
87
|
-
|
|
72
|
+
const mapping = loadSessionMapping();
|
|
73
|
+
if (mapping[ctx.threadId]?.opencodeSessionId) openCodeSession = await resumeSession(mapping[ctx.threadId].opencodeSessionId);
|
|
74
|
+
}
|
|
75
|
+
if (!openCodeSession) {
|
|
76
|
+
openCodeSession = await createSession(ctx.threadId, `Weixin ${ctx.threadId}`);
|
|
77
|
+
if (!openCodeSession) { await adapter.reply(ctx.threadId, '❌ 无法创建 OpenCode 会话'); return; }
|
|
88
78
|
}
|
|
89
|
-
|
|
79
|
+
openCodeSessions.set(ctx.threadId, openCodeSession);
|
|
80
|
+
session.opencodeSessionId = openCodeSession.sessionId;
|
|
81
|
+
saveSessionMapping();
|
|
90
82
|
}
|
|
91
|
-
openCodeSessions.set(ctx.threadId, openCodeSession);
|
|
92
|
-
session.opencodeSessionId = openCodeSession.sessionId;
|
|
93
|
-
const key = `weixin:${ctx.threadId}:${ctx.threadId}`;
|
|
94
|
-
sessionManager.saveSession(key, session).catch(() => {});
|
|
95
|
-
saveSessionMapping();
|
|
96
83
|
}
|
|
97
84
|
|
|
98
|
-
if (session.modelOverride)
|
|
99
|
-
openCodeSession.model = session.modelOverride;
|
|
100
|
-
}
|
|
85
|
+
if (session.modelOverride) openCodeSession.model = session.modelOverride;
|
|
101
86
|
|
|
102
87
|
session.taskStartTime = Date.now();
|
|
103
88
|
session.currentTool = null;
|
|
104
|
-
|
|
105
|
-
let lastToolNotified = '';
|
|
106
89
|
const stopHeartbeat = () => {
|
|
107
|
-
session.taskStartTime = null;
|
|
108
|
-
session.
|
|
109
|
-
if (session.modifiedFiles instanceof Set) {
|
|
110
|
-
session.modifiedFiles = Array.from(session.modifiedFiles);
|
|
111
|
-
}
|
|
90
|
+
session.taskStartTime = null; session.currentTool = null;
|
|
91
|
+
if (session.modifiedFiles instanceof Set) session.modifiedFiles = Array.from(session.modifiedFiles);
|
|
112
92
|
};
|
|
113
93
|
|
|
114
94
|
console.log(`📤 Message sent: → ${text}`);
|
|
115
|
-
|
|
116
95
|
const projectDir = session.projectDir || globalThis.__autoProjectDir;
|
|
117
96
|
|
|
118
97
|
let scopedText = text;
|
|
119
|
-
if (session._contextScope) {
|
|
120
|
-
|
|
121
|
-
}
|
|
98
|
+
if (session._contextScope) scopedText = `[上下文范围: ${session._contextScope}]\n\n${text}`;
|
|
99
|
+
if (projectDir && !scopedText.includes('项目目录')) scopedText = `[当前项目目录: ${projectDir}]\n\n${scopedText}`;
|
|
100
|
+
if (expertPrompt) scopedText = `${expertPrompt}\n\n${scopedText}`;
|
|
122
101
|
|
|
123
|
-
if (projectDir && !scopedText.includes('项目目录')) {
|
|
124
|
-
scopedText = `[当前项目目录: ${projectDir}]\n\n${scopedText}`;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
if (session.expertMode && session.systemPrompt) {
|
|
128
|
-
scopedText = `${session.systemPrompt}\n\n${scopedText}`;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
let response = '';
|
|
132
102
|
let hasToolActivity = false;
|
|
133
|
-
|
|
103
|
+
let toolCount = 0;
|
|
104
|
+
|
|
105
|
+
const typingPing = startTypingPing(adapter, ctx.threadId);
|
|
134
106
|
|
|
107
|
+
const replyWithTyping = async (text) => {
|
|
108
|
+
const snippet = text.length > 80 ? text.slice(0, 80) + '...' : text;
|
|
109
|
+
console.log(`[→发送] ${snippet}`);
|
|
110
|
+
try { await adapter.reply(ctx.threadId, text); } catch (e) { console.error('[→发送] 失败:', e.message); }
|
|
111
|
+
adapter.sendTypingIndicator(ctx.threadId).catch(() => {});
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
let contentSent = false;
|
|
135
115
|
const result = await sendToOpenCode(openCodeSession, scopedText, {
|
|
116
|
+
idleThreshold: expertPrompt ? 30 : 10,
|
|
117
|
+
onNewContent: (delta) => {
|
|
118
|
+
const trimmed = delta.trim();
|
|
119
|
+
if (trimmed) { replyWithTyping(trimmed); typingPing.poke(); contentSent = true; }
|
|
120
|
+
},
|
|
136
121
|
onEvent: (event) => {
|
|
137
122
|
if (event.type === 'tool.call') {
|
|
138
123
|
const props = event.properties || {};
|
|
139
124
|
const toolName = props.name || props.tool_name || 'unknown';
|
|
140
125
|
const input = props.input || {};
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
adapter.reply(ctx.threadId, toolDesc).catch(() => {});
|
|
149
|
-
|
|
126
|
+
hasToolActivity = true; toolCount++;
|
|
127
|
+
let toolDesc = `🔧 ${toolName}`;
|
|
128
|
+
if (input.path) toolDesc += ` 📁${input.path}`;
|
|
129
|
+
if (input.command) toolDesc += ` 💻${input.command}`;
|
|
130
|
+
replyWithTyping(toolDesc);
|
|
131
|
+
typingPing.poke();
|
|
150
132
|
if (input.path) {
|
|
151
133
|
if (!session.modifiedFiles) session.modifiedFiles = new Set();
|
|
152
134
|
session.modifiedFiles.add(input.path);
|
|
@@ -162,29 +144,20 @@ async function forwardToOpenCode(adapter, ctx, text, openCodeSessions, session)
|
|
|
162
144
|
});
|
|
163
145
|
|
|
164
146
|
stopHeartbeat();
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
const responseMsgs = splitMessage(trimmedResponse);
|
|
181
|
-
for (const m of responseMsgs) {
|
|
182
|
-
const trimmed = m.trim();
|
|
183
|
-
if (!trimmed) continue;
|
|
184
|
-
try {
|
|
185
|
-
await adapter.reply(ctx.threadId, m);
|
|
186
|
-
} catch (replyErr) {
|
|
187
|
-
console.error('[forwardToOpenCode] reply failed:', replyErr.message);
|
|
147
|
+
typingPing.done();
|
|
148
|
+
|
|
149
|
+
// onNewContent 已发过内容就不兜底了,避免重复
|
|
150
|
+
if (!contentSent) {
|
|
151
|
+
const finalText = (result || '').trim();
|
|
152
|
+
if (finalText && !finalText.startsWith('⏰') && !finalText.startsWith('❌')) {
|
|
153
|
+
const msgs = splitMessage(finalText);
|
|
154
|
+
for (const m of msgs) {
|
|
155
|
+
if (m.trim()) adapter.reply(ctx.threadId, m).catch(e => console.error('[reply] 兜底失败:', e.message));
|
|
156
|
+
}
|
|
157
|
+
} else if (finalText) {
|
|
158
|
+
adapter.reply(ctx.threadId, finalText).catch(e => console.error('[reply] 兜底失败:', e.message));
|
|
159
|
+
} else {
|
|
160
|
+
adapter.reply(ctx.threadId, '⚠️ AI 返回为空(可能是超时),请重试或 /diagnose').catch(e => console.error('[reply] 失败:', e.message));
|
|
188
161
|
}
|
|
189
162
|
}
|
|
190
163
|
|
|
@@ -197,7 +170,7 @@ async function forwardToOpenCode(adapter, ctx, text, openCodeSessions, session)
|
|
|
197
170
|
await sendWeixinMessage({
|
|
198
171
|
baseUrl: adapter._baseUrl,
|
|
199
172
|
token: adapter._token,
|
|
200
|
-
body: { msg: { from_user_id: adapter._botId, to_user_id: otherThreadId, client_id: `${Date.now()}-${randomBytes(8).toString('hex')}`, message_type: 2, message_state: 2, context_token: otherContextToken, item_list: [{ type: 1, text_item: { text: `[来自 ${ctx.threadId} 的会话]\n\n${
|
|
173
|
+
body: { msg: { from_user_id: adapter._botId, to_user_id: otherThreadId, client_id: `${Date.now()}-${randomBytes(8).toString('hex')}`, message_type: 2, message_state: 2, context_token: otherContextToken, item_list: [{ type: 1, text_item: { text: `[来自 ${ctx.threadId} 的会话]\n\n${result || ''}` } }] } }
|
|
201
174
|
});
|
|
202
175
|
} catch (e) {
|
|
203
176
|
console.error(`Failed to broadcast to ${otherThreadId}:`, e.message);
|
|
@@ -220,29 +193,29 @@ async function forwardToOpenCode(adapter, ctx, text, openCodeSessions, session)
|
|
|
220
193
|
async function handleMessage(adapter, ctx, text, openCodeSessions) {
|
|
221
194
|
const session = await getOrCreateSession(ctx.threadId, 'weixin');
|
|
222
195
|
|
|
223
|
-
const expertTriggers = ['z', '叫全部专家', '叫所有专家', '呼叫专家点评', '专家点评', '专家意见', 'call all experts', 'expert review'];
|
|
196
|
+
const expertTriggers = ['z', 'Z', '叫全部专家', '叫所有专家', '呼叫专家点评', '专家点评', '专家意见', 'call all experts', 'expert review', '专家会诊', '团队评审', '代码审查', '全员review', 'review all', '请专家', '叫专家', '找专家'];
|
|
224
197
|
const trimmedLower = text.trim().toLowerCase();
|
|
198
|
+
let expertPrompt = null;
|
|
225
199
|
if (text.startsWith('/z')) {
|
|
226
200
|
const arg = text.slice(2).trim();
|
|
227
201
|
if (arg === 'off' || arg === 'reset' || arg === '关闭') {
|
|
228
|
-
|
|
229
|
-
session.systemPrompt = null;
|
|
230
|
-
await adapter.reply(ctx.threadId, '⏹️ 专家模式已关闭');
|
|
202
|
+
await adapter.reply(ctx.threadId, '⏹️ 自定义 prompt 已清除');
|
|
231
203
|
return;
|
|
232
204
|
}
|
|
233
205
|
if (arg) {
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
if (!session.expertMode) {
|
|
240
|
-
session.expertMode = true;
|
|
241
|
-
session.systemPrompt = EXPERT_SYSTEM_PROMPT;
|
|
206
|
+
expertPrompt = arg;
|
|
207
|
+
await adapter.reply(ctx.threadId, `✅ 自定义专家 prompt (${arg.length}字),本消息生效`);
|
|
208
|
+
} else {
|
|
209
|
+
expertPrompt = EXPERT_SYSTEM_PROMPT;
|
|
210
|
+
await adapter.reply(ctx.threadId, '✅ 专家评审已启动');
|
|
242
211
|
}
|
|
243
|
-
|
|
212
|
+
// /z 直接走专家模式,不继续向下走
|
|
213
|
+
await forwardToOpenCode(adapter, ctx, text, openCodeSessions, session, expertPrompt);
|
|
244
214
|
return;
|
|
245
215
|
}
|
|
216
|
+
if (expertTriggers.some(t => trimmedLower.includes(t))) {
|
|
217
|
+
expertPrompt = EXPERT_SYSTEM_PROMPT;
|
|
218
|
+
}
|
|
246
219
|
|
|
247
220
|
const detected = detectCommand(text);
|
|
248
221
|
if (detected) {
|
|
@@ -371,7 +344,7 @@ async function handleMessage(adapter, ctx, text, openCodeSessions) {
|
|
|
371
344
|
session.currentTool = null;
|
|
372
345
|
session.modifiedFiles = null;
|
|
373
346
|
const key = `weixin:${ctx.userId}:${ctx.threadId}`;
|
|
374
|
-
|
|
347
|
+
sessionManager.saveSession(key, session).catch(e => console.error('[session] save failed:', e.message));
|
|
375
348
|
saveSessionMapping();
|
|
376
349
|
if (target.directory) {
|
|
377
350
|
session.projectDir = target.directory;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yvhitxcel/opencode-remote",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.16.1",
|
|
4
|
+
"description": "🤖 AI 专家团队随时待命!只需输入 /z,自动分析项目、诊断问题、给出改进方案。支持微信/飞书/Telegram 远程控制 OpenCode、Claude Code、Codex、Copilot。手机也能搞开发。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"opencode-remote": "bin/opencode-remote.js",
|
|
@@ -25,7 +25,6 @@
|
|
|
25
25
|
"dependencies": {
|
|
26
26
|
"@larksuiteoapi/node-sdk": "^1.59.0",
|
|
27
27
|
"@opencode-ai/sdk": "^1.2.27",
|
|
28
|
-
"express": "^5.2.1",
|
|
29
28
|
"grammy": "^1.30.0",
|
|
30
29
|
"qiniu": "^7.15.2",
|
|
31
30
|
"undici": "^7.24.5"
|