@wu529778790/open-im 1.11.8-beta.1 → 1.11.8-beta.3
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/adapters/opencode-adapter.d.ts +0 -5
- package/dist/adapters/opencode-adapter.js +7 -9
- package/dist/adapters/registry.js +10 -1
- package/dist/config-web-page-i18n.d.ts +2 -2
- package/dist/config-web-page-i18n.js +2 -2
- package/dist/opencode/sdk-manager.d.ts +11 -0
- package/dist/opencode/sdk-manager.js +46 -0
- package/dist/opencode/sdk-runner.d.ts +27 -0
- package/dist/opencode/sdk-runner.js +151 -0
- package/dist/shared/ai-task.js +3 -3
- package/package.json +2 -1
- package/web/dist/assets/{index-yDVGC_84.js → index-BuoFTJc-.js} +1 -1
- package/web/dist/index.html +1 -1
|
@@ -1,10 +1,5 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* OpenCode Adapter — run tasks through OpenCode CLI (`opencode run`)
|
|
3
|
-
*/
|
|
4
1
|
import type { RunCallbacks, RunHandle, RunOptions, ToolAdapter } from './tool-adapter.interface.js';
|
|
5
2
|
export declare class OpenCodeAdapter implements ToolAdapter {
|
|
6
|
-
private cliPath;
|
|
7
3
|
readonly toolId = "opencode";
|
|
8
|
-
constructor(cliPath: string);
|
|
9
4
|
run(prompt: string, sessionId: string | undefined, workDir: string, callbacks: RunCallbacks, options?: RunOptions): RunHandle;
|
|
10
5
|
}
|
|
@@ -1,15 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
* OpenCode Adapter — run tasks through OpenCode CLI (`opencode run`)
|
|
3
|
-
*/
|
|
4
|
-
import { runOpenCode } from '../opencode/cli-runner.js';
|
|
1
|
+
import { runOpenCodeSdk } from '../opencode/sdk-runner.js';
|
|
5
2
|
export class OpenCodeAdapter {
|
|
6
|
-
cliPath;
|
|
7
3
|
toolId = 'opencode';
|
|
8
|
-
constructor(cliPath) {
|
|
9
|
-
this.cliPath = cliPath;
|
|
10
|
-
}
|
|
11
4
|
run(prompt, sessionId, workDir, callbacks, options) {
|
|
12
|
-
|
|
5
|
+
const handle = runOpenCodeSdk(prompt, sessionId, workDir, {
|
|
13
6
|
onText: callbacks.onText,
|
|
14
7
|
onThinking: callbacks.onThinking,
|
|
15
8
|
onToolUse: callbacks.onToolUse,
|
|
@@ -41,5 +34,10 @@ export class OpenCodeAdapter {
|
|
|
41
34
|
skipPermissions: options?.skipPermissions,
|
|
42
35
|
model: options?.model,
|
|
43
36
|
});
|
|
37
|
+
return {
|
|
38
|
+
abort: () => {
|
|
39
|
+
handle.then(h => h.abort());
|
|
40
|
+
},
|
|
41
|
+
};
|
|
44
42
|
}
|
|
45
43
|
}
|
|
@@ -3,6 +3,7 @@ import { ClaudeSDKAdapter } from './claude-sdk-adapter.js';
|
|
|
3
3
|
import { CodexAdapter } from './codex-adapter.js';
|
|
4
4
|
import { CodeBuddyAdapter } from './codebuddy-adapter.js';
|
|
5
5
|
import { OpenCodeAdapter } from './opencode-adapter.js';
|
|
6
|
+
import { startOpencode, stopOpencode, isOpencodeRunning } from '../opencode/sdk-manager.js';
|
|
6
7
|
import { createLogger } from '../logger.js';
|
|
7
8
|
import { destroyAllLiveChildren } from '../shared/process-kill.js';
|
|
8
9
|
const log = createLogger('Registry');
|
|
@@ -15,7 +16,7 @@ const ADAPTER_FACTORIES = {
|
|
|
15
16
|
claude: () => new ClaudeSDKAdapter(),
|
|
16
17
|
codex: (c) => new CodexAdapter(c.codexCliPath),
|
|
17
18
|
codebuddy: (c) => new CodeBuddyAdapter(c.codebuddyCliPath),
|
|
18
|
-
opencode: (
|
|
19
|
+
opencode: () => new OpenCodeAdapter(),
|
|
19
20
|
};
|
|
20
21
|
export function initAdapters(config) {
|
|
21
22
|
adapters.clear();
|
|
@@ -27,6 +28,12 @@ export function initAdapters(config) {
|
|
|
27
28
|
}
|
|
28
29
|
log.info(`${aiCommand} adapter enabled`);
|
|
29
30
|
adapters.set(aiCommand, factory(config));
|
|
31
|
+
// SDK 工具需要懒启动 server,这里先触发预热
|
|
32
|
+
if (aiCommand === 'opencode' && !isOpencodeRunning()) {
|
|
33
|
+
startOpencode().catch((err) => {
|
|
34
|
+
log.warn(`OpenCode SDK server prewarm failed (will retry on first use): ${err}`);
|
|
35
|
+
});
|
|
36
|
+
}
|
|
30
37
|
}
|
|
31
38
|
}
|
|
32
39
|
export function getAdapter(aiCommand) {
|
|
@@ -34,6 +41,8 @@ export function getAdapter(aiCommand) {
|
|
|
34
41
|
}
|
|
35
42
|
export function cleanupAdapters() {
|
|
36
43
|
ClaudeSDKAdapter.destroy();
|
|
44
|
+
// 关闭 opencode SDK server
|
|
45
|
+
stopOpencode();
|
|
37
46
|
// 强制终止仍在运行的 CLI 子进程(Codex/CodeBuddy),避免僵尸 / 孤儿
|
|
38
47
|
destroyAllLiveChildren();
|
|
39
48
|
adapters.clear();
|
|
@@ -109,7 +109,7 @@ export declare const PAGE_TEXTS: {
|
|
|
109
109
|
readonly claudeCli: "Claude CLI path";
|
|
110
110
|
readonly codexCli: "Codex CLI path";
|
|
111
111
|
readonly codebuddyCli: "CodeBuddy CLI path";
|
|
112
|
-
readonly
|
|
112
|
+
readonly opencodeSdkInfo: "OpenCode uses the built-in SDK integration. No CLI path needed.";
|
|
113
113
|
readonly codexProxy: "Codex proxy";
|
|
114
114
|
readonly codexApiKey: "OPENAI_API_KEY";
|
|
115
115
|
readonly codexApiKeyTip: "Set the OpenAI API key used by Codex. You can also edit the auth file below.";
|
|
@@ -320,7 +320,7 @@ export declare const PAGE_TEXTS: {
|
|
|
320
320
|
readonly claudeCli: "Claude CLI 路径";
|
|
321
321
|
readonly codexCli: "Codex CLI 路径";
|
|
322
322
|
readonly codebuddyCli: "CodeBuddy CLI 路径";
|
|
323
|
-
readonly
|
|
323
|
+
readonly opencodeSdkInfo: "OpenCode 使用 SDK 集成,无需配置 CLI 路径。";
|
|
324
324
|
readonly codexProxy: "Codex 代理";
|
|
325
325
|
readonly codexApiKey: "OPENAI_API_KEY";
|
|
326
326
|
readonly codexApiKeyTip: "设置 Codex 使用的 OpenAI API Key。也可以在下方编辑 auth 文件。";
|
|
@@ -109,7 +109,7 @@ export const PAGE_TEXTS = {
|
|
|
109
109
|
claudeCli: "Claude CLI path",
|
|
110
110
|
codexCli: "Codex CLI path",
|
|
111
111
|
codebuddyCli: "CodeBuddy CLI path",
|
|
112
|
-
|
|
112
|
+
opencodeSdkInfo: "OpenCode uses the built-in SDK integration. No CLI path needed.",
|
|
113
113
|
codexProxy: "Codex proxy",
|
|
114
114
|
codexApiKey: "OPENAI_API_KEY",
|
|
115
115
|
codexApiKeyTip: "Set the OpenAI API key used by Codex. You can also edit the auth file below.",
|
|
@@ -321,7 +321,7 @@ export const PAGE_TEXTS = {
|
|
|
321
321
|
claudeCli: "Claude CLI \u8def\u5f84",
|
|
322
322
|
codexCli: "Codex CLI \u8def\u5f84",
|
|
323
323
|
codebuddyCli: "CodeBuddy CLI \u8def\u5f84",
|
|
324
|
-
|
|
324
|
+
opencodeSdkInfo: "OpenCode \u4f7f\u7528 SDK \u96c6\u6210\uff0c\u65e0\u9700\u914d\u7f6e CLI \u8def\u5f84\u3002",
|
|
325
325
|
codexProxy: "Codex \u4ee3\u7406",
|
|
326
326
|
codexApiKey: "OPENAI_API_KEY",
|
|
327
327
|
codexApiKeyTip: "\u8bbe\u7f6e Codex \u4f7f\u7528\u7684 OpenAI API Key\u3002\u4e5f\u53ef\u4ee5\u5728\u4e0b\u65b9\u7f16\u8f91 auth \u6587\u4ef6\u3002",
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { OpencodeClient } from '@opencode-ai/sdk/v2/client';
|
|
2
|
+
export declare function startOpencode(options?: {
|
|
3
|
+
hostname?: string;
|
|
4
|
+
port?: number;
|
|
5
|
+
signal?: AbortSignal;
|
|
6
|
+
timeout?: number;
|
|
7
|
+
}): Promise<OpencodeClient>;
|
|
8
|
+
export declare function stopOpencode(): void;
|
|
9
|
+
export declare function getOpencodeClient(): OpencodeClient;
|
|
10
|
+
export declare function isOpencodeRunning(): boolean;
|
|
11
|
+
export declare function connectToOpencode(baseUrl?: string): Promise<OpencodeClient>;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { createOpencode, createOpencodeClient } from '@opencode-ai/sdk/v2';
|
|
2
|
+
import { createLogger } from '../logger.js';
|
|
3
|
+
const log = createLogger('OpenCodeSDK');
|
|
4
|
+
let instance = null;
|
|
5
|
+
export async function startOpencode(options) {
|
|
6
|
+
if (instance) {
|
|
7
|
+
log.info('OpenCode SDK already running');
|
|
8
|
+
return instance.client;
|
|
9
|
+
}
|
|
10
|
+
log.info('Starting OpenCode SDK server...');
|
|
11
|
+
const result = await createOpencode({
|
|
12
|
+
hostname: options?.hostname ?? '127.0.0.1',
|
|
13
|
+
port: options?.port ?? 0,
|
|
14
|
+
signal: options?.signal,
|
|
15
|
+
timeout: options?.timeout ?? 15000,
|
|
16
|
+
});
|
|
17
|
+
log.info(`OpenCode server started at ${result.server.url}`);
|
|
18
|
+
instance = result;
|
|
19
|
+
return instance.client;
|
|
20
|
+
}
|
|
21
|
+
export function stopOpencode() {
|
|
22
|
+
if (instance) {
|
|
23
|
+
log.info('Stopping OpenCode SDK server...');
|
|
24
|
+
instance.server.close();
|
|
25
|
+
instance = null;
|
|
26
|
+
log.info('OpenCode SDK server stopped');
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
export function getOpencodeClient() {
|
|
30
|
+
if (!instance) {
|
|
31
|
+
throw new Error('OpenCode SDK not started');
|
|
32
|
+
}
|
|
33
|
+
return instance.client;
|
|
34
|
+
}
|
|
35
|
+
export function isOpencodeRunning() {
|
|
36
|
+
return instance !== null;
|
|
37
|
+
}
|
|
38
|
+
export async function connectToOpencode(baseUrl) {
|
|
39
|
+
if (instance)
|
|
40
|
+
return instance.client;
|
|
41
|
+
const url = baseUrl ?? 'http://127.0.0.1:4096';
|
|
42
|
+
log.info(`Connecting to existing OpenCode server at ${url}`);
|
|
43
|
+
const client = createOpencodeClient({ baseUrl: url });
|
|
44
|
+
instance = { client, server: { url, close() { } } };
|
|
45
|
+
return client;
|
|
46
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export interface OpenCodeSdkRunCallbacks {
|
|
2
|
+
onText: (accumulated: string) => void;
|
|
3
|
+
onThinking?: (accumulated: string) => void;
|
|
4
|
+
onToolUse?: (toolName: string, toolInput?: Record<string, unknown>) => void;
|
|
5
|
+
onComplete: (result: {
|
|
6
|
+
success: boolean;
|
|
7
|
+
result: string;
|
|
8
|
+
accumulated: string;
|
|
9
|
+
cost: number;
|
|
10
|
+
durationMs: number;
|
|
11
|
+
model?: string;
|
|
12
|
+
numTurns: number;
|
|
13
|
+
toolStats: Record<string, number>;
|
|
14
|
+
}) => void;
|
|
15
|
+
onError: (error: string) => void;
|
|
16
|
+
onSessionId?: (sessionId: string) => void;
|
|
17
|
+
onSessionInvalid?: () => void;
|
|
18
|
+
}
|
|
19
|
+
export interface OpenCodeSdkRunOptions {
|
|
20
|
+
skipPermissions?: boolean;
|
|
21
|
+
model?: string;
|
|
22
|
+
}
|
|
23
|
+
export interface OpenCodeSdkRunHandle {
|
|
24
|
+
abort: () => void;
|
|
25
|
+
}
|
|
26
|
+
export declare function runOpenCodeSdk(prompt: string, sessionId: string | undefined, workDir: string, callbacks: OpenCodeSdkRunCallbacks, options?: OpenCodeSdkRunOptions): Promise<OpenCodeSdkRunHandle>;
|
|
27
|
+
export declare function clearSessionCache(workDir?: string): void;
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { startOpencode } from './sdk-manager.js';
|
|
2
|
+
import { createLogger } from '../logger.js';
|
|
3
|
+
const log = createLogger('OpenCodeSDKRunner');
|
|
4
|
+
const sessionCache = new Map();
|
|
5
|
+
function parseModel(modelStr) {
|
|
6
|
+
const slashIdx = modelStr.indexOf('/');
|
|
7
|
+
if (slashIdx > 0) {
|
|
8
|
+
return { providerID: modelStr.slice(0, slashIdx), modelID: modelStr.slice(slashIdx + 1) };
|
|
9
|
+
}
|
|
10
|
+
return { providerID: 'anthropic', modelID: modelStr };
|
|
11
|
+
}
|
|
12
|
+
export async function runOpenCodeSdk(prompt, sessionId, workDir, callbacks, options) {
|
|
13
|
+
const client = await startOpencode();
|
|
14
|
+
const abortController = new AbortController();
|
|
15
|
+
const startTime = Date.now();
|
|
16
|
+
let currentSessionId = sessionId;
|
|
17
|
+
if (!currentSessionId) {
|
|
18
|
+
currentSessionId = sessionCache.get(workDir);
|
|
19
|
+
}
|
|
20
|
+
if (!currentSessionId) {
|
|
21
|
+
try {
|
|
22
|
+
const projectName = workDir.split('/').filter(Boolean).pop() || 'project';
|
|
23
|
+
const result = await client.session.create({
|
|
24
|
+
directory: workDir,
|
|
25
|
+
title: `open-im: ${projectName}`,
|
|
26
|
+
});
|
|
27
|
+
currentSessionId = result.data?.id;
|
|
28
|
+
if (!currentSessionId) {
|
|
29
|
+
throw new Error('Session creation returned no ID');
|
|
30
|
+
}
|
|
31
|
+
sessionCache.set(workDir, currentSessionId);
|
|
32
|
+
callbacks.onSessionId?.(currentSessionId);
|
|
33
|
+
log.info(`Created session ${currentSessionId} for ${workDir}`);
|
|
34
|
+
}
|
|
35
|
+
catch (err) {
|
|
36
|
+
const msg = (err instanceof Error ? err.message : String(err));
|
|
37
|
+
log.error('Failed to create session:', msg);
|
|
38
|
+
callbacks.onError(msg);
|
|
39
|
+
return { abort: () => { } };
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
log.info(`Using ${sessionId ? 'provided' : 'cached'} session ${currentSessionId}`);
|
|
44
|
+
}
|
|
45
|
+
let accumulatedText = '';
|
|
46
|
+
let accumulatedThinking = '';
|
|
47
|
+
const toolStats = {};
|
|
48
|
+
let runSettled = false;
|
|
49
|
+
const subscribeEvents = async () => {
|
|
50
|
+
try {
|
|
51
|
+
const sse = await client.global.event({ signal: abortController.signal });
|
|
52
|
+
for await (const raw of sse.stream) {
|
|
53
|
+
const ev = raw;
|
|
54
|
+
const payload = ev?.payload;
|
|
55
|
+
if (!payload?.properties)
|
|
56
|
+
continue;
|
|
57
|
+
if (payload.properties.sessionID !== currentSessionId)
|
|
58
|
+
continue;
|
|
59
|
+
switch (payload.type) {
|
|
60
|
+
case 'session.next.text.delta': {
|
|
61
|
+
const delta = payload.properties.delta;
|
|
62
|
+
if (delta) {
|
|
63
|
+
accumulatedText += delta;
|
|
64
|
+
callbacks.onText(accumulatedText);
|
|
65
|
+
}
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
case 'session.next.reasoning.delta': {
|
|
69
|
+
const delta = payload.properties.delta;
|
|
70
|
+
if (delta) {
|
|
71
|
+
accumulatedThinking += delta;
|
|
72
|
+
callbacks.onThinking?.(accumulatedThinking);
|
|
73
|
+
}
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
case 'session.next.tool.called': {
|
|
77
|
+
const tool = payload.properties.tool;
|
|
78
|
+
if (tool) {
|
|
79
|
+
toolStats[tool] = (toolStats[tool] || 0) + 1;
|
|
80
|
+
callbacks.onToolUse?.(tool, payload.properties.input);
|
|
81
|
+
}
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
catch (err) {
|
|
88
|
+
if (!abortController.signal.aborted) {
|
|
89
|
+
log.debug('SSE stream ended:', err?.message);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
const background = subscribeEvents();
|
|
94
|
+
try {
|
|
95
|
+
const result = await client.session.prompt({
|
|
96
|
+
sessionID: currentSessionId,
|
|
97
|
+
directory: workDir,
|
|
98
|
+
parts: [{ type: 'text', text: prompt }],
|
|
99
|
+
...(options?.model ? { model: parseModel(options.model) } : {}),
|
|
100
|
+
});
|
|
101
|
+
abortController.abort();
|
|
102
|
+
await background.catch(() => { });
|
|
103
|
+
runSettled = true;
|
|
104
|
+
const data = result.data;
|
|
105
|
+
const parts = data?.parts ?? [];
|
|
106
|
+
const finalText = parts
|
|
107
|
+
.filter((p) => p.type === 'text' && typeof p.text === 'string')
|
|
108
|
+
.map(p => p.text)
|
|
109
|
+
.join('\n');
|
|
110
|
+
const cost = data?.info?.cost ?? 0;
|
|
111
|
+
callbacks.onComplete({
|
|
112
|
+
success: true,
|
|
113
|
+
result: finalText,
|
|
114
|
+
accumulated: accumulatedText || finalText,
|
|
115
|
+
cost,
|
|
116
|
+
durationMs: Date.now() - startTime,
|
|
117
|
+
numTurns: 1,
|
|
118
|
+
toolStats,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
catch (err) {
|
|
122
|
+
abortController.abort();
|
|
123
|
+
await background.catch(() => { });
|
|
124
|
+
if (runSettled)
|
|
125
|
+
return { abort: () => { } };
|
|
126
|
+
const msg = (err instanceof Error ? err.message : String(err));
|
|
127
|
+
log.error('OpenCode SDK error:', msg);
|
|
128
|
+
if (/session\s*(not found|expired|corrupt)|no\s*session/i.test(msg)) {
|
|
129
|
+
sessionCache.delete(workDir);
|
|
130
|
+
callbacks.onSessionInvalid?.();
|
|
131
|
+
}
|
|
132
|
+
runSettled = true;
|
|
133
|
+
callbacks.onError(msg);
|
|
134
|
+
}
|
|
135
|
+
return {
|
|
136
|
+
abort: () => {
|
|
137
|
+
if (!abortController.signal.aborted) {
|
|
138
|
+
abortController.abort();
|
|
139
|
+
client.session.abort({ sessionID: currentSessionId }).catch(() => { });
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
export function clearSessionCache(workDir) {
|
|
145
|
+
if (workDir) {
|
|
146
|
+
sessionCache.delete(workDir);
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
sessionCache.clear();
|
|
150
|
+
}
|
|
151
|
+
}
|
package/dist/shared/ai-task.js
CHANGED
|
@@ -135,7 +135,8 @@ export function runAITask(deps, ctx, prompt, toolAdapter, platformAdapter) {
|
|
|
135
135
|
clearTimeout(pendingUpdate);
|
|
136
136
|
pendingUpdate = null;
|
|
137
137
|
}
|
|
138
|
-
|
|
138
|
+
// 只在 force=true(工具调用)时传 toolNote,普通文本更新不传
|
|
139
|
+
const toolNote = force && toolLines.length > 0 ? toolLines.slice(-3).join('\n') : undefined;
|
|
139
140
|
platformAdapter.streamUpdate(content, toolNote);
|
|
140
141
|
}
|
|
141
142
|
else if (!pendingUpdate) {
|
|
@@ -143,8 +144,7 @@ export function runAITask(deps, ctx, prompt, toolAdapter, platformAdapter) {
|
|
|
143
144
|
pendingUpdate = null;
|
|
144
145
|
lastUpdateTime = Date.now();
|
|
145
146
|
lastSentContentLength = taskState.latestContent.length;
|
|
146
|
-
|
|
147
|
-
platformAdapter.streamUpdate(taskState.latestContent, toolNote);
|
|
147
|
+
platformAdapter.streamUpdate(taskState.latestContent, undefined);
|
|
148
148
|
}, platformAdapter.throttleMs - elapsed);
|
|
149
149
|
}
|
|
150
150
|
};
|
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.3",
|
|
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",
|
|
@@ -55,6 +55,7 @@
|
|
|
55
55
|
"@anthropic-ai/sdk": "^0.104.2",
|
|
56
56
|
"@aws-sdk/client-s3": "^3.1035.0",
|
|
57
57
|
"@larksuiteoapi/node-sdk": "^1.59.0",
|
|
58
|
+
"@opencode-ai/sdk": "^1.17.9",
|
|
58
59
|
"@sentry/node": "^10.58.0",
|
|
59
60
|
"centrifuge": "^5.5.3",
|
|
60
61
|
"dingtalk-stream": "^2.1.4",
|