Pi HTTP开发
@evalexp/pi-http为Pi增加一个httpTool,让Agent直接执行HTTP(S)请求,不需要临时生成curl、wget、PowerShell或脚本。
通用Extension机制直接参考Pi Extension。这里只记录pi-http自身的实现、设计取舍和维护注意点。
设计目标
这个Extension解决的不是“缺少HTTP Client”,而是Agent缺少稳定、结构化、跨平台的HTTP执行能力。
让Agent退回Shell执行HTTP会带来额外不确定性:
curl、wget不保证存在;- Windows和Unix命令不同;
- Shell quoting容易破坏JSON、Header和Multipart参数;
- 二进制响应不适合直接经过终端再进入Context;
- Shell输出缺少稳定结构;
- Agent会额外消耗Token生成命令和解析输出。
因此这里把HTTP能力收敛成一个Pi Tool:
flowchart LR A[Agent表达HTTP意图] --> B[http Tool] B --> C[结构化参数] C --> D[HTTP执行层] D --> E{响应处理} E -->|普通响应| F[文本 / Base64] E -->|下载| G[直接流式落盘] F --> H[Tool Result] G --> H
设计原则:
- Agent只表达请求,不拼Shell;
- Pi入口和HTTP实现解耦;
- HTTP Status和Tool执行错误分离;
- 普通Response必须限制进入Context的大小;
- 文件下载直接落盘;
- 下载失败不能留下Partial File;
- 默认不覆盖本地文件;
- 请求同时支持Timeout和Pi取消;
- 尽量兼容Node和Bun运行时。
项目结构
当前仓库很小,业务边界也比较明确:
| 文件 | 职责 |
|---|---|
index.ts | Pi Tool定义、Schema、Prompt规则、最终Context截断 |
request.ts | 请求构造、HTTP执行、Response解析、下载、TLS、错误分类 |
request.test.ts | HTTP执行层的本地HTTP/HTTPS集成测试 |
test-fixtures/ | 自签名HTTPS测试证书和私钥 |
package.json | Pi Package、依赖、测试和发布约束 |
.gitignore | 本地开发产物和环境文件忽略规则 |
README.md | 对外安装、参数、响应格式和安全说明 |
核心依赖关系:
flowchart TD PI[Pi Runtime] --> INDEX[index.ts] INDEX --> TOOL[http Tool] TOOL --> REQUEST[executeHttpRequest] REQUEST --> UNDICI[undici] REQUEST --> FS[Node File API] TEST[request.test.ts] --> REQUEST TEST --> HTTP[Local HTTP Server] TEST --> HTTPS[Local HTTPS Server] HTTPS --> TLS[test-fixtures]
这里最重要的是request.ts不依赖Pi运行时对象。
ExtensionAPI、ctx、TUI、Session等只存在于index.ts,因此请求执行层可以直接使用Node Test Runner测试。
Pi Extension#项目结构里“入口薄、业务实现独立”的实际应用。
分层
可以把代码分成两层。
| 层 | 文件 | 关注点 |
|---|---|---|
| Pi Adapter | index.ts | Tool契约、LLM Prompt、Context预算 |
| HTTP Core | request.ts | HTTP和本地文件语义 |
Pi Adapter
index.ts只需要回答:
- Agent能传什么参数;
- 什么时候应该调用这个Tool;
- 如何调用HTTP Core;
- 最终多少内容允许进入Context;
- 哪些结构化数据放
details。
HTTP Core
request.ts负责:
- 参数组合语义;
- Body构造;
- 请求取消和Timeout;
- TLS行为;
- Response Header保真;
- Response Body读取;
- 文本/二进制编码;
- 下载事务;
- 错误分类。
这样以后即使Pi Tool接口调整,HTTP实现基本不用动;反过来替换HTTP Client时,也不需要重写Tool Schema。
Tool入口
Tool名称是http,入口使用Pi的defineTool()构造。
参数主要分成四组:
| 类别 | 参数 |
|---|---|
| 基础请求 | url、method、headers |
| Body | body、json、form、files |
| 连接 | timeoutMs、insecure |
| 输出 | maxBytes、downloadPath、overwrite |
Schema禁止额外字段:
Type.Object(properties, {
additionalProperties: false,
});这里主要是限制模型自由发挥参数名,避免Tool层收到“看起来合理但实际上没有实现”的选项。
通用Tool定义参考Pi Extension#Tool开发。
Prompt约束
Tool除了description,还提供了promptGuidelines。
规则核心是:
- 直接HTTP(S)请求优先调用
http; - 默认不要生成Shell HTTP命令;
- 只有用户明确要求其它客户端、Tool表达不了请求、或已有专用搜索/内容提取工具时才绕过;
- 下载文件时使用
downloadPath; overwrite只在确实需要替换文件时启用。
这部分很重要,因为Extension不是单纯“把Tool注册进去”,还需要告诉Agent能力边界和选择优先级。
请求执行总流程
executeHttpRequest()是HTTP Core的Orchestrator:
flowchart TD A[executeHttpRequest] --> B[校验Body / Download参数] B --> C[创建Headers] C --> D[构造Request Body] D --> E[推断Method] E --> F[组合Timeout和外部AbortSignal] F --> G{insecure?} G -->|否| H[undici fetch] G -->|是| I[配置TLS] I --> H H --> J{downloadPath?} J -->|是| K[Response流式写文件] J -->|否| L[按maxBytes读取Body] K --> M[格式化Download Result] L --> N[识别文本 / 二进制] N --> O[格式化Response Result] M --> P[返回index.ts] O --> P P --> Q[Pi级二次截断]
请求层返回统一的:
interface HttpExecutionResult {
text: string;
details: HttpDetails;
}text给LLM直接读,details保留结构化状态。
Body参数
支持四种Body输入:
| 参数 | 实际编码 |
|---|---|
body | Raw String |
json | JSON |
form | URL Encoded Form |
files | Multipart Form |
互斥关系
Body参数不是任意组合:
| 组合 | 允许 |
|---|---|
body | 是 |
json | 是 |
form | 是 |
files | 是 |
form + files | 是 |
body + json | 否 |
body + form | 否 |
json + form | 否 |
files + body | 否 |
files + json | 否 |
这种跨字段约束没有硬塞进TypeBox,而是在HTTP Core里单独校验。
这样做的好处:
- 规则直观;
- 错误信息容易稳定;
- HTTP Core即使脱离Pi调用,也保留自己的业务约束。
Presence判断
实现里不是简单判断值是否Truthy,而是判断字段是否实际存在。
这个细节保证了下面这些合法值不会被误判成“没传”:
- 空字符串Body;
false;0;nullJSON;- 空对象。
这是构造通用请求参数时比较值得保留的做法。
Method推断
如果显式提供method,优先使用。
没有提供时:
| 最终Body | Method |
|---|---|
| 无 | GET |
| 有 | POST |
因此JSON、Form和Multipart都会自然得到POST。
Method只是做最小推断,没有自己维护完整HTTP方法白名单。
协议是否合法继续交给Undici处理,避免Tool层重新实现HTTP规范。
JSON
JSON模式:
- 序列化为JSON;
- 没有显式
Content-Type时补application/json; - 用户手动提供的Header优先。
设计规则是:
自动默认值只能补缺省值,不能覆盖调用方显式配置。
URL Encoded Form
纯form使用URL Encoded编码。
默认Content-Type为:
application/x-www-form-urlencoded;charset=UTF-8
如果用户已经提供Content-Type,不会覆盖。
Multipart
存在files后切换到Multipart:
flowchart TD A[files存在] --> B[创建FormData] B --> C[加入form字段] C --> D[遍历files] D --> E[解析本地路径] E --> F[读取文件] F --> G[创建Blob] G --> H[追加文件字段] H --> I[交给Undici发送]
files类型是:
Record<string, string>含义:
| 部分 | 含义 |
|---|---|
| Key | Multipart字段名 |
| Value | 本地文件路径 |
例如概念上:
files: {
file: "./report.txt",
}文件路径
相对路径基于Pi当前cwd解析。
同时接受@前缀:
| 输入 | 处理 |
|---|---|
./report.txt | 正常解析 |
@./report.txt | 去掉@后解析 |
这里的@只是为了让调用习惯更接近常见CLI,并没有引入新的文件引用协议。
Multipart的当前取舍
上传文件使用readFile()整体读入内存,再包装成Blob。
优点:
- 实现简单;
- 和FormData接口契合;
- 测试容易;
- 小文件上传足够稳定。
限制:
- 大文件上传会占用明显内存;
- 没有上传进度;
- 不是Streaming Upload。
如果以后支持大文件,优先考虑重构这里。
另外Multipart Header最好继续交给Undici自动生成Boundary;调用方手动写Multipart Content-Type时要特别谨慎。
Timeout和取消
请求有两个取消来源:
| 来源 | 作用 |
|---|---|
Pi传入AbortSignal | 用户或Agent中止Tool |
timeoutMs | 请求自身超时 |
两个Signal会组合成请求Signal:
flowchart LR A[Pi AbortSignal] --> C[Combined Signal] B[Timeout Signal] --> C C --> D[undici fetch]
异常发生后再检查是哪一个Signal触发。
分类逻辑:
flowchart TD A[请求抛出异常] --> B{Timeout触发且外部未Abort?} B -->|是| C[HTTP_TIMEOUT] B -->|否| D{外部Signal Abort?} D -->|是| E[HTTP_ABORTED] D -->|否| F[HTTP_REQUEST_FAILED]
这里比直接把Undici异常原样抛给Agent更稳定。
特别是Timeout不只覆盖“等待响应头”,测试也覆盖了响应头已经返回,但Body迟迟未结束的情况。
TLS
insecure=true关闭证书校验。
实现专门区分Node和Bun:
| Runtime | 实现方式 |
|---|---|
| Bun | 请求TLS Option |
| Node / Undici | 临时创建自定义Agent |
flowchart TD A[insecure=true] --> B{Bun?} B -->|是| C[TLS rejectUnauthorized=false] B -->|否| D[创建Undici Agent] D --> E[Agent connect.rejectUnauthorized=false] C --> F[发送请求] E --> F
实现没有把Pi宿主环境硬编码成Node。
Dispatcher清理
Node分支创建的Agent只服务当前请求。
无论成功失败,最后都会尝试关闭。
如果清理自身失败,不会覆盖真正的HTTP执行结果。
这里应该保持:
Cleanup Error不能替代Primary Error。
测试使用本地自签名HTTPS服务器验证:
- 默认请求失败;
insecure=true后成功。
Response Header
Response Header最终保存为数组,而不是普通Object:
interface HttpHeader {
name: string;
value: string;
}原因是Header可能重复。
最典型的是:
Set-Cookie
实现会单独读取重复Cookie,再逐条放入结果。
如果使用Record<string, string>,多个Cookie会发生信息损失。
所以details.headers保持List语义是刻意设计,不应该随意简化成Object。
普通Response读取
没有downloadPath时,Response Body需要进入Context。
这里没有直接一次性读取完整Body,而是读取Stream并在达到上限后停止:
flowchart TD A[Response Stream] --> B[读取Chunk] B --> C{达到maxBytes?} C -->|否| D[记录Chunk] D --> B C -->|是| E[保留允许的部分] E --> F[Cancel Reader] B -->|EOF| G[合并结果] F --> G
这样可以在网络读取阶段控制内存和Context,而不是先把巨大Response完整读完再截断。
maxBytes
maxBytes表示原始响应Body字节限制。
它不等于:
- 字符数量;
- Base64编码后的长度;
- 最终Tool Result总大小。
Tool Schema把最大值限制在Pi的默认最大输出字节数以内。
文本和二进制
读取Body后,通过Content-Type判断编码方式。
视为文本的主要类型:
- Content-Type缺失;
text/*;- JSON;
- XML;
- JavaScript;
- URL Encoded;
- SVG。
其它类型按二进制处理并转Base64。
flowchart LR A[Body Bytes] --> B{Text-like Content-Type?} B -->|是| C[UTF-8 Decode] B -->|否| D[Base64] C --> E[Response Text] D --> E
这个策略是基于Content-Type的启发式处理。
当前没有:
- MIME sniffing;
- charset转换;
- gzip等协议层手工处理。
这些继续交给Fetch/服务端Header语义。
因此如果服务端错误声明MIME,Tool也可能采用错误编码方式。
HTTP Status不是Tool Error
实现没有使用response.ok决定是否抛异常。
因此:
- 400;
- 401;
- 404;
- 500;
都会正常返回Status、Header和Body。
flowchart LR A[HTTP 500] --> B[有效HTTP Response] B --> C[保留诊断Body] C --> D[Agent继续分析]
这是Agent HTTP Tool比较重要的边界:
HTTP Error属于协议结果;Transport / Local Error才属于Tool执行错误。
如果直接对所有非2xx抛异常,Agent反而拿不到服务端的诊断正文。
下载模式也沿用同样语义,所以404/500的响应体仍可能被写到downloadPath;调用方必须查看返回的HTTP Status,而不能只根据“文件写成功”判断业务请求成功。
错误分类
当前使用稳定字符串前缀分类,而不是自定义Error Class:
| Prefix | 含义 |
|---|---|
HTTP_INVALID_BODY | Body参数组合非法 |
HTTP_INVALID_DOWNLOAD | 下载参数非法 |
HTTP_FILE_ERROR | 上传文件读取失败 |
HTTP_DOWNLOAD_FAILED | 下载本地写入失败 |
HTTP_TIMEOUT | 请求超时 |
HTTP_ABORTED | Tool被取消 |
HTTP_REQUEST_FAILED | 其它网络/TLS/客户端错误 |
这种方案目前足够:
- Agent容易读;
- Test容易断言;
- 不需要维护Error继承树;
- 以后需要程序化处理时还能升级。
Error边界
校验和上传文件读取发生在Fetch主try之前,因此这些业务错误不会被重新包装成通用网络错误。
下载错误发生在Fetch流程内部,所以会单独保留HTTP_DOWNLOAD_FAILED。
Timeout和Abort优先根据Signal重新分类,避免被下载层或Undici错误文本掩盖。
Response格式
普通Response返回:
- HTTP Status;
- 最终URL;
- Body Metadata;
- Headers;
- Body。
Body Metadata至少包含:
| 字段 | 含义 |
|---|---|
encoding | utf-8、base64或file |
bytes | 已读取/写入的原始Body字节数 |
truncated | Body或最终Tool文本是否发生截断 |
同时保存结构化details:
flowchart LR A[HTTP Result] --> B[text] A --> C[details] B --> D[LLM阅读] C --> E[结构化状态]
这样不需要让Agent从自然语言里反解析Status等字段。
参考Pi Extension#content和details。
两层截断
这里有两种不同的限制。
HTTP Body限制
在request.ts发生:
flowchart LR A[Network Stream] --> B[Body Reader] B -->|maxBytes| C[有限Body]
解决:
- 网络响应过大;
- 内存增长;
- Body Context占用。
Tool Result限制
回到index.ts以后,再使用Pi的truncateHead()限制完整文本:
flowchart LR A[Status + URL + Headers + Body] --> B[truncateHead] B --> C[DEFAULT_MAX_BYTES] B --> D[DEFAULT_MAX_LINES] C --> E[最终Tool content] D --> E
解决:
- Header很多;
- Base64膨胀;
- Body虽然有限,但整个输出仍可能超限;
- 行数过多。
因此:
maxBytes是HTTP Body预算,不是最终Context预算。
对应Pi Extension#输出截断里的“业务层限制 + Tool层最终兜底”。
Base64膨胀
二进制经过Base64后文本体积会增长,所以第二层截断仍然有必要:
flowchart LR A[Binary Body] --> B[maxBytes限制原始字节] B --> C[Base64编码] C --> D[Tool级再次截断]
truncated同步
如果第二层截断发生:
details.body.truncated会改成true;- 文本里的Body Metadata也同步改成
true。
避免结构化数据和Agent看到的文本状态不一致。
文件下载
存在downloadPath时,响应Body不进入模型Context。
flowchart TD A[HTTP Response Stream] --> B[本地File Handle] B --> C[逐Chunk写入] C --> D[记录总字节数] D --> E[返回Path + Metadata]
下载模式:
- 不受Body
maxBytes限制; - 不进行Base64;
- 自动创建父目录;
- 默认不覆盖已有文件;
- 失败时清理Partial File。
下载事务
下载是实现里最值得记录的文件设计。
默认不覆盖
overwrite=false时直接独占创建最终文件:
flowchart TD A[目标路径] --> B[Exclusive Create] B -->|不存在| C[开始写入] B -->|已存在| D[HTTP_DOWNLOAD_FAILED] C --> E{完整完成?} E -->|是| F[保留文件] E -->|否| G[删除Partial File]
因此默认不会因为Agent一次Tool调用直接覆盖用户现有文件。
overwrite=true
允许覆盖时,不直接写原文件:
flowchart TD A[目标文件] --> B[同目录创建随机临时文件] B --> C[完整下载到临时文件] C --> D{下载成功?} D -->|否| E[删除临时文件] D -->|是| F[Rename到最终路径]
原文件在下载完成前保持不变。
这个设计可以看成简化的Transactional Write。
临时文件和目标文件位于同一目录,也避免跨文件系统Rename的问题。
Partial Write
底层写Chunk时没有假设一次write()一定写完整。
逻辑会持续写到整个Chunk全部提交。
这是文件API里容易漏掉的小细节,后面如果重构下载代码不要退化掉。
失败清理
失败路径会:
- Cancel Response Reader;
- Close File Handle;
- 删除已经创建但未完成的目标/临时文件。
Timeout和Abort也会走到清理逻辑。
测试明确验证Timeout时不会留下Partial Download。
并发
Pi Tool可能并发,参考Pi Extension#并行Tool和文件修改。
当前文件下载已经提供:
- Exclusive Create;
- 随机临时文件;
- 完成后Rename;
- 失败清理。
但两个请求如果都:
- 指向同一个
downloadPath; - 同时设置
overwrite=true;
仍然属于Last Writer Wins语义。
如果以后需要更严格的“同路径串行修改”,可以在Pi Adapter层按最终路径接入withFileMutationQueue()。
当前没有这样做,应该视为一个明确的并发边界,而不是默认认为下载完全串行安全。
测试架构
request.test.ts没有Mock Fetch,而是在测试进程内启动真实HTTP Server。
flowchart TD A[node:test before] --> B[创建临时目录] B --> C[创建上传Fixture] C --> D[启动HTTP Server] D --> E[随机本地端口] C --> F[读取TLS Fixture] F --> G[启动HTTPS Server] G --> H[随机本地端口] I[Test Cases] --> D I --> G I --> J[executeHttpRequest] J --> D J --> G K[node:test after] --> L[关闭Server] K --> M[删除临时目录]
这种方式介于Unit Test和Integration Test之间,更接近HTTP Core Contract Test:
- 不需要Internet;
- 不依赖第三方API;
- 真实经过Socket;
- 真实经过Undici;
- 真实操作文件系统;
- TLS也是真实握手。
相比Mock fetch(),更容易发现Stream、Header、Abort和TLS这类行为差异。
测试覆盖
当前测试覆盖的行为:
| 场景 | 主要验证 |
|---|---|
| GET | 默认Method和规范化输出 |
| insecure HTTPS | 自签名证书默认失败,insecure成功 |
| JSON | 自动POST、Content-Type、自定义Header |
| Raw Body | 自定义Method |
| URL Encoded | Form编码和Content-Type |
| Multipart | Form + File上传 |
| HTTP 500 | 非2xx保持正常Result |
| Set-Cookie | 重复Header不丢失 |
| Binary | Base64编码 |
| Download | Body不进入Result、完整落盘 |
| Overwrite | 默认拒绝,显式允许替换 |
| maxBytes | Body按字节截断 |
| Timeout | Header前和Body阶段都能超时 |
| Abort | 外部Signal取消 |
| 参数冲突 | Body和Download组合校验 |
| Partial Download | Timeout后文件被清理 |
| Missing Upload | 本地文件错误分类 |
值得注意的是Timeout测试同时覆盖:
- 服务端迟迟不返回完整请求结果;
- 已经发出Response Header但Body迟到。
这能避免只测试连接阶段Timeout。
TLS Fixture
test-fixtures/中的证书和私钥只是公开的测试Fixture,用于本地自签名HTTPS Server。
它们:
- 不是生产Credential;
- 只用于验证证书校验开关;
- 不进入npm发布包。
这里保留Fixture比测试时动态调用OpenSSL更稳定,也减少测试环境依赖。
Package设计
package.json当前关键配置:
| 配置 | 值/作用 |
|---|---|
name | @evalexp/pi-http |
version | 0.1.1 |
type | ESM |
engines.node | >=20.18.1 |
| Runtime Dependency | undici |
| Peer Dependency | Pi Coding Agent、TypeBox |
| Dev Dependency | tsx |
| Pi入口 | ./index.ts |
| Publish Access | Public |
Package直接发布TypeScript源码,没有构建产物。
这依赖Pi本身支持直接加载Extension TypeScript,通用机制参考Pi Extension#TypeScript直接加载。
files白名单
npm包只发布运行时必要内容:
index.ts;request.ts;README.md;LICENSE。
测试代码、TLS Fixture和其它本地文件不会进入最终Package。
这样npm包保持很小,也不会把测试私钥Fixture误当运行时资源发布出去。
依赖分类
undici是运行时真正使用的第三方库,所以放dependencies。
Pi和TypeBox由宿主环境提供,所以放peerDependencies。
tsx只负责运行测试,所以放devDependencies。
这和Pi Extension#依赖管理里的依赖边界一致。
测试和发布
测试脚本:
npm test底层使用Node Test Runner,通过tsx直接执行TypeScript测试。
发布前还有:
prepublishOnly
因此npm publish前会自动跑测试。
当前发布流程可以理解为:
flowchart LR A[修改源码] --> B[npm test] B --> C[npm pack --dry-run] C --> D[npm publish] D --> E[Pi安装/更新验证]
目前仓库没有额外CI Workflow,所以发布质量门主要依赖本地prepublishOnly和手动检查tarball。
.gitignore
仓库忽略:
node_modules;package-lock.json;- Coverage;
- npm tarball;
- Log;
.env*;- IDE元数据;
- TypeScript增量构建文件。
其中.env.example允许提交。
当前没有提交Lockfile,因此依赖安装依靠SemVer范围重新解析。
对于这样的小型Extension问题不大,但如果以后依赖明显增多,需要重新考虑测试可复现性。
安全边界
这个Tool能力比较高,因为它同时拥有网络和本地文件权限。
主要风险:
网络
能够访问Pi宿主能访问的URL,包括:
- localhost;
- 内网服务;
- Metadata Endpoint;
- 管理接口。
所以它天然具备SSRF式能力。
这不是代码漏洞,而是Tool能力本身的权限边界。
上传
files可以读取当前Pi进程权限范围内的本地文件。
Agent生成上传请求时,需要特别关注:
- Secret;
- SSH Key;
- Token;
- 配置文件;
- 浏览器/工具Credential。
下载
downloadPath能够创建本地文件。
overwrite=true还能替换已有文件。
默认overwrite=false就是这里最重要的安全默认值。
insecure
insecure=true会绕过TLS证书验证,只应显式使用。
它不能作为“请求失败后的自动重试策略”。
Response
限制Response大小只解决Context预算问题,不代表远端内容可信。
Agent仍可能读取恶意Prompt Injection内容。
设计取舍
不实现完整HTTP客户端UI
没有加入:
- Cookie Jar;
- Session;
- Retry;
- Proxy配置;
- Auth Profile;
- Redirect Policy UI;
- Cache;
- Response JSONPath;
- HAR;
- 请求历史。
原因是这个Tool定位是Agent基础HTTP能力,不是Postman替代品。
功能越多,Schema越大,模型越难稳定调用。
不对HTTP状态抛异常
因为Agent通常正需要读取错误Body继续分析。
下载与普通Response分开
二进制先Base64再进Context很浪费。
有明确文件目标时直接落盘更合理。
两层截断
Body层解决网络和内存,Tool层解决最终Context。
只做其中一层都不够。
Header使用数组
为了保留重复Header,而不是追求API看起来简单。
Runtime兼容
TLS分支同时考虑Node和Bun,而不是只按标准Node CLI开发。
Error使用Prefix
当前项目规模小,不需要自定义Error Class体系。
当前限制
以后维护时优先记住这些边界。
上传不是流式
Multipart文件全部读入内存。
如果未来需要上传大型镜像、压缩包等,需要重构。
charset固定按UTF-8
文本Response目前没有根据:
charset=...
切换Decoder。
非UTF-8网站可能乱码。
Content-Type依赖服务端
二进制/文本判断是Header启发式,不做内容探测。
Multipart Header由调用方影响
如果调用方手动设置错误的Multipart Content-Type,可能破坏Boundary。
后续可以考虑明确禁止或修正这种配置。
同路径overwrite并发
两个并发下载都允许覆盖时,目前没有文件Mutation Queue。
HTTP状态和下载成功是两个维度
下载404正文也可能成功写文件。
调用方必须检查Status。
没有Retry
网络瞬时失败直接作为Tool Error返回。
是否增加Retry需要谨慎,因为:
- POST可能不是幂等;
- 自动Retry可能产生重复副作用;
- Agent自己可以决定是否重试。
当前不自动Retry是合理默认。
后续改动优先级
如果继续增强,建议按下面顺序考虑:
- 大文件Streaming Upload;
- charset处理;
- 下载路径并发Mutation Queue;
- 更明确的Multipart Content-Type保护;
- 根据需要增加Proxy;
- 最后才考虑Cookie Session或更复杂HTTP Client能力。
不要优先扩充大量Schema字段。
这个Tool的价值在于:
小、稳定、Agent容易正确调用。
维护时的核心约束
以后改代码时,至少保持:
index.ts继续薄;request.ts不直接依赖Pi Context;- 非2xx继续作为正常HTTP结果;
- Transport Error继续抛Tool Error;
- 普通Response始终有限制;
- 下载不受Body
maxBytes截断; - 默认禁止覆盖;
- overwrite下载先写临时文件;
- 失败不能留下Partial File;
- 重复
Set-Cookie不能丢; - Timeout必须覆盖Response Body阶段;
- Abort和Timeout错误要区分;
- 临时TLS Dispatcher必须清理;
- Cleanup失败不能覆盖Primary Error;
- 测试继续使用本地Server,不依赖公网。
问题快速定位
| 问题 | 先看 |
|---|---|
Agent没有选择http | index.ts Prompt Guidelines |
| 参数Schema不对 | index.ts HttpParameters |
| Body组合报错 | validateBodyInput() |
| JSON/Form/Multipart异常 | buildRequestBody() |
| 上传路径异常 | normalizeFilePath() |
| Timeout/Abort异常 | combineSignals()和主Catch |
| insecure异常 | TLS Runtime分支 |
| 响应被截断 | readResponseBody() + index.ts truncateHead() |
| 二进制乱码 | isTextualContentType() |
| Cookie缺失 | getResponseHeaders() |
| 下载覆盖/残留 | downloadResponseBody() |
| Tool返回格式 | formatResponse() / formatDownloadResponse() |
| 发布缺文件 | package.json files |
| HTTPS测试 | test-fixtures/ |