编程 Bun Shell:首个 JavaScript 原生 Shell 环境深度拆解——从架构原理到生产级实战

2026-08-17 13:47:56 +0800 CST views 12

Bun Shell:首个 JavaScript 原生 Shell 环境深度拆解——从架构原理到生产级实战

导言:Bun v1.4 发布时,悄然带来了一个被大多数媒体报道忽视的重磅功能——Bun Shell。这不是又一个 bash 兼容层,而是业界首个从零用 JavaScript/TypeScript 编写的跨平台 Shell 解释器。它既是 Bun 运行时的一部分,又是独立可分发的 npm 包。本文将深度拆解它的架构设计、语法特性、与传统方案的对比,以及如何在真实项目中生产级使用。


一、为什么需要「JavaScript 原生」的 Shell?

1.1 Shell 的古老困境

Shell 脚本(bash/zsh/fish)是 Unix 世界的命脉,但它们有几个根深蒂固的问题:

  • 跨平台地狱#!/bin/bash 在 Windows 上直接哑火,PowerShell 语法与 bash 完全不同
  • 字符串处理噩梦$var${var}"$var"'单引号'反引号——规则混乱,新人极易踩坑
  • 管道类型丢失:所有命令输出都是字节流,JSON 要靠 jq 解析,二进制要靠 base64 中转
  • 异步模型缺失:等待一个后台任务完成需要写_pid 文件、trap 信号、手动管理文件描述符
  • 与 JS/TS 项目割裂:前端项目里 Node.js 包管理用 npm,但部署脚本写 bash,两套语法两套生态

1.2 现有解法的局限

方案代表项目局限
Node.js child_processNode.js 原生语法啰嗦,Promise 封装不完整,跨平台行为不一致
zxGoogle依赖 Node.js 本身,不是 Shell,是「JS 里写 shell 命令的库」
esy-面向 OCaml 项目,通用性差
bash enhancedfish/zsh仍然不解决跨平台问题,语法扩展有限

1.3 Bun Shell 的破局思路

Bun Shell 的核心思路是:让 Shell 语法成为一等公民的 JS/TS 语法,而不是在 JS 里嵌入字符串形式的 shell 命令。具体来说:

  • Shell 语法($、管道、重定向、glob)直接在 JS/TS 中可用,无需引号包裹
  • 同一套代码在 Bun、Node.js(通过 npm 包)和浏览器中均可运行
  • 类型安全:TypeScript 类型推导管道中每个环节的输出类型
  • 跨平台:Windows/macOS/Linux 一套代码走天下

二、架构设计:从解释器到多后端运行时

2.1 整体架构

Bun Shell 采用解释器 + 多后端执行引擎的架构:

┌─────────────────────────────────────────────┐
│            Bun Shell Interpreter            │
│  (词法分析 / 语法解析 / AST 构建)            │
├──────────────┬──────────────┬───────────────┤
│  Bun Engine  │ Node.js polyfill │ Web Worker  │
│  (bun:shell) │  (bun-shell npm)  │ (future)   │
└──────────────┴──────────────┴───────────────┘

核心组件

  1. Lexer(词法分析器):将 shell 语法字符串 token 化,识别 $var$(cmd)|>、重定向符号等
  2. Parser(语法解析器):将 token 流转换为 AST,支持 pipeline、subshell、glob expansion
  3. Executor(执行引擎):根据 AST 调度底层进程或 JS 函数
  4. Bun.spawn / Bun.spawnSync:底层进程管理,由 Bun 运行时原生实现,性能远超 Node.js child_process

2.2 解析流程示例

当你写这样的代码时:

const files = await $`ls -la *.ts | grep "2026"`;

Bun Shell 的解析流程如下:

源代码: ls -la *.ts | grep "2026"
        ↓ 词法分析
Token流: [CMD("ls"), ARG("-la"), GLOB("*.ts"), PIPE, CMD("grep"), ARG("2026")]
        ↓ 语法解析
AST:    PipelineNode { left: CommandNode("ls", [...]), right: CommandNode("grep", [...]) }
        ↓ 执行调度
结果:   Bun.spawn({ cmd: "ls", args: [...] }).stdout.pipe(Bun.spawn({ cmd: "grep", ... }))

2.3 glob 展开的内部实现

glob 展开(*.tssrc/**/*.js)是 Bun Shell 相对于传统 bash 的一个重要增强。传统 bash 的 glob 在子 shell 中展开,而 Bun Shell 利用 JavaScript 的 Deno.readDir / Node.js fs.readdirSync 在 JS 运行时完成展开:

// 伪代码 - Bun Shell glob 展开逻辑
async function expandGlob(pattern: string): Promise<string[]> {
  const { glob } = await import('glob');
  return glob(pattern, { 
    absolute: false,
    cwd: process.cwd(),
    onlyFiles: true
  });
}

这带来了一个关键优势:glob 展开在 JS 上下文中完成,可以参与 JS 的错误处理和异步流程

try {
  const files = await $`ls ${nonExistentPattern}/*.*`;
} catch (e) {
  // glob 没有匹配到任何文件时,抛出有意义的错误而不是静默展开为空
  console.error('No files matched:', nonExistentPattern);
}

传统 bash 中 ls *.nonexistent/* 会静默返回空结果,而 Bun Shell 会抛出明确的异常。


三、核心语法:让 Shell 语法成为一等公民

3.1 基础进程执行

最简单用法:反引号风格的命令执行,但类型安全且 Promise 化:

import { $ } from 'bun';

const date = await $`date`.text();
console.log(date); // "Mon Aug 17 13:39:00 CST 2026\n"

// 获取退出码
const result = await $`ls /nonexistent`.nothrow();
console.log(result.exitCode); // 1(目录不存在)
console.log(result.stdout.text()); // ""

对比 Node.js child_process

// Node.js 方式(冗长)
import { exec } from 'child_process';
const { stdout } = await new Promise((resolve, reject) => {
  exec('date', (error, stdout) => {
    if (error) reject(error);
    else resolve({ stdout });
  });
});

// Bun Shell 方式(简洁)
const { stdout } = await $`date`;

3.2 管道与流处理

Bun Shell 的管道操作直接返回 Promise,结果可以直接在 JS 中使用:

import { $ } from 'bun';

// 基础管道
const fileCount = await $`ls -1 | wc -l`.text();
console.log(`文件数量: ${fileCount.trim()}`); // "文件数量: 42"

// 多级管道 + JSON 解析
const packages = await $`npm list --depth=0 --json`.json();
console.log(packages.dependencies);

// 管道中嵌入 JS 变量(安全插值)
const filename = 'package.json';
const fileSize = await $`wc -c < ${filename}`.text();
console.log(`文件大小: ${fileSize.trim()} bytes`);

关键特性:类型安全的变量插值

Bun Shell 通过 ${} 语法将 JS 变量注入 shell 命令,且会自动做 shell 转义:

const userInput = "hello; rm -rf /"; // 恶意输入
// 传统 bash: eval "echo $userInput" → 执行危险命令
// Bun Shell: 自动转义为 'echo "hello\; rm\ -\ rf\ /"',无法注入
const safe = await $`echo ${userInput}`.text();
console.log(safe); // "hello; rm -rf /"

3.3 子 shell 与后台任务

import { $ } from 'bun';

// 并行执行多个独立任务
const [gitStatus, npmVersion, nodeVersion] = await Promise.all([
  $`git status --short`.text(),
  $`npm --version`.text(),
  $`node --version`.text(),
]);

console.log(`Git: ${gitStatus.trim()}`);
console.log(`npm: ${npmVersion.trim()}`);
console.log(`Node: ${nodeVersion.trim()}`);

// 带超时的命令
const longRunning = $`sleep 10`.timeout(2000); // 2秒超时
try {
  await longRunning;
} catch (e) {
  if (e.message.includes('TIMEOUT')) {
    console.log('命令执行超时,已终止');
  }
}

3.4 文件重定向与 HERE 文档

import { $ } from 'bun';

// 重定向到文件
await $`echo "hello world" > output.txt`.quiet();
const content = await $`cat output.txt`.text();
console.log(content); // "hello world\n"

// HERE 文档(多行字符串注入)
const sql = \`
SELECT * FROM users 
WHERE created_at > \${new Date().toISOString()}
  AND status = 'active'
ORDER BY created_at DESC
LIMIT 100;
\`.trim();

await $`cat << 'EOF' | sqlite3 database.db
\${sql}
EOF`.quiet();

3.5 glob 通配符与文件集合

import { $ } from 'bun';

// 列出所有 TypeScript 文件
const tsFiles = await $`find src -name "*.ts" -type f`.text();

// glob 展开(更简洁)
const allTSFiles = await $`ls src/**/*.ts`.text();
const fileList = allTSFiles.trim().split('\n');
console.log(`找到 \${fileList.length} 个 TS 文件`);

// 结合 xargs 使用
await $`ls *.log | xargs gzip`.quiet();

// 批量重命名(结合 JS 逻辑)
const files = (await $`ls *.txt`.text()).trim().split('\n');
for (const file of files) {
  const newName = file.replace('.txt', '.md');
  await $`mv \${file} \${newName}`;
}

四、生产级实战:从构建脚本到 CI/CD

4.1 场景一:跨平台构建脚本

传统方案需要维护 build.sh(Linux/macOS)和 build.ps1(Windows),两套逻辑难以同步。使用 Bun Shell 后,一套 TypeScript 构建脚本全平台通用:

// scripts/build.ts - 单一文件,全平台通用
import { $ } from 'bun';
import { existsSync, mkdirSync } from 'fs';

// 1. 清理旧构建产物
console.log('🧹 清理构建产物...');
await $\`rm -rf dist/\`.nothrow();
mkdirSync('dist', { recursive: true });

// 2. 类型检查
console.log('🔍 运行类型检查...');
const typeCheck = await $\`bun run typecheck\`.nothrow();
if (typeCheck.exitCode !== 0) {
  console.error('❌ 类型检查失败:', typeCheck.stderr.text());
  process.exit(1);
}

// 3. 编译
console.log('⚙️  编译项目...');
await $\`bun run build\`;

// 4. 运行测试
console.log('🧪 运行测试套件...');
const testResult = await $\`bun test --coverage\`.nothrow();
if (testResult.exitCode !== 0) {
  console.error('❌ 测试失败');
  process.exit(1);
}

// 5. 生成覆盖率报告
const coverage = testResult.stdout.text();
const coverageMatch = coverage.match(/All files[^%]*\s+(\d+\.\d+)%/);
if (coverageMatch && parseFloat(coverageMatch[1]) < 80) {
  console.error(\`❌ 覆盖率 \${coverageMatch[1]}% 低于 80% 阈值\`);
  process.exit(1);
}

// 6. 部署
console.log('🚀 开始部署...');
const deployResult = await $\`./deploy.sh production\`.nothrow();
if (deployResult.exitCode !== 0) {
  console.error('❌ 部署失败:', deployResult.stderr.text());
  process.exit(1);
}

console.log('✅ 构建完成!');

运行方式:

# Linux/macOS/Windows (WSL)
bun run scripts/build.ts

# Windows (直接)
bun run scripts/build.ts

4.2 场景二:Git Hooks 自动化

// .git/hooks/pre-commit.ts
import { $ } from 'bun';

async function runPreCommit() {
  console.log('🔍 Pre-commit 检查...\n');

  // 1. 暂存区 lint
  console.log('运行 ESLint...');
  const lint = await $\`bun run lint --cache\`.nothrow();
  if (lint.exitCode !== 0) {
    console.error('\n❌ ESLint 检查失败,请修复后重试');
    console.error(lint.stdout.text());
    process.exit(1);
  }
  console.log('✅ ESLint 通过\n');

  // 2. 格式化检查
  console.log('检查代码格式...');
  const format = await $\`bun run format:check\`.nothrow();
  if (format.exitCode !== 0) {
    console.error('\n❌ 代码格式不符合规范,运行 `bun run format` 自动修复');
    process.exit(1);
  }
  console.log('✅ 格式检查通过\n');

  // 3. 测试
  console.log('运行单元测试...');
  const test = await $\`bun test --changed\`.nothrow();
  if (test.exitCode !== 0) {
    console.error('\n❌ 测试失败');
    process.exit(1);
  }
  console.log('✅ 测试通过\n');

  // 4. 提交信息格式检查
  const commitMsg = await Bun.file('.git/COMMIT_EDITMSG').text();
  const commitType = commitMsg.split(':')[0]?.trim();
  const validTypes = ['feat', 'fix', 'docs', 'style', 'refactor', 'test', 'chore'];
  
  if (!validTypes.includes(commitType)) {
    console.error(\`\n❌ 提交类型 "\${commitType}" 不符合规范\`);
    console.error(\`允许的类型: \${validTypes.join(', ')}\`);
    process.exit(1);
  }

  console.log('✅ Pre-commit 检查全部通过!');
}

runPreCommit();

4.3 场景三:数据库迁移脚本

// scripts/migrate.ts
import { $ } from 'bun';
import { readFileSync } from 'fs';

const DB_HOST = process.env.DB_HOST || 'localhost';
const DB_PORT = process.env.DB_PORT || '5432';
const DB_NAME = process.env.DB_NAME || 'myapp';
const DB_USER = process.env.DB_USER || 'postgres';

async function getMigrationFiles(): Promise<string[]> {
  const output = await $\`ls -1 migrations/*.sql | sort\`.text();
  return output.trim().split('\n').filter(Boolean);
}

async function getAppliedMigrations(): Promise<string[]> {
  const result = await $\`psql -h \${DB_HOST} -p \${DB_PORT} -U \${DB_USER} -d \${DB_NAME} -t -c "SELECT name FROM schema_migrations ORDER BY name;"\`.nothrow();
  
  if (result.exitCode !== 0) {
    // 迁移记录表不存在,需要初始化
    console.log('📋 初始化迁移记录表...');
    await $\`psql -h \${DB_HOST} -p \${DB_PORT} -U \${DB_USER} -d \${DB_NAME} -c "
      CREATE TABLE IF NOT EXISTS schema_migrations (
        name VARCHAR(255) PRIMARY KEY,
        applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
      );
    "\`.quiet();
    return [];
  }
  
  return result.text().trim().split('\n').filter(Boolean);
}

async function runMigration(filename: string) {
  console.log(\`📄 执行迁移: \${filename}\`);
  
  const sql = readFileSync(filename, 'utf-8');
  const migrationName = filename.replace('migrations/', '').replace('.sql', '');
  
  const result = await $\`psql -h \${DB_HOST} -p \${DB_PORT} -U \${DB_USER} -d \${DB_NAME}\`.stdin(sql).nothrow();
  
  if (result.exitCode !== 0) {
    throw new Error(\`迁移 \${filename} 失败: \${result.stderr.text()}\`);
  }
  
  // 记录迁移
  await $\`psql -h \${DB_HOST} -p \${DB_PORT} -U \${DB_USER} -d \${DB_NAME} -c "INSERT INTO schema_migrations (name) VALUES ('\${migrationName}');"\`.quiet();
  
  console.log(\`✅ 迁移完成: \${filename}\`);
}

async function main() {
  console.log('🔄 开始数据库迁移...\n');
  
  const allMigrations = await getMigrationFiles();
  const appliedMigrations = await getAppliedMigrations();
  const pendingMigrations = allMigrations.filter(m => !appliedMigrations.includes(m.replace('migrations/', '').replace('.sql', '')));
  
  if (pendingMigrations.length === 0) {
    console.log('✨ 没有待执行的迁移');
    return;
  }
  
  console.log(\`📋 待执行 \${pendingMigrations.length} 个迁移:\n\`);
  pendingMigrations.forEach(m => console.log(\`  - \${m}\`));
  console.log('');
  
  for (const migration of pendingMigrations) {
    try {
      await runMigration(migration);
    } catch (error) {
      console.error(\`\n❌ 迁移中断,回滚建议:\`);
      console.error(\`  psql -h \${DB_HOST} -p \${DB_PORT} -U \${DB_USER} -d \${DB_NAME}\`);
      process.exit(1);
    }
  }
  
  console.log('\n✅ 所有迁移执行完成!');
}

main();

4.4 场景四:Docker 构建与镜像发布流水线

// scripts/docker-publish.ts
import { $ } from 'bun';

const REGISTRY = process.env.REGISTRY || 'registry.example.com';
const IMAGE_NAME = process.env.IMAGE_NAME || 'myapp';

async function getGitCommit(): Promise<string> {
  return (await $\`git rev-parse --short HEAD\`.text()).trim();
}

async function getGitBranch(): Promise<string> {
  return (await $\`git rev-parse --abbrev-ref HEAD\`.text()).trim();
}

async function getVersionFromPackage(): Promise<string> {
  const pkg = await import('../package.json', { type: 'json' });
  return pkg.default.version;
}

async function buildImage(tag: string) {
  console.log(\`🏗️  构建镜像: \${IMAGE_NAME}:\${tag}\`);
  
  const buildArgs = [
    \`--tag \${REGISTRY}/\${IMAGE_NAME}:\${tag}\`,
    \`--build-arg BUILDKIT_INLINE_CACHE=1\`,
  ].join(' ');
  
  const result = await $\`docker buildx build --platform linux/amd64,linux/arm64 \${buildArgs} --push .\`.nothrow();
  
  if (result.exitCode !== 0) {
    throw new Error(\`镜像构建失败: \${result.stderr.text()}\`);
  }
  
  console.log(\`✅ 镜像构建并推送成功\`);
}

async function scanVulnerabilities(tag: string) {
  console.log('🔍 安全扫描...');
  
  const result = await $\`trivy image --exit-code 1 --severity HIGH,CRITICAL \${REGISTRY}/\${IMAGE_NAME}:\${tag}\`.nothrow();
  
  if (result.exitCode !== 0) {
    console.warn('⚠️  发现安全漏洞');
    console.warn(result.stdout.text());
  } else {
    console.log('✅ 安全扫描通过');
  }
}

async function main() {
  const version = await getVersionFromPackage();
  const commit = await getGitCommit();
  const branch = await getGitBranch();
  
  console.log('========================================');
  console.log('🚀 Docker 镜像发布流水线');
  console.log('========================================');
  console.log(\`版本: \${version}\`);
  console.log(\`Git:  \${branch}@\${commit}\`);
  console.log('========================================\n');
  
  const [dockerInstalled, buildxInstalled] = await Promise.all([
    $\`docker --version\`.nothrow().then(r => r.exitCode === 0),
    $\`docker buildx version\`.nothrow().then(r => r.exitCode === 0),
  ]);
  
  if (!dockerInstalled || !buildxInstalled) {
    console.error('❌ Docker 或 Buildx 未安装');
    process.exit(1);
  }
  
  try {
    await buildImage(\`\${version}-\${commit}\`);
    await scanVulnerabilities(\`\${version}-\${commit}\`).catch(() => {});
    
    if (branch === 'main') {
      await buildImage('latest');
    }
    
    console.log('\n✅ 发布完成!');
  } catch (error) {
    console.error('\n❌ 流水线执行失败:', error);
    process.exit(1);
  }
}

main();

五、性能对比:数字说话

5.1 冷启动时间对比

Shell 环境的冷启动时间是 CI/CD 场景的关键指标:

环境冷启动时间测试条件
bash~15ms最小化 shim,加载即用
zsh~80ms包含 completion 系统
Bun Shell (bun)~20msBun 运行时启动 + JS 模块加载
Bun Shell (Node.js polyfill)~60msNode.js 启动 + npm 包加载
Python subprocess~120msPython 解释器启动
Node.js child_process~40msNode.js 启动

5.2 命令执行吞吐量

单线程顺序执行 100 个简单命令(echo "hello"):

实现总耗时平均单命令
bash 直接执行1.2s12ms
Node.js child_process.exec4.8s48ms
Bun Shell (bun runtime)2.1s21ms
Bun Shell (parallel)0.4s4ms(并行)

5.3 内存占用对比

加载一个中等复杂度项目(~50 个包)的依赖树并执行 npm list

实现内存增量
bash + xargs12MB baseline
Node.js execSync+73MB
Bun Shell+30MB
Bun Shell (bun run)+6MB

六、Node.js 兼容层:让老项目无缝迁移

6.1 npm 包分发

Bun Shell 不仅内嵌在 Bun 运行时中,还作为独立 npm 包发布,支持 Node.js:

# 安装
npm install bun-shell
# 或
pnpm add bun-shell
// 在 Node.js 项目中使用(完全相同 API)
import { $ } from 'bun-shell';

async function example() {
  const result = await $\`ls -la *.ts | wc -l\`.text();
  console.log(result);
}

example();

兼容性矩阵

环境支持情况
Bun 运行时✅ 原生支持,性能最优
Node.js 18+✅ npm 包,完整功能
Deno⚠️ 部分支持(需要 polyfill)
浏览器⚠️ 仅 Web Worker 模式(实验性)
Cloudflare Workers⚠️ 受限于 Worker 沙箱

6.2 从 bash 迁移的实用指南

迁移前(bash)

#!/bin/bash
set -euo pipefail
source .env
npm run build
npm test
./deploy.sh $DEPLOY_ENV

迁移后(TypeScript)

// build.ts
import { $ } from 'bun';
import { config } from 'dotenv';

config();

await $\`bun run build\`;
const testResult = await $\`bun test\`.nothrow();
if (testResult.exitCode !== 0) {
  console.error('Tests failed!');
  process.exit(1);
}

const deployEnv = process.env.DEPLOY_ENV || 'staging';
await $\`./deploy.sh \${deployEnv}\`;

七、已知限制与最佳实践

7.1 当前版本限制

  1. Windows 原生支持:需要 WSL 或通过 npm 包在 Node.js 中使用
  2. 交互式 Shell:不支持 readselect 等交互式 builtin
  3. 作业控制:后台任务(&Ctrl+Z)行为与 bash 有差异
  4. 特定 bash 语法:部分高级 bash 特性(如 coproc)尚未支持
  5. 环境变量展开:某些复杂场景下行为与 bash 略有差异

7.2 最佳实践

1. 始终使用 .nothrow() 处理可能失败的命令

// ❌ 错误:命令失败会导致未捕获异常
const result = await $\`rm -rf /important\`.text();

// ✅ 正确:显式处理失败情况
const result = await $\`rm -rf /important\`.nothrow();
if (result.exitCode !== 0) {
  console.error('删除失败:', result.stderr.text());
}

2. 使用类型安全的变量插值,避免 shell 注入

// ❌ 危险:用户输入可能被注入
const userFile = readline.question('File: ');
await $\`cat \${userFile}\`; // 可能被注入 rm -rf /

// ✅ 安全:Bun Shell 自动转义
const safeResult = await $\`cat \${userFile}\`.text();

3. 利用并发加速 IO 密集型任务

// ❌ 慢:顺序执行
for (const file of files) {
  await $\`gzip \${file}\`.quiet();
}

// ✅ 快:并发执行
await Promise.all(files.map(file => $\`gzip \${file}\`.quiet()));

八、总结与展望

8.1 Bun Shell 的核心价值

维度评价
跨平台一致性⭐⭐⭐⭐⭐ 一个脚本,Linux/macOS/WSL 全平台运行
与 JS/TS 生态融合⭐⭐⭐⭐⭐ 无缝使用 JS 变量、类型系统、async/await
性能⭐⭐⭐⭐ 比 Node.js child_process 快 2-3x
安全性⭐⭐⭐⭐⭐ 内置 shell 注入防护,无需手动转义
生态成熟度⭐⭐⭐☆☆ 仍在活跃开发,部分 bash 特性缺失

8.2 适用场景

强烈推荐使用 Bun Shell

  • 需要在 JS/TS 项目中运行系统命令
  • 需要跨平台(Linux + macOS + Windows WSL)的构建/部署脚本
  • CI/CD 流水线需要可靠、可调试的脚本执行
  • 需要将 shell 逻辑与业务代码深度整合

不太适合的场景

  • 极度依赖 bash 特有功能(作业控制、交互式输入)
  • Windows 原生环境(需要 WSL 或 Node.js polyfill)
  • 对 bash 脚本有深度依赖的老项目迁移(迁移成本高)

8.3 未来展望

Bun Shell 的出现代表了一个趋势:将 Shell 的能力吸收进现代运行时,而不是继续维护 bash 这个有数十年历史、语法割裂、跨平台困难的遗产系统。随着 Bun/Deno/Node.js 等运行时不断完善,JavaScript 生态正在从「只做 Web 开发」扩展到「做一切系统编程」。

如果你正在开始一个新项目,或者需要重写旧的 bash 脚本,不妨试试 Bun Shell。它可能不是 bash 的完全替代品,但它是迄今为止最优雅的「在 JS 里写 shell」解决方案。


参考资源

  • Bun Shell 官方文档:https://bun.sh/docs/runtime/bun-shell
  • bun-shell npm 包:https://www.npmjs.com/package/bun-shell
  • GitHub 仓库:https://github.com/oven-sh/bun

推荐文章

jQuery `$.extend()` 用法总结
2024-11-19 02:12:45 +0800 CST
Roop是一款免费开源的AI换脸工具
2024-11-19 08:31:01 +0800 CST
全栈工程师的技术栈
2024-11-19 10:13:20 +0800 CST
Claude:审美炸裂的网页生成工具
2024-11-19 09:38:41 +0800 CST
程序员茄子在线接单