@wu529778790/open-im 1.11.8-beta.3 → 1.11.8-beta.5
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/dist/clawbot/client.js +28 -0
- package/dist/config/types.d.ts +4 -0
- package/dist/config.js +2 -0
- package/dist/opencode/sdk-runner.js +73 -0
- package/dist/shared/ai-task.js +3 -1
- package/package.json +1 -1
package/dist/clawbot/client.js
CHANGED
|
@@ -122,6 +122,17 @@ function startPolling() {
|
|
|
122
122
|
const extracted = extractTextContent(msg);
|
|
123
123
|
if (!extracted)
|
|
124
124
|
continue;
|
|
125
|
+
// Skip echoed bot messages (iLink API echoes bot's own messages as USER type)
|
|
126
|
+
// 1) Messages sent by this bot have client_id starting with "open-im:"
|
|
127
|
+
if (msg.client_id?.startsWith('open-im:')) {
|
|
128
|
+
log.debug(`Skipping echoed bot message: client_id=${msg.client_id}`);
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
// 2) Bot lifecycle notifications and tool call notifications echoed back
|
|
132
|
+
if (isBotNotificationEcho(extracted)) {
|
|
133
|
+
log.debug(`Skipping bot notification echo: content="${extracted.substring(0, 80)}"`);
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
125
136
|
const chatId = msg.from_user_id ?? '';
|
|
126
137
|
const msgId = String(msg.message_id ?? msg.seq ?? '');
|
|
127
138
|
if (!chatId) {
|
|
@@ -313,6 +324,23 @@ async function extractImages(msg) {
|
|
|
313
324
|
}
|
|
314
325
|
return paths;
|
|
315
326
|
}
|
|
327
|
+
/**
|
|
328
|
+
* Check if extracted text is a bot notification being echoed back by the iLink API.
|
|
329
|
+
* Bot notifications have distinctive emoji prefixes that are unlikely in real user messages.
|
|
330
|
+
*/
|
|
331
|
+
function isBotNotificationEcho(text) {
|
|
332
|
+
const firstLine = text.split('\n')[0]?.trim() ?? '';
|
|
333
|
+
// Lifecycle/AI info notifications
|
|
334
|
+
if (firstLine.startsWith('🤖 AI:'))
|
|
335
|
+
return true;
|
|
336
|
+
// Service status notifications
|
|
337
|
+
if (firstLine.startsWith('✅ ') || firstLine.startsWith('🔄 ') || firstLine.startsWith('ℹ️ '))
|
|
338
|
+
return true;
|
|
339
|
+
// Tool call notifications (sent by streamUpdate in event-handler)
|
|
340
|
+
if (firstLine.startsWith('⚙️ '))
|
|
341
|
+
return true;
|
|
342
|
+
return false;
|
|
343
|
+
}
|
|
316
344
|
/**
|
|
317
345
|
* Extract text content from an iLink message's item_list.
|
|
318
346
|
* Returns the first text item found, or a placeholder for media types.
|
package/dist/config/types.d.ts
CHANGED
|
@@ -26,6 +26,8 @@ export interface Config {
|
|
|
26
26
|
codexCliPath: string;
|
|
27
27
|
codebuddyCliPath: string;
|
|
28
28
|
opencodeCliPath: string;
|
|
29
|
+
/** OpenCode SDK 使用的模型(格式: providerID/modelID,如 zhipuai-coding-plan/glm-5.1) */
|
|
30
|
+
opencodeModel?: string;
|
|
29
31
|
/** Claude 访问 API 的代理(如 http://127.0.0.1:7890) */
|
|
30
32
|
claudeProxy?: string;
|
|
31
33
|
/** Codex 访问 chatgpt.com 的代理(如 http://127.0.0.1:7890) */
|
|
@@ -186,6 +188,8 @@ export interface FileToolCodeBuddy {
|
|
|
186
188
|
}
|
|
187
189
|
export interface FileToolOpenCode {
|
|
188
190
|
cliPath?: string;
|
|
191
|
+
/** 模型(格式: providerID/modelID) */
|
|
192
|
+
model?: string;
|
|
189
193
|
}
|
|
190
194
|
export interface FileConfig {
|
|
191
195
|
telegramBotToken?: string;
|
package/dist/config.js
CHANGED
|
@@ -226,6 +226,7 @@ export function loadConfig() {
|
|
|
226
226
|
const codebuddyCliPath = process.env.CODEBUDDY_CLI_PATH ?? tcb.cliPath ?? resolveWindowsCliPath(AI_TOOL_BY_ID.codebuddy.cliDefault ?? 'codebuddy');
|
|
227
227
|
const topencode = file.tools?.opencode ?? {};
|
|
228
228
|
const opencodeCliPath = process.env.OPENCODE_CLI_PATH ?? topencode.cliPath ?? resolveWindowsCliPath(AI_TOOL_BY_ID.opencode.cliDefault ?? 'opencode');
|
|
229
|
+
const opencodeModel = process.env.OPENCODE_MODEL ?? topencode.model;
|
|
229
230
|
const claudeWorkDir = process.env.CLAUDE_WORK_DIR ?? tc.workDir ?? process.cwd();
|
|
230
231
|
const skipPermissions = process.env.OPEN_IM_SKIP_PERMISSIONS === 'false'
|
|
231
232
|
? false
|
|
@@ -519,6 +520,7 @@ export function loadConfig() {
|
|
|
519
520
|
codexCliPath,
|
|
520
521
|
codebuddyCliPath,
|
|
521
522
|
opencodeCliPath,
|
|
523
|
+
opencodeModel,
|
|
522
524
|
claudeProxy,
|
|
523
525
|
codexProxy,
|
|
524
526
|
claudeWorkDir,
|
|
@@ -46,9 +46,15 @@ export async function runOpenCodeSdk(prompt, sessionId, workDir, callbacks, opti
|
|
|
46
46
|
let accumulatedThinking = '';
|
|
47
47
|
const toolStats = {};
|
|
48
48
|
let runSettled = false;
|
|
49
|
+
let sseConnected = false;
|
|
50
|
+
// 用于等待 SSE 连接就绪后再发 prompt,避免竞态
|
|
51
|
+
const sseReadyResolve = () => { sseConnected = true; };
|
|
49
52
|
const subscribeEvents = async () => {
|
|
50
53
|
try {
|
|
51
54
|
const sse = await client.global.event({ signal: abortController.signal });
|
|
55
|
+
// SSE 连接已建立,通知主流程可以开始 prompt
|
|
56
|
+
sseReadyResolve();
|
|
57
|
+
log.debug('SSE stream connected');
|
|
52
58
|
for await (const raw of sse.stream) {
|
|
53
59
|
const ev = raw;
|
|
54
60
|
const payload = ev?.payload;
|
|
@@ -65,6 +71,16 @@ export async function runOpenCodeSdk(prompt, sessionId, workDir, callbacks, opti
|
|
|
65
71
|
}
|
|
66
72
|
break;
|
|
67
73
|
}
|
|
74
|
+
case 'session.next.text.ended': {
|
|
75
|
+
// text.ended 携带完整文本,作为 SSE 流的兜底
|
|
76
|
+
const fullText = payload.properties.text;
|
|
77
|
+
if (fullText && !accumulatedText) {
|
|
78
|
+
accumulatedText = fullText;
|
|
79
|
+
callbacks.onText(accumulatedText);
|
|
80
|
+
log.debug(`SSE text.ended fallback: got ${fullText.length} chars`);
|
|
81
|
+
}
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
68
84
|
case 'session.next.reasoning.delta': {
|
|
69
85
|
const delta = payload.properties.delta;
|
|
70
86
|
if (delta) {
|
|
@@ -73,6 +89,15 @@ export async function runOpenCodeSdk(prompt, sessionId, workDir, callbacks, opti
|
|
|
73
89
|
}
|
|
74
90
|
break;
|
|
75
91
|
}
|
|
92
|
+
case 'session.next.reasoning.ended': {
|
|
93
|
+
// reasoning.ended 携带完整推理文本
|
|
94
|
+
const fullReasoning = payload.properties.text;
|
|
95
|
+
if (fullReasoning && !accumulatedThinking) {
|
|
96
|
+
accumulatedThinking = fullReasoning;
|
|
97
|
+
callbacks.onThinking?.(accumulatedThinking);
|
|
98
|
+
}
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
76
101
|
case 'session.next.tool.called': {
|
|
77
102
|
const tool = payload.properties.tool;
|
|
78
103
|
if (tool) {
|
|
@@ -81,6 +106,9 @@ export async function runOpenCodeSdk(prompt, sessionId, workDir, callbacks, opti
|
|
|
81
106
|
}
|
|
82
107
|
break;
|
|
83
108
|
}
|
|
109
|
+
default:
|
|
110
|
+
log.debug(`SSE unhandled event: ${payload.type}`);
|
|
111
|
+
break;
|
|
84
112
|
}
|
|
85
113
|
}
|
|
86
114
|
}
|
|
@@ -91,6 +119,24 @@ export async function runOpenCodeSdk(prompt, sessionId, workDir, callbacks, opti
|
|
|
91
119
|
}
|
|
92
120
|
};
|
|
93
121
|
const background = subscribeEvents();
|
|
122
|
+
// 等待 SSE 连接就绪(最多 3 秒),避免 prompt 在 SSE 未连接时就开始
|
|
123
|
+
// 如果 3 秒内 SSE 未连接,仍然继续执行(SSE 可能后续连接并补收事件)
|
|
124
|
+
const sseReady = new Promise((resolve) => {
|
|
125
|
+
const check = setInterval(() => {
|
|
126
|
+
if (sseConnected) {
|
|
127
|
+
clearInterval(check);
|
|
128
|
+
resolve();
|
|
129
|
+
}
|
|
130
|
+
}, 100);
|
|
131
|
+
// 3 秒超时:SSE 可能需要重试连接,不等太久
|
|
132
|
+
setTimeout(() => {
|
|
133
|
+
clearInterval(check);
|
|
134
|
+
if (!sseConnected)
|
|
135
|
+
log.warn('SSE not connected within 3s, proceeding with prompt anyway');
|
|
136
|
+
resolve();
|
|
137
|
+
}, 3000);
|
|
138
|
+
});
|
|
139
|
+
await sseReady;
|
|
94
140
|
try {
|
|
95
141
|
const result = await client.session.prompt({
|
|
96
142
|
sessionID: currentSessionId,
|
|
@@ -98,16 +144,43 @@ export async function runOpenCodeSdk(prompt, sessionId, workDir, callbacks, opti
|
|
|
98
144
|
parts: [{ type: 'text', text: prompt }],
|
|
99
145
|
...(options?.model ? { model: parseModel(options.model) } : {}),
|
|
100
146
|
});
|
|
147
|
+
// 调试:dump SDK 返回值结构
|
|
148
|
+
log.info(`SDK prompt raw result: hasData=${!!result.data}, hasError=${!!result.error}, keys=${Object.keys(result).join(',')}`);
|
|
149
|
+
if (result.error) {
|
|
150
|
+
const errStr = result.error instanceof Error ? result.error.message : JSON.stringify(result.error).substring(0, 500);
|
|
151
|
+
log.info(`SDK prompt error detail: ${errStr}`);
|
|
152
|
+
}
|
|
153
|
+
if (result.data) {
|
|
154
|
+
const d = result.data;
|
|
155
|
+
log.info(`SDK prompt data keys: ${Object.keys(d).join(',')}, parts count: ${Array.isArray(d.parts) ? d.parts.length : 'N/A'}, info keys: ${d.info && typeof d.info === 'object' ? Object.keys(d.info).join(',') : 'N/A'}`);
|
|
156
|
+
}
|
|
101
157
|
abortController.abort();
|
|
102
158
|
await background.catch(() => { });
|
|
103
159
|
runSettled = true;
|
|
160
|
+
// SDK 返回 { data, error } 二元组;error 存在时 data 为 undefined
|
|
161
|
+
const sdkError = result.error;
|
|
162
|
+
if (sdkError) {
|
|
163
|
+
const errMsg = sdkError instanceof Error ? sdkError.message : JSON.stringify(sdkError);
|
|
164
|
+
log.error(`SDK prompt returned error: ${errMsg}`);
|
|
165
|
+
callbacks.onError(errMsg);
|
|
166
|
+
return { abort: () => { } };
|
|
167
|
+
}
|
|
104
168
|
const data = result.data;
|
|
169
|
+
if (!data) {
|
|
170
|
+
log.error(`SDK prompt returned no data and no error — result keys: ${Object.keys(result).join(',')}`);
|
|
171
|
+
callbacks.onError('SDK 返回空数据');
|
|
172
|
+
return { abort: () => { } };
|
|
173
|
+
}
|
|
105
174
|
const parts = data?.parts ?? [];
|
|
106
175
|
const finalText = parts
|
|
107
176
|
.filter((p) => p.type === 'text' && typeof p.text === 'string')
|
|
108
177
|
.map(p => p.text)
|
|
109
178
|
.join('\n');
|
|
110
179
|
const cost = data?.info?.cost ?? 0;
|
|
180
|
+
log.info(`SDK prompt completed: accumulatedText=${accumulatedText.length}, finalText=${finalText.length}, parts=${parts.length}, cost=${cost}`);
|
|
181
|
+
if (!accumulatedText && !finalText) {
|
|
182
|
+
log.warn(`SDK prompt returned empty output — data keys: ${data ? Object.keys(data).join(',') : 'null'}, parts types: ${parts.map(p => p.type).join(',')}`);
|
|
183
|
+
}
|
|
111
184
|
callbacks.onComplete({
|
|
112
185
|
success: true,
|
|
113
186
|
result: finalText,
|
package/dist/shared/ai-task.js
CHANGED
|
@@ -290,7 +290,9 @@ export function runAITask(deps, ctx, prompt, toolAdapter, platformAdapter) {
|
|
|
290
290
|
}, {
|
|
291
291
|
model: aiCommand === 'claude'
|
|
292
292
|
? (sessionManager.getModel(ctx.userId, ctx.threadId) ?? config.claudeModel)
|
|
293
|
-
:
|
|
293
|
+
: aiCommand === 'opencode'
|
|
294
|
+
? config.opencodeModel
|
|
295
|
+
: undefined,
|
|
294
296
|
chatId: ctx.chatId,
|
|
295
297
|
// 默认跳过权限确认,保持全自动执行(可通过 config 或环境变量关闭)
|
|
296
298
|
skipPermissions: config.skipPermissions ?? true,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wu529778790/open-im",
|
|
3
|
-
"version": "1.11.8-beta.
|
|
3
|
+
"version": "1.11.8-beta.5",
|
|
4
4
|
"description": "Your AI coding assistant, in every chat app. Multi-platform IM bridge for Claude Code, Codex, and CodeBuddy.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|