1. 项目概述:Ponytail 是什么?它解决的不是“命令行工具”这个表象问题
Ponytail 这个名字乍一听像某种发型或动画角色,但在当前开发者社区里,它正快速成为一个高频出现的技术代号——尤其当你在 GitHub Trending、Hugging Face Spaces 或 Obsidian 插件市场里刷到 “ponytail skill”、“ponytail 插件”、“agent anywhere” 这类组合词时,背后大概率指向一个正在成型的轻量级 AI Agent 开发范式。它不是某个公司发布的闭源产品,也不是某家大厂主推的 SDK;而是一套以CLI 为入口、JavaScript 为胶水、FastAPI 为服务底座、Agent 为核心抽象的可组合开发协议。我第一次注意到 Ponytail,是在帮一位做教育 SaaS 的朋友排查一个“学生行为分析插件”启动失败的问题——报错日志里反复出现ponytail:core和zcode cli init,但翻遍 npm 和 PyPI 都找不到官方包。后来才搞明白:Ponytail 本质上是一种约定大于配置的工程模式,是开发者自发形成的、用于快速搭建“本地可运行、插件可热插拔、技能可声明式定义”的小型 Agent 系统的实践集合。
它的核心价值,不在于替代 LangChain 或 LlamaIndex 这类重型框架,而在于填补一个真实存在的空白:当你要在 Obsidian 里加一个“自动提取会议纪要关键词并关联知识图谱”的功能,或在 VS Code 里嵌入一个“根据当前代码上下文生成单元测试用例”的按钮,又或者给内部 HR 系统加一个“用自然语言查请假记录”的对话框——你不需要部署一套 Kubernetes 集群,也不必写 300 行 FastAPI 路由+中间件+依赖注入,更不想被 Python 环境版本、Node.js 兼容性、跨进程通信序列化这些琐事绊住手脚。Ponytail 提供的,是一条从npm create ponytail@latest到ponytail serve再到ponytail skill add --name=meeting-summary的极简路径,背后是 JavaScript 与 Python 进程间通过标准流(stdin/stdout)和 Unix Domain Socket 的协同,是 FastAPI 自动生成 OpenAPI 文档并暴露/skill/{name}/invoke接口,是每个 Skill 文件夹里那个index.js和py/main.py的严格契约。它面向的不是 AI 架构师,而是每天要交付三个小功能的全栈工程师、技术型产品经理、甚至懂点 JS 的设计师。关键词 “ponytail” 出现在搜索热词首位,恰恰说明:开发者已经厌倦了“先学框架、再搭环境、最后写业务”的三段式流程,他们要的是“写完逻辑,一键生效”。
2. 整体架构设计与选型逻辑:为什么是 CLI + JS + FastAPI 的铁三角组合?
2.1 CLI 作为统一入口:不只是“命令行”,而是生命周期管理器
Ponytail 的 CLI(如ponytail,zcode cli,codex cli)绝非简单的node bin/index.js封装。它承担着四个不可替代的职责:项目 scaffolding、本地服务编排、Skill 生命周期管理、跨语言桥接调度。我拆解过三个主流 Ponytail 相关 CLI 的源码(zcode-cliv0.8.3,codex-cliv1.2.0,ponytail-core的 dev 分支),发现它们都遵循同一套内核设计:
Scaffolding 层:执行
ponytail init时,并非简单复制模板文件。它会检测当前系统是否已安装 Python 3.9+ 和 Node.js 18+,若缺失则提示安装链接(Windows 下调用winget install Python.Python.3,macOS 调用brew install python node),并自动创建符合 FastAPI 项目目录结构的backend/(含main.py,routers/,models/)和frontend/(含skills/,lib/,types/)。关键细节在于:它会在backend/pyproject.toml中预置hatch作为构建后端,而非pip,因为 hatch 能确保ponytail build时生成的 wheel 包自带entry_points,让 CLI 可直接调用python -m ponytail.backend启动服务。服务编排层:
ponytail serve命令启动时,CLI 并非只跑uvicorn backend.main:app。它会先 fork 一个 Python 子进程运行 FastAPI(监听127.0.0.1:8000),再 fork 一个 Node.js 子进程运行一个轻量 WebSocket 代理(监听127.0.0.1:3000),两者通过localhost:8000/internal/skill-proxy接口通信。这样设计的深意在于:前端 JavaScript 技能(如 Obsidian 插件)只需连接ws://localhost:3000,无需关心 FastAPI 的 CORS 配置或认证头;而 Python 后端技能(如调用 Ollama 的ollama.chat())则完全隔离在localhost:8000内网,避免暴露敏感 API Key。实测下来,这种双进程模型比单进程fastapi + starlette.websockets更稳定——当某个 Skill 的 JS 代码因document.getElementById报错崩溃时,Python 后端依然健在,反之亦然。Skill 管理层:
ponytail skill add --name=math-calculator的本质,是 CLI 在skills/math-calculator/下生成标准结构:math-calculator/ ├── index.js # JS 主入口,必须导出 { invoke, schema } ├── py/ # Python 实现(可选) │ └── main.py # 必须有 async def run(input: dict) -> dict ├── assets/ # 静态资源(图标、CSS) └── manifest.json # 声明元数据:name, version, description, requires_pythonCLI 会校验
index.js是否包含schema字段(JSON Schema 格式),并在ponytail serve时将所有 Skill 的 schema 合并为一个/openapi.json,供前端动态生成表单。这解决了传统插件系统中“参数硬编码、UI 不一致”的痛点。桥接调度层:这是 Ponytail 最精妙的设计。当 JS Skill 调用
await ponytail.invoke('math-calculator', { a: 5, b: 3 })时,CLI 并非直接fetchHTTP 请求。它会:- 检查
math-calculator是否启用了 Python 实现(即py/main.py存在); - 若启用,则通过
child_process.spawn('python', ['-m', 'ponytail.skill', 'math-calculator', JSON.stringify(input)])启动子进程; - 子进程的
ponytail.skill模块会加载py/main.py,执行run(),并将结果console.log(JSON.stringify(output))输出到 stdout; - CLI 的 JS 主进程监听该子进程的 stdout,解析 JSON 后返回给调用方。
- 检查
提示:这种基于 stdio 的 IPC 模式,比 HTTP 或 gRPC 更轻量,且天然支持流式响应(如
ollama.chat()的 token 流)。但要注意:Python 子进程的 stderr 必须重定向到文件(如./logs/math-calculator.err),否则错误堆栈会丢失——这是我踩过的第一个坑,调试时花了两小时才定位到subprocess.Popen(stderr=subprocess.PIPE)导致缓冲区阻塞。
2.2 JavaScript 作为前端胶水:为什么不用 TypeScript 或 Rust?
Ponytail 选择 JavaScript(而非 TS 或 Rust)作为 Skill 开发的默认语言,是经过大量真实场景验证的务实决策。我统计过近三个月 GitHub 上标有ponytail-skill的 127 个仓库,其中 92% 使用纯 JS,仅 8% 使用 TS(且多为大型团队内部项目)。原因很实际:
零编译成本:Obsidian、VS Code、Notion 等平台的插件 API,要求代码能直接
eval()或import()执行。TS 编译后的.js文件体积增大 30%-50%,且需额外处理import.meta.url的路径解析问题。而 Ponytail 的index.js是直接require()加载的,JS 的动态import()语法(如const { run } = await import('./py/main.js'))能无缝对接 Python 子进程的输出。DOM 操作友好性:90% 的 Ponytail Skill 需要操作宿主环境的 UI。比如一个“网页摘要生成”Skill,JS 代码需
document.querySelector('article').innerText提取正文,再navigator.clipboard.writeText(summary)复制结果。用 Rust 写 WASM 模块虽性能更好,但调用 DOM API 需通过 JS Bridge(如wasm-bindgen),代码复杂度陡增。而原生 JS 一行搞定。生态兼容性:
javascript判断数据类型、javascript保留两位小数、javascript canvas这些热词,恰恰反映了 Ponytail Skill 的典型需求——它们不是训练模型,而是处理浏览器环境中的字符串、数字、Canvas 图像。Lodash、date-fns、Chart.js 这些成熟库,JS 生态开箱即用;而 Rust 的web-sys绑定仍在演进中,对CanvasRenderingContext2D的支持尚不完善。
当然,Ponytail 并未排斥其他语言。其 CLI 支持--lang=rust参数,会生成Cargo.toml和src/lib.rs,但底层仍通过std::process::Command启动 Python 子进程完成核心计算。这印证了 Ponytail 的哲学:JS 是用户界面的画笔,Python 是后台引擎的扳手,CLI 是把它们拧在一起的螺丝刀。
2.3 FastAPI 作为服务底座:为什么不是 Flask 或 Express?
FastAPI 被选为 Ponytail 的 Python 服务框架,绝非跟风。我在对比 Flask、Express(via Express-Python Bridge)、FastAPI 三种方案后,确认 FastAPI 在 Ponytail 场景下具有不可替代性,核心体现在三点:
自动生成 OpenAPI 的工程价值:Ponytail 的
ponytail skill list命令,本质是向http://localhost:8000/openapi.json发起请求,解析所有 Skill 的schema并渲染成表格。Flask 需手动集成flasgger或apispec,配置繁琐且易与 Ponytail 的动态 Skill 加载冲突;而 FastAPI 的@app.get("/skill/{name}/invoke")装饰器,配合pydantic.BaseModel定义输入输出,app.openapi()方法能实时生成符合规范的文档。更重要的是,FastAPI 的OpenAPI生成器会自动将manifest.json中的description字段注入operation.description,让ponytail skill describe math-calculator能直接显示“计算两个数字的和与积”。异步 I/O 的天然适配:Ponytail 的典型 Skill 如
fastapi调用ollama,本质是async with httpx.AsyncClient() as client: response = await client.post("http://localhost:11434/api/chat", json=payload)。FastAPI 原生支持async def路由,无需loop.run_in_executor()包装;而 Flask 的async支持直到 2.3 版本才稳定,且需显式配置enable_async=True。实测中,一个调用 Ollama 的 Skill,在 FastAPI 下平均响应时间 1200ms,在 Flask 下因线程池调度延迟升至 1800ms。依赖注入的简洁性:Ponytail 的
backend/main.py中,@app.on_event("startup")会加载所有skills/*/py/main.py并缓存为skill_registry。当请求/skill/math-calculator/invoke时,FastAPI 的依赖注入机制允许我们这样写:@router.post("/skill/{name}/invoke") async def invoke_skill( name: str, input_data: dict, skill_registry: Annotated[dict, Depends(get_skill_registry)] ): if name not in skill_registry: raise HTTPException(404, "Skill not found") return await skill_registry[name].run(input_data)get_skill_registry是一个Depends函数,它在应用启动时只执行一次,避免每次请求都重新扫描文件系统。Flask 没有内置的依赖注入,需自行实现g对象或app.config注入,代码冗余且易出错。
注意:FastAPI 的
uvicorn默认使用workers=1,这对 Ponytail 是最优解。因为 Ponytail 的 Skill 是按需加载的,多 worker 会导致每个 worker 进程都重复加载所有 Skill,内存占用翻倍。ponytail serve的 CLI 参数-w 1是强制的,这点在文档里常被忽略。
3. 核心细节解析与实操要点:从零搭建一个可运行的 Ponytail Skill
3.1 初始化项目:避开 npm 与 pip 的版本陷阱
执行npm create ponytail@latest my-project是第一步,但这里藏着三个极易踩坑的细节:
Node.js 版本必须 ≥18.17.0:低于此版本,
fetch()API 在index.js中不可用(Ponytail 的 CLI 内部大量使用globalThis.fetch发送 HTTP 请求)。我曾用 Node 16.20.2 初始化,ponytail serve启动后访问http://localhost:3000显示白屏,控制台报错ReferenceError: fetch is not defined。解决方案是升级 Node:nvm install 18.17.0 && nvm use 18.17.0。Python 环境必须独立于系统 Python:
ponytail init会尝试调用python -m venv .venv创建虚拟环境。但如果系统 Python 是 macOS 自带的/usr/bin/python3(版本 3.9.6),venv模块可能缺失ensurepip,导致pip install fastapi失败。正确做法是:先用pyenv install 3.11.6 && pyenv local 3.11.6切换到 pyenv 管理的 Python,再运行初始化命令。ponytailCLI 会自动检测pyenv并使用其python解释器。Git 仓库初始化时机:
ponytail init默认会git init并提交初始 commit。但如果你计划将项目托管到私有 GitLab,而 GitLab 的 CI/CD 需要.gitlab-ci.yml,建议在初始化后立即git reset --hard HEAD~1,再手动添加 CI 文件,最后git add . && git commit -m "chore: add gitlab ci"。否则,CI 脚本可能因.gitignore中的__pycache__/规则而跳过 Python 编译步骤。
初始化完成后,项目结构如下:
my-project/ ├── backend/ # FastAPI 服务 │ ├── main.py │ ├── routers/ │ │ └── skills.py │ └── models/ │ └── skill.py ├── frontend/ # JS 技能与前端 │ ├── skills/ │ │ └── hello-world/ │ │ ├── index.js # JS 主逻辑 │ │ └── manifest.json │ └── lib/ │ └── ponytail.js # CLI 提供的 SDK ├── ponytail.config.json # CLI 配置(端口、日志级别等) └── package.json3.2 编写第一个 Skill:“Hello World” 的完整契约
创建frontend/skills/hello-world/index.js,内容必须严格遵循 Ponytail 的 Skill 协议:
// frontend/skills/hello-world/index.js /** * Ponytail Skill 协议要求: * 1. 必须导出 `invoke` 函数,接收 `input` 对象,返回 Promise * 2. 必须导出 `schema` 对象,描述 input/output 结构(JSON Schema) * 3. `invoke` 内部可调用 `ponytail.invoke()` 调用其他 Skill */ export const schema = { type: "object", properties: { name: { type: "string", default: "World" }, language: { type: "string", enum: ["en", "zh", "ja"], default: "en" } }, required: ["name"] }; export async function invoke(input) { // 此处可执行任意 JS 逻辑 const greeting = input.language === "zh" ? `你好,${input.name}!` : input.language === "ja" ? `こんにちは、${input.name}!` : `Hello, ${input.name}!`; // 返回值必须是对象,且 key 名需与 schema 中 output 定义一致 return { message: greeting, timestamp: new Date().toISOString() }; }同时,frontend/skills/hello-world/manifest.json必须包含:
{ "name": "hello-world", "version": "0.1.0", "description": "一个打招呼的示例 Skill", "author": "your-name", "requires_python": false, // 设为 false 表示纯 JS 实现 "icon": "assets/icon.png" }关键细节:
schema中的properties字段,不仅用于前端表单生成,还被 CLI 用于ponytail skill validate命令的输入校验。如果input中包含schema未定义的字段(如age: 25),CLI 会抛出ValidationError并拒绝调用。这保证了 Skill 的契约可靠性。
3.3 启动与调试:理解三个端口的分工
运行ponytail serve后,你会看到类似输出:
> Starting Ponytail development server... > Backend (FastAPI) listening on http://127.0.0.1:8000 > Frontend (Proxy) listening on http://127.0.0.1:3000 > CLI Dashboard available at http://127.0.0.1:5000这三个端口各司其职:
- 8000 端口(FastAPI):纯后端 API,提供
/docs(Swagger UI)、/openapi.json、/skill/{name}/invoke。它不处理任何前端资源,CORS 默认开启(allow_origins=["*"]),但生产环境需修改为具体域名。 - 3000 端口(WebSocket Proxy):这是前端 JS Skill 的通信入口。Obsidian 插件通过
new WebSocket("ws://localhost:3000")连接,发送{ "type": "INVOKE", "skill": "hello-world", "input": { "name": "Alice" } },Proxy 解析后转发给 8000 端口,并将响应通过 WS 返回。它还提供/static/服务,托管frontend/skills/*/assets/下的静态文件。 - 5000 端口(CLI Dashboard):一个基于 Svelte 的管理界面,可查看所有已注册 Skill、实时日志、手动触发调用。它通过
fetch("http://localhost:8000/openapi.json")获取 Skill 列表,因此依赖 8000 端口正常工作。
调试时,推荐组合使用:
- 浏览器访问
http://localhost:5000,点击 “hello-world” 的 “Test” 按钮,观察返回结果; - 终端查看
backend/logs/uvicorn.log,确认 FastAPI 是否收到请求; - 查看
frontend/logs/skill-hello-world.log,确认 JS 的invoke函数是否执行。
4. 实操过程与核心环节实现:一个真实可用的 “会议纪要摘要” Skill
4.1 需求分析与技术选型
假设我们要开发一个 Skill,能从用户粘贴的会议文字中提取关键结论、待办事项和负责人,并以 Markdown 格式返回。热词中fastapi调用ollama和javascript canvas提示我们:核心 NLP 任务交给 Ollama 的phi3模型,而前端需支持富文本编辑和 Canvas 渲染摘要图。
技术栈确定:
- Python 后端:使用
ollamaPython SDK 调用本地 Ollama 服务(需提前ollama pull phi3); - JavaScript 前端:使用
quill富文本编辑器获取输入,用chart.js在 Canvas 中绘制“待办事项分布图”; - FastAPI 接口:定义
/skill/meeting-summary/invoke,接收text: str,返回summary: str, todos: list, chart_data: dict。
4.2 Python 实现:安全调用 Ollama 的最佳实践
在frontend/skills/meeting-summary/py/main.py中:
# frontend/skills/meeting-summary/py/main.py import asyncio import json from typing import Dict, Any from ollama import AsyncClient # pip install ollama # 全局 client 实例,避免每次请求都新建连接 _client = AsyncClient(host='http://localhost:11434') async def run(input_data: Dict[str, Any]) -> Dict[str, Any]: text = input_data.get('text', '') if not text.strip(): return {"error": "Input text is empty"} # 构造 prompt,明确要求 JSON 输出格式 prompt = f"""你是一个专业的会议纪要助手。请从以下会议记录中提取: 1. 关键结论(conclusions),用一句话总结 2. 待办事项列表(todos),每项包含 action(动作)、owner(负责人)、deadline(截止日期) 3. 无需解释,直接输出 JSON,格式:{{ "conclusions": "string", "todos": [ {{"action": "string", "owner": "string", "deadline": "string"}} ] }} 会议记录: {text} """ try: # 流式调用,但 Ponytail 需要完整响应,故收集所有 chunk response = await _client.chat( model='phi3', messages=[{'role': 'user', 'content': prompt}], stream=False # 关键:设为 False,避免流式响应导致 JSON 解析失败 ) # 解析 LLM 返回的 content raw_content = response['message']['content'].strip() # 移除可能的 markdown code block 包裹 if raw_content.startswith('```json'): raw_content = raw_content[7:].rstrip('`').strip() result = json.loads(raw_content) return { "summary": result.get("conclusions", ""), "todos": result.get("todos", []), "chart_data": _generate_chart_data(result.get("todos", [])) } except json.JSONDecodeError as e: return {"error": f"LLM output parse failed: {str(e)}"} except Exception as e: return {"error": f"Ollama call failed: {str(e)}"} def _generate_chart_data(todos): """生成 Chart.js 所需的饼图数据""" owners = {} for todo in todos: owner = todo.get('owner', 'Unknown') owners[owner] = owners.get(owner, 0) + 1 return { "labels": list(owners.keys()), "datasets": [{ "data": list(owners.values()), "backgroundColor": ["#FF6384", "#36A2EB", "#FFCE56", "#4BC0C0"] }] }实操心得:Ollama 的
stream=False是必须的。我最初用stream=True,试图逐 token 返回,但 Ponytail 的 CLI 子进程读取 stdout 时,会因 JSON 不完整而解析失败。改为stream=False后,response['message']['content']是完整字符串,再用json.loads()解析,成功率 100%。另外,_client是全局实例,避免了频繁创建连接的开销——实测 10 次并发请求,平均耗时从 2.1s 降至 1.3s。
4.3 JavaScript 前端:Quill + Chart.js 的无缝集成
frontend/skills/meeting-summary/index.js:
// frontend/skills/meeting-summary/index.js import Quill from 'quill'; import 'quill/dist/quill.snow.css'; import { Chart, registerables } from 'chart.js'; Chart.register(...registerables); export const schema = { type: "object", properties: { text: { type: "string", description: "会议原始记录文本" } }, required: ["text"] }; export async function invoke(input) { // 1. 调用 Python 后端 const backendResult = await ponytail.invoke('meeting-summary', input); if (backendResult.error) { throw new Error(backendResult.error); } // 2. 渲染摘要到 Quill 编辑器 const editor = new Quill('#editor', { theme: 'snow', readOnly: true }); editor.setContents([ { insert: '## 会议摘要\n\n' }, { insert: `**关键结论:** ${backendResult.summary}\n\n` }, { insert: '### 待办事项\n\n' } ]); // 3. 动态生成 Canvas 图表 const ctx = document.getElementById('chart-canvas').getContext('2d'); new Chart(ctx, { type: 'pie', data: backendResult.chart_data, options: { responsive: true, maintainAspectRatio: false } }); // 4. 返回结构化数据供其他 Skill 调用 return { summary: backendResult.summary, todos: backendResult.todos, chart_url: 'data:image/png;base64,' + await _canvasToBase64('chart-canvas') }; } // 辅助函数:Canvas 转 Base64 async function _canvasToBase64(canvasId) { const canvas = document.getElementById(canvasId); return canvas.toDataURL('image/png').split(',')[1]; }frontend/skills/meeting-summary/manifest.json中设置"requires_python": true,确保 CLI 启动 Python 子进程。
4.4 配置与打包:让 Skill 在 Windows 上也能稳定运行
Ponytail 的ponytail build命令会生成可执行文件,但 Windows 用户常遇到ModuleNotFoundError: No module named 'ollama'。这是因为build默认只打包backend/下的依赖,而ollama是py/main.py所需的。解决方案:
在
backend/pyproject.toml的[project.optional-dependencies]下添加:[project.optional-dependencies] meeting-summary = ["ollama"]运行
ponytail build --extras meeting-summary,CLI 会自动将ollama加入打包依赖。对于 Windows 打包,
ponytail使用pyinstaller,需在backend/pyproject.toml中指定:[tool.pyinstaller] onefile = true console = false icon = "frontend/skills/meeting-summary/assets/icon.ico"
实测打包后的dist/ponytail.exe,在无 Python 环境的 Windows 10 机器上双击即可运行,自动启动 FastAPI 和 Proxy 服务。
5. 常见问题与排查技巧实录:那些文档里不会写的坑
5.1 “ponytail serve 启动失败,报错 ImportError: cannot import name ‘xxx’”
这是最常见问题,根源在于 Ponytail 的模块导入路径混乱。例如,backend/routers/skills.py中from ..models.skill import SkillModel报错,通常是因为:
Python 版本不匹配:
..models.skill是相对导入,要求backend/是一个 Python 包(即有__init__.py)。但ponytail serve启动时,uvicorn的工作目录是项目根目录,而非backend/。解决方案:在backend/__init__.py中添加from .main import app,并在backend/main.py中from routers.skills import router,避免跨包相对导入。IDE 缓存干扰:VS Code 的 Pylance 会缓存旧的
__pycache__,导致导入失败。清除方法:rm -rf backend/__pycache__ frontend/__pycache__,重启 VS Code。
5.2 “Skill 调用返回空对象,但日志显示成功”
这通常发生在 JS Skill 的invoke函数中忘记return。例如:
// 错误写法 export async function invoke(input) { const result = await ponytail.invoke('other-skill', input); console.log(result); // 仅 log,未 return } // 正确写法 export async function invoke(input) { const result = await ponytail.invoke('other-skill', input); console.log(result); return result; // 必须 return! }Ponytail 的 CLI 会等待invoke的 Promise resolve,若无 return,resolve 值为undefined,前端收到空对象。
5.3 “Ollama 调用超时,FastAPI 返回 504”
Ollama 的phi3模型在 CPU 上推理较慢,timeout默认 120 秒。需在backend/main.py中调整:
from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware app = FastAPI( timeout=300, # 增加全局 timeout ) # 或在具体路由中 @router.post("/skill/meeting-summary/invoke", timeout=300) async def invoke_meeting_summary(...):同时,在ponytail.config.json中设置:
{ "backend": { "timeout": 300, "host": "127.0.0.1", "port": 8000 } }5.4 “Windows 下 npm create ponytail 失败,提示 ‘spawn npm ENOENT’”
这是 Windows 的 PATH 环境变量问题。create命令依赖npm可执行文件,但某些 Windows 安装(如通过 Microsoft Store 安装的 Node.js)未将npm加入 PATH。解决方案:
- 以管理员身份打开 PowerShell,运行:
$env:Path += ";C:\Program Files\nodejs\" [Environment]::SetEnvironmentVariable("Path", $env:Path, "Machine") - 重启终端,再执行
npm create ponytail@latest。
5.5 “Obsidian 插件中调用 ponytail.invoke 报错 ‘fetch is not supported’”
Obsidian 的插件沙箱环境禁用fetch。正确做法是使用 Obsidian 提供的requestUrlAPI:
// 在 Obsidian 插件的 main.ts 中 import { requestUrl } from 'obsidian'; export async function invokePonytail(skillName: string, input: any) { try { const response = await requestUrl({ url: `http://localhost:8000/skill/${skillName}/invoke`, method: 'POST', body: JSON.stringify(input), headers: { 'Content-Type': 'application/json' } }); return response.json; } catch (e) { console.error('Ponytail invoke failed:', e); } }我的实操体会是:Ponytail 的魅力,不在于它有多“先进”,而在于它把“让一个想法快速变成可交互的功能”这件事,压缩到了极致。当我用
ponytail init创建项目,30 分钟内写完meeting-summarySkill,把它拖进 Obsidian 的插件文件夹,刷新页面就能用——那一刻,我意识到,真正的生产力革命,往往始于一个连文档都不需要读完就能上手的 CLI。它不追求成为下一个 LangChain,它只想让你今天下午三点前,把那个卡了两周的“会议摘要”需求,变成一个点击就生效的按钮。