让 Agent 有"打字机效果",从脚本变成可全局调用的终端工具
一句话:前 8 步它是个"能跑的脚本",这一步让它长成"像样的终端产品"。
npm link 后任何目录都能启动)。| 文件 | 变化 | 程度 |
|---|---|---|
src/ui.ts | 新增:ANSI 颜色函数(green/yellow/red/cyan)+ isTTY 检测 | 新增文件 |
src/llm.ts | 新增 parseSSEChunk(SSE 解析 + 半行累积缓冲)、makeRequestStream(stream:true + 逐块读取 + onChunk 回调);askLLM 加可选参数 onChunk(传了走流式);超时放宽到 60s;非流式 makeRequest 保留给工具调用 | 核心改造 |
src/agent.ts | 最终答案那一步改用流式(process.stdout.write 增量打印 + 回填历史);所有输出按角色上色;工具循环仍非流式 | 改造 |
src/main.ts | 提示符/命令输出/告别语上色;try/catch 接住 runAgent 抛出的错误(task 8 埋的坑在此补上);/help 补充 /clear | 改造 |
package.json | 新增 "bin": {"my-agent": "src/main.ts"}——npm link 后全局可调用 | 小改 |
config.ts / types.ts / skills.ts / tools.ts / history.ts | 沿用 task 8,一字未改 | 无变化 |
四个颜色函数 + 一个 isTTY 检测。颜色只在"真终端"里开——管道重定向到文件时,ANSI 码会变成肉眼可见的乱码。
右边是 task 9 新版(232 行,新增 SSE 解析 + 流式请求体),左边是 task 8 原版(155 行)。要点:流式只服务"纯文本最终答案",工具调用仍走非流式——工具参数是分片 JSON,流式解析复杂,教学版不展开,而"最终答案流式"已覆盖 99% 的体验提升。
data: {...} 行切成两半——所以 sseBuffer 把"还没出现换行符的尾巴"攒着,等下一个块补齐再解析;② 流结束标记:data: [DONE] 没有 JSON,continue 跳过。这两处不处理,长回答偶尔会丢字或报错。右边是 task 9 新版(113 行),左边是 task 8 原版(107 行)。差异集中在:输出全部包上 yellow/cyan/green/red,最终答案那步改用 askLLM(history, currentTools, onChunk) 流式打印并用 process.stdout.write(而非 console.log,否则每段都换行、拼不出打字机效果)。
task 8 埋的坑在这里补上:runAgent 现在被 try/catch 包住,请求失败只打印红色 [错误] 行,REPL 继续等下一轮输入。另外提示符、命令输出、告别语都上了色。
askLLM 学会"抛得出来",task 9 的 try/catch 让 REPL 学会"接得住"——断网、错 key、超时,现在都只是打印一行红色 [错误],进程稳如泰山。这就是"错误处理的两半":抛得出来 + 接得住,缺一半都算没做完。加一个 bin 字段,npm link 后系统就有了 my-agent 命令,任何目录都能启动。
[错误] 一行后 REPL 还在,还能继续输入。pi 的 packages/tui 把这一步的"颜色 + 打字机"做到了极致:它不是一行行读,而是全屏渲染(ALT 屏)、状态栏、多面板——但底层就是同一套 ANSI 转义序列 + 流式回调。你在 task 9 写的 isTTY 检测、onChunk 回调、stream:true,是 pi 的 tui-render.ts / streams.ts 的最小雏形。
| 名称 | 含义 | 来源 |
|---|---|---|
green / yellow / red / cyan | ANSI 颜色函数(非 TTY 时返回原文) | ui.ts 导出 |
isTTY | 标准输出是否为真终端 | ui.ts 内部常量 |
stream: true | 请求体里开启流式响应 | llm.ts makeRequestStream |
sseBuffer | SSE 半行累积缓冲 | llm.ts 模块级变量 |
parseSSEChunk(chunk, onDelta) | 按行解析 SSE,提取 delta.content | llm.ts 内部函数 |
makeRequestStream(...) | 流式请求体(reader.read + TextDecoder) | llm.ts 内部函数 |
onChunk | askLLM 可选回调:每段增量调用一次 | llm.ts askLLM 参数 |
TIMEOUT_MS = 60_000 | 流式场景超时放宽到 60s | llm.ts 常量 |
process.stdout.write | 不换行输出(打字机效果的关键) | agent.ts 最终答案分支 |
try/catch 包 runAgent | 错误只打印 [错误],REPL 不崩 | main.ts line 事件 |
"bin": {"my-agent": "src/main.ts"} | 全局命令入口(配合 npm link) | package.json |