@yvhitxcel/opencode-remote 0.16.2 → 0.17.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,15 +1,10 @@
1
- import { getOrCreateSession } from '../core/session.js';
2
1
  import { splitMessage } from '../core/notifications.js';
3
- import { initOpenCode, createSession, sendMessage, checkConnection, resumeSession, shareSession } from '../opencode/client.js';
2
+ import { createSession, sendMessage, checkConnection, listOpenCodeSessions, resumeSession } from '../opencode/client.js';
4
3
  import { isAuthorized, hasOwner } from '../core/auth.js';
5
4
  import { detectCommand, EXPERT_SYSTEM_PROMPT } from '../core/router.js';
6
- import { handleCommand, formatTimeAgo } from './commands.js';
7
- import { existsSync, readFileSync } from 'fs';
8
- import { join } from 'path';
5
+ import { handleCommand } from './commands.js';
9
6
 
10
7
  async function handleMessage(adapter, ctx, text, openCodeSessions) {
11
- const session = await getOrCreateSession(ctx.threadId, 'feishu');
12
-
13
8
  const expertTriggers = ['z', 'Z', '叫全部专家', '叫所有专家', '呼叫专家点评', '专家点评', '专家意见', 'call all experts', 'expert review', '专家会诊', '团队评审', '代码审查', '全员review', 'review all', '请专家', '叫专家', '找专家'];
14
9
  let expertPrompt = null;
15
10
 
@@ -24,7 +19,7 @@ async function handleMessage(adapter, ctx, text, openCodeSessions) {
24
19
  } else {
25
20
  expertPrompt = EXPERT_SYSTEM_PROMPT;
26
21
  }
27
- await forwardToOpenCode(adapter, ctx, text, openCodeSessions, session, expertPrompt);
22
+ await forwardToOpenCode(adapter, ctx, text, openCodeSessions, expertPrompt);
28
23
  return;
29
24
  }
30
25
 
@@ -37,155 +32,6 @@ async function handleMessage(adapter, ctx, text, openCodeSessions) {
37
32
  await handleCommand(adapter, ctx, parsed.name, parsed.arg, openCodeSessions);
38
33
  return;
39
34
  }
40
- const trimmed = text.trim();
41
- if (session._deleteSessionList) {
42
- if (/^\d+$/.test(trimmed)) {
43
- const num = parseInt(trimmed, 10);
44
- const list = session._deleteSessionList;
45
- if (num >= 1 && num <= list.length) {
46
- const target = list[num - 1];
47
- try {
48
- const opencode = await initOpenCode();
49
- if (!opencode) { await adapter.reply(ctx.threadId, '❌ 无法删除会话'); return; }
50
- await opencode.client.session.delete({ path: { id: target.id } });
51
- if (session.opencodeSessionId === target.id) {
52
- openCodeSessions.delete(ctx.threadId);
53
- session.opencodeSessionId = undefined;
54
- }
55
- const result = await opencode.client.session.list();
56
- if (!result.error && result.data && result.data.length > 0) {
57
- const sorted = result.data.sort((a, b) => (b.time?.updated || 0) - (a.time?.updated || 0));
58
- session._deleteSessionList = sorted;
59
- let msg = `🗑️ 已删除: ${target.title || '无标题'}\n\n📂 选择要删除的会话(回复编号):\n\n`;
60
- sorted.slice(0, 10).forEach((s, i) => {
61
- const n = i + 1;
62
- msg += `${n}. ${s.title || '无标题'} (${s.updated_at ? formatTimeAgo(s.updated_at * 1000) : ''})\n`;
63
- });
64
- if (sorted.length > 10) msg += `\n... 共 ${sorted.length} 个会话`;
65
- msg += '\n\n回复编号删除';
66
- for (const m of splitMessage(msg)) await adapter.reply(ctx.threadId, m);
67
- } else {
68
- session._deleteSessionList = null;
69
- await adapter.reply(ctx.threadId, `🗑️ 已删除: ${target.title || '无标题'}\n📭 没有更多会话了`);
70
- }
71
- } catch (e) {
72
- await adapter.reply(ctx.threadId, `❌ 删除失败: ${e.message}`);
73
- }
74
- } else {
75
- await adapter.reply(ctx.threadId, `❌ 无效编号,请输入 1-${session._deleteSessionList.length}`);
76
- }
77
- return;
78
- }
79
- session._deleteSessionList = null;
80
- }
81
- if (session._switchSessionList) {
82
- if (/^\d+$/.test(trimmed)) {
83
- const num = parseInt(trimmed, 10);
84
- const list = session._switchSessionList;
85
- if (num >= 1 && num <= list.length) {
86
- const target = list[num - 1];
87
- if (session._showSessionState) {
88
- session._showSessionState = null;
89
- session._pendingSwitchSession = target;
90
- const targetDir = target.directory || process.cwd();
91
- let stateMsg = `📋 会话: ${target.title || '无标题'}\n`;
92
- stateMsg += `ID: ${target.id.slice(0, 8)}...\n`;
93
- if (target.directory) stateMsg += `📁 目录: ${target.directory}\n`;
94
- stateMsg += '\n';
95
- try {
96
- const memoryPath = join(targetDir, 'MEMORY.md');
97
- if (existsSync(memoryPath)) {
98
- const content = readFileSync(memoryPath, 'utf-8');
99
- const summaryMatch = content.match(/## 最近会话摘要\n([\s\S]*?)(?=##|$)/);
100
- if (summaryMatch) {
101
- const lines = summaryMatch[1].trim().split('\n').filter(l => l.trim());
102
- const recent = lines.slice(-3);
103
- if (recent.length > 0) {
104
- stateMsg += `📝 最近会话:\n${recent.map(l => ` ${l.trim()}`).join('\n')}\n\n`;
105
- }
106
- }
107
- }
108
- } catch { console.debug('[session-switch] 无法读取MEMORY.md'); }
109
- stateMsg += `💡 回复 "确认" 切换到此会话,或回复其他内容取消`;
110
- await adapter.reply(ctx.threadId, stateMsg);
111
- return;
112
- }
113
- try {
114
- const resumed = await resumeSession(target.id);
115
- if (resumed) {
116
- openCodeSessions.set(ctx.threadId, resumed);
117
- session.opencodeSessionId = resumed.sessionId;
118
- session.taskStartTime = null;
119
- session.currentTool = null;
120
- session.modifiedFiles = null;
121
- if (target.directory) {
122
- session.projectDir = target.directory;
123
- globalThis.__autoProjectDir = target.directory;
124
- }
125
- await adapter.reply(ctx.threadId, `✅ 已切换至: ${target.title || '无标题'}\nID: ${resumed.sessionId.slice(0, 8)}...`);
126
- } else {
127
- await adapter.reply(ctx.threadId, '❌ 切换失败');
128
- }
129
- } catch (e) {
130
- await adapter.reply(ctx.threadId, `❌ 切换失败: ${e.message}`);
131
- }
132
- } else {
133
- await adapter.reply(ctx.threadId, `❌ 无效编号,请输入 1-${session._switchSessionList.length}`);
134
- }
135
- session._switchSessionList = null;
136
- session._showSessionState = null;
137
- session._pendingSwitchSession = null;
138
- return;
139
- }
140
- session._switchSessionList = null;
141
- session._showSessionState = null;
142
- session._pendingSwitchSession = null;
143
- }
144
- if (session._pendingSwitchSession) {
145
- const confirm = trimmed.toLowerCase();
146
- const target = session._pendingSwitchSession;
147
- session._pendingSwitchSession = null;
148
- if (confirm === '确认' || confirm === 'confirm' || confirm === 'y' || confirm === '1') {
149
- try {
150
- const resumed = await resumeSession(target.id);
151
- if (resumed) {
152
- openCodeSessions.set(ctx.threadId, resumed);
153
- session.opencodeSessionId = resumed.sessionId;
154
- session.taskStartTime = null;
155
- session.currentTool = null;
156
- session.modifiedFiles = null;
157
- if (target.directory) {
158
- session.projectDir = target.directory;
159
- globalThis.__autoProjectDir = target.directory;
160
- }
161
- await adapter.reply(ctx.threadId, `✅ 已切换至: ${target.title || '无标题'}\nID: ${resumed.sessionId.slice(0, 8)}...`);
162
- } else {
163
- await adapter.reply(ctx.threadId, '❌ 切换失败');
164
- }
165
- } catch (e) {
166
- await adapter.reply(ctx.threadId, `❌ 切换失败: ${e.message}`);
167
- }
168
- return;
169
- }
170
- await adapter.reply(ctx.threadId, '已取消切换');
171
- return;
172
- }
173
- if (session._analyzeMode) {
174
- const confirm = trimmed.toLowerCase();
175
- if (confirm === '执行' || confirm === 'execute' || confirm === '开始' || confirm === 'go') {
176
- session._analyzeMode = false;
177
- const task = session._analyzeTask || text;
178
- session._analyzeTask = null;
179
- await adapter.reply(ctx.threadId, `🔧 开始执行: ${task}\n\nAI 将实施最小改动,完成后列出变更点和验证步骤。`);
180
- const execPrompt = `现在开始施工队模式。任务:${task}\n\n请严格执行以下步骤:\n\n1. 实施最小改动:只修改必要的代码,不要重构其他地方\n2. 保持风格一致:遵循项目现有的代码风格\n3. 列出变更点:修改完成后,列出所有改动的文件和代码\n4. 验证步骤:说明需要运行哪些测试或检查来验证修改\n5. 列出未验证的部分:说明还有哪些边界情况需要人工检查\n\n注意:\n- 只做最小改动\n- 不要顺手重构\n- 不要修改不相关的文件`;
181
- await forwardToOpenCode(adapter, ctx, execPrompt, openCodeSessions, session);
182
- return;
183
- }
184
- session._analyzeMode = false;
185
- session._analyzeTask = null;
186
- await adapter.reply(ctx.threadId, '已取消分析模式');
187
- return;
188
- }
189
35
  if (!isAuthorized('feishu', ctx.userId)) {
190
36
  if (!hasOwner('feishu')) {
191
37
  await adapter.reply(ctx.threadId, `🔐 **需要认证**
@@ -209,18 +55,27 @@ async function handleMessage(adapter, ctx, text, openCodeSessions) {
209
55
  🔄 /retry — 重试连接`);
210
56
  return;
211
57
  }
212
- await forwardToOpenCode(adapter, ctx, text, openCodeSessions, session);
58
+ await forwardToOpenCode(adapter, ctx, text, openCodeSessions);
213
59
  }
214
60
 
215
- async function forwardToOpenCode(adapter, ctx, text, openCodeSessions, session, expertPrompt) {
61
+ async function forwardToOpenCode(adapter, ctx, text, openCodeSessions, expertPrompt) {
216
62
  await adapter.sendTypingIndicator(ctx.threadId);
217
63
  let openCodeSession = openCodeSessions.get(ctx.threadId);
218
64
  if (!openCodeSession) {
219
- if (session.opencodeSessionId) {
220
- openCodeSession = await resumeSession(session.opencodeSessionId);
221
- }
222
- if (!openCodeSession && globalThis.__latestOpenCodeSession?.id) {
223
- openCodeSession = await resumeSession(globalThis.__latestOpenCodeSession.id);
65
+ console.log(`[feishu-forward] no in-memory session, trying to resume most recent...`);
66
+ try {
67
+ const sessions = await listOpenCodeSessions();
68
+ if (sessions.length > 0) {
69
+ const latest = sessions.sort((a, b) => (b.updated_at || 0) - (a.updated_at || 0))[0];
70
+ const resumed = await resumeSession(latest.id);
71
+ if (resumed) {
72
+ openCodeSession = resumed;
73
+ openCodeSessions.set(ctx.threadId, openCodeSession);
74
+ console.log(`[feishu-forward] resumed session ${latest.id.slice(0, 8)}`);
75
+ }
76
+ }
77
+ } catch (e) {
78
+ console.log(`[feishu-forward] failed to resume: ${e.message}`);
224
79
  }
225
80
  if (!openCodeSession) {
226
81
  openCodeSession = await createSession(ctx.threadId, `Feishu ${ctx.threadId}`);
@@ -228,25 +83,11 @@ async function forwardToOpenCode(adapter, ctx, text, openCodeSessions, session,
228
83
  await adapter.reply(ctx.threadId, '❌ 无法创建 OpenCode 会话');
229
84
  return;
230
85
  }
86
+ openCodeSessions.set(ctx.threadId, openCodeSession);
231
87
  }
232
- openCodeSessions.set(ctx.threadId, openCodeSession);
233
- session.opencodeSessionId = openCodeSession.sessionId;
234
88
  }
235
- if (session.modelOverride) {
236
- openCodeSession.model = session.modelOverride;
237
- }
238
- session.taskStartTime = Date.now();
239
- session.currentTool = null;
240
- const projectDir = session.projectDir || globalThis.__autoProjectDir;
89
+
241
90
  let scopedText = text;
242
- if (session._contextScope) {
243
- scopedText = `[上下文范围: ${session._contextScope}]\n\n${text}`;
244
- }
245
- if (projectDir) {
246
- if (!scopedText.includes('项目目录')) {
247
- scopedText = `[当前项目目录: ${projectDir}]\n\n${scopedText}`;
248
- }
249
- }
250
91
 
251
92
  if (expertPrompt) {
252
93
  scopedText = `${expertPrompt}\n\n${scopedText}`;
@@ -254,9 +95,6 @@ async function forwardToOpenCode(adapter, ctx, text, openCodeSessions, session,
254
95
 
255
96
  console.log(`📤 Forwarding to OpenCode: ${text.substring(0, 80)}...`);
256
97
  try {
257
- let lastToolNotified = '';
258
- let hasToolActivity = false;
259
- let toolCallCount = 0;
260
98
  let response = await sendMessage(openCodeSession, scopedText, {
261
99
  idleThreshold: expertPrompt ? 30 : 10,
262
100
  onEvent: (event) => {
@@ -264,7 +102,6 @@ async function forwardToOpenCode(adapter, ctx, text, openCodeSessions, session,
264
102
  const props = event.properties || {};
265
103
  const toolName = props.name || props.tool_name || 'unknown';
266
104
  const input = props.input || {};
267
- toolCallCount++;
268
105
  let toolDesc = `🔧 执行工具: ${toolName}`;
269
106
  if (input.path) {
270
107
  toolDesc += `\n📁 ${input.path}`;
@@ -273,10 +110,6 @@ async function forwardToOpenCode(adapter, ctx, text, openCodeSessions, session,
273
110
  toolDesc += `\n💻 ${input.command}`;
274
111
  }
275
112
  adapter.reply(ctx.threadId, toolDesc).catch(() => {});
276
- if (input.path) {
277
- if (!session.modifiedFiles) session.modifiedFiles = new Set();
278
- session.modifiedFiles.add(input.path);
279
- }
280
113
  console.log(`[feishu-tool] Executing: ${toolName}`);
281
114
  }
282
115
  if (event.type && !event.type.includes('delta')) {
@@ -284,20 +117,7 @@ async function forwardToOpenCode(adapter, ctx, text, openCodeSessions, session,
284
117
  }
285
118
  },
286
119
  onTextDelta: () => {},
287
- onStatusChange: (status) => {
288
- if (status.hasToolActivity) {
289
- hasToolActivity = true;
290
- }
291
- if (status.type === 'retry') {
292
- adapter.updateMessage(ctx.threadId, '', `⏳ 重试中 (${status.attempt})...`).catch(() => {});
293
- }
294
- },
295
- });
296
- session.taskStartTime = null;
297
- session.currentTool = null;
298
- if (session.modifiedFiles instanceof Set) {
299
- session.modifiedFiles = Array.from(session.modifiedFiles);
300
- }
120
+ }, ctx.threadId);
301
121
  if (!response || typeof response !== 'string') {
302
122
  await adapter.reply(ctx.threadId, '...');
303
123
  return;
@@ -307,8 +127,10 @@ async function forwardToOpenCode(adapter, ctx, text, openCodeSessions, session,
307
127
  await adapter.reply(ctx.threadId, '...');
308
128
  return;
309
129
  }
130
+ // 超时/错误 → 清除 session 让下次重建
310
131
  if (trimmedResponse.startsWith('⏰') || trimmedResponse.startsWith('❌')) {
311
132
  console.error('[feishu-forward] Error response:', trimmedResponse);
133
+ openCodeSessions.delete(ctx.threadId);
312
134
  await adapter.reply(ctx.threadId, trimmedResponse);
313
135
  return;
314
136
  }
@@ -317,8 +139,8 @@ async function forwardToOpenCode(adapter, ctx, text, openCodeSessions, session,
317
139
  await new Promise(r => setTimeout(r, 5000));
318
140
  try {
319
141
  const msgsResult = await openCodeSession.client.session.messages({
320
- path: { id: openCodeSession.sessionId },
321
- query: { limit: 5 }
142
+ sessionID: openCodeSession.sessionId,
143
+ limit: 5,
322
144
  });
323
145
  if (!msgsResult.error && msgsResult.data && msgsResult.data.length > 0) {
324
146
  for (let i = msgsResult.data.length - 1; i >= 0; i--) {
@@ -349,20 +171,7 @@ async function forwardToOpenCode(adapter, ctx, text, openCodeSessions, session,
349
171
  console.error('[feishu-forward] 回复失败:', replyErr.message);
350
172
  }
351
173
  }
352
- const filesCount = (session.modifiedFiles?.length || session.modifiedFiles?.size || 0);
353
- if (filesCount > 0) {
354
- try {
355
- const shareUrl = await shareSession(openCodeSession);
356
- if (shareUrl) {
357
- await adapter.reply(ctx.threadId, `🔗 ${shareUrl}`);
358
- }
359
- } catch (e) {
360
- console.error('[feishu-forward] 分享会话失败:', e.message);
361
- }
362
- }
363
174
  } catch (error) {
364
- session.taskStartTime = null;
365
- session.currentTool = null;
366
175
  console.error('❌ Feishu 错误:', error);
367
176
  await adapter.reply(ctx.threadId, `❌ 错误: ${error instanceof Error ? error.message : '未知错误'}`);
368
177
  }