OpenCode 实践课:4 分钟用 Node.js 写出一个命令行 Base64 编解码工具

项目介绍

Base64 是开发中高频出现的需求——调试 JWT、处理文件上传、编码 API 凭证,都离不开它。每次打开浏览器搜"base64 decode online"多少有点繁琐。

今天用 OpenCode 写一个命令行 Base64 编解码工具,支持标准 Base64 和 URL 安全 Base64,输入可以是字符串或文件,约 100 行 Node.js 代码。

准备工作

  • Node.js ≥ 16.x(Node.js 内置了 Buffer,无需额外依赖)
  • OpenCode 已安装配置(参考 opencode.ai 文档)

实践过程

第一步:初始化项目与核心编解码

打开终端,进入项目目录,对 OpenCode 发出第一条指令:

> 我对 OpenCode 说:
> ```
> 用 Node.js 写一个命令行工具 base64.js,接收 --encode/-e 和 --decode/-d 参数,
> 支持从命令行参数或 stdin 读取输入,输出编码或解码结果。
> 不依赖任何第三方包,只用 Node.js 内置模块。
> ```

OpenCode 会自动分析需求,生成完整代码。它选择了 Buffer 处理编解码,用 process.argv 解析参数,并处理了 stdin 管道输入的情况。

> OpenCode 生成的代码骨架:
> ```javascript
> const args = process.argv.slice(2);
> const mode = args.includes('-d') || args.includes('--decode') ? 'decode' : 'encode';
> const input = args.find(a => !a.startsWith('-')) || '';
>
> function encode(str) { return Buffer.from(str, 'utf-8').toString('base64'); }
> function decode(str) { return Buffer.from(str, 'base64').toString('utf-8'); }
> ```

要点: 描述需求时明确"不依赖第三方包"很重要,否则 OpenCode 可能会引入 commander 之类的包。

第二步:增加 URL 安全 Base64 支持

标准 Base64 会将 +/= 这些字符放进 URL 会很麻烦。我对 OpenCode 追加需求:

> 我对 OpenCode 说:
> ```
> 增加 --url 选项,支持 URL 安全的 Base64 编解码(用 - 和 _ 替换 + 和 /,去掉末尾 =)。
> 同时更新帮助信息。
> ```

OpenCode 在原有逻辑上增加了 --url 标志,编解码时自动做字符替换:

> OpenCode 的修改方案:
> ```javascript
> function toUrlSafe(b64) {
> return b64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
> }
> function fromUrlSafe(str) {
> str = str.replace(/-/g, '+').replace(/_/g, '/');
> while (str.length % 4) str += '=';
> return str;
> }
> ```

要点: 追加需求时 OpenCode 能理解上下文,只改动相关部分,不会重写整个文件。

第三步:添加帮助信息与错误处理

一个成熟的 CLI 工具需要友好的帮助信息。继续对 OpenCode 提需求:

> 我对 OpenCode 说:
> ```
> 增加 --help/-h 输出使用说明,包含所有选项的说明和使用示例。
> 同时处理无效输入(空字符串、不合法的 Base64 字符串)的错误提示。
> ```

OpenCode 加入了 showHelp() 函数,输出格式清晰的帮助信息,并给 decode 加了 try-catch 错误处理。

第四步:测试运行

代码写完后直接用命令行验证:

# 编码
node base64.js -e "Hello OpenCode"
# 输出: SGVsbG8gT3BlbkNvZGU=

# 解码
node base64.js -d "SGVsbG8gT3BlbkNvZGU="
# 输出: Hello OpenCode

# URL 安全编码
node base64.js -e --url "Hello+World/Test"
# 输出: SGVsbG8rV29ybGQvVGVzdA (无 +/=)

# 管道输入
echo "Hello Pipe" | node base64.js -e
# 输出: SGVsbG8gUGlwZQ==

# 文件
node base64.js -e < README.md
# 输出文件内容的 Base64 编码

全部一次通过,不需要任何调试。

完整代码

#!/usr/bin/env node
const { Buffer } = require('buffer');
const args = process.argv.slice(2);

if (args.includes('-h') || args.includes('--help')) {
  console.log(`用法: node base64.js [选项] [输入]
选项:
  -e, --encode   编码 (默认)
  -d, --decode   解码
  --url          URL 安全模式 (替换 +/ 为 -_, 去掉 =)
  -h, --help     显示帮助
示例:
  node base64.js -e "hello"              编码字符串
  node base64.js -d "aGVsbG8="           解码字符串
  node base64.js -e --url "hello"         URL 安全编码
  echo "hello" | node base64.js -e       管道输入
  node base64.js -e < file.txt           文件输入`);
  process.exit(0);
}

const isDecode = args.includes('-d') || args.includes('--decode');
const isUrlSafe = args.includes('--url');

const input = args.filter(a => a !== '-e' && a !== '--encode' &&
  a !== '-d' && a !== '--decode' && a !== '--url').join(' ').trim();

function encode(str) {
  if (!str) throw new Error('输入不能为空');
  let b64 = Buffer.from(str, 'utf-8').toString('base64');
  return isUrlSafe ? toUrlSafe(b64) : b64;
}

function decode(str) {
  if (!str) throw new Error('输入不能为空');
  if (isUrlSafe) str = fromUrlSafe(str);
  try {
    return Buffer.from(str, 'base64').toString('utf-8');
  } catch {
    throw new Error('无效的 Base64 字符串');
  }
}

function toUrlSafe(b64) {
  return b64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}

function fromUrlSafe(str) {
  str = str.replace(/-/g, '+').replace(/_/g, '/');
  while (str.length % 4) str += '=';
  return str;
}

function run(input) {
  try {
    return isDecode ? decode(input) : encode(input);
  } catch (e) {
    process.stderr.write(`错误: ${e.message}\n`);
    process.exit(1);
  }
}

if (input) {
  console.log(run(input));
} else {
  let data = '';
  process.stdin.setEncoding('utf-8');
  process.stdin.on('data', chunk => { data += chunk; });
  process.stdin.on('end', () => {
    console.log(run(data.replace(/\n$/, '')));
  });
  process.stdin.on('error', () => {
    process.stderr.write('错误: 无法读取标准输入\n');
    process.exit(1);
  });
  if (process.stdin.isTTY) {
    process.stderr.write('错误: 请提供输入内容或使用管道\n');
    process.stderr.write('使用 node base64.js --help 查看帮助\n');
    process.exit(1);
  }
}

代码共 70 行,零依赖,下载即可使用。

小结

整个过程从提出需求到可运行的完整工具,耗时约 4 分钟。对比传统开发方式:

| 环节 | 传统方式 | 用 OpenCode |
|------|---------|------------|
| 查 Buffer API 文档 | 5 分钟 | 0 |
| 写参数解析逻辑 | 5 分钟 | 0 |
| 写编解码函数 | 3 分钟 | 0 |
| stdin 管道处理 | 8 分钟 | 0 |
| 错误处理与帮助信息 | 5 分钟 | 0 |
| 总计 | ~25 分钟 | ~4 分钟 |

OpenCode 最大的价值不是替你写代码,而是把"查文档 → 想实现 → 敲代码 → 调试"这个循环压缩成一句话。你只需要描述做什么,它帮你完成怎么做

当然,它生成的代码需要你的判断力——理解逻辑、确认边界情况、决定是否接受修改。AI 是副驾驶,方向盘始终在你手里。