-
Notifications
You must be signed in to change notification settings - Fork 6
Expand file tree
/
Copy pathserver.mjs
More file actions
113 lines (98 loc) · 3.09 KB
/
Copy pathserver.mjs
File metadata and controls
113 lines (98 loc) · 3.09 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
/**
* OpenCode → OpenAI 兼容 HTTP 服务
*
* 通过 `opencode run "prompt"` 子进程调用 OpenCode,
* 包装成标准 HTTP 接口供 weiclaw 连接。
*
* 前置条件:
* npm i -g opencode-ai (或 brew install anomalyco/tap/opencode)
* 配置好 AI provider: opencode providers login
*
* 用法:
* node server.mjs
* # 然后另一个终端:
* npx weiclaw http://localhost:3000/v1
*
* 可选环境变量:
* PORT=3000 HTTP 端口
* OPENCODE_MODEL=xxx 指定模型 (格式: provider/model)
*/
import { createServer } from "node:http";
import { spawn } from "node:child_process";
// Windows 下 npm 全局安装的 CLI 是 .cmd 文件,需要 shell: true
const IS_WIN = process.platform === "win32";
function spawnCLI(cmd, args, opts) {
return spawn(IS_WIN ? `${cmd}.cmd` : cmd, args, { shell: IS_WIN, ...opts });
}
const PORT = process.env.PORT || 3000;
const MODEL = process.env.OPENCODE_MODEL || "";
const server = createServer(async (req, res) => {
if (req.method !== "POST" || !req.url.startsWith("/v1/chat/completions")) {
res.writeHead(404);
res.end("Not Found");
return;
}
const body = await readBody(req);
const { messages } = JSON.parse(body);
const userMessage =
messages?.findLast((m) => m.role === "user")?.content || "";
try {
const result = await runOpenCode(userMessage);
res.writeHead(200, { "Content-Type": "application/json" });
res.end(
JSON.stringify({
choices: [
{
message: { role: "assistant", content: result },
},
],
})
);
} catch (err) {
console.error(` ✗ ${err.message.slice(0, 120)}`);
res.writeHead(500, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: err.message }));
}
});
server.listen(PORT, () => {
console.log(`⌬ OpenCode Agent 运行在 http://localhost:${PORT}/v1`);
if (MODEL) console.log(` 模型: ${MODEL}`);
console.log(
` 然后运行: npx weiclaw http://localhost:${PORT}/v1`
);
});
/**
* 通过 opencode run "prompt" 非交互模式调用
*/
function runOpenCode(prompt) {
return new Promise((resolve, reject) => {
const args = ["run", prompt];
if (MODEL) args.push("-m", MODEL);
const child = spawnCLI("opencode", args, {
stdio: ["ignore", "pipe", "pipe"],
timeout: 300_000,
});
let stdout = "";
let stderr = "";
child.stdout.on("data", (d) => (stdout += d));
child.stderr.on("data", (d) => (stderr += d));
child.on("close", (code) => {
// opencode run 输出可能包含 ANSI 颜色码,清理掉
const clean = (s) => s.replace(/\x1b\[[0-9;]*m/g, "").trim();
if (code !== 0) {
const errMsg = clean(stderr + stdout) || `exit code ${code}`;
reject(new Error(errMsg.slice(0, 300)));
} else {
resolve(clean(stdout) || "(empty response)");
}
});
child.on("error", (err) => reject(err));
});
}
function readBody(req) {
return new Promise((resolve) => {
let data = "";
req.on("data", (chunk) => (data += chunk));
req.on("end", () => resolve(data));
});
}