From 003ff2d6cdd856772532d7a70bb019c973b38f6c Mon Sep 17 00:00:00 2001 From: y574444354 <574444354@qq.com> Date: Wed, 15 Apr 2026 18:22:44 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A2=9E=E5=BC=BA=20Node.js=20?= =?UTF-8?q?=E5=85=BC=E5=AE=B9=E6=80=A7=20=E2=80=94=20=E6=89=A9=E5=B1=95=20?= =?UTF-8?q?Bun=20API=20polyfill=20=E8=A6=86=E7=9B=96=E8=8C=83=E5=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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) --- build.ts | 442 ++++++++++++++++++++++++++++++++- package.json | 27 +- scripts/defines.ts | 2 +- src/costrict/provider/fetch.ts | 18 +- 4 files changed, 464 insertions(+), 25 deletions(-) diff --git a/build.ts b/build.ts index 857aefe8e..dda2d666e 100644 --- a/build.ts +++ b/build.ts @@ -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 (当 stdout: 'pipe' 时) + // proc.stderr → ReadableStream (当 stderr: 'pipe' 时) + // proc.exited → Promise (退出码) + // 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, + }; + + // 处理 argv0:Bun 用 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,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 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, 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); + } + // = 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" ` diff --git a/package.json b/package.json index e58822117..c25ae4e7d 100644 --- a/package.json +++ b/package.json @@ -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 ", "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" } } diff --git a/scripts/defines.ts b/scripts/defines.ts index 777c9465f..404dc3425 100644 --- a/scripts/defines.ts +++ b/scripts/defines.ts @@ -7,7 +7,7 @@ */ export function getMacroDefines(): Record { 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(""), diff --git a/src/costrict/provider/fetch.ts b/src/costrict/provider/fetch.ts index ecda30171..dabbf532a 100644 --- a/src/costrict/provider/fetch.ts +++ b/src/costrict/provider/fetch.ts @@ -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 }