@yvhitxcel/opencode-remote 0.17.0 → 0.18.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.
@@ -1,17 +1,6 @@
1
- import { splitMessage } from '../core/notifications.js';
2
- import { createSession, sendMessage as sendToOpenCode, checkConnection, shareSession, listOpenCodeSessions, resumeSession } from '../opencode/client.js';
3
- import { isAuthorized, hasOwner } from '../core/auth.js';
4
-
5
-
6
- import { detectCommand, EXPERT_SYSTEM_PROMPT, startTypingPing } from '../core/router.js';
7
- import { handleCommand, sharedRoom, isSharedMember, threadAgent } from './commands.js';
1
+ import { createHandler } from '../core/handler.js';
2
+ import { handleCommand, sharedRoom, isSharedMember } from './commands.js';
8
3
  import { userAdapterMap } from './user-adapter-map.js';
9
- import { hasPendingDecision, resolveDecision } from '../autonomous/decisions.js';
10
- import { registry } from '../core/registry.js';
11
-
12
- const IDLE_MODEL_HINT_MS = 5 * 60 * 1000;
13
- const threadLastActive = new Map();
14
- const threadHistory = new Map();
15
4
 
16
5
  function replyTo(userId, text, fallbackAdapter) {
17
6
  const a = userAdapterMap.get(userId) || fallbackAdapter;
@@ -26,9 +15,7 @@ function wrapAdapterForShared(adapter, senderId) {
26
15
  const result = await target.reply(threadId, msg).catch(() => {});
27
16
  if (threadId === senderId) {
28
17
  for (const tid of sharedRoom.members) {
29
- if (tid !== senderId) {
30
- replyTo(tid, msg, adapter).catch(() => {});
31
- }
18
+ if (tid !== senderId) replyTo(tid, msg, adapter).catch(() => {});
32
19
  }
33
20
  }
34
21
  return result;
@@ -37,262 +24,13 @@ function wrapAdapterForShared(adapter, senderId) {
37
24
  });
38
25
  }
39
26
 
40
- async function forwardToOpenCode(adapter, ctx, text, openCodeSessions, expertPrompt) {
41
- adapter.sendTypingIndicator(ctx.threadId).catch(() => {});
42
- let openCodeSession = null;
43
- let pendingModelHint = null;
44
-
45
- // 共享模式:使用共享会话
46
- const isShared = isSharedMember(ctx.threadId);
47
- if (isShared) {
48
- if (sharedRoom.busy) {
49
- await adapter.reply(ctx.threadId, '⏳ 当前有人在用,请稍等...');
50
- return;
51
- }
52
- if (sharedRoom.session) {
53
- openCodeSession = sharedRoom.session;
54
- } else {
55
- // 首次使用共享,创建会话
56
- openCodeSession = await createSession(`shared-${Date.now()}`, '共享会话');
57
- if (!openCodeSession) {
58
- await adapter.reply(ctx.threadId, '❌ 无法创建共享会话');
59
- return;
60
- }
61
- sharedRoom.session = openCodeSession;
62
- console.log(`✅ 共享会话已创建: ${openCodeSession.sessionId.slice(0, 8)}`);
63
- }
64
- sharedRoom.busy = true;
65
- } else {
66
- // 原有个体逻辑
67
- // 检查是否长时间未活跃,提示模型信息
68
- const lastActive = threadLastActive.get(ctx.threadId) || 0;
69
- const isIdle = !expertPrompt && lastActive > 0 && (Date.now() - lastActive) > IDLE_MODEL_HINT_MS;
70
- if (isIdle) {
71
- pendingModelHint = true;
72
- }
73
-
74
- // 专家评审每次开独立会话,避免旧历史干扰
75
- if (expertPrompt) {
76
- openCodeSession = await createSession(`expert-${Date.now()}`, `专家评审 ${Date.now()}`);
77
- if (!openCodeSession) {
78
- await adapter.reply(ctx.threadId, '❌ 无法创建评审会话');
79
- return;
80
- }
81
- console.log(`✅ 新建评审会话: ${openCodeSession.sessionId.slice(0, 8)}`);
82
- } else {
83
- openCodeSession = openCodeSessions.get(ctx.threadId);
84
- if (!openCodeSession) {
85
- console.log(`[forwardToOpenCode] no in-memory session, trying to resume most recent...`);
86
- try {
87
- const sessions = await listOpenCodeSessions();
88
- if (sessions.length > 0) {
89
- const latest = sessions.sort((a, b) => (b.lastActivity || 0) - (a.lastActivity || 0))[0];
90
- const resumed = await resumeSession(latest.id);
91
- if (resumed) {
92
- openCodeSession = resumed;
93
- openCodeSessions.set(ctx.threadId, openCodeSession);
94
- console.log(`[forwardToOpenCode] resumed session ${latest.id.slice(0, 8)}`);
95
- }
96
- }
97
- } catch (e) {
98
- console.log(`[forwardToOpenCode] failed to resume: ${e.message}`);
99
- }
100
- if (!openCodeSession) {
101
- console.log(`[forwardToOpenCode] creating new session for thread=${ctx.threadId}`);
102
- openCodeSession = await createSession(ctx.threadId, `Weixin ${ctx.threadId}`);
103
- if (!openCodeSession) { await adapter.reply(ctx.threadId, '❌ 无法创建 OpenCode 会话'); return; }
104
- openCodeSessions.set(ctx.threadId, openCodeSession);
105
- }
106
- }
107
- }
108
- }
109
-
110
- console.log(`📤 Message sent: → ${text}`);
111
- let scopedText = text;
112
- if (expertPrompt) scopedText = `${expertPrompt}\n\n${scopedText}`;
113
-
114
- const typingPing = startTypingPing(adapter, ctx.threadId);
115
-
116
- const result = await sendToOpenCode(openCodeSession, scopedText, {
117
- idleThreshold: expertPrompt ? 30 : 10,
118
- onNewContent: (delta) => {
119
- typingPing.poke();
120
- },
121
- onResponseMeta: (meta) => {
122
- if (pendingModelHint && meta.modelID) {
123
- pendingModelHint = `🧠 ${meta.providerID}/${meta.modelID}`;
124
- }
125
- },
126
- onEvent: (event) => {
127
- if (event.type === 'tool.call') {
128
- const props = event.properties || {};
129
- const toolName = props.name || props.tool_name || 'unknown';
130
- const input = props.input || {};
131
- let toolDesc = `🔧 ${toolName}`;
132
- if (input.path) toolDesc += ` 📁${input.path}`;
133
- if (input.command) toolDesc += ` 💻${input.command}`;
134
- console.log(`[→发送] ${toolDesc}`);
135
- const replyTargets = isShared ? [...sharedRoom.members] : [ctx.threadId];
136
- for (const tid of replyTargets) {
137
- replyTo(tid, toolDesc, adapter).catch(e => console.error('[→发送] 失败:', e.message));
138
- }
139
- typingPing.poke();
140
- }
141
- },
142
- }, ctx.threadId).catch((e) => {
143
- console.error('[forwardToOpenCode] Task error:', e.message);
144
- return '';
145
- });
146
-
147
- typingPing.done();
148
-
149
- // 共享模式:广播到所有成员
150
- const replyTargets = isShared ? [...sharedRoom.members] : [ctx.threadId];
151
- if (isShared) sharedRoom.busy = false;
152
-
153
- // 错误/超时 → 清除 session 让下次重建
154
- const finalText = (result || '').trim();
155
- console.log(`📥 AI response (${finalText.length} chars): ${finalText.slice(0, 200)}${finalText.length > 200 ? '...' : ''}`);
156
- const displayText = pendingModelHint && typeof pendingModelHint === 'string'
157
- ? `${pendingModelHint}\n${finalText}`
158
- : finalText;
159
- if (!finalText || finalText.startsWith('⏰') || finalText.startsWith('❌')) {
160
- if (!isShared) openCodeSessions.delete(ctx.threadId);
161
- for (const tid of replyTargets) {
162
- replyTo(tid, finalText || '⚠️ AI 返回为空(可能是超时),请重试或 /diagnose', adapter)
163
- .catch(e => console.error('[reply] 失败:', e.message));
164
- }
165
- } else {
166
- const msgs = splitMessage(displayText);
167
- for (const m of msgs) {
168
- if (!m.trim()) continue;
169
- for (const tid of replyTargets) {
170
- replyTo(tid, m, adapter).catch(e => console.error('[reply] 失败:', e.message));
171
- }
172
- }
173
- }
174
-
175
- // 更新活跃时间
176
- threadLastActive.set(ctx.threadId, Date.now());
177
-
178
-
179
-
180
- const shareUrl = await shareSession(openCodeSession);
181
- if (shareUrl) {
182
- try {
183
- await adapter.reply(ctx.threadId, `🔗 ${shareUrl}`);
184
- } catch (e) {
185
- console.error('[forwardToOpenCode] share URL reply failed:', e.message);
186
- }
187
- }
188
-
189
- }
190
-
191
- async function handleMessage(adapter, ctx, text, openCodeSessions) {
192
- const session = {};
193
-
194
- const expertTriggers = ['z', 'Z', '叫全部专家', '叫所有专家', '呼叫专家点评', '专家点评', '专家意见', 'call all experts', 'expert review', '专家会诊', '团队评审', '代码审查', '全员review', 'review all', '请专家', '叫专家', '找专家'];
195
- const trimmedLower = text.trim().toLowerCase();
196
- let expertPrompt = null;
197
- if (text.startsWith('/z')) {
198
- const arg = text.slice(2).trim();
199
- if (arg === 'off' || arg === 'reset' || arg === '关闭') {
200
- await adapter.reply(ctx.threadId, '⏹️ 自定义 prompt 已清除');
201
- return;
202
- }
203
- if (arg) {
204
- expertPrompt = arg;
205
- await adapter.reply(ctx.threadId, `✅ 自定义专家 prompt (${arg.length}字),本消息生效`);
206
- } else {
207
- expertPrompt = EXPERT_SYSTEM_PROMPT;
208
- await adapter.reply(ctx.threadId, '✅ 专家评审已启动');
209
- }
210
- await forwardToOpenCode(adapter, ctx, text, openCodeSessions, expertPrompt);
211
- return;
212
- }
213
- if (expertTriggers.some(t => trimmedLower.includes(t))) {
214
- expertPrompt = EXPERT_SYSTEM_PROMPT;
215
- }
216
-
217
- // 自主开发决策回复拦截
218
- if (hasPendingDecision(ctx.threadId)) {
219
- if (/^\d+$/.test(text.trim())) {
220
- if (resolveDecision(ctx.threadId, text.trim())) return;
221
- }
222
- if (/^\/(\d+)$/.test(text.trim())) {
223
- if (resolveDecision(ctx.threadId, text.trim().slice(1))) return;
224
- }
225
- }
226
-
227
- const detected = detectCommand(text);
228
- if (detected) {
229
- const cmdAdapter = isSharedMember(ctx.threadId) ? wrapAdapterForShared(adapter, ctx.threadId) : adapter;
230
- const handled = await handleCommand(cmdAdapter, ctx, detected.name, detected.arg, openCodeSessions);
231
- if (handled) return;
232
- }
233
-
234
- if (!isAuthorized('weixin', ctx.userId)) {
235
- if (!hasOwner('weixin')) {
236
- await adapter.reply(ctx.threadId, '🔐 请先发送 /start 进行安全认证');
237
- return;
238
- }
239
- // 共享成员跳过认证
240
- if (!isSharedMember(ctx.threadId)) {
241
- await adapter.reply(ctx.threadId, '🚫 你无权使用此 bot');
242
- return;
243
- }
244
- }
245
-
246
- // 活跃 agent 路由
247
- const activeAgentName = threadAgent.get(ctx.threadId);
248
- if (activeAgentName && activeAgentName !== 'opencode') {
249
- const agent = registry.findAgent(activeAgentName);
250
- if (agent) {
251
- const available = await agent.isAvailable().catch(() => false);
252
- if (available) {
253
- const t0 = Date.now();
254
- console.log(`\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`);
255
- console.log(`[→${activeAgentName}] ${text}`);
256
- console.log(`──────────────────────────────────────────────────────`);
257
-
258
- // 构建历史记录
259
- const history = threadHistory.get(ctx.threadId) || [];
260
- const maxHistory = 20;
261
- const recentHistory = history.slice(-maxHistory);
262
-
263
- try {
264
- const response = await agent.sendPrompt(activeAgentName, text, recentHistory, { projectDir: globalThis.__autoProjectDir });
265
- const elapsed = ((Date.now() - t0) / 1000).toFixed(1);
266
- console.log(`[←${activeAgentName}] ⏱ ${elapsed}s | chars: ${(response || '').length}`);
267
- console.log(`[RESPONSE] ${(response || '').slice(0, 200)}${(response || '').length > 200 ? '...' : ''}`);
268
-
269
- // 更新历史
270
- history.push({ role: 'user', content: text }, { role: 'assistant', content: response || '' });
271
- threadHistory.set(ctx.threadId, history);
272
-
273
- const chunks = splitMessage(response || '无响应');
274
- for (const chunk of chunks) {
275
- await adapter.reply(ctx.threadId, chunk);
276
- }
277
- } catch (error) {
278
- console.error(`[${activeAgentName}] ❌ ${error.message}`);
279
- await adapter.reply(ctx.threadId, `❌ ${activeAgentName} 错误: ${error.message}`);
280
- }
281
- return;
282
- }
283
- console.warn(`[agent] ${activeAgentName} 不可用,回退到 OpenCode`);
284
- }
285
- // agent 不可用,清除状态回退到 OpenCode
286
- threadAgent.delete(ctx.threadId);
287
- }
288
-
289
- const connected = await checkConnection();
290
- if (!connected) {
291
- await adapter.reply(ctx.threadId, '❌ OpenCode 离线,请检查服务是否运行');
292
- return;
293
- }
294
-
295
- await forwardToOpenCode(adapter, ctx, text, openCodeSessions);
296
- }
27
+ const handler = createHandler({
28
+ handleCommand,
29
+ replyTo,
30
+ wrapAdapterForShared,
31
+ isSharedMember,
32
+ sharedRoom,
33
+ });
297
34
 
298
- export { handleMessage, forwardToOpenCode };
35
+ export const handleMessage = handler.handleMessage;
36
+ export const forwardToOpenCode = handler.forwardToOpenCode;
@@ -1 +1,12 @@
1
+ // userId → adapter 映射,用于跨用户消息路由
2
+ // 定期清理防内存泄漏
3
+ const MAX_SIZE = 5000;
1
4
  export const userAdapterMap = new Map();
5
+
6
+ setInterval(() => {
7
+ if (userAdapterMap.size > MAX_SIZE) {
8
+ const toDelete = userAdapterMap.size - MAX_SIZE;
9
+ const keys = [...userAdapterMap.keys()];
10
+ for (let i = 0; i < toDelete; i++) userAdapterMap.delete(keys[i]);
11
+ }
12
+ }, 30 * 60 * 1000).unref();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yvhitxcel/opencode-remote",
3
- "version": "0.17.0",
3
+ "version": "0.18.0",
4
4
  "description": "🤖 AI 专家团队随时待命!只需输入 /z,自动分析项目、诊断问题、给出改进方案。支持微信/飞书/Telegram 远程控制 OpenCode、Claude Code、Codex、Copilot。手机也能搞开发。",
5
5
  "type": "module",
6
6
  "bin": {
@@ -16,8 +16,10 @@
16
16
  "start": "node dist/index.js",
17
17
  "lint": "node scripts/check-syntax.mjs",
18
18
  "test": "npm run lint && node --test test/*.test.mjs",
19
- "precommit": "npm run lint",
20
- "prepublishOnly": "npm run lint"
19
+ "typecheck": "tsc --noEmit",
20
+ "precommit": "npm run lint && npm run typecheck",
21
+ "prepublishOnly": "npm run lint",
22
+ "check-refs": "node scripts/check-ref-errors.mjs"
21
23
  },
22
24
  "files": [
23
25
  "dist"