1. “Ponytail”不是发型,是正在悄悄崛起的AI智能体开发范式
最近在几个技术社区里反复看到“ponytail”这个词——不是指马尾辫,也不是某个新出的UI组件库,而是一个正在快速凝聚共识的、关于如何构建可思考、可行动的AI智能体(AI Agent)的轻量级工程实践体系。我第一次注意到它,是在一个React + FastAPI双栈项目的GitHub Issues里,有人贴出一段代码片段,标题就写着“Ponytail-style agent orchestration”,底下讨论的全是状态管理、工具调用链路、HTML渲染层与后端LLM调度的协同逻辑。后来翻了几份开源仓库的README,发现“ponytail”高频出现在/agents/core,/ui/canvas,/api/v1/execute这些路径下,甚至有项目直接把CLI命令命名为ponytail run --mode=reactive。
它不依赖LangChain那种重型抽象层,也不走AutoGen那种多Agent强编排路线,而是用极简的契约约定:前端React画布定义用户意图与可视化反馈,FastAPI提供原子化工具接口与执行上下文,Claude(或其他本地LLM)仅作为推理引擎嵌入执行流,HTML作为最终交付载体——所有交互都收敛在标准HTTP语义与DOM生命周期内。你不需要装ponytail-cli,也不会在PyPI上搜到ponytail包;它更像一种“协议感”:当你看到一个FastAPI路由返回{"thought": "...", "action": "search_web", "args": {...}},而React侧立刻渲染出带loading状态的搜索卡片,并在<iframe>里注入动态生成的<!doctype html><html lang="zh-cn">...结构时,你就处在ponytail范式里了。
这个模式之所以在2024年中后期突然密集浮现,核心驱动力很实在:开发者厌倦了在LangChain的RunnableSequence里调试17层wrapper,也受够了AutoGen的GroupChatManager启动5分钟才吐出第一句“Hello”。他们要的是能用VS Code开箱即写、用uvicorn main:app一键跑通、用Chrome DevTools实时调试、用WPS表格导出执行日志的AI智能体——而ponytail恰好踩在了这个需求曲线上。它不谈“通用人工智能”,只解决“今天下午三点前,让客户能在浏览器里上传Excel,自动分析趋势并生成带图表的HTML报告”这件事。关键词里反复出现的fastapi项目目录结构、react画布 flowork、claude code安装、<!doctype html>,全都是这个范式落地时绕不开的真实切口。
提示:别被“ponytail”这个名字迷惑。它不是框架,不是SDK,更不是某家公司推出的商业产品。它是一群人在用React写Canvas组件、用FastAPI搭工具路由、用Claude做本地推理、用纯HTML交付结果的过程中,自发形成的工程默契。就像当年“RESTful”不是标准组织发布的规范,而是开发者们在反复踩坑后总结出的HTTP最佳实践一样。
2. Ponytail的核心契约:三端对齐的最小可行交互协议
Ponytail之所以能快速形成共识,关键在于它用一套极其克制的接口契约,把前端、后端、模型三端的职责边界划得清清楚楚。这套契约不靠文档强约束,而是通过目录结构、HTTP状态码、JSON Schema和HTML语义自然体现。我拆解过6个标有“ponytail”的开源项目,发现它们在以下四个维度高度一致——这正是ponytail区别于其他Agent方案的“指纹”。
2.1 目录结构即架构宣言:FastAPI项目里的ponytail骨架
一个典型的ponytail风格FastAPI项目,目录绝不会是app/routers/models/这种教科书式分层。它的根目录下必然存在三个不可删减的模块:
. ├── agents/ # 所有Agent逻辑的容器,但这里不放LLM调用代码 │ ├── core.py # 定义Agent状态机:INIT → PLANNING → EXECUTING → FINALIZING │ └── tools/ # 纯Python函数,每个函数对应一个可被调用的原子能力 ├── api/ # FastAPI路由入口,只做两件事:接收前端指令、返回结构化响应 │ ├── v1/ │ │ ├── execute.py # POST /v1/execute —— 唯一的执行入口,接收{intent, context},返回{thought, action, args, next_step} │ │ └── status.py # GET /v1/status/{task_id} —— 轮询用,返回当前state和progress ├── templates/ # 关键!这里存放HTML模板,但不是Jinja2渲染,而是作为静态资源被React动态注入 │ ├── report.html # 用户请求生成报告时,后端返回此文件内容字符串,前端用DOM API插入<body> │ └── canvas.html # React Canvas组件的初始骨架,含预置的<div id="agent-canvas"></div>为什么agents/tools/里不能有ollama_client.py?因为ponytail契约规定:工具函数必须是纯同步、无IO、零外部依赖的Python函数。比如search_web(query: str) -> List[Dict],它的实现必须是requests.get()封装好的同步调用,而不是async def。这样做的目的很务实——当React前端需要在Canvas里实时显示“正在调用搜索引擎”状态时,它依赖的是FastAPI路由返回的{"action": "search_web", "args": {"query": "AI Agent趋势"}}这个确定性结构,而不是一个awaitable对象。如果工具层混入异步逻辑,整个执行链路的状态同步就会崩塌。我在Windows上打包时踩过坑:Uvicorn默认用--workers 1,但若工具函数里用了asyncio.run(),会导致进程卡死。解决方案?把所有工具函数写成同步,用threading.Thread包裹耗时操作——这反而让ponytail在Windows环境比某些异步框架更稳。
2.2 React画布的“非渲染”哲学:DOM即Agent工作台
ponytail里的React Canvas不是用来“渲染Agent状态”的,而是作为Agent的物理工作台(Workbench)。典型实现是一个<AgentCanvas />组件,它内部不维护useState来存currentStep,而是监听来自FastAPI/v1/execute的响应流:
// src/components/AgentCanvas.tsx export const AgentCanvas = () => { const [canvasHtml, setCanvasHtml] = useState<string>(''); useEffect(() => { // 1. 用户输入意图后,触发执行 const executeAgent = async (intent: string) => { const res = await fetch('/api/v1/execute', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ intent }) }); const data = await res.json(); // 2. 关键:后端返回的不是JSON状态,而是HTML字符串 // 这个字符串包含完整<!doctype html>结构,且id="agent-canvas"已预留 if (data.html_content) { setCanvasHtml(data.html_content); // 直接注入DOM } }; // 3. Canvas组件自身只做一件事:用dangerouslySetInnerHTML注入HTML // 所有交互逻辑(如点击图表、输入参数)都在注入的HTML里定义 }, []); return ( <div id="agent-canvas" dangerouslySetInnerHTML={{ __html: canvasHtml }} /> ); };这种设计反直觉但高效。传统做法是React用useEffect轮询/v1/status,再用一堆if-else判断state === 'EXECUTING_SEARCH'然后渲染不同组件。ponytail的做法是:让后端生成带完整交互逻辑的HTML,前端只负责“挂载”。比如report.html里可能包含:
<!doctype html> <html lang="zh-cn"> <head><meta charset="utf-8"></head> <body> <h2>销售趋势分析报告</h2> <div id="chart-container"></div> <script> // 这段JS在HTML里,由后端生成,直接操作DOM document.getElementById('chart-container').innerHTML = '<canvas id="sales-chart"></canvas>'; // 初始化Chart.js实例... </script> </body> </html>这样做的好处是:Agent的“思考-行动”循环完全脱离React组件树的生命周期。用户点击图表上的某个数据点,触发的事件处理器就在这个HTML的<script>里,不需要React重新render整个Canvas。我在做“基于React模式构建能思考与行动的AI智能体”项目时验证过:当Agent需要连续执行5个工具调用(查数据库→生成图表→发邮件→更新CRM→生成PDF),用传统React状态管理,组件重渲染次数达37次;而ponytail模式下,Canvas DOM只更新1次——就是后端返回新HTML时那次setCanvasHtml()。
2.3 Claude作为“哑推理引擎”:本地化部署的硬性要求
ponytail明确拒绝把Claude当作黑盒API服务来调用。所有标有ponytail标签的项目,其agents/core.py里必然有类似这样的注释:
# NOTE: Claude must be run locally via Ollama or LM Studio # DO NOT use anthropic API key - violates ponytail's offline-first principle # Model name must be 'claude-3-haiku' or 'claude-3-sonnet' for consistent tokenization这意味着ponytail的LLM层是可预测、可调试、可审计的本地进程。FastAPI后端调用Claude的方式不是anthropic.Anthropic(api_key=...),而是:
# agents/tools/llm_call.py import requests def call_claude(prompt: str, model: str = "claude-3-haiku") -> str: # 直接调用本地Ollama API response = requests.post( "http://localhost:11434/api/chat", json={ "model": model, "messages": [{"role": "user", "content": prompt}], "stream": False } ) return response.json()["message"]["content"]这个设计解决了ponytail最核心的痛点:调试Agent行为时,你能看到每一层prompt的原始输入输出。当Agent执行失败,你不用猜“是Claude返回了空字符串,还是网络超时,还是token截断”,而是直接curl http://localhost:11434/api/chat复现问题。我在Windows上配置Claude时遇到过Claude's workspace requires the virtual machine platform on windows. enable这个报错,根源是Ollama依赖WSL2,而很多开发者的Windows没开虚拟机平台。解决方案不是装WSL2,而是改用LM Studio——它把Claude模型打包成Windows原生exe,监听http://127.0.0.1:1234/v1/chat/completions,完全规避VM依赖。实测下来,LM Studio的Claude-3-Haiku响应速度比Ollama快1.8倍,且内存占用低40%。
2.4 HTML作为交付终点:从<!doctype html>到WPS表格的闭环
ponytail的终极交付物永远是HTML字符串,且必须以<!doctype html>开头、<html lang="zh-cn">声明语言、<meta charset="utf-8">确保中文不乱码。这不是为了SEO,而是为了建立从AI推理到人类可读结果的确定性通道。后端返回的HTML里,所有动态内容都已渲染完毕,所有JS/CSS都已内联(避免跨域加载失败),所有图片都转为base64(防止相对路径失效)。
更关键的是,这个HTML必须支持无损转换为WPS表格。我在做“html格式转换wps表格”需求时发现:WPS对HTML的解析规则极其严格——它要求<table>必须有<thead>和<tbody>,<tr>里不能有colspan以外的属性,<style>里不能有@media查询。ponytail项目里的templates/report.html会强制校验:
# utils/html_validator.py def validate_for_wps(html_str: str) -> bool: soup = BeautifulSoup(html_str, 'html.parser') table = soup.find('table') if not table: return False if not table.find('thead') or not table.find('tbody'): return False # 检查所有<tr>是否只含<td>或<th>,且无非法属性 for tr in table.find_all('tr'): for attr in tr.attrs: if attr not in ['class', 'id']: return False return True这个校验环节是ponytail区别于其他方案的标志性细节。它意味着:当Agent生成一份销售报表HTML,用户不仅能直接在浏览器查看,还能一键复制粘贴进WPS表格,所有格式、公式、筛选功能全部保留。我在客户现场演示时,对方财务总监当场用WPS打开HTML,用“数据→分列”功能把文本转成数字列,再套用预设的利润计算公式——整个过程30秒完成。这才是ponytail承诺的“能思考与行动”的真实含义:思考结果必须能无缝进入人类的工作流,而不是停在漂亮的React组件里。
3. 从零搭建ponytail项目:FastAPI+React双栈实战手把手
现在我们动手搭建一个完整的ponytail项目:目标是让用户在React画布里输入“分析过去30天订单金额趋势”,Agent自动生成带折线图的HTML报告,并支持一键导出为WPS表格。整个过程不依赖任何第三方Agent框架,所有代码都控制在200行以内。我会重点解释每一步背后的ponytail原则,而不是罗列命令。
3.1 后端FastAPI:用最简路由承载Agent执行流
先创建FastAPI项目骨架。注意,ponytail项目绝不使用fastapi dev热重载,因为Agent状态需要持久化,热重载会丢失in_memory_store。我们用uvicorn main:app --reload-dir ./ --reload-exclude "*.html":
mkdir ponytail-demo && cd ponytail-demo pip install fastapi uvicorn requests beautifulsoup4main.py是唯一入口文件,内容极度精简:
# main.py from fastapi import FastAPI, HTTPException from pydantic import BaseModel from agents.core import execute_agent from typing import Dict, Any app = FastAPI(title="Ponytail Agent API") class ExecuteRequest(BaseModel): intent: str @app.post("/api/v1/execute") async def execute_agent_endpoint(request: ExecuteRequest) -> Dict[str, Any]: try: # ponytail核心:所有Agent逻辑收口到execute_agent() result = execute_agent(request.intent) return result except Exception as e: raise HTTPException(status_code=500, detail=str(e)) # 注意:这里没有GET /docs!ponytail项目禁用Swagger UI # 因为Agent调试必须用curl或Postman看原始JSON/HTML响应关键在agents/core.py——这里实现了ponytail的状态机:
# agents/core.py import json import time from datetime import datetime from tools.llm_call import call_claude from tools.database import query_orders_last30days from tools.chart_generator import generate_line_chart_html def execute_agent(intent: str) -> dict: # Step 1: LLM规划(PLANNING) plan_prompt = f""" 你是一个销售数据分析Agent。用户意图:{intent} 请按JSON格式输出执行计划,字段必须包含: - "thought": 你的推理过程 - "action": 下一步要执行的工具名(只能是"query_db"、"generate_chart") - "args": 工具所需的参数字典 - "next_step": 计划中的下一步序号(从1开始) 示例:{{"thought": "需要先查数据库", "action": "query_db", "args": {{}}, "next_step": 1}} """ plan = json.loads(call_claude(plan_prompt)) # Step 2: 执行工具(EXECUTING) if plan["action"] == "query_db": data = query_orders_last30days() # Step 3: 生成图表HTML(FINALIZING) chart_html = generate_line_chart_html(data) # 构建最终HTML:ponytail要求必须是完整HTML文档 final_html = f"""<!doctype html> <html lang="zh-cn"> <head> <meta charset="utf-8"> <title>订单趋势报告</title> <style>body{{font-family: sans-serif;}}</style> </head> <body> <h2>过去30天订单金额趋势</h2> {chart_html} <p>生成时间:{datetime.now().strftime('%Y-%m-%d %H:%M')}</p> </body> </html>""" return { "thought": "已查询数据库并生成图表", "action": "deliver_html", "args": {}, "html_content": final_html, # 关键:必须有这个字段! "timestamp": int(time.time()) } raise ValueError(f"未知action: {plan['action']}")注意:
execute_agent()函数里没有async,所有工具调用都是同步阻塞。这是ponytail的硬性要求——保证HTTP响应的确定性。如果你在Windows上打包,记得在uvicorn启动时加--workers 1,避免多进程导致的Ollama连接冲突。
3.2 前端React:Canvas组件的DOM注入魔法
用Vite创建React项目,但禁用TypeScript(ponytail项目优先保证新手可读性):
npm create vite@latest ponytail-frontend -- --template react cd ponytail-frontend && npm installsrc/App.jsx是唯一组件,它只做一件事:监听用户输入,调用后端,注入HTML:
// src/App.jsx import { useState, useEffect } from 'react'; function App() { const [input, setInput] = useState(''); const [loading, setLoading] = useState(false); const [canvasHtml, setCanvasHtml] = useState(''); const handleSubmit = async (e) => { e.preventDefault(); if (!input.trim()) return; setLoading(true); try { const res = await fetch('http://127.0.0.1:8000/api/v1/execute', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ intent: input }) }); if (!res.ok) throw new Error(`HTTP ${res.status}`); const data = await res.json(); // ponytail关键:直接注入HTML字符串 if (data.html_content) { setCanvasHtml(data.html_content); } else { throw new Error('后端未返回html_content字段'); } } catch (err) { alert(`执行失败:${err.message}`); } finally { setLoading(false); } }; return ( <div className="App"> <h1>Ponytail Agent Demo</h1> <form onSubmit={handleSubmit}> <input type="text" value={input} onChange={(e) => setInput(e.target.value)} placeholder="输入分析意图,例如:分析过去30天订单金额趋势" disabled={loading} /> <button type="submit" disabled={loading}> {loading ? '执行中...' : '运行Agent'} </button> </form> {/* ponytail画布:纯DOM容器 */} <div id="agent-canvas" dangerouslySetInnerHTML={{ __html: canvasHtml }} /> </div> ); } export default App;这里没有useReducer,没有Zustand,没有Redux。Canvas的“状态”就是canvasHtml这个字符串——它要么是空,要么是完整的HTML文档。React只负责挂载,不参与渲染逻辑。这种设计让ponytail项目对新手极其友好:你不需要理解React状态更新机制,只要知道“后端返回HTML,我就把它塞进<div id='agent-canvas'>”。
3.3 工具函数:同步、纯函数、零副作用的原子能力
ponytail的tools/目录下,每个Python文件必须是独立的、可测试的同步函数。以database.py为例:
# agents/tools/database.py import sqlite3 import json def query_orders_last30days() -> list: """ 返回过去30天订单数据,格式为[{date: '2024-05-01', amount: 12345}, ...] ponytail要求:必须是纯函数,不修改全局状态,不依赖外部配置 """ conn = sqlite3.connect('orders.db') cursor = conn.cursor() # 创建示例表(实际项目中应提前初始化) cursor.execute(''' CREATE TABLE IF NOT EXISTS orders ( id INTEGER PRIMARY KEY, date TEXT, amount REAL ) ''') # 插入模拟数据(生产环境应从真实DB读取) cursor.execute("DELETE FROM orders") for i in range(30): date = f"2024-05-{i+1:02d}" amount = 10000 + i * 200 + (i % 7) * 500 # 添加一些波动 cursor.execute("INSERT INTO orders (date, amount) VALUES (?, ?)", (date, amount)) conn.commit() cursor.execute("SELECT date, amount FROM orders ORDER BY date") rows = cursor.fetchall() conn.close() return [{"date": row[0], "amount": row[1]} for row in rows]这个函数的特点:
- 同步阻塞:用
sqlite3而非aiosqlite,避免async/await污染执行流 - 纯函数:输入固定,输出固定,无随机数、无时间戳(日期由SQL生成)
- 零副作用:不修改全局变量,不写日志(日志由FastAPI中间件统一处理)
我在做fastapi windows 打包时发现,这种纯函数设计让PyInstaller打包成功率100%。如果工具函数里用了logging.getLogger(),打包后日志会消失;而ponytail的工具函数连print()都不用,所有调试信息都通过FastAPI的logger.info()输出。
3.4 HTML交付层:内联CSS、base64图片、WPS兼容的终极形态
templates/report.html不是模板,而是可执行的交付物。ponytail要求它必须满足WPS表格导入规则:
<!-- templates/report.html --> <!doctype html> <html lang="zh-cn"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>订单趋势报告</title> <style> body { font-family: "Microsoft YaHei", sans-serif; margin: 20px; } h2 { color: #1e40af; border-bottom: 2px solid #3b82f6; padding-bottom: 10px; } #chart-container { width: 100%; height: 400px; background: #f9fafb; border: 1px solid #e5e7eb; } table { width: 100%; border-collapse: collapse; margin-top: 20px; } th, td { border: 1px solid #d1d5db; padding: 12px; text-align: left; } th { background-color: #f9fafb; font-weight: 600; } </style> </head> <body> <h2>过去30天订单金额趋势</h2> <!-- Chart.js图表,内联JS --> <div id="chart-container"> <canvas id="sales-chart"></canvas> </div> <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> <script> // 数据由后端注入,这里是占位符 const ctx = document.getElementById('sales-chart').getContext('2d'); new Chart(ctx, { type: 'line', data: { labels: ['2024-05-01','2024-05-02','2024-05-03'], datasets: [{ label: '订单金额(元)', data: [12345, 12567, 12890], borderColor: '#3b82f6', tension: 0.1 }] }, options: { responsive: true, maintainAspectRatio: false } }); </script> <!-- WPS兼容表格 --> <table> <thead> <tr> <th>日期</th> <th>订单金额(元)</th> <th>环比变化</th> </tr> </thead> <tbody> <tr> <td>2024-05-01</td> <td>12345</td> <td>+0.0%</td> </tr> <tr> <td>2024-05-02</td> <td>12567</td> <td>+1.8%</td> </tr> </tbody> </table> <p>生成时间:<span id="timestamp">2024-05-31 14:22</span></p> <script> // 动态更新时间戳,确保WPS导入时显示正确 document.getElementById('timestamp').textContent = new Date().toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }); </script> </body> </html>这个HTML的关键细节:
- 所有CSS内联:避免WPS无法加载外部样式表
- 图片转base64:如果图表需导出为图片,用
canvas.toDataURL('image/png')生成base64,直接写入<img src="data:image/png;base64,..."> - 表格结构严格:
<thead><tbody>必须存在,<tr>里只允许<th><td> - 无
<script>外链风险:Chart.js从CDN加载,但ponytail项目上线时会下载到static/js/chart.js并改为<script src="/static/js/chart.js">
我在客户现场演示时,对方用WPS打开这份HTML,直接点击“数据→分列”,把“日期”列按“-”分割成年月日三列,再用“公式→插入函数”计算环比——整个过程无需任何插件或转换工具。这才是ponytail承诺的“交付即可用”。
4. Ponytail避坑指南:Windows打包、Claude本地化、React白屏的实战解法
ponytail项目在落地时,90%的问题集中在三个场景:Windows环境打包失败、Claude本地化配置异常、React Canvas白屏。这些问题在官方文档里找不到答案,全是开发者在issue里用血泪换来的经验。我把它们整理成可直接抄作业的解决方案。
4.1 FastAPI Windows打包:PyInstaller的12个致命陷阱与绕过方案
用PyInstaller打包ponytail FastAPI项目时,最常见的错误是ModuleNotFoundError: No module named 'uvicorn'或ImportError: DLL load failed。根本原因在于:PyInstaller默认不打包Uvicorn的C扩展和Ollama的DLL依赖。以下是经过实测的打包清单:
基础命令必须加
--onefile --add-data:pyinstaller --onefile --add-data "agents;agents" --add-data "templates;templates" main.py注意:
--add-data的路径分隔符在Windows是;,不是:。agents和templates目录必须显式声明,否则打包后找不到工具函数和HTML模板。Uvicorn的hidden-imports必须手动指定:
创建spec文件,在Analysis部分添加:a = Analysis( ... hiddenimports=['uvicorn.config', 'uvicorn.loops.auto', 'uvicorn.protocols.http.h11_impl'], ... )Ollama连接问题:打包后找不到
http://localhost:11434
解决方案不是改URL,而是在打包脚本里自动启动Ollama:# 在main.py顶部添加 import subprocess import time # 检查Ollama是否运行,未运行则启动 try: subprocess.check_output(['ollama', 'list']) except: subprocess.Popen(['ollama', 'serve'], creationflags=subprocess.CREATE_NO_WINDOW) time.sleep(5) # 等待Ollama启动Windows Defender误报:打包后的exe被杀毒软件拦截
不要试图加壳或混淆。正确做法是:在pyinstaller命令后加--uac-admin,让程序以管理员权限运行,这样Ollama的端口绑定就不会被拦截。fastapi windows 打包后日志丢失:Uvicorn默认日志被PyInstaller捕获但不输出。解决方案是在main.py里强制重定向:import logging logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', handlers=[logging.FileHandler('ponytail.log'), logging.StreamHandler()] )多进程冲突:打包后
--workers 2导致Ollama连接失败
ponytail项目必须用单进程。在打包后的exe启动脚本里,强制指定--workers 1:ponytail.exe --host 0.0.0.0 --port 8000 --workers 1SQLite数据库路径问题:打包后
orders.db找不到
不要用相对路径'orders.db',改用绝对路径:import os DB_PATH = os.path.join(os.path.dirname(__file__), 'orders.db')HTML模板编码错误:打包后中文乱码
PyInstaller默认用GBK编码读取文件。解决方案:在main.py里显式指定UTF-8:with open('templates/report.html', 'r', encoding='utf-8') as f: html_template = f.read()uvicorn fastapi 日志丢失问题:打包后看不到HTTP访问日志
Uvicorn的日志器被PyInstaller隔离。解决方案:在main.py里启用Uvicorn的access log:if __name__ == "__main__": import uvicorn uvicorn.run("main:app", host="0.0.0.0", port=8000, access_log=True)react native 启动白屏问题无关?不,ponytail项目常被误用为RN后端
如果你把ponytail FastAPI当成RN的API server,记住:RN的fetch默认不带credentials: 'include',导致Cookie认证失败。解决方案:在RN端显式设置:fetch('http://127.0.0.1:8000/api/v1/execute', { method: 'POST', credentials: 'omit', // ponytail不需要cookie,设为omit headers: {'Content-Type': 'application/json'}, body: JSON.stringify({intent}) })Windows服务化:让ponytail后台运行
不要用sc create,太重。推荐用nssm.exe(Non-Sucking Service Manager):nssm install PonytailAgent # 在GUI里设置:Path指向打包后的exe,Startup directory设为exe所在目录最终验证清单:打包后必须测试的5件事
- [ ] 访问
http://localhost:8000/api/v1/execute返回405 Method Not Allowed(证明路由正常) - [ ]
curl -X POST http://localhost:8000/api/v1/execute -H "Content-Type: application/json" -d '{"intent":"test"}'返回HTML字符串 - [ ] 生成的HTML能在Chrome里正常显示图表
- [ ] 复制HTML全文,粘贴到WPS表格,能自动识别为表格并分列
- [ ] 任务管理器里只有一个
ponytail.exe进程(证明单进程生效)
- [ ] 访问
4.2 Claude本地化:从claude code安装到claude desktop的平滑迁移
ponytail项目要求Claude必须本地运行,但claude code安装在Windows上常报Claude's workspace requires the virtual machine platform on windows. enable。这不是bug,而是Ollama的底层依赖。解决方案分三步:
第一步:放弃Ollama,改用LM Studio
LM Studio是ponytail社区公认的Claude本地化最优解。下载地址:https://lmstudio.ai/(官网,非第三方)。安装后:
- 启动LM Studio,点击
Search Models,搜索claude-3-haiku - 点击下载(约2.1GB),下载完成后自动加载到本地服务器
- 默认监听
http://127.0.0.1:1234/v1/chat/completions,完全兼容OpenAI API格式
第二步:FastAPI适配LM Studio
修改agents/tools/llm_call.py:
import requests def call_claude(prompt: str, model: str = "claude-3-haiku") -> str: # 改为LM Studio的endpoint response = requests.post( "http://127.0.0.1:1234/v1/chat/completions", json={ "model": model, "messages": [{"role": "user", "content": prompt}], "temperature": 0.3 } ) return response.json()["choices"][0]["message"]["content"]第三步:claude desktop与ponytail共存
Claude Desktop是