tai-ai
v0.4.0
Published
Tai CLI — AI Gateway client for skills, LLM, AIGC, ASR, TTS
Maintainers
Readme
Tai 命令行工具
AI 网关的统一命令行客户端。一个 tai 命令覆盖技能管理、大模型对话、图片视频生成、语音识别与合成。
npm install -g tai-ai
tai config set --endpoint https://your-gateway.com --api-key YOUR_KEY
tai skill search "代码审查"为什么用命令行,而不是开发包、接口、图形界面?
| 维度 | 开发包 (SDK) | 接口 (REST) | 命令行 (CLI) |
|------|-------------|-------------|-------------|
| 智能体兼容性 | 需要定制工具 | 需要写请求代码 | 天然适配——所有智能体都有 run(command) |
| 上手难度 | 读文档 → 写代码 → 调试 | 拼请求头和请求体 | tai -h 三秒上手 |
| 可组合性 | 函数调用 | 无 | 管道 + 重定向,Unix 50 年验证 |
| 训练数据覆盖 | 低 | 中 | 极高——大模型见过海量终端命令 |
核心洞察:Unix 的一切皆文本流和大模型的一切皆文本片段在 50 年后收敛到同一个接口模型。命令行对大模型不是"也能用",是天然最优解。
给智能体 10 个独立工具(搜索网页、读文件、生成图片……),它需要在不相关的接口间做模式切换。给它一个命令行命名空间(tai skill search、tai aigc image、tai llm chat),它只需做字符串组合——认知负荷降一个量级。
设计哲学
一、单工具假说 —— 一个 run(command) 够了
不要给大模型一堆独立工具,通过命令行子命令统一暴露:
tai skill search "代码审查" # 搜索技能
tai llm chat "你好" # 大模型对话
tai aigc image "日落" # 生成图片
tai asr transcribe <音频地址> # 语音识别
tai tts speak "你好" --voice xxx # 语音合成智能体只需记住一个工具名 tai,剩下的靠 -h 自己发现。
二、渐进式帮助 —— 三层递进,按需加载
绝不把所有文档塞进系统提示词。 让智能体(或人类)按需逐层深入:
第 0 层:工具注册时的描述
→ "tai: AI 网关命令行,支持技能、大模型、图片视频、语音"
第 1 层:tai -h
→ 列出所有子命令 + 一行描述
第 2 层:tai aigc -h
→ 列出 image/video/status + 所有参数 + 示例
第 3 层:tai aigc image(缺参数时)
→ "[error] 缺少 <prompt>"
→ "用法:tai aigc image <提示词> [--model 模型] [--size 尺寸]"每一层只暴露当前需要的信息量。智能体不需要提前知道语音合成有哪些音色,直到它要合成语音的那一刻。
实现规则:
- 无参数 → 显示帮助(不报错)
-h/--help→ 任何位置都能触发帮助- 缺少必需参数 → 报错 + 正确用法 + 示例
三、错误即导航 —— 每个错误指向正确方向
智能体不能搜索引擎。每个错误必须同时回答**"出了什么问题"和"该怎么做"**:
# 差的写法:
[error] skill not found
# 好的写法:
[error] "code-review" 在 SkillHub 和 ClawHub 均未找到。
试试:tai skill search <关键词>
# 差的写法:
[error] unauthorized
# 好的写法:
[error] 缺少 API Key。
运行:tai config set --api-key YOUR_KEY实现规则:
- 每条错误后面跟一个可直接执行的命令建议
- 用
\n缩进建议命令,视觉上区分"问题"和"方案" - 多个可能的解决路径时,按可能性排列
四、输出尾注 —— 持续反馈
每次输出末尾追加元数据:
结果内容...
[exit:0 | 320ms]- 退出码:智能体通过 Unix 约定判断成败(0=成功,非0=失败)
- 耗时:智能体感知操作成本
12ms→ 廉价操作,可以多调几次45000ms→ 昂贵操作,下次要谨慎- 这个信息对智能体的决策至关重要
实现方式:
function out(text, code = 0, t = Date.now()) {
console.log(text);
console.log(`[exit:${code} | ${Date.now() - t}ms]`);
process.exit(code);
}五、溢出模式 —— 给地图,不给全部领土
输出过大时:截断 + 给探索路径。
# 标识符 名称 作者 下载 版本
- ---------- -------- ----- -- -----
1 code-review 代码审查 admin 42 1.0.0
...
--- 显示前 20 条,共 128 条 ---
128 个结果。安装:tai skill install <标识符>
[exit:0 | 450ms]给智能体一个"地图"(前 20 条 + 总数 + 翻页命令)比给它"全部领土"(128 条全量)有效得多。全量输出淹没上下文窗口,截断输出给出下一步行动路径。
六、双层架构 —— 执行层 与 呈现层
┌──────────────────────────────────────┐
│ 第二层:呈现层 (output.js) │ ← 格式化、截断、尾注、错误导航
├──────────────────────────────────────┤
│ 第一层:执行层 (http.js + modules/) │ ← 纯业务:接口调用、文件操作
└──────────────────────────────────────┘- 执行层只关心业务语义:发请求、拿数据、写文件
- 呈现层只关心认知优化:截断大输出、格式化表格、追加退出码
为什么必须分层?
- 如果在执行层截断 → 管道结果不完整
- 如果在执行层加尾注 → 元数据流入下一个管道
- 分层不是优化,是逻辑必然
三个手段的递进关系
帮助信息 → "我能做什么?" → 主动发现
错误提示 → "做错了怎么办?" → 被动纠偏
输出格式 → "做得怎么样?" → 持续学习这三个机制覆盖了智能体交互的完整生命周期。缺任何一个,智能体都会在某个阶段卡住。
项目架构
tai-cli/
├── src/ # 源码(不发布到 npm)
│ ├── cli.js # 入口:参数解析 + 分发 + 版本号
│ ├── lib/ # 基础设施层
│ │ ├── config.js # 配置读写 (~/.tai/config.json)
│ │ ├── http.js # 接口客户端(统一鉴权、错误处理)
│ │ ├── output.js # 输出格式化(表格、尾注、参数解析)
│ │ └── update.js # 自动更新检查 + 更新命令
│ └── modules/ # 业务模块层(模块间零依赖)
│ ├── skill.js # 搜索 / 安装 / 发布 / 详情 / 列表
│ ├── aigc.js # 生图 / 生视频 / 查状态
│ ├── llm.js # 对话 / 模型列表
│ ├── asr.js # 语音识别 / 查结果
│ ├── tts.js # 语音合成 / 音色 / 下载
│ └── config.js # 设置 / 查看 / 重置
├── dist/ # 构建产物(发布到 npm 的唯一内容)
│ └── tai.min.js # 打包 + 混淆后的单文件
├── scripts/
│ └── pre-publish-check.js # 发布安全门禁(7 项检查)
├── build.js # 构建脚本
├── package.json
└── .npmignore分层职责
| 层 | 目录 | 职责 | 依赖方向 |
|----|------|------|---------|
| 入口层 | cli.js | 解析参数,分发到对应模块 | 依赖所有模块 |
| 基础设施层 | lib/ | 配置、网络、输出、更新 | 不依赖任何业务 |
| 业务模块层 | modules/ | 各领域的命令实现 | 只依赖 lib/ |
模块之间零依赖——skill.js 不引用 aigc.js。新增一个模块只需要两步:
- 在
modules/下新建文件 - 在
cli.js加一行分发
数据流
用户输入 命令行内部 网关
───────── ───────── ─────────
tai aigc image "猫"
│
▼
cli.js(分发)
│
▼
modules/aigc.js
│ 调用 lib/http.js
▼
api('POST', '/aigc/images/generations', {body})
│ ──→ 网关接口
│ ←── 返回结果
▼
lib/output.js
│ 格式化 + 尾注
▼
标准输出: "任务:aigc_xxx\n[exit:0 | 3200ms]"构建与发布
为什么要混淆?
命令行工具发布到 npm 是公开的。不混淆意味着:
- 接口路径暴露(攻击者知道所有端点)
- 鉴权逻辑暴露(攻击者知道请求头格式)
- 内部域名暴露(攻击者知道目标服务器)
构建流程
npm run build
│
▼
[第一步] esbuild:所有模块 → 单文件打包 (21KB)
│ - 目标平台:Node.js 18
│ - 模块格式:ESM
│ - 不生成源码映射文件
│ - 内置模块标记为外部依赖
│
▼
[第二步] javascript-obfuscator:打包产物 → 混淆 (137KB)
│ - 控制流平坦化:打乱代码执行顺序
│ - 字符串加密:所有字符串编码为 base64 数组
│ - 死代码注入:插入无用代码干扰逆向
│ - 标识符替换:变量名改为十六进制
│ - 字符串分割:长字符串拆成碎片
│
▼
dist/tai.min.js ← 这是唯一发布到 npm 的文件发布安全门禁
npm publish 时自动触发构建前置钩子:
npm publish
↓
[第一步] build.js → 构建混淆产物
↓
[第二步] pre-publish-check.js → 7 项安全检查
├─ 源码目录不在包内? (源码泄露检测)
├─ 源码映射文件不在包内? (映射文件泄露检测)
├─ 混淆产物存在? (构建产物检测)
├─ 只有白名单文件? (意外文件检测)
├─ 接口路径已被混淆? (混淆有效性检测)
├─ 函数名已被混淆? (混淆有效性检测)
└─ 配置文件不含源码目录? (配置正确性检测)
↓
任何一项失败 → 退出码 1 → 发布中止
全部通过 → npm 上传即使有人误改了配置把源码目录加进去,门禁也会拦住。这是最后一道自动化防线。
版本发布步骤
# 1. 修改代码
# 2. 更新版本号(两处同步改)
# package.json → version: "0.4.0"
# src/cli.js → const VERSION = '0.4.0'
# 3. 发布(自动构建 + 安全检查 + 上传)
npm publish --access public
# 4. 用户侧更新
tai update自动更新机制
用户运行任意命令
│
▼
检查更新(非阻塞,不影响命令执行)
│
├─ 读缓存文件 ~/.tai/.update-check
│ └─ 距上次检查不到 24 小时?→ 跳过(只看缓存结果)
│
├─ 请求 npm 仓库(3 秒超时)
│ └─ 拿到最新版本号
│
├─ 写缓存
│
└─ 最新版本 ≠ 当前版本?
→ 提示:"有新版本:v0.3.1 → v0.4.0,运行 tai update 更新"- 每天最多查一次(不浪费网络)
- 3 秒超时(断网不阻塞正常使用)
- 只提示不强制(用户决定何时更新)
tai update一键更新到最新版
跨平台兼容
| 操作 | macOS / Linux | Windows |
|------|---------------|---------|
| 解压压缩包 | unzip -o | PowerShell Expand-Archive |
| 删除目录 | Node.js rmSync() | Node.js rmSync() |
| 技能安装路径 | ~/.codex/skills/<名称>/ | C:\Users\<用户>\.codex\skills\<名称>\ |
| 配置文件路径 | ~/.tai/config.json | C:\Users\<用户>\.tai\config.json |
通过 os.platform() 检测操作系统,在解压等系统操作处做分支处理。路径拼接、用户目录等一律使用 Node.js 内置接口(path.join、homedir()),不依赖特定操作系统的命令。
鉴权设计
用户只需一个项目密钥,覆盖所有操作:
tai config set --endpoint 地址 --api-key 密钥
│
┌───────────────────────┼───────────────────────┐
▼ ▼ ▼
技能搜索/安装 大模型/图片/语音 技能发布
(公开读操作) (业务调用) (写操作)
│ │ │
▼ ▼ ▼
密钥 → 项目编号 密钥 → 项目编号 密钥 → 项目编号
│
▼
项目 → 所有者
(自动解析作者身份)没有令牌、没有授权流程、没有登录页面。一个密钥走天下。
发布技能时,后端通过"项目密钥 → 项目 → 项目所有者"自动关联作者,不需要额外的用户认证。
血泪教训
教训一:一张图片引发 20 轮崩溃
直接输出二进制文件内容 → 大模型收到乱码 → 反复盲试。
结论: 工具的输出就是智能体的眼睛,返回垃圾等于让智能体失明。
我们的做法: 压缩包下载不输出内容,引导到文件路径。
教训二:沉默的错误信息与 10 次盲猜
依赖不存在但错误被吞掉了,智能体盲猜了 10 种包管理器。
结论: 永远不要丢弃错误信息。
我们的做法: 所有异常捕获都通过 fail() 输出完整错误描述 + 解决建议。
教训三:5000 行日志淹没上下文
全文塞进上下文导致大模型注意力被淹没,关键信息被噪音掩盖。
结论: 截断 + 过滤探索,比全量输出高效 10 倍。
我们的做法: 搜索结果最多显示 20 条,超出部分提示总数和翻页方式。
教训四:源码直接发到公网
v0.2.0 把原始源代码发到 npm,接口路径和鉴权逻辑全部暴露,任何人都能分析出攻击路径。
结论: 公开发布 = 攻击面暴露,必须在发布前混淆。
我们的做法: 构建混淆 + 7 项安全门禁,源码永远不出代码仓库。
教训五:Windows 没有 unzip 命令
Linux 思维定势,用了 unzip 命令,Windows 用户直接报错"命令不存在"。
结论: 命令行的跨平台不只是路径分隔符,系统命令也不能想当然。
我们的做法: 检测操作系统,Windows 自动切换到 PowerShell 的 Expand-Archive。
从零设计命令行的检查清单
如果你要设计一个新的命令行工具,对照以下清单逐项确认:
- [ ] 每个命令有帮助:无参数时显示帮助信息,不报错
- [ ] 每个错误带建议:告诉用户出了什么问题 + 该执行什么命令
- [ ] 每个输出带尾注:退出码 + 耗时,让智能体感知操作结果和成本
- [ ] 大结果集截断:显示前 N 条 + 总数 + 翻页或过滤提示
- [ ] 统一鉴权:一个密钥覆盖所有操作,不要让用户管理多套凭证
- [ ] 跨平台测试:至少覆盖 macOS 和 Windows,不依赖特定系统命令
- [ ] 不发布源码:构建混淆 + 发布前自动检查,源码不出仓库
- [ ] 自动更新:后台检查版本 + 一键更新命令,降低用户维护成本
- [ ] 尽量零依赖:减少供应链攻击面,Node.js 内置能做的不引第三方包
- [ ] 分层清晰:入口层(分发)/ 基础设施层(配置、网络、输出)/ 业务层(各模块)
许可证
MIT
