@aipanel/provider-opencode 1.2.0-beta.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.
- package/es/api.d.ts +16 -0
- package/es/api.js +225 -0
- package/es/bridge-script.d.ts +14 -0
- package/es/bridge-script.js +683 -0
- package/es/constants.d.ts +32 -0
- package/es/constants.js +29 -0
- package/es/index.d.ts +15 -0
- package/es/index.js +24 -0
- package/es/opencode-web.d.ts +4 -0
- package/es/opencode-web.js +276 -0
- package/es/provider.d.ts +49 -0
- package/es/provider.js +257 -0
- package/es/system.d.ts +3 -0
- package/es/system.js +175 -0
- package/es/types.d.ts +160 -0
- package/es/types.js +0 -0
- package/lib/api.cjs +251 -0
- package/lib/api.d.ts +16 -0
- package/lib/bridge-script.cjs +706 -0
- package/lib/bridge-script.d.ts +14 -0
- package/lib/constants.cjs +55 -0
- package/lib/constants.d.ts +32 -0
- package/lib/index.cjs +55 -0
- package/lib/index.d.ts +15 -0
- package/lib/opencode-web.cjs +302 -0
- package/lib/opencode-web.d.ts +4 -0
- package/lib/provider.cjs +289 -0
- package/lib/provider.d.ts +49 -0
- package/lib/system.cjs +200 -0
- package/lib/system.d.ts +3 -0
- package/lib/types.cjs +15 -0
- package/lib/types.d.ts +160 -0
- package/package.json +32 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenCode Provider 专属常量与默认值
|
|
3
|
+
* 与 OpenCode Web 绑定的常量自包含于此,核心层不感知。
|
|
4
|
+
*/
|
|
5
|
+
import type { OpenCodeProviderOptions } from "./types";
|
|
6
|
+
/** ==================== OpenCode localStorage 键 ==================== */
|
|
7
|
+
/** OpenCode localStorage 配置键 */
|
|
8
|
+
export declare const OPENCODE_STORAGE_KEYS: {
|
|
9
|
+
/** 设置键 (settings.v3) */
|
|
10
|
+
readonly SETTINGS: "settings.v3";
|
|
11
|
+
/** 配色方案键 */
|
|
12
|
+
readonly COLOR_SCHEME: "opencode-color-scheme";
|
|
13
|
+
/** 主题 ID 键 */
|
|
14
|
+
readonly THEME_ID: "opencode-theme-id";
|
|
15
|
+
};
|
|
16
|
+
/** ==================== OpenCode 默认设置 ==================== */
|
|
17
|
+
/** OpenCode 默认设置(与 OpenCode Web localStorage settings.v3 对应) */
|
|
18
|
+
export declare const DEFAULT_OPENCODE_SETTINGS: {
|
|
19
|
+
general: {
|
|
20
|
+
showReasoningSummaries: boolean;
|
|
21
|
+
newLayoutDesigns: boolean;
|
|
22
|
+
showFileTree: boolean;
|
|
23
|
+
editToolPartsExpanded: boolean;
|
|
24
|
+
shellToolPartsExpanded: boolean;
|
|
25
|
+
};
|
|
26
|
+
};
|
|
27
|
+
/** ==================== 运行环境 ==================== */
|
|
28
|
+
/** OpenCode 缓存目录(相对于项目根目录,存放 opencode.json 等运行状态) */
|
|
29
|
+
export declare const OPENCODE_CACHE_DIR = "node_modules/.cache/opencode";
|
|
30
|
+
/** ==================== Provider 专属配置默认值 ==================== */
|
|
31
|
+
/** OpenCode Provider 专属配置默认值(插件组装 config 时使用) */
|
|
32
|
+
export declare const DEFAULT_OPENCODE_PROVIDER_OPTIONS: OpenCodeProviderOptions;
|
package/es/constants.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
const OPENCODE_STORAGE_KEYS = {
|
|
2
|
+
/** 设置键 (settings.v3) */
|
|
3
|
+
SETTINGS: "settings.v3",
|
|
4
|
+
/** 配色方案键 */
|
|
5
|
+
COLOR_SCHEME: "opencode-color-scheme",
|
|
6
|
+
/** 主题 ID 键 */
|
|
7
|
+
THEME_ID: "opencode-theme-id"
|
|
8
|
+
};
|
|
9
|
+
const DEFAULT_OPENCODE_SETTINGS = {
|
|
10
|
+
general: {
|
|
11
|
+
showReasoningSummaries: true,
|
|
12
|
+
newLayoutDesigns: true,
|
|
13
|
+
showFileTree: false,
|
|
14
|
+
editToolPartsExpanded: true,
|
|
15
|
+
shellToolPartsExpanded: true
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
const OPENCODE_CACHE_DIR = "node_modules/.cache/opencode";
|
|
19
|
+
const DEFAULT_OPENCODE_PROVIDER_OPTIONS = {
|
|
20
|
+
enableLsp: true,
|
|
21
|
+
enableBlockOnError: false,
|
|
22
|
+
enablePrettier: true
|
|
23
|
+
};
|
|
24
|
+
export {
|
|
25
|
+
DEFAULT_OPENCODE_PROVIDER_OPTIONS,
|
|
26
|
+
DEFAULT_OPENCODE_SETTINGS,
|
|
27
|
+
OPENCODE_CACHE_DIR,
|
|
28
|
+
OPENCODE_STORAGE_KEYS
|
|
29
|
+
};
|
package/es/index.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenCode Web Provider
|
|
3
|
+
* 实现 WebProvider 契约:进程管理、REST 会话 API、桥接脚本、CLI 环境检查。
|
|
4
|
+
* 所有 OpenCode 专属类型与常量自包含于此包。
|
|
5
|
+
*/
|
|
6
|
+
import type { ProviderInitContext, WebProvider } from "@aipanel/core";
|
|
7
|
+
/** 约定工厂:核心层动态加载本包后调用,初始化动作完全由 Provider 定义 */
|
|
8
|
+
export declare function createProvider(ctx: ProviderInitContext): WebProvider;
|
|
9
|
+
export { OpenCodeAPI } from "./api";
|
|
10
|
+
export type { DefaultWebProviderConfig, DefaultWebProviderDeps } from "./provider";
|
|
11
|
+
export { prepareOpenCodeRuntime, startOpenCodeWeb } from "./opencode-web";
|
|
12
|
+
export { generateBridgeScript, type BridgeScriptOptions } from "./bridge-script";
|
|
13
|
+
export { checkOpenCodeInstalled, getOpenCodeVersion, killOrphanOpenCodeProcesses } from "./system";
|
|
14
|
+
export { DEFAULT_OPENCODE_PROVIDER_OPTIONS } from "./constants";
|
|
15
|
+
export type { OpenCodeProviderOptions, OpenCodeLanguage, OpenCodeSettings, SessionInfo, WebOptions, } from "./types";
|
package/es/index.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { DefaultWebProvider } from "./provider.js";
|
|
2
|
+
function createProvider(ctx) {
|
|
3
|
+
return new DefaultWebProvider(
|
|
4
|
+
{ hostname: ctx.hostname, chromeDevtoolsPort: ctx.chromeDevtoolsPort },
|
|
5
|
+
{ getWebPort: ctx.getWebPort, getProxyPort: ctx.getProxyPort },
|
|
6
|
+
ctx.options
|
|
7
|
+
);
|
|
8
|
+
}
|
|
9
|
+
import { OpenCodeAPI } from "./api.js";
|
|
10
|
+
import { prepareOpenCodeRuntime, startOpenCodeWeb } from "./opencode-web.js";
|
|
11
|
+
import { generateBridgeScript } from "./bridge-script.js";
|
|
12
|
+
import { checkOpenCodeInstalled, getOpenCodeVersion, killOrphanOpenCodeProcesses } from "./system.js";
|
|
13
|
+
import { DEFAULT_OPENCODE_PROVIDER_OPTIONS } from "./constants.js";
|
|
14
|
+
export {
|
|
15
|
+
DEFAULT_OPENCODE_PROVIDER_OPTIONS,
|
|
16
|
+
OpenCodeAPI,
|
|
17
|
+
checkOpenCodeInstalled,
|
|
18
|
+
createProvider,
|
|
19
|
+
generateBridgeScript,
|
|
20
|
+
getOpenCodeVersion,
|
|
21
|
+
killOrphanOpenCodeProcesses,
|
|
22
|
+
prepareOpenCodeRuntime,
|
|
23
|
+
startOpenCodeWeb
|
|
24
|
+
};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { ResultPromise } from "execa";
|
|
2
|
+
import type { WebOptions } from "./types";
|
|
3
|
+
export declare function prepareOpenCodeRuntime(cwd: string, vitePort: number, enableLsp?: boolean, enablePrettier?: boolean): string;
|
|
4
|
+
export declare function startOpenCodeWeb(options: WebOptions): ResultPromise;
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import { execa } from "execa";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { pathToFileURL } from "url";
|
|
5
|
+
import { OPENCODE_CACHE_DIR } from "./constants.js";
|
|
6
|
+
import {
|
|
7
|
+
MCP_API_PATH,
|
|
8
|
+
VSCODE_EXTENSION_PORT,
|
|
9
|
+
ENV_VSCODE_PORT,
|
|
10
|
+
createLogger,
|
|
11
|
+
getProcessLogBuffer,
|
|
12
|
+
createPackageRequire,
|
|
13
|
+
resolvePackageDir
|
|
14
|
+
} from "@aipanel/core/node";
|
|
15
|
+
const require2 = createPackageRequire();
|
|
16
|
+
const packageDir = resolvePackageDir("@aipanel/opencode-plugins");
|
|
17
|
+
const log = createLogger("OpenCodeWeb");
|
|
18
|
+
function prepareOpenCodeRuntime(cwd, vitePort, enableLsp, enablePrettier) {
|
|
19
|
+
const cacheDir = path.join(cwd, OPENCODE_CACHE_DIR);
|
|
20
|
+
log.debug("Setting up OpenCode runtime", { cacheDir, enableLsp });
|
|
21
|
+
if (!fs.existsSync(cacheDir)) {
|
|
22
|
+
fs.mkdirSync(cacheDir, { recursive: true });
|
|
23
|
+
}
|
|
24
|
+
const sourcePluginsDir = resolveSourcePluginsDir();
|
|
25
|
+
const plugins = resolvePluginEntries(sourcePluginsDir);
|
|
26
|
+
const formatterConfig = buildFormatterConfig(enablePrettier);
|
|
27
|
+
const opencodeConfigPath = path.join(cacheDir, "opencode.json");
|
|
28
|
+
const config = {
|
|
29
|
+
plugin: plugins,
|
|
30
|
+
formatter: formatterConfig,
|
|
31
|
+
mcp: {
|
|
32
|
+
"chrome-devtools": {
|
|
33
|
+
type: "remote",
|
|
34
|
+
url: `http://localhost:${vitePort}${MCP_API_PATH}`
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
fs.writeFileSync(opencodeConfigPath, JSON.stringify(config, null, 2));
|
|
39
|
+
log.debug("OpenCode runtime ready", {
|
|
40
|
+
cacheDir,
|
|
41
|
+
opencodeConfigPath,
|
|
42
|
+
pluginCount: plugins.length
|
|
43
|
+
});
|
|
44
|
+
return cacheDir;
|
|
45
|
+
}
|
|
46
|
+
function startOpenCodeWeb(options) {
|
|
47
|
+
const {
|
|
48
|
+
port,
|
|
49
|
+
hostname,
|
|
50
|
+
cwd,
|
|
51
|
+
configDir,
|
|
52
|
+
corsOrigins,
|
|
53
|
+
contextApiUrl,
|
|
54
|
+
logsApiUrl,
|
|
55
|
+
logFilesJson,
|
|
56
|
+
enableBlockOnError,
|
|
57
|
+
verbose,
|
|
58
|
+
enableLsp,
|
|
59
|
+
vueDevtoolsApiUrl
|
|
60
|
+
} = options;
|
|
61
|
+
const stateDir = createStateDirectory(cwd);
|
|
62
|
+
log.debug("Building process environment", {
|
|
63
|
+
stateDir,
|
|
64
|
+
configDir,
|
|
65
|
+
contextApiUrl,
|
|
66
|
+
logsApiUrl,
|
|
67
|
+
logFilesJson,
|
|
68
|
+
enableBlockOnError,
|
|
69
|
+
verbose,
|
|
70
|
+
enableLsp
|
|
71
|
+
});
|
|
72
|
+
const env = buildProcessEnv(
|
|
73
|
+
stateDir,
|
|
74
|
+
configDir,
|
|
75
|
+
contextApiUrl,
|
|
76
|
+
logsApiUrl,
|
|
77
|
+
logFilesJson,
|
|
78
|
+
enableBlockOnError,
|
|
79
|
+
verbose,
|
|
80
|
+
enableLsp,
|
|
81
|
+
vueDevtoolsApiUrl,
|
|
82
|
+
cwd
|
|
83
|
+
);
|
|
84
|
+
const args = ["serve", "--port", String(port), "--hostname", hostname];
|
|
85
|
+
if (corsOrigins && corsOrigins.length > 0) {
|
|
86
|
+
corsOrigins.forEach((origin) => {
|
|
87
|
+
args.push("--cors", origin);
|
|
88
|
+
});
|
|
89
|
+
log.debug("CORS origins added", { origins: corsOrigins });
|
|
90
|
+
}
|
|
91
|
+
log.debug("Spawning OpenCode process", {
|
|
92
|
+
command: "opencode",
|
|
93
|
+
args: args.join(" "),
|
|
94
|
+
cwd
|
|
95
|
+
});
|
|
96
|
+
const proc = execa("opencode", args, {
|
|
97
|
+
cwd,
|
|
98
|
+
env,
|
|
99
|
+
reject: false,
|
|
100
|
+
cleanup: true,
|
|
101
|
+
shell: true
|
|
102
|
+
});
|
|
103
|
+
proc.stdout?.on("data", (data) => {
|
|
104
|
+
const output = data.toString().trim();
|
|
105
|
+
if (output) {
|
|
106
|
+
log.debug("[OpenCode stdout]", { output });
|
|
107
|
+
getProcessLogBuffer().addProviderStdout(output);
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
proc.stderr?.on("data", (data) => {
|
|
111
|
+
const output = data.toString().trim();
|
|
112
|
+
if (output) {
|
|
113
|
+
if (output.includes("MaxListenersExceededWarning")) return;
|
|
114
|
+
log.warn("[OpenCode stderr]", { output });
|
|
115
|
+
getProcessLogBuffer().addProviderStderr(output);
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
return proc;
|
|
119
|
+
}
|
|
120
|
+
function createStateDirectory(cwd) {
|
|
121
|
+
const stateDir = path.join(cwd, OPENCODE_CACHE_DIR);
|
|
122
|
+
if (!fs.existsSync(stateDir)) {
|
|
123
|
+
fs.mkdirSync(stateDir, { recursive: true });
|
|
124
|
+
log.debug("Created state directory", { stateDir });
|
|
125
|
+
}
|
|
126
|
+
return stateDir;
|
|
127
|
+
}
|
|
128
|
+
function buildFormatterConfig(enablePrettier) {
|
|
129
|
+
if (enablePrettier === false) {
|
|
130
|
+
log.debug("enablePrettier is false, formatter disabled");
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
const bridgePath = resolveFormatBridgePath();
|
|
134
|
+
if (!bridgePath) {
|
|
135
|
+
log.debug("format-bridge not found, using built-in formatters");
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
log.debug("Format bridge configured");
|
|
139
|
+
if (!isFormatServiceRunning()) {
|
|
140
|
+
log.debug("VS Code format service not running, using built-in formatters only");
|
|
141
|
+
return true;
|
|
142
|
+
}
|
|
143
|
+
log.debug("VS Code format service detected, enabling bridge");
|
|
144
|
+
log.info("\u5DF2\u8FDE\u63A5 VS Code \u683C\u5F0F\u5316\u670D\u52A1");
|
|
145
|
+
const extensions = [
|
|
146
|
+
".ts",
|
|
147
|
+
".tsx",
|
|
148
|
+
".mts",
|
|
149
|
+
".cts",
|
|
150
|
+
".js",
|
|
151
|
+
".jsx",
|
|
152
|
+
".mjs",
|
|
153
|
+
".cjs",
|
|
154
|
+
".vue",
|
|
155
|
+
".svelte",
|
|
156
|
+
".astro",
|
|
157
|
+
".css",
|
|
158
|
+
".scss",
|
|
159
|
+
".sass",
|
|
160
|
+
".less",
|
|
161
|
+
".pcss",
|
|
162
|
+
".html",
|
|
163
|
+
".htm",
|
|
164
|
+
".xml",
|
|
165
|
+
".svg",
|
|
166
|
+
".json",
|
|
167
|
+
".jsonc",
|
|
168
|
+
".yaml",
|
|
169
|
+
".yml",
|
|
170
|
+
".toml",
|
|
171
|
+
".md",
|
|
172
|
+
".mdx",
|
|
173
|
+
".graphql",
|
|
174
|
+
".gql"
|
|
175
|
+
];
|
|
176
|
+
return {
|
|
177
|
+
format_bridge: {
|
|
178
|
+
command: ["node", bridgePath, "$FILE"],
|
|
179
|
+
extensions
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
let _formatServiceRunning;
|
|
184
|
+
function isFormatServiceRunning() {
|
|
185
|
+
if (_formatServiceRunning !== void 0) return _formatServiceRunning;
|
|
186
|
+
try {
|
|
187
|
+
require2("child_process").execSync(
|
|
188
|
+
`node -e "const h=require('http');h.get('http://127.0.0.1:${VSCODE_EXTENSION_PORT}/health',r=>{r.resume();process.exit(r.statusCode===200?0:1)}).on('error',()=>process.exit(1))"`,
|
|
189
|
+
{ timeout: 500, stdio: "ignore" }
|
|
190
|
+
);
|
|
191
|
+
_formatServiceRunning = true;
|
|
192
|
+
} catch {
|
|
193
|
+
_formatServiceRunning = false;
|
|
194
|
+
}
|
|
195
|
+
return _formatServiceRunning;
|
|
196
|
+
}
|
|
197
|
+
function resolveFormatBridgePath() {
|
|
198
|
+
const viteEntry = require2.resolve("vite-plugin-aipanel");
|
|
199
|
+
const bridgePath = path.resolve(path.dirname(viteEntry), "utils", "format-bridge.cjs");
|
|
200
|
+
if (fs.existsSync(bridgePath)) return bridgePath;
|
|
201
|
+
return void 0;
|
|
202
|
+
}
|
|
203
|
+
function resolveSourcePluginsDir() {
|
|
204
|
+
const candidatePaths = [path.join(packageDir, "es", "plugins")];
|
|
205
|
+
for (const candidatePath of candidatePaths) {
|
|
206
|
+
if (fs.existsSync(candidatePath)) {
|
|
207
|
+
return candidatePath;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return candidatePaths[0];
|
|
211
|
+
}
|
|
212
|
+
const MIGRATED_TO_MCP_PLUGINS = /* @__PURE__ */ new Set(["vue-devtools.js", "vite-logs.js", "service-logs.js"]);
|
|
213
|
+
function resolvePluginEntries(sourceDir) {
|
|
214
|
+
const files = fs.readdirSync(sourceDir).filter((f) => f.endsWith(".js") && !MIGRATED_TO_MCP_PLUGINS.has(f));
|
|
215
|
+
const entries = files.map((file) => {
|
|
216
|
+
const absolutePath = path.join(sourceDir, file);
|
|
217
|
+
return pathToFileURL(absolutePath).href;
|
|
218
|
+
});
|
|
219
|
+
log.debug("Resolved plugin entries", { count: entries.length, entries });
|
|
220
|
+
return entries;
|
|
221
|
+
}
|
|
222
|
+
function buildProcessEnv(stateDir, configDir, contextApiUrl, logsApiUrl, logFilesJson, enableBlockOnError, verbose, enableLsp, vueDevtoolsApiUrl, workspace) {
|
|
223
|
+
const env = {
|
|
224
|
+
...Object.fromEntries(
|
|
225
|
+
Object.entries(process.env).filter(([, v]) => v !== void 0)
|
|
226
|
+
),
|
|
227
|
+
XDG_STATE_HOME: stateDir,
|
|
228
|
+
// 指向缓存目录,OpenCode 通过 opencode.json 中 plugins 字段加载插件
|
|
229
|
+
OPENCODE_CONFIG_DIR: stateDir
|
|
230
|
+
};
|
|
231
|
+
if (configDir) {
|
|
232
|
+
env.OPENCODE_CONFIG_DIR = configDir;
|
|
233
|
+
log.debug("Set OPENCODE_CONFIG_DIR", { configDir });
|
|
234
|
+
}
|
|
235
|
+
if (contextApiUrl) {
|
|
236
|
+
env.OPENCODE_CONTEXT_API_URL = contextApiUrl;
|
|
237
|
+
log.debug("Set OPENCODE_CONTEXT_API_URL", { contextApiUrl });
|
|
238
|
+
}
|
|
239
|
+
if (logsApiUrl) {
|
|
240
|
+
env.OPENCODE_VITE_LOGS_API_URL = logsApiUrl;
|
|
241
|
+
log.debug("Set OPENCODE_VITE_LOGS_API_URL", { logsApiUrl });
|
|
242
|
+
}
|
|
243
|
+
if (logFilesJson) {
|
|
244
|
+
env.OPENCODE_LOG_FILES_JSON = logFilesJson;
|
|
245
|
+
log.debug("Set OPENCODE_LOG_FILES_JSON", { logFilesJson });
|
|
246
|
+
}
|
|
247
|
+
if (enableBlockOnError) {
|
|
248
|
+
env.OPENCODE_BLOCK_ON_ERROR = "1";
|
|
249
|
+
log.debug("Set OPENCODE_BLOCK_ON_ERROR=1");
|
|
250
|
+
}
|
|
251
|
+
if (verbose) {
|
|
252
|
+
env.OPENCODE_VERBOSE = "1";
|
|
253
|
+
log.debug("Set OPENCODE_VERBOSE=1");
|
|
254
|
+
}
|
|
255
|
+
if (enableLsp) {
|
|
256
|
+
env.OPENCODE_ENABLE_LINT = "1";
|
|
257
|
+
log.debug("Set OPENCODE_ENABLE_LINT=1");
|
|
258
|
+
}
|
|
259
|
+
if (vueDevtoolsApiUrl) {
|
|
260
|
+
env.OPENCODE_VUE_DEVTOOLS_API_URL = vueDevtoolsApiUrl;
|
|
261
|
+
log.debug("Set OPENCODE_VUE_DEVTOOLS_API_URL", { vueDevtoolsApiUrl });
|
|
262
|
+
}
|
|
263
|
+
if (workspace) {
|
|
264
|
+
env.OPENCODE_WORKSPACE = workspace;
|
|
265
|
+
log.debug("Set OPENCODE_WORKSPACE", { workspace });
|
|
266
|
+
}
|
|
267
|
+
if (isFormatServiceRunning()) {
|
|
268
|
+
env[ENV_VSCODE_PORT] = String(VSCODE_EXTENSION_PORT);
|
|
269
|
+
log.debug("Set OPENCODE_VSCODE_PORT");
|
|
270
|
+
}
|
|
271
|
+
return env;
|
|
272
|
+
}
|
|
273
|
+
export {
|
|
274
|
+
prepareOpenCodeRuntime,
|
|
275
|
+
startOpenCodeWeb
|
|
276
|
+
};
|
package/es/provider.d.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { ChatSession, ProviderConfig, ProviderEnvironmentInfo, ProviderEvent, ProviderStartOptions, ProviderStartResult, WebProvider } from "@aipanel/core";
|
|
2
|
+
/** DefaultWebProvider 构造配置(核心层传入的运行时参数) */
|
|
3
|
+
export interface DefaultWebProviderConfig {
|
|
4
|
+
/** 服务主机名 */
|
|
5
|
+
hostname: string;
|
|
6
|
+
/** Chrome DevTools Protocol 端口 */
|
|
7
|
+
chromeDevtoolsPort: number;
|
|
8
|
+
}
|
|
9
|
+
/** DefaultWebProvider 构造依赖(端口等运行时状态由编排层提供) */
|
|
10
|
+
export interface DefaultWebProviderDeps {
|
|
11
|
+
/** 实际 Web 端口读取器 */
|
|
12
|
+
getWebPort: () => number;
|
|
13
|
+
/** 实际代理端口读取器 */
|
|
14
|
+
getProxyPort: () => number;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* 默认 Web Provider
|
|
18
|
+
* 组合 CLI 进程管理、REST API、桥接脚本,向核心层暴露 WebProvider 契约。
|
|
19
|
+
*/
|
|
20
|
+
export declare class DefaultWebProvider implements WebProvider {
|
|
21
|
+
private config;
|
|
22
|
+
readonly id = "opencode";
|
|
23
|
+
readonly displayName = "OpenCode Web";
|
|
24
|
+
/** 支持会话 URL 深链;支持代码审查面板(右上角 </> 按钮,由 bridge 渲染) */
|
|
25
|
+
readonly capabilities: {
|
|
26
|
+
readonly deepLink: true;
|
|
27
|
+
readonly reviewPanel: true;
|
|
28
|
+
};
|
|
29
|
+
/** REST 会话 API(Provider 内部使用) */
|
|
30
|
+
private readonly api;
|
|
31
|
+
private deps;
|
|
32
|
+
private process;
|
|
33
|
+
private bridgeOptions;
|
|
34
|
+
private readonly opts;
|
|
35
|
+
constructor(config: DefaultWebProviderConfig, deps: DefaultWebProviderDeps, options?: Record<string, unknown>);
|
|
36
|
+
/** 代理注入到 HTML 的桥接脚本(Provider 资产) */
|
|
37
|
+
get bridgeScript(): string | undefined;
|
|
38
|
+
/** 初始化桥接配置(主题/语言/设置) */
|
|
39
|
+
applyConfig(config: ProviderConfig): void;
|
|
40
|
+
checkEnvironment(): Promise<ProviderEnvironmentInfo>;
|
|
41
|
+
start(options: ProviderStartOptions): Promise<ProviderStartResult>;
|
|
42
|
+
stop(): Promise<void>;
|
|
43
|
+
killOrphans(): Promise<number>;
|
|
44
|
+
listSessions(projectDir: string): Promise<ChatSession[]>;
|
|
45
|
+
createSession(projectDir: string, title?: string): Promise<ChatSession>;
|
|
46
|
+
deleteSession(sessionId: string): Promise<void>;
|
|
47
|
+
buildSessionUrl(projectDir: string, sessionId: string): string;
|
|
48
|
+
subscribeEvents(handler: (e: ProviderEvent) => void): () => void;
|
|
49
|
+
}
|
package/es/provider.js
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
3
|
+
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
4
|
+
import http from "http";
|
|
5
|
+
import { RETRY_DELAY } from "@aipanel/core";
|
|
6
|
+
import { createLogger } from "@aipanel/core/node";
|
|
7
|
+
import { DEFAULT_OPENCODE_PROVIDER_OPTIONS } from "./constants.js";
|
|
8
|
+
import { OpenCodeAPI } from "./api.js";
|
|
9
|
+
import { generateBridgeScript } from "./bridge-script.js";
|
|
10
|
+
import { prepareOpenCodeRuntime, startOpenCodeWeb } from "./opencode-web.js";
|
|
11
|
+
import { checkOpenCodeInstalled, getOpenCodeVersion, killOrphanOpenCodeProcesses } from "./system.js";
|
|
12
|
+
const log = createLogger("DefaultWebProvider");
|
|
13
|
+
class DefaultWebProvider {
|
|
14
|
+
constructor(config, deps, options) {
|
|
15
|
+
__publicField(this, "config", config);
|
|
16
|
+
__publicField(this, "id", "opencode");
|
|
17
|
+
__publicField(this, "displayName", "OpenCode Web");
|
|
18
|
+
/** 支持会话 URL 深链;支持代码审查面板(右上角 </> 按钮,由 bridge 渲染) */
|
|
19
|
+
__publicField(this, "capabilities", { deepLink: true, reviewPanel: true });
|
|
20
|
+
/** REST 会话 API(Provider 内部使用) */
|
|
21
|
+
__publicField(this, "api");
|
|
22
|
+
__publicField(this, "deps");
|
|
23
|
+
__publicField(this, "process", null);
|
|
24
|
+
__publicField(this, "bridgeOptions", {});
|
|
25
|
+
__publicField(this, "opts");
|
|
26
|
+
this.deps = deps;
|
|
27
|
+
this.opts = resolveOpenCodeOptions(options);
|
|
28
|
+
this.api = new OpenCodeAPI(
|
|
29
|
+
config.hostname,
|
|
30
|
+
deps.getWebPort,
|
|
31
|
+
deps.getProxyPort,
|
|
32
|
+
config.chromeDevtoolsPort
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
/** 代理注入到 HTML 的桥接脚本(Provider 资产) */
|
|
36
|
+
get bridgeScript() {
|
|
37
|
+
return generateBridgeScript(this.bridgeOptions);
|
|
38
|
+
}
|
|
39
|
+
/** 初始化桥接配置(主题/语言/设置) */
|
|
40
|
+
applyConfig(config) {
|
|
41
|
+
this.bridgeOptions = {
|
|
42
|
+
theme: config.theme,
|
|
43
|
+
language: this.opts.language,
|
|
44
|
+
settings: this.opts.settings
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
async checkEnvironment() {
|
|
48
|
+
if (!await checkOpenCodeInstalled()) {
|
|
49
|
+
return {
|
|
50
|
+
ok: false,
|
|
51
|
+
message: `OpenCode is not installed!
|
|
52
|
+
|
|
53
|
+
Please install OpenCode first:
|
|
54
|
+
|
|
55
|
+
# YOLO
|
|
56
|
+
curl -fsSL https://opencode.ai/install | bash
|
|
57
|
+
|
|
58
|
+
# Package managers
|
|
59
|
+
npm i -g opencode-ai@latest # or bun/pnpm/yarn
|
|
60
|
+
scoop install opencode # Windows
|
|
61
|
+
choco install opencode # Windows
|
|
62
|
+
brew install anomalyco/tap/opencode # macOS and Linux (recommended, always up to date)
|
|
63
|
+
brew install opencode # macOS and Linux (official brew formula, updated less)
|
|
64
|
+
sudo pacman -S opencode # Arch Linux (Stable)
|
|
65
|
+
paru -S opencode-bin # Arch Linux (Latest from AUR)
|
|
66
|
+
mise use -g opencode # Any OS
|
|
67
|
+
nix run nixpkgs#opencode # or github:anomalyco/opencode for latest dev branch
|
|
68
|
+
`
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
const version = await getOpenCodeVersion();
|
|
72
|
+
return { ok: true, version: version ?? void 0 };
|
|
73
|
+
}
|
|
74
|
+
async start(options) {
|
|
75
|
+
log.debug("Preparing OpenCode runtime", { cwd: options.cwd, vitePort: options.vitePort });
|
|
76
|
+
const configDir = prepareOpenCodeRuntime(
|
|
77
|
+
options.cwd,
|
|
78
|
+
options.vitePort,
|
|
79
|
+
this.opts.enableLsp,
|
|
80
|
+
this.opts.enablePrettier
|
|
81
|
+
);
|
|
82
|
+
log.debug("Starting OpenCode Web process", {
|
|
83
|
+
port: options.port,
|
|
84
|
+
hostname: options.hostname,
|
|
85
|
+
configDir
|
|
86
|
+
});
|
|
87
|
+
const proc = startOpenCodeWeb({
|
|
88
|
+
port: options.port,
|
|
89
|
+
hostname: options.hostname,
|
|
90
|
+
serverUrl: "",
|
|
91
|
+
cwd: options.cwd,
|
|
92
|
+
configDir,
|
|
93
|
+
corsOrigins: options.corsOrigins,
|
|
94
|
+
contextApiUrl: options.contextApiUrl,
|
|
95
|
+
logsApiUrl: options.logsApiUrl,
|
|
96
|
+
logFilesJson: this.opts.logFiles ? JSON.stringify(this.opts.logFiles) : void 0,
|
|
97
|
+
enableBlockOnError: this.opts.enableBlockOnError,
|
|
98
|
+
verbose: options.verbose,
|
|
99
|
+
enableLsp: this.opts.enableLsp,
|
|
100
|
+
enablePrettier: this.opts.enablePrettier,
|
|
101
|
+
vueDevtoolsApiUrl: options.vueDevtoolsApiUrl
|
|
102
|
+
});
|
|
103
|
+
this.process = proc;
|
|
104
|
+
return { url: `http://${options.hostname}:${options.port}`, processHandle: proc };
|
|
105
|
+
}
|
|
106
|
+
async stop() {
|
|
107
|
+
if (this.process) {
|
|
108
|
+
log.debug("Killing web process", { pid: this.process.pid });
|
|
109
|
+
this.process.kill("SIGTERM");
|
|
110
|
+
this.process = null;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
async killOrphans() {
|
|
114
|
+
return killOrphanOpenCodeProcesses();
|
|
115
|
+
}
|
|
116
|
+
async listSessions(projectDir) {
|
|
117
|
+
const sessions = await this.api.getSessions(projectDir);
|
|
118
|
+
return sessions.filter((s) => {
|
|
119
|
+
if (s.title === "__chrome_mcp_warmup__") return false;
|
|
120
|
+
if (s.parentID) return false;
|
|
121
|
+
if (s.time?.archived) return false;
|
|
122
|
+
return true;
|
|
123
|
+
}).map((s) => toChatSession(s));
|
|
124
|
+
}
|
|
125
|
+
async createSession(projectDir, title) {
|
|
126
|
+
const session = await this.api.createSession(projectDir, void 0, title);
|
|
127
|
+
return toChatSession(session);
|
|
128
|
+
}
|
|
129
|
+
async deleteSession(sessionId) {
|
|
130
|
+
await this.api.deleteSession(sessionId);
|
|
131
|
+
}
|
|
132
|
+
buildSessionUrl(projectDir, sessionId) {
|
|
133
|
+
return this.api.buildSessionProxyUrl(projectDir, sessionId);
|
|
134
|
+
}
|
|
135
|
+
subscribeEvents(handler) {
|
|
136
|
+
const port = this.deps.getWebPort();
|
|
137
|
+
const url = `http://${this.config.hostname}:${port}/global/event`;
|
|
138
|
+
log.debug("Subscribing to provider event stream", { url });
|
|
139
|
+
let aborted = false;
|
|
140
|
+
let currentReq = null;
|
|
141
|
+
let retryTimer = null;
|
|
142
|
+
const cleanup = () => {
|
|
143
|
+
if (retryTimer) {
|
|
144
|
+
clearTimeout(retryTimer);
|
|
145
|
+
retryTimer = null;
|
|
146
|
+
}
|
|
147
|
+
currentReq?.destroy();
|
|
148
|
+
currentReq = null;
|
|
149
|
+
};
|
|
150
|
+
const scheduleReconnect = () => {
|
|
151
|
+
if (aborted) return;
|
|
152
|
+
cleanup();
|
|
153
|
+
retryTimer = setTimeout(connect, RETRY_DELAY);
|
|
154
|
+
};
|
|
155
|
+
const connect = () => {
|
|
156
|
+
if (aborted) return;
|
|
157
|
+
const req = http.get(url, (res) => {
|
|
158
|
+
res.setEncoding("utf-8");
|
|
159
|
+
let buffer = "";
|
|
160
|
+
res.on("data", (chunk) => {
|
|
161
|
+
buffer += chunk;
|
|
162
|
+
const lines = buffer.split("\n");
|
|
163
|
+
buffer = lines.pop() ?? "";
|
|
164
|
+
for (const line of lines) {
|
|
165
|
+
const trimmed = line.trim();
|
|
166
|
+
if (!trimmed.startsWith("data:")) continue;
|
|
167
|
+
const data = trimmed.slice(5).trim();
|
|
168
|
+
if (!data) continue;
|
|
169
|
+
try {
|
|
170
|
+
const message = JSON.parse(data);
|
|
171
|
+
const event = mapEvent(message.payload);
|
|
172
|
+
if (event) handler(event);
|
|
173
|
+
} catch {
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
res.on("end", scheduleReconnect);
|
|
178
|
+
res.on("error", scheduleReconnect);
|
|
179
|
+
});
|
|
180
|
+
req.on("error", scheduleReconnect);
|
|
181
|
+
currentReq = req;
|
|
182
|
+
};
|
|
183
|
+
connect();
|
|
184
|
+
return () => {
|
|
185
|
+
aborted = true;
|
|
186
|
+
cleanup();
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
function mapEvent(payload) {
|
|
191
|
+
if (!payload || typeof payload !== "object") return null;
|
|
192
|
+
const msg = payload;
|
|
193
|
+
const props = msg.properties;
|
|
194
|
+
if (!props) return null;
|
|
195
|
+
switch (msg.type) {
|
|
196
|
+
case "session.updated": {
|
|
197
|
+
const info = props.info;
|
|
198
|
+
if (!info?.id) return null;
|
|
199
|
+
return {
|
|
200
|
+
type: "session.updated",
|
|
201
|
+
session: {
|
|
202
|
+
id: info.id,
|
|
203
|
+
title: info.title ?? "",
|
|
204
|
+
createdAt: info.time?.created,
|
|
205
|
+
updatedAt: info.time?.updated,
|
|
206
|
+
archived: info.time?.archived !== void 0
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
case "session.status": {
|
|
211
|
+
const sessionId = props.sessionID;
|
|
212
|
+
if (!sessionId) return null;
|
|
213
|
+
const status = props.status?.type ?? "idle";
|
|
214
|
+
return { type: "session.status", sessionId, status };
|
|
215
|
+
}
|
|
216
|
+
case "message.updated": {
|
|
217
|
+
const info = props.info;
|
|
218
|
+
if (info?.role !== "assistant" || !info.sessionID) return null;
|
|
219
|
+
const thinking = typeof info.time?.completed !== "number";
|
|
220
|
+
return { type: "thinking", sessionId: info.sessionID, thinking };
|
|
221
|
+
}
|
|
222
|
+
case "message.part.delta": {
|
|
223
|
+
const sessionId = props.sessionID;
|
|
224
|
+
if (!sessionId) return null;
|
|
225
|
+
return { type: "thinking", sessionId, thinking: true };
|
|
226
|
+
}
|
|
227
|
+
default:
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
function toChatSession(s) {
|
|
232
|
+
return {
|
|
233
|
+
id: s.id,
|
|
234
|
+
title: s.title,
|
|
235
|
+
createdAt: s.time?.created,
|
|
236
|
+
updatedAt: s.time?.updated,
|
|
237
|
+
archived: s.time?.archived !== void 0,
|
|
238
|
+
parentId: s.parentID,
|
|
239
|
+
url: s.url
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
function resolveOpenCodeOptions(options) {
|
|
243
|
+
if (!options) return { ...DEFAULT_OPENCODE_PROVIDER_OPTIONS };
|
|
244
|
+
const po = options.providerOptions ?? {};
|
|
245
|
+
return {
|
|
246
|
+
...DEFAULT_OPENCODE_PROVIDER_OPTIONS,
|
|
247
|
+
language: po.language ?? options.language,
|
|
248
|
+
settings: po.settings ?? options.settings,
|
|
249
|
+
logFiles: options.logFiles,
|
|
250
|
+
enableLsp: po.enableLsp ?? options.enableLsp,
|
|
251
|
+
enableBlockOnError: po.enableBlockOnError ?? options.enableBlockOnError,
|
|
252
|
+
enablePrettier: po.enablePrettier ?? options.enablePrettier
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
export {
|
|
256
|
+
DefaultWebProvider
|
|
257
|
+
};
|
package/es/system.d.ts
ADDED