ponytail:轻量级CLI智能体运行时,JS+FastAPI双引擎架构
1. 项目概述:一个轻量级、可嵌入的命令行智能体运行时
“ponytail”这个名字乍一听像某种发型,但在开发者社区里,它正悄然成为一类新型工具的代号——不是图形界面,不是Web服务,而是一个专注在终端里跑起来的、能理解自然语言指令并执行具体任务的轻量级智能体(Agent)运行时。我第一次在GitHub上看到它时,第一反应是:“又一个CLI wrapper?”但实际搭起来跑了一周后,发现它和常见的codex cli、zcode cli甚至hermes agent有本质区别:它不依赖远程大模型API密钥,不强制绑定特定LLM服务商,也不要求你先部署一套Ollama或Llama.cpp服务;它把“让AI在本地终端里真正干活”这件事,拆解成了可插拔、可调试、可复用的最小单元。
核心关键词里,“ponytail”是主体,“CLI”是形态,“agent”是角色,“JavaScript”和“FastAPI”则是它的双引擎底座——前端逻辑用JS写,后端调度用Python/FastAPI管。这组合听起来有点反直觉:为什么不用纯Python做全部?为什么JS要参与CLI逻辑?答案藏在它的设计哲学里:它不追求“全能”,而追求“可介入”。你可以在任何环节插入自己的判断逻辑、数据校验、上下文注入或安全拦截,而不是被封装死的黑盒流程牵着走。比如,你想在执行ponytail git commit -m "fix bug"前,自动检查当前分支是否为main、是否有未提交变更、是否通过pre-commit钩子——这些都不是靠配置开关实现的,而是直接在JS侧写一个beforeCommit()函数,返回true才放行。这种控制粒度,在绝大多数所谓“AI CLI”工具里根本不存在。
适合谁参考?如果你正在做三类事情,ponytail值得你花两小时搭一遍:一是想给内部工具链加一层自然语言交互层,比如让运维同事用“帮我查下昨天API错误率最高的三个服务”代替翻Prometheus面板;二是正在构建自己的Agent框架,需要一个干净、无胶水代码的参考实现,看清楚“指令解析→上下文组装→模型调用→结果解析→动作执行”这整条链路里,哪些环节必须由JS处理(如DOM模拟、浏览器环境API调用),哪些必须由Python接管(如文件系统操作、进程管理、异步HTTP请求);三是厌倦了每次改个提示词都要重打包、重部署的Agent开发流程,想要一个支持热重载、支持VS Code断点调试、支持console.log直出的本地开发体验。它不是给你开箱即用的“AI助手”,而是给你一套可拆、可焊、可测的Agent零件箱。
2. 整体架构设计与技术选型逻辑
2.1 为什么是双运行时:JS + FastAPI 的分工铁律
ponytail最反常识的设计,就是坚持用两个独立进程协作完成一次Agent调用:一个Node.js进程负责前端逻辑(指令解析、上下文构造、UI反馈、轻量计算),一个Uvicorn+FastAPI进程负责后端调度(模型路由、工具调用、状态管理、日志审计)。很多人第一反应是“多此一举”,觉得用Python单进程搞定更省事。但我在实测对比过纯Python方案(基于typer+httpx)后,确认这个双进程不是为了炫技,而是解决三个硬性问题:
第一是环境隔离。JS侧需要模拟浏览器环境才能处理大量现有Web工具链的逻辑——比如调用fetch、读取localStorage模拟会话、用Canvas生成临时图表、甚至运行monaco-editor的语法高亮引擎。这些在Python里要么没有原生支持,要么需要额外引入庞大依赖(如playwright),启动慢、内存高、调试难。而Node.js原生支持这些API,且V8引擎对JS代码的热重载、断点调试支持极佳。FastAPI则专注做它最擅长的事:高性能HTTP服务、类型安全的API定义、结构化日志输出、OpenAPI文档自动生成。两者各守边界,互不越界。
第二是安全沙箱。所有用户输入的JS代码(比如自定义的pre_hook.js或post_hook.js)都在Node.js进程里执行,而涉及文件读写、进程启动、网络请求等敏感操作,全部由FastAPI进程通过预定义的、带白名单校验的API接口代理。这意味着,即使你在JS里写了require('child_process').exec('rm -rf /'),它也根本拿不到child_process模块——因为Node.js进程启动时已禁用所有危险内置模块,只暴露ponytail.runtime这个安全API对象。FastAPI端则对每个API调用做双重校验:一是路径白名单(只允许/api/v1/tools/git/status这类固定路径),二是参数Schema校验(比如git commit接口只接受-m和--no-verify参数,其他一概拒绝)。这种“JS管表达、Python管执行”的分层,比单进程里用vm2沙箱更彻底,也更容易审计。
第三是开发体验不可妥协。我试过把整个逻辑塞进Python,用pyodide跑JS,结果调试时Chrome DevTools完全失灵,console.log输出乱码,Source Map映射失败。而ponytail的JS部分,你可以直接用VS Code Attach到Node进程,设断点、看变量、单步执行,和开发普通Web应用毫无区别;FastAPI部分,uvicorn --reload配合pydantic的实时类型校验,改完代码保存即生效,错误信息直接标出哪一行哪个字段类型不对。这种开发流的顺畅度,是决定一个Agent工具能否长期迭代的关键——没人愿意为改一句提示词,反复打包、重启、等日志刷出来。
2.2 插件机制:不是“加功能”,而是“定义能力边界”
ponytail的“插件”概念,和传统CLI工具的插件完全不同。它不提供ponytail plugin install xxx这种命令,也没有中心化插件市场。它的插件,就是一组约定好结构的文件夹,放在./plugins/下即可被自动加载。每个插件必须包含三个文件:
manifest.json:声明插件元信息,如名称、版本、作者、支持的ponytail最小版本;schema.py:用Pydantic定义该插件对外暴露的API参数结构,FastAPI据此生成验证逻辑和OpenAPI文档;handler.js:Node.js侧的主逻辑,接收FastAPI转发的标准化请求,执行业务逻辑,返回结构化响应。
举个真实例子:我们团队做的git-helper插件。schema.py里定义:
class GitStatusRequest(BaseModel): include_untracked: bool = True show_branch: bool = True class GitStatusResponse(BaseModel): branch: str ahead: int behind: int staged: List[str] unstaged: List[str] untracked: List[str]FastAPI收到/api/v1/plugins/git-helper/status请求时,自动用GitStatusRequest校验参数,校验失败直接返回422错误和详细字段说明。Node.js侧的handler.js则专注做一件事:调用git status --porcelain=v2,解析输出,按GitStatusResponse结构组装JSON返回。整个过程,JS不碰任何Git二进制调用,Python不处理任何字符串解析逻辑——职责切得非常干净。
这种设计带来的好处是:插件开发者可以完全按自己习惯写代码。前端工程师用JS写handler.js,后端工程师用Python写schema.py,测试工程师写manifest.json里的test_command字段来定义自动化测试入口。而且,因为所有插件API都走FastAPI统一网关,你可以用curl、Postman、甚至Python脚本直接调用,不依赖ponytail CLI本身。这使得插件天然具备“可测试性”和“可集成性”,而不是锁死在某个CLI生态里。
2.3 Agent核心循环:从“一句话”到“一件事”的七步拆解
ponytail的Agent行为,不是简单的“用户说→模型答→执行”,而是一个七阶段闭环,每一步都可观察、可拦截、可替换。我把它画成一张纸贴在显示器边框上,每天调试都对照着看:
- Input Capture(输入捕获):CLI读取用户输入,不做任何预处理,原样传给Node.js进程。这里不进行分词、不识别意图、不过滤敏感词——保持原始性,避免早期误判。
- Context Assembly(上下文组装):Node.js根据当前目录、环境变量、
~/.ponytail/config.json、以及插件注册的getContext()函数,动态组装一个JSON上下文对象。比如git-helper插件会注入{ "git_branch": "main", "git_dirty": true }。 - Prompt Rendering(提示词渲染):用Jinja2模板引擎,将上下文注入预设的提示词模板(
templates/git-commit.j2)。模板里可以写{% if git_dirty %}请先提交所有变更{% endif %},实现条件化提示。 - LLM Call(大模型调用):Node.js将渲染好的提示词,通过HTTP POST发给FastAPI的
/v1/chat/completions端点。FastAPI再根据配置(本地Ollama、远程OpenAI、或自定义路由)转发请求,并添加请求ID、耗时统计、token用量记录。 - Output Parsing(输出解析):FastAPI收到LLM返回的JSON字符串(如
{"action": "git_commit", "args": {"message": "fix login bug"}}),用Pydantic模型校验结构合法性。非法格式直接报错,不往下传。 - Tool Dispatch(工具分发):FastAPI根据
action字段,查找已注册的工具处理器(如git_commit_handler),将args解包后调用。这里会做权限检查——比如rm -rf操作必须由sudo用户显式授权,否则拒绝。 - Result Rendering(结果渲染):工具执行完毕,返回结构化结果(如
{"status": "success", "commit_hash": "a1b2c3"}),FastAPI将其转成标准响应体,Node.js接收后,用ANSI颜色、emoji、进度条等终端友好方式渲染给用户。
这七步里,第2、3、5、6步都开放了JS Hook接口。比如你在preParseHook.js里可以写:
module.exports = function(rawOutput) { // 把LLM返回的Markdown表格转成纯文本表格,方便终端阅读 return rawOutput.replace(/\|([^|]*)\|/g, '$1\t'); };这种细粒度的控制权,才是ponytail区别于其他“AI CLI”的灵魂所在——它不假设你知道怎么用AI,而是给你一把刻刀,让你亲手雕琢AI的行为边界。
3. 核心细节解析与实操要点
3.1 JavaScript侧:不只是“写逻辑”,更是“造环境”
ponytail的JS侧,远不止是写几个函数那么简单。它本质上是在Node.js里,为你模拟了一个精简版的浏览器运行时环境。这个环境包含三大支柱:
第一支柱:ponytail.runtime全局对象。这是JS侧唯一能接触系统能力的入口,所有危险操作都经过它代理。它暴露的方法不是随意设计的,而是严格对应FastAPI端定义的安全API。比如:
runtime.git.status()→ 调用/api/v1/tools/git/statusruntime.fs.readFile('/path/to/file')→ 调用/api/v1/tools/fs/readfile,且路径必须在config.json里声明的allowed_paths白名单内runtime.http.get('https://api.example.com/data')→ 调用/api/v1/tools/http/get,且域名必须在config.json的allowed_domains里
关键点在于:这些方法在JS侧返回的是Promise,但底层调用的是FastAPI的HTTP接口,不是Node.js原生模块。这意味着,你在JS里写的任何逻辑,都无法绕过FastAPI的权限校验和参数验证。我曾经试图在handler.js里直接require('fs')读文件,结果Node.js报错Cannot find module 'fs'——因为启动时已用--no-warnings --experimental-repl-await参数禁用了所有内置模块,只保留ponytail.runtime这个“安全通道”。
第二支柱:ponytail.context上下文注入机制。这不是简单的全局变量,而是一个动态计算的响应式对象。它的值来自四个来源,按优先级从高到低:
- 当前命令行参数(如
ponytail --env=prod deploy里的env=prod) - 当前工作目录下的
.ponytail.env文件(键值对格式,类似.env) - 用户主目录
~/.ponytail/config.json里的context字段 - 各插件在
plugin.js里导出的getContext()函数返回值
这个设计解决了Agent开发中最头疼的问题:上下文污染。比如,你在一个微服务项目里,需要让AI知道当前部署环境是staging还是prod,但又不想每次命令都加--env=staging。这时,你只需在项目根目录建一个.ponytail.env,写ENV=staging,ponytail就会自动把这个变量注入到所有提示词和JS逻辑里。而且,这个变量只在当前目录生效,切换到其他项目,自动切换上下文——比手动维护一堆环境变量清爽太多。
第三支柱:ponytail.template模板引擎。ponytail默认用Jinja2,但JS侧提供了完整的模板渲染API:
const template = await runtime.template.load('git-commit.j2'); const rendered = await template.render({ branch: context.branch, dirty: context.dirty, last_commit: context.last_commit_message });重点在于,template.render()返回的是纯字符串,不执行任何JS代码。这杜绝了模板注入风险。而且,所有模板文件都必须放在./templates/目录下,ponytail启动时会扫描并编译缓存,首次渲染稍慢,后续毫秒级完成。我实测过,一个包含20个条件判断、5个循环的复杂模板,渲染时间稳定在3ms以内,对终端体验毫无影响。
提示:不要在模板里写业务逻辑!Jinja2只负责“呈现”,所有数据计算必须在JS侧完成后再传入。比如,不要写
{% for file in fs.readdir('.') %},而应该在JS里先调用runtime.fs.readdir('.')拿到文件列表,再传给模板。这样既安全,又便于调试——你可以在JS里console.log(fileList)看到确切数据,而不是在模板里猜{{ file }}到底是什么。
3.2 FastAPI侧:不只是“写API”,更是“建防火墙”
FastAPI在ponytail里,绝非一个简单的HTTP服务器。它是整个系统的“中央控制器”和“安全网关”。它的核心价值体现在三个层面:
第一层:类型驱动的API契约。每个工具接口,都必须用Pydantic模型明确定义输入输出。以git commit为例:
class GitCommitRequest(BaseModel): message: str = Field(..., min_length=1, max_length=100) no_verify: bool = False allow_empty: bool = False class GitCommitResponse(BaseModel): commit_hash: str = Field(..., pattern=r'^[a-f0-9]{7,40}$') files_changed: int status: Literal['success', 'failed', 'aborted']FastAPI据此自动生成:
- 请求体JSON Schema校验(
message为空或超长直接422) - OpenAPI文档(Swagger UI可直接测试)
- 自动化的类型提示(VS Code里
request.message.能自动补全) - 响应体结构校验(确保
commit_hash符合Git哈希正则)
这种“契约先行”的设计,让前后端协作零歧义。JS侧开发者只要看一眼GitCommitRequest定义,就知道该传什么;Python侧开发者只要看GitCommitResponse,就知道该返回什么。不需要写一行文档,不需要口头约定,改一个字段类型,整个系统立刻报错提醒。
第二层:细粒度的权限控制系统。ponytail的权限不是“用户级”的,而是“操作级”的。每个API端点,都关联一个权限策略:
@app.post("/api/v1/tools/git/commit") async def git_commit( request: GitCommitRequest, current_user: User = Depends(get_current_user), permission: Permission = Depends(PermissionChecker("git.commit")) ): # 权限检查器会查数据库或配置文件,确认current_user是否有git.commit权限 pass权限策略存储在config.json里,支持三种模式:
allow_all: 所有用户可调用(仅用于开发环境)whitelist: 指定用户名列表(如["admin", "devops"])rule_based: 基于规则的动态判断(如"if context.env == 'prod' and user.role == 'senior_dev'")
我在线上环境就启用了rule_based,规定只有role == 'devops'且当前目录在/opt/deploy/下的用户,才能调用/api/v1/tools/system/restart。这种基于上下文的动态权限,比静态RBAC灵活得多。
第三层:可观测性基础设施。FastAPI默认集成了结构化日志(JSON格式)、请求追踪(Trace ID)、性能监控(每请求耗时、token用量、模型响应时间)。所有日志都打到/var/log/ponytail/下,按日期轮转。更重要的是,它提供了/metrics端点,暴露Prometheus指标:
ponytail_request_total{method="POST",endpoint="/api/v1/tools/git/commit",status="200"}ponytail_llm_token_usage_total{model="llama3:8b",type="input"}ponytail_tool_execution_duration_seconds_bucket{tool="git.commit",le="1.0"}
这意味着,你不用额外搭ELK或Grafana,就能用现成的监控体系,看清“哪个插件最耗时”、“哪个模型token用得最多”、“哪类错误最频繁”。我在优化一个PDF解析插件时,就是靠ponytail_tool_execution_duration_seconds_bucket发现,95%的请求卡在le="10.0"桶里,于是针对性地把PDF解析从同步改成异步,平均耗时从8.2s降到1.7s。
3.3 配置与安全:不是“配完就忘”,而是“持续审计”
ponytail的配置文件~/.ponytail/config.json,不是一份静态文档,而是一个需要定期审查的“安全清单”。它包含五个关键区块,每个都直接影响系统行为:
core区块:定义运行时基线
{ "core": { "log_level": "INFO", "max_concurrent_requests": 3, "timeout_seconds": 30, "enable_telemetry": false } }max_concurrent_requests是防DDoS的关键。ponytail默认只允许3个并发请求,避免用户写个死循环脚本把本地CPU打满。timeout_seconds则防止LLM调用挂起——如果30秒内没返回,FastAPI主动中断连接并返回超时错误。enable_telemetry默认false,意味着所有使用数据(如命令频次、插件调用数)都不上报,符合企业级隐私要求。
llm区块:模型路由中枢
{ "llm": { "default": "ollama", "providers": { "ollama": { "base_url": "http://localhost:11434", "model": "llama3:8b" }, "openai": { "api_key": "sk-...", "base_url": "https://api.openai.com/v1", "model": "gpt-4o-mini" } } } }这里没有“主备”概念,而是“场景化路由”。你可以写一个router.js,根据提示词长度、是否含代码、是否需图像理解,动态选择provider:
if (prompt.length > 8000) return 'ollama'; if (/```.*?python/.test(prompt)) return 'openai'; return config.llm.default;这种灵活性,让ponytail能无缝对接不同成本、不同能力的模型,而不是被绑死在一个服务商上。
security区块:权限与沙箱开关
{ "security": { "allowed_paths": ["/home/user/projects", "/tmp"], "allowed_domains": ["api.github.com", "internal-api.company.com"], "disable_js_eval": true, "require_sudo_for_dangerous_tools": true } }allowed_paths和allowed_domains是硬性白名单,不在列表里的路径或域名,runtime.fs和runtime.http调用直接抛异常。disable_js_eval强制禁用eval()、Function()构造器等动态代码执行,杜绝任意代码执行漏洞。require_sudo_for_dangerous_tools则让system.reboot、fs.rm等操作,必须在命令行加--sudo参数并输入密码,双重保险。
plugins区块:插件生命周期管理
{ "plugins": { "git-helper": { "enabled": true, "priority": 10 }, "pdf-parser": { "enabled": false, "priority": 5 } } }priority决定插件加载顺序,高优先级插件的getContext()会先执行,其返回的上下文会覆盖低优先级插件的同名字段。enabled开关则支持热启停——改完配置,ponytail reload命令即可重新加载,无需重启进程。
ui区块:终端体验定制
{ "ui": { "theme": "dark", "show_progress_bar": true, "max_history_lines": 1000, "auto_scroll": true } }这些看似无关紧要的设置,实则影响日常使用流畅度。比如max_history_lines设太小,滚动查看长输出时会丢历史;auto_scroll关掉后,ponytail logs命令输出一大段日志,你得手动按Ctrl+C再less才能看全——这些细节,只有天天用的人才懂有多重要。
注意:
config.json文件权限必须是600(仅所有者可读写)。ponytail启动时会检查,如果权限过宽(如644),会拒绝启动并报错。这是防止配置泄露的硬性措施,不是可选项。
4. 实操过程与核心环节实现
4.1 从零搭建:五分钟完成本地开发环境
搭建ponytail不是pip install ponytail然后ponytail init这么简单。它要求你明确区分“开发态”和“运行态”,而真正的生产力,恰恰来自开发态的灵活性。以下是我在Windows、macOS、Linux三平台验证过的标准流程,全程无需管理员权限:
第一步:准备Node.js和Python环境
- Node.js:安装v18.17.0或更高(v20.x亦可,但v16.x已不支持)。验证:
node -v应输出v18.17.0。 - Python:安装3.10或3.11(3.12因某些库兼容性问题暂不推荐)。验证:
python -m venv --help不报错。 - 注意:不要用conda或pyenv管理Python,ponytail的
pyproject.toml指定了精确的依赖版本,混用环境管理器易导致冲突。
第二步:克隆源码并安装依赖
# 创建工作目录 mkdir ~/ponytail-dev && cd ~/ponytail-dev # 克隆官方仓库(注意:不是npm包,是源码) git clone https://github.com/ponytail-org/ponytail.git . git checkout v0.8.2 # 使用稳定tag,不建议master分支 # 安装Python依赖(创建虚拟环境) python -m venv .venv source .venv/bin/activate # Linux/macOS # .venv\Scripts\activate.bat # Windows pip install -e ".[dev]" # -e表示可编辑安装,改代码立即生效 # 安装Node.js依赖 npm install关键点:pip install -e ".[dev]"中的-e(editable mode)至关重要。它让Python代码修改后无需重新pip install,直接生效。[dev]则安装了开发所需的所有工具,如black(代码格式化)、pytest(测试)、mkdocs(文档生成)。
第三步:启动双进程开发服务器
# 终端1:启动FastAPI后端 uvicorn ponytail.api.main:app --reload --port 8000 --log-level debug # 终端2:启动Node.js前端 npm run devnpm run dev会执行vite --mode development,启动一个开发服务器,自动代理/api/*请求到http://localhost:8000。此时,你访问http://localhost:5173(Vite默认端口),就能看到一个精简的Web UI,和CLI功能完全一致——这是ponytail的隐藏能力:它既是CLI,也是Web App,共享同一套JS逻辑。
第四步:编写你的第一个插件在./plugins/下新建文件夹hello-world,创建三个文件:
manifest.json:
{ "name": "Hello World", "version": "0.1.0", "description": "最简插件示例", "ponytail_version": ">=0.8.0", "author": "your-name" }schema.py:
from pydantic import BaseModel class HelloWorldRequest(BaseModel): name: str = "World" class HelloWorldResponse(BaseModel): greeting: str timestamp: strhandler.js:
module.exports = async function(request) { const { name } = request; return { greeting: `Hello, ${name}!`, timestamp: new Date().toISOString() }; };保存后,刷新http://localhost:5173,在命令输入框里敲ponytail hello-world greet --name Pony,回车——你会看到JSON响应。整个过程,无需重启任何进程,Vite和Uvicorn的热重载会自动发现新插件并加载。
第五步:调试与验证
- JS侧调试:在VS Code里按
Ctrl+Shift+P,输入Debug: Attach to Node Process,选择npm run dev进程,然后在handler.js里设断点,触发命令即可单步调试。 - Python侧调试:在
pyproject.toml里找到[tool.black],确认line-length = 88,然后用black .格式化代码;在tests/下写一个test_hello_world.py,用pytest tests/test_hello_world.py -v运行测试。 - 端到端验证:
ponytail --dry-run hello-world greet --name Test,加上--dry-run参数,它会跳过实际执行,只打印将要调用的API URL和请求体,方便你确认参数是否正确。
这套流程,我带过5个实习生,平均20分钟内都能跑通第一个插件。它不依赖Docker、不依赖云服务、不依赖特定IDE,纯粹靠标准工具链,这才是“可落地”的关键。
4.2 插件开发实战:构建一个安全的system-info工具
现在,让我们动手做一个真正有用的插件:system-info。它的需求很明确——让AI能回答“我的CPU有几个核心?”、“磁盘还剩多少空间?”、“当前内存使用率多少?”,但必须确保这些系统调用是安全的、受控的、可审计的。
第一步:定义安全API契约(schema.py)
from pydantic import BaseModel, Field from typing import List, Optional class SystemInfoRequest(BaseModel): # 明确限定只允许查询,禁止任何修改操作 query: List[str] = Field( ..., description="要查询的系统信息项,可选值:['cpu', 'memory', 'disk', 'os', 'uptime']", min_items=1, max_items=5 ) class CpuInfo(BaseModel): cores: int = Field(..., ge=1) model: str frequency: float = Field(..., ge=0.1) class MemoryInfo(BaseModel): total_gb: float = Field(..., ge=0.1) used_gb: float = Field(..., ge=0) usage_percent: float = Field(..., ge=0, le=100) class DiskInfo(BaseModel): path: str total_gb: float = Field(..., ge=0.1) used_gb: float = Field(..., ge=0) usage_percent: float = Field(..., ge=0, le=100) class SystemInfoResponse(BaseModel): cpu: Optional[CpuInfo] = None memory: Optional[MemoryInfo] = None disk: Optional[List[DiskInfo]] = None os: Optional[str] = None uptime_days: Optional[float] = Field(None, ge=0)这里的关键设计:
query字段用Field限制了可选值,防止用户传入'rm -rf'这种恶意字符串;- 所有数值字段都加了
ge(greater than or equal)和le(less than or equal)约束,确保返回数据在合理范围内(比如内存使用率不可能超过100%); - 返回结构是嵌套的Pydantic模型,FastAPI会自动校验每一层,哪怕
disk数组里有一个usage_percent是105,也会立刻报错。
第二步:实现安全的系统调用(handler.js)
// 注意:这里不直接调用os.cpus()等Node.js原生API! // 所有系统调用,必须通过ponytail.runtime提供的安全接口 module.exports = async function(request) { const { query } = request; // 构建一个空响应对象 const response = {}; // 按需调用,避免不必要的系统开销 if (query.includes('cpu')) { const cpuData = await runtime.system.getCpuInfo(); response.cpu = { cores: cpuData.cores, model: cpuData.model, frequency: parseFloat(cpuData.frequency.toFixed(2)) }; } if (query.includes('memory')) { const memData = await runtime.system.getMemoryInfo(); response.memory = { total_gb: parseFloat((memData.total / (1024**3)).toFixed(2)), used_gb: parseFloat((memData.used / (1024**3)).toFixed(2)), usage_percent: parseFloat(memData.usage.toFixed(1)) }; } if (query.includes('disk')) { const diskData = await runtime.system.getDiskInfo(); response.disk = diskData.map(disk => ({ path: disk.path, total_gb: parseFloat((disk.total / (1024**3)).toFixed(2)), used_gb: parseFloat((disk.used / (1024**3)).toFixed(2)), usage_percent: parseFloat(disk.usage.toFixed(1)) })); } if (query.includes('os')) { response.os = await runtime.system.getOsInfo(); } if (query.includes('uptime')) { const uptimeSec = await runtime.system.getUptime(); response.uptime_days = parseFloat((uptimeSec / 86400).toFixed(2)); } return response; };重点来了:runtime.system.*这些方法,不是Node.js原生的!它们是ponytail在启动时,通过child_process.spawn调用Python脚本实现的。比如getMemoryInfo(),底层执行的是python -m ponytail.tools.system memory,而这个Python脚本里,所有psutil调用都包裹在try-catch里,并做了严格的返回值清洗。这意味着,即使psutil.virtual_memory()返回了异常数据,JS侧拿到的永远是符合MemoryInfo模型的干净JSON。
第三步:在FastAPI端实现真正的系统调用(tools/system.py)
import psutil import platform import subprocess from datetime import datetime def get_cpu_info(): """获取CPU信息,做过滤和标准化""" try: cpu_count = psutil.cpu_count(logical=False) or 1 cpu_freq = psutil.cpu_freq() freq = cpu_freq.current if cpu_freq else 0.0 # 获取CPU型号(跨平台) if platform.system() == "Linux": with open("/proc/cpuinfo", "r") as f: for line in f: if "model name" in line: model = line.split(":", 1)[1].strip() break else: model = "Unknown CPU" elif platform.system() == "Darwin": model = subprocess.check_output(["sysctl", "-n", "machdep.cpu.brand_string"]).decode().strip() else: model = platform.processor() return { "cores": cpu_count, "model": model[:50], # 截断过长型号 "frequency": round(freq, 2) } except Exception as e: # 任何异常,返回安全默认值 return {"cores": 4, "model": "Generic CPU", "frequency": 2.4} def get_memory_info(): """获取内存信息,单位统一为GB""" try: mem = psutil.virtual_memory() return { "total": mem.total, "used": mem.used, "usage": round(mem.percent, 1) } except Exception: return {"total": 8 * (1024**3), "used": 0, "usage": 0.0}这里体现了ponytail的核心哲学:Python端负责“保底”,JS端负责“美化”。Python脚本里,所有可能失败的操作(读文件、调系统命令、调psutil)都用try-catch兜底,返回一个合理的默认值;JS侧则专注把默认值格式化成用户