@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.
@@ -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 = openCodeSessions.get(ctx.threadId);
67
- if (!openCodeSession) {
68
- if (session.opencodeSessionId) {
69
- openCodeSession = await resumeSession(session.opencodeSessionId);
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
- const mapping = loadSessionMapping();
77
- const saved = mapping[ctx.threadId];
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
- console.log('Creating new WeChat session...');
84
- openCodeSession = await createSession(ctx.threadId, `Weixin ${ctx.threadId}`);
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
- await adapter.reply(ctx.threadId, '❌ 无法创建 OpenCode 会话');
87
- return;
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
- console.log(`✅ Created new WeChat session: ${openCodeSession.sessionId}`);
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.currentTool = null;
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
- scopedText = `[上下文范围: ${session._contextScope}]\n\n${text}`;
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
- const IDLE_TIMEOUT = session.expertMode ? 120000 : 30000;
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
- hasToolActivity = true;
143
-
144
- let toolDesc = `🔧 执行工具: ${toolName}`;
145
- if (input.path) toolDesc += `\n📁 ${input.path}`;
146
- if (input.command) toolDesc += `\n💻 ${input.command}`;
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
- const trimmedResponse = (result || response).trim();
167
- if (!trimmedResponse) {
168
- console.error(`[forwardToOpenCode] Empty response (race: ${whoWon})`);
169
- const msg = whoWon === 'idle' ? '⏰ OpenCode 响应超时,请重试' : 'AI 返回空响应,请重试';
170
- await adapter.reply(ctx.threadId, msg);
171
- return;
172
- }
173
-
174
- if (trimmedResponse.startsWith('⏰') || trimmedResponse.startsWith('❌')) {
175
- console.error('[forwardToOpenCode] Error response:', trimmedResponse);
176
- await adapter.reply(ctx.threadId, trimmedResponse);
177
- return;
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${response}` } }] } }
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
- session.expertMode = false;
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
- session.expertMode = true;
235
- session.systemPrompt = arg;
236
- await adapter.reply(ctx.threadId, `✅ 自定义专家 prompt 已设置 (${arg.length}字)`);
237
- return;
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
- await adapter.reply(ctx.threadId, '✅ 专家模式已启动,直接发送你的问题\n/z off — 关闭\n/z <内容> — 自定义 prompt');
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
- sessionManager.saveSession(key, session).catch(() => {});
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.15.1",
4
- "description": "通过微信/飞书/Telegram 远程控制 AI 编码助手(OpenCode、Claude Code、Codex、Copilot),随时随地审代码、提需求、查状态",
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"