feat: 增强 Node.js 兼容性 — 扩展 Bun API polyfill 覆盖范围

- build.ts: 大幅扩展 Node.js Bun polyfill,新增 spawn、spawnSync、
  file、gc、generateHeapSnapshot、embeddedFiles、semver、YAML、
  version、stringWidth、wrapAnsi、listen 等 API 实现
- package.json: 更新描述为多模型终端 AI 编程助手,Node.js 为主要
  运行时依赖,ws/yaml 移至 dependencies,bin 入口指向 cli-node.js
- scripts/defines.ts: 版本号更新至 4.0.5.1
- fetch.ts: fetch.preconnect 添加 Node.js 降级实现(net.createConnection)
This commit is contained in:
y574444354 2026-04-15 18:22:44 +08:00
parent d7f171e2dc
commit 003ff2d6cd
4 changed files with 464 additions and 25 deletions

442
build.ts
View File

@ -120,14 +120,28 @@ const cliNode = join(outdir, 'cli-node.js')
await writeFile(cliBun, '#!/usr/bin/env bun\nimport "./cli.js"\n')
// Node.js entry needs a Bun API polyfill because Bun.build({ target: 'bun' })
// emits globalThis.Bun references (e.g. Bun.$ shell tag in computer-use-input,
// Bun.which in chunk-ys6smqg9) that crash at import time under plain Node.js.
// emits globalThis.Bun references that crash at import time under plain Node.js.
//
// Polyfill coverage:
// - Bun.which → PATH + accessSync lookup
// - Bun.hash → FNV-1a 32-bit (compatible with Bun.hash for strings)
// - Bun.$ → stub (only used by computer-use-input/darwin)
// - Bun.spawn → child_process.spawn (async, returns Subprocess-like)
// - Bun.spawnSync → child_process.spawnSync (sync, returns SpawnSyncResult)
// - Bun.file → fs.promises.readFile wrapper returning Blob-like object
// - Bun.gc → no-op (Node's V8 GC is automatic)
// - Bun.generateHeapSnapshot → v8.getHeapSnapshot()
// - Bun.embeddedFiles → [] (never present in npm package)
const NODE_BUN_POLYFILL = `#!/usr/bin/env node
// Bun API polyfill for Node.js runtime
// Bun API polyfill for Node.js runtime — makes target: 'bun' output runnable under Node.js
if (typeof globalThis.Bun === "undefined") {
const { execFileSync } = await import("child_process");
const cp = await import("child_process");
const { resolve, delimiter } = await import("path");
const { accessSync, constants: { X_OK } } = await import("fs");
const { accessSync, constants: { X_OK }, statSync } = await import("fs");
const fsp = await import("fs/promises");
// ── Bun.which ─────────────────────────────────────────────────────────
// 沿 PATH 查找可执行文件路径
function which(bin) {
const isWin = process.platform === "win32";
const pathExt = isWin ? (process.env.PATHEXT || ".EXE").split(";") : [""];
@ -139,13 +153,11 @@ if (typeof globalThis.Bun === "undefined") {
}
return null;
}
// Bun.$ is the shell template tag (e.g. $\`osascript ...\`). Only used by
// computer-use-input/darwin — stub it so the top-level destructuring
// \`var { $ } = globalThis.Bun\` doesn't crash.
function $(parts, ...args) {
throw new Error("Bun.$ shell API is not available in Node.js. Use Bun runtime for this feature.");
}
// ── Bun.hash ──────────────────────────────────────────────────────────
// FNV-1a 32-bit 散列,与 Bun.hash 行为对齐
function hash(data, seed) {
if (typeof data !== "string") return 0;
let h = ((seed || 0) ^ 0x811c9dc5) >>> 0;
for (let i = 0; i < data.length; i++) {
h ^= data.charCodeAt(i);
@ -153,7 +165,413 @@ if (typeof globalThis.Bun === "undefined") {
}
return h;
}
globalThis.Bun = { which, $, hash };
// ── Bun.$ ─────────────────────────────────────────────────────────────
// Bun shell template tag仅在 computer-use-input/darwin 中使用
// 提供 stub 使顶层解构 var { $ } = globalThis.Bun 不崩溃
function $(parts, ...args) {
throw new Error("Bun.$ shell API is not available in Node.js. Use Bun runtime for this feature.");
}
// ── Bun.spawn ─────────────────────────────────────────────────────────
// 异步子进程启动,返回与 Bun.Subprocess 兼容的对象
//
// 调用签名(本代码库实际使用的模式):
// Bun.spawn([cmd, ...args], { stdin, stdout, stderr, env, argv0 })
// Bun.spawn({ cmd: [cmd, ...args], stdin, stdout, stderr }) — 不存在,但防错
//
// 返回值需支持:
// proc.stdin → Writable (当 stdin: 'pipe' 时)
// proc.stdout → ReadableStream<Uint8Array> (当 stdout: 'pipe' 时)
// proc.stderr → ReadableStream<Uint8Array> (当 stderr: 'pipe' 时)
// proc.exited → Promise<number> (退出码)
// proc.kill() → 终止进程
// proc.pid → 进程 ID
function spawn(cmdOrOpts, opts) {
let cmd, spawnOpts;
if (Array.isArray(cmdOrOpts)) {
cmd = cmdOrOpts;
spawnOpts = opts || {};
} else {
const o = cmdOrOpts || {};
cmd = o.cmd;
spawnOpts = o;
}
if (!Array.isArray(cmd) || cmd.length === 0) {
throw new TypeError("Bun.spawn requires a non-empty command array");
}
// 解析 stdin/stdout/stderr 管道配置
const toStdio = (v) => {
if (v === "pipe") return "pipe";
if (v === "ignore") return "ignore";
if (v === "inherit") return "inherit";
return v == null ? "pipe" : "pipe";
};
const nodeOpts = {
stdio: [toStdio(spawnOpts.stdin), toStdio(spawnOpts.stdout), toStdio(spawnOpts.stderr)],
env: spawnOpts.env || process.env,
windowsHide: true,
};
// 处理 argv0Bun 用 argv0 重命名进程Node 中通过 spawn 的第一个参数传递
const executable = spawnOpts.argv0 || cmd[0];
const args = spawnOpts.argv0 ? cmd : cmd.slice(1);
const child = cp.spawn(executable, args, nodeOpts);
// 构建 Bun 兼容的 stdin 封装
let bunStdin = null;
if (spawnOpts.stdin === "pipe" || spawnOpts.stdin == null) {
bunStdin = child.stdin;
}
// 构建 Bun 兼容的 stdout/stderr 封装
// Bun.spawn 返回 ReadableStream<Uint8Array>Node 返回 Readable
// 需要转换以支持 .getReader() 和 new Response(stream).text()
function toReadableStream(nodeReadable) {
if (!nodeReadable) return null;
if (nodeReadable instanceof ReadableStream) return nodeReadable;
return new ReadableStream({
start(controller) {
nodeReadable.on("data", (chunk) => controller.enqueue(new Uint8Array(chunk)));
nodeReadable.on("end", () => controller.close());
nodeReadable.on("error", (err) => controller.error(err));
},
cancel() {
nodeReadable.destroy();
},
});
}
const bunStdout = toReadableStream(child.stdout);
const bunStderr = toReadableStream(child.stderr);
// 构建 exited Promise
const exited = new Promise((resolve) => {
child.on("close", (code) => resolve(code ?? 0));
});
return {
stdin: bunStdin,
stdout: bunStdout,
stderr: bunStderr,
exited,
pid: child.pid,
kill(sig) { child.kill(sig || "SIGTERM"); },
get exitCode() { return child.exitCode; },
};
}
// ── Bun.spawnSync ─────────────────────────────────────────────────────
// 同步子进程执行,返回与 Bun.SpawnSyncResult 兼容的对象
//
// 调用签名(本代码库实际使用的模式):
// Bun.spawnSync({ cmd: [cmd, ...args], stdin, stdout, stderr, env, timeout })
// Bun.spawnSync({ cmd: [cmd, ...args], stdin: Buffer, stdout: 'pipe' })
//
// 返回值需支持:
// result.stdout → Uint8Array | null
// result.stderr → Uint8Array | null
// result.exitCode → number
function spawnSync(optsOrCmd) {
let cmd, syncOpts;
if (optsOrCmd && typeof optsOrCmd === "object" && !Array.isArray(optsOrCmd)) {
syncOpts = optsOrCmd;
cmd = syncOpts.cmd;
} else {
throw new TypeError("Bun.spawnSync requires an options object with 'cmd' property");
}
if (!Array.isArray(cmd) || cmd.length === 0) {
throw new TypeError("Bun.spawnSync requires a non-empty cmd array");
}
const toStdio = (v) => {
if (v === "pipe") return "pipe";
if (v === "ignore") return "ignore";
if (v === "inherit") return "inherit";
return v == null ? "pipe" : "pipe";
};
const nodeOpts = {
stdio: [toStdio(syncOpts.stdin), toStdio(syncOpts.stdout), toStdio(syncOpts.stderr)],
env: syncOpts.env || process.env,
windowsHide: true,
timeout: syncOpts.timeout,
};
// 如果 stdin 是 Buffer 或 string将其作为输入传入
if (Buffer.isBuffer(syncOpts.stdin) || typeof syncOpts.stdin === "string") {
nodeOpts.input = Buffer.from(syncOpts.stdin);
nodeOpts.stdio[0] = "pipe";
}
const result = cp.spawnSync(cmd[0], cmd.slice(1), nodeOpts);
return {
stdout: result.stdout ? Uint8Array.from(Buffer.isBuffer(result.stdout) ? result.stdout : Buffer.from(result.stdout)) : null,
stderr: result.stderr ? Uint8Array.from(Buffer.isBuffer(result.stderr) ? result.stderr : Buffer.from(result.stderr)) : null,
exitCode: result.status ?? 1,
success: result.status === 0,
};
}
// ── Bun.file ──────────────────────────────────────────────────────────
// 返回类 BunFile 对象,支持 .arrayBuffer()、.text()、.json()、.exists()
// 本代码库中仅 linux.ts 使用Bun.file(path).arrayBuffer()
function bunFile(filePath) {
return {
async arrayBuffer() {
const buf = await fsp.readFile(filePath);
return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
},
async text() {
return await fsp.readFile(filePath, "utf-8");
},
async json() {
return JSON.parse(await fsp.readFile(filePath, "utf-8"));
},
async exists() {
try { await fsp.access(filePath); return true; } catch { return false; }
},
get size() {
try { return statSync(filePath).size; } catch { return 0; }
},
};
}
// ── Bun.gc ────────────────────────────────────────────────────────────
// Node.js 的 V8 GC 是自动的,无需手动触发;暴露为 no-op
function gc() {}
// ── Bun.generateHeapSnapshot ──────────────────────────────────────────
// Node.js 用 v8.getHeapSnapshot() 替代
function generateHeapSnapshot() {
const v8 = require("v8");
const stream = v8.getHeapSnapshot();
const chunks = [];
for (const chunk of stream) chunks.push(chunk);
return Buffer.concat(chunks).toString("utf-8");
}
// ── Bun.embeddedFiles ─────────────────────────────────────────────────
// npm 包中不存在嵌入文件
const embeddedFiles = [];
// ── Bun.semver ────────────────────────────────────────────────────────
// Bun 内置 semver 比较,产物中通过 typeof Bun 守卫调用
// 用 semver 包的子集实现 order 和 satisfies
// semver.order(a, b): 1 if a>b, 0 if a==b, -1 if a<b
const semver = {
order(a, b) {
const pa = parseSemver(a);
const pb = parseSemver(b);
if (!pa || !pb) return 0;
const cmp = pa[0] - pb[0] || pa[1] - pb[1] || pa[2] - pb[2];
return cmp > 0 ? 1 : cmp < 0 ? -1 : 0;
},
satisfies(version, range) {
// 简化的 range 解析:支持 >=x.y.z, ^x.y.z, ~x.y.z, x.y.z, >x.y.z, <x.y.z, <=x.y.z, x.y.z - a.b.c
const pv = parseSemver(version);
if (!pv) return false;
const r = range.trim();
// 范围用 || 分隔
const ors = r.split("||").map(s => s.trim());
for (const part of ors) {
if (satisfiesRange(pv, part)) return true;
}
return false;
},
};
// 解析 x.y.z 为 [major, minor, patch],忽略 pre-release
function parseSemver(v) {
if (!v) return null;
const m = String(v).match(/(\\d+)\\.(\\d+)\\.(\\d+)/);
if (!m) return null;
return [Number(m[1]), Number(m[2]), Number(m[3])];
}
function satisfiesRange(pv, range) {
// 空范围或 * 匹配所有
if (!range || range === "*") return true;
// 空格分隔的 AND 条件
const conditions = range.split(/\\s+/).filter(Boolean);
for (const cond of conditions) {
if (!satisfiesCondition(pv, cond)) return false;
}
return true;
}
function satisfiesCondition(pv, cond) {
if (cond === "*") return true;
// ^x.y.z — 兼容 major
let m = cond.match(/^\\^(\\d+)\\.(\\d+)\\.(\\d+)/);
if (m) {
const [_, M, m2, p] = m.map(Number);
return pv[0] === M && (pv[0] > 0 ? pv[1] >= m2 || (pv[1] === m2 && pv[2] >= p) : pv[1] === m2 && pv[2] >= p) || pv[0] === M && pv[1] > m2;
}
// ~x.y.z — 兼容 major.minor
m = cond.match(/^~(>?=?(\\d+)\\.(\\d+)\\.(\\d+))/);
if (m) {
const raw = m[1];
const cm = raw.match(/(\\d+)\\.(\\d+)\\.(\\d+)/);
if (cm) {
const [_, M, m2] = cm.map(Number);
return pv[0] === M && pv[1] === m2 && pv[2] >= Number(cm[3]);
}
}
// ~x.y
m = cond.match(/^~(\\d+)\\.(\\d+)/);
if (m) {
const [_, M, m2] = m.map(Number);
return pv[0] === M && pv[1] === m2;
}
// >=x.y.z
m = cond.match(/^>=(\\d+)\\.(\\d+)\\.(\\d+)/);
if (m) {
const [_, M, m2, p] = m.map(Number);
return pv[0] > M || (pv[0] === M && pv[1] > m2) || (pv[0] === M && pv[1] === m2 && pv[2] >= p);
}
// >x.y.z
m = cond.match(/^>(\\d+)\\.(\\d+)\\.(\\d+)/);
if (m) {
const [_, M, m2, p] = m.map(Number);
return pv[0] > M || (pv[0] === M && pv[1] > m2) || (pv[0] === M && pv[1] === m2 && pv[2] > p);
}
// <=x.y.z
m = cond.match(/^<=(\\d+)\\.(\\d+)\\.(\\d+)/);
if (m) {
const [_, M, m2, p] = m.map(Number);
return pv[0] < M || (pv[0] === M && pv[1] < m2) || (pv[0] === M && pv[1] === m2 && pv[2] <= p);
}
// <x.y.z
m = cond.match(/^<(\\d+)\\.(\\d+)\\.(\\d+)/);
if (m) {
const [_, M, m2, p] = m.map(Number);
return pv[0] < M || (pv[0] === M && pv[1] < m2) || (pv[0] === M && pv[1] === m2 && pv[2] < p);
}
// 精确匹配 x.y.z
m = cond.match(/^(\\d+)\\.(\\d+)\\.(\\d+)$/);
if (m) {
const [_, M, m2, p] = m.map(Number);
return pv[0] === M && pv[1] === m2 && pv[2] === p;
}
// x.y.z - a.b.c 范围
m = cond.match(/^(\\d+\\.\\d+\\.\\d+)\\s*-\\s*(\\d+\\.\\d+\\.\\d+)$/);
if (m) {
const lo = parseSemver(m[1]);
const hi = parseSemver(m[2]);
if (!lo || !hi) return false;
const ordLo = semver.order([pv[0], pv[1], pv[2]].join("."), lo.join(".")) >= 0;
const ordHi = semver.order([pv[0], pv[1], pv[2]].join("."), hi.join(".")) <= 0;
return ordLo && ordHi;
}
// 无法识别,宽松通过
return true;
}
// ── Bun.YAML ──────────────────────────────────────────────────────────
// Bun 内置 YAML 解析器,产物中通过 typeof Bun 守卫调用
// 降级到 yaml 包(已作为依赖存在)
const YAML = {
parse(input) {
try {
const yaml = require("yaml");
return yaml.parse(input);
} catch {
throw new Error("Bun.YAML polyfill: 'yaml' package not available. Install it or use Bun runtime.");
}
},
};
// ── Bun.version ───────────────────────────────────────────────────────
// Bun 运行时版本字符串,用于 typeof 检测
const version = "1.2.0-polyfill";
// ── Bun.stringWidth ───────────────────────────────────────────────────
// 东亚宽度感知的字符串显示宽度,产物中有 typeof 守卫降级
// 提供基础实现,按 CJK 字符双宽计算
function stringWidth(str) {
let w = 0;
for (const ch of str) {
const code = ch.codePointAt(0);
if (code >= 0x1100 && (
code <= 0x115F ||
code === 0x2329 || code === 0x232A ||
(code >= 0x2E80 && code <= 0xA4CF && code !== 0x303F) ||
(code >= 0xAC00 && code <= 0xD7A3) ||
(code >= 0xF900 && code <= 0xFAFF) ||
(code >= 0xFE10 && code <= 0xFE19) ||
(code >= 0xFE30 && code <= 0xFE6F) ||
(code >= 0xFF01 && code <= 0xFF60) ||
(code >= 0xFFE0 && code <= 0xFFE6) ||
(code >= 0x1F300 && code <= 0x1F9FF) ||
(code >= 0x20000 && code <= 0x2FFFD) ||
(code >= 0x30000 && code <= 0x3FFFD)
)) {
w += 2;
} else if (code >= 0x20) {
w += 1;
}
}
return w;
}
// ── Bun.wrapAnsi ──────────────────────────────────────────────────────
// ANSI 转义序列感知的自动换行,产物中有 typeof 守卫降级到 null
// 提供 stub 即可
const wrapAnsi = null;
// ── Bun.listen ────────────────────────────────────────────────────────
// Daemon 模式专用 TCP 监听feature-gated (DAEMON)
// Node.js 中用 net.Server 替代
function listen(opts) {
const net = require("net");
const server = net.createServer((socket) => {
if (opts.socket && opts.socket.data) {
socket.data = { ...opts.socket.data() };
}
if (opts.socket && opts.socket.open) {
opts.socket.open(socket);
}
socket.on("data", (data) => {
if (opts.socket && opts.socket.data) {
opts.socket.data(socket, data);
}
});
socket.on("close", () => {
if (opts.socket && opts.socket.close) {
opts.socket.close(socket);
}
});
socket.on("error", () => {});
});
server.listen(opts.port || 0, opts.hostname || "0.0.0.0", () => {
const addr = server.address();
server.port = addr.port;
});
return server;
}
globalThis.Bun = {
which,
$,
hash,
spawn,
spawnSync,
file: bunFile,
gc,
generateHeapSnapshot,
embeddedFiles,
semver,
YAML,
version,
stringWidth,
wrapAnsi,
listen,
};
}
import "./cli.js"
`

View File

@ -1,8 +1,8 @@
{
"name": "@costrict/csc",
"version": "4.0.5",
"version": "4.0.5-3",
"description": "Reverse-engineered Anthropic Claude Code CLI — interactive AI coding assistant in the terminal",
"description": "CSC — 多模型终端 AI 编程助手,支持 Anthropic/OpenAI/Gemini/Grok 等多种 API 端点",
"type": "module",
"author": "claude-code-best <claude-code-best@proton.me>",
"repository": {
@ -14,21 +14,26 @@
"url": "https://github.com/y574444354/csc/issues"
},
"keywords": [
"claude",
"anthropic",
"csc",
"costrict",
"cli",
"ai",
"coding-assistant",
"terminal",
"repl"
"repl",
"multi-model",
"anthropic",
"openai",
"gemini",
"grok"
],
"engines": {
"bun": ">=1.2.0"
"node": ">=18.0.0"
},
"bin": {
"csc": "dist/cli.js",
"csc": "dist/cli-node.js",
"csc-bun": "dist/cli-bun.js",
"claude-code-best": "dist/cli.js"
"claude-code-best": "dist/cli-node.js"
},
"workspaces": [
"packages/*",
@ -58,7 +63,9 @@
"rcs": "bun run scripts/rcs.ts"
},
"dependencies": {
"@claude-code-best/mcp-chrome-bridge": "^2.0.7"
"@claude-code-best/mcp-chrome-bridge": "^2.0.7",
"ws": "^8.20.0",
"yaml": "^2.8.3"
},
"devDependencies": {
"@types/he": "^1.2.3",
@ -187,9 +194,7 @@
"vscode-languageserver-protocol": "^3.17.5",
"vscode-languageserver-types": "^3.17.5",
"wrap-ansi": "^10.0.0",
"ws": "^8.20.0",
"xss": "^1.0.15",
"yaml": "^2.8.3",
"zod": "^4.3.6"
}
}

View File

@ -7,7 +7,7 @@
*/
export function getMacroDefines(): Record<string, string> {
return {
"MACRO.VERSION": JSON.stringify("4.0.5"),
"MACRO.VERSION": JSON.stringify("4.0.5.1"),
"MACRO.BUILD_TIME": JSON.stringify(new Date().toISOString()),
"MACRO.FEEDBACK_CHANNEL": JSON.stringify(""),
"MACRO.ISSUES_EXPLAINER": JSON.stringify(""),

View File

@ -124,6 +124,22 @@ export function createCoStrictFetch() {
return response
}
costrictFetch.preconnect = fetch.preconnect.bind(fetch)
// Bun 原生支持 fetch.preconnect共享连接池预热
// Node.js 没有 preconnect API降级为 net.createConnection 做 TCP 预热
if (typeof fetch.preconnect === 'function') {
costrictFetch.preconnect = fetch.preconnect.bind(fetch)
} else {
costrictFetch.preconnect = (url: string | URL) => {
try {
const { hostname, port } = new URL(typeof url === 'string' ? url : url.toString())
const { createConnection } = require('node:net') as typeof import('node:net')
const sock = createConnection(Number(port) || 443, hostname, () => sock.destroy())
sock.setTimeout(3000, () => sock.destroy())
sock.on('error', () => sock.destroy())
} catch {
// 预连接失败不影响正常流程
}
}
}
return costrictFetch
}