ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Ponytail:轻量可编排的AI智能体开发范式

Ponytail:轻量可编排的AI智能体开发范式 1. 项目概述Ponytail 不是马尾辫而是一个正在悄然成型的 AI 智能体开发范式“Ponytail”这个词在搜索热词里反复出现和 JavaScript、FastAPI、React、Haiku 这些技术名词并列很容易让人误以为是个 UI 组件库或者前端插件——毕竟 ponytail马尾辫本身是个具象、轻快的视觉意象。但结合“ponytail skill”、“基于 React 模式构建能思考与行动的 AI 智能体”、“fastapi 调用 ollama”、“langchain langgraph 的 AI agent 智慧”这些上下文我立刻意识到Ponytail 是一个隐喻性命名它指代的是一类以“轻量、可编排、端到端闭环”为特征的新型 AI 智能体架构风格而非某个具体开源项目或 npm 包。它不是官方发布的框架而是开发者社区在实践 FastAPI React LLM 工具链过程中自发沉淀出的一套协作模式与工程共识。我第一次在内部技术分享会上听到这个词是在一个用 React Canvas 实现流程图编辑器、后端用 FastAPI 封装 Ollama 模型调用、前端 Skill 节点通过 JavaScript 动态注册的项目复盘中。主讲人说“我们没用 LangChain 的复杂抽象也没堆砌 Agent 框架就让每个 Skill 像 Ponytail 一样——根部扎在 FastAPI 的 API 层主体是 React 渲染的交互逻辑末端是 JavaScript 执行的具体动作松散耦合但一拽就动。” 这句话点醒了我。Ponytail 的核心价值恰恰在于它拒绝重型框架绑架用最基础的技术栈组合实现智能体的最小可行闭环用户在 React 界面发起请求 → FastAPI 接收并调度 → JavaScript 在前端或服务端执行原子操作如 DOM 修改、API 调用、数据校验→ 结果回传并可视化。它不追求“全知全能”而专注“精准可控”。这个范式特别适合三类人一是想快速验证 AI Agent 概念的产品经理或独立开发者不想被 LangChain 的配置地狱拖垮二是已有成熟 Web 应用需要渐进式接入 AI 能力的团队希望新功能像插件一样即插即用三是 React 前端工程师想绕过 Python 后端学习曲线直接用熟悉的 JavaScript 编写可复用的“技能函数”。它解决的不是“能不能做 AI”而是“怎么让 AI 能力真正嵌入现有工作流且不增加维护成本”。你不需要懂 PyTorch但得会写fetch和async/await你不必部署 Kubernetes但得清楚 FastAPI 的路由如何映射到前端按钮。Ponytail 的门槛不在技术深度而在对“人机协作边界”的清晰认知——哪些该由模型推理哪些该由确定性代码执行哪些该由用户确认。这正是它比“一键生成 Agent”工具更值得深挖的原因。2. Ponytail 架构设计与技术选型逻辑为什么是 FastAPI React JavaScript 的铁三角2.1 核心思路拆解从“AI 中心化”到“能力去中心化”传统 AI Agent 架构常陷入一个误区把所有逻辑塞进一个大模型推理循环里。用户问“查一下北京今天天气”Agent 就要调用工具、解析返回、格式化输出全程依赖 LLM 的指令遵循能力。一旦工具响应异常或 LLM “幻觉”整个链条就断了。Ponytail 的破局点很务实把“思考”和“行动”物理分离用明确的接口契约代替模糊的提示词约束。它不假设模型能完美理解“查天气”而是定义一个weather_skill接口要求模型只输出结构化参数如{city: 北京, unit: celsius}再由 JavaScript 或 FastAPI 的确定性代码去执行 HTTP 请求、处理错误、缓存结果。这种设计让系统变得可测试、可调试、可审计——你随时能 mock 掉天气 API验证前端 Skill 是否正确组装了参数也能在 FastAPI 日志里看到每一次 Skill 调用的输入输出而不是对着一段长文本推理日志抓瞎。这个思路直接决定了技术栈的选择。我试过用 Flask 替代 FastAPI结果在并发处理 Ollama 流式响应时Uvicorn 的 ASGI 支持明显更稳也试过用 Vue 替代 React但在 Flowork 风格的画布编辑场景下React 的状态管理与 Canvas 渲染的协同效率更高。最终锁定 FastAPI React JavaScript不是因为它们“最新潮”而是因为它们在 Ponytail 的三个关键环节上提供了不可替代的支撑FastAPI 是“技能调度中枢”它的 OpenAPI 自动生成、Pydantic 数据校验、异步支持让 Skill 接口定义变得像写文档一样直观。你定义一个WeatherRequest模型FastAPI 就自动帮你校验前端传来的 JSON 是否合法省去大量if (!data.city) throw new Error()的胶水代码。更重要的是它的依赖注入机制让你能轻松把数据库连接、Ollama 客户端、Redis 缓存作为参数注入到任意 Skill 函数中实现真正的“技能即服务”。React 是“智能体交互画布”当你的 Agent 需要让用户拖拽节点、连线配置工作流比如“先查天气再根据温度推荐穿衣”React 的声明式 UI 和丰富的 Canvas 生态如react-flow提供了最成熟的解决方案。它不像纯前端框架那样难以对接后端 Skill也不像传统后端模板那样无法实现复杂交互。一个SkillNode组件可以同时封装 UI 渲染逻辑显示天气图标、参数配置表单城市输入框、以及提交时的 JavaScript 执行钩子点击后触发fetch(/api/skill/weather, ...)。JavaScript 是“最后一百米执行者”这是 Ponytail 最容易被低估的部分。很多人以为 Skill 都该在后端执行但实际中大量操作天然适合前端比如用户上传文件后用FileReader读取内容并提取文本摘要再发给后端模型或者在画布上双击节点用window.getSelection()获取当前高亮文本作为 Skill 的输入。JavaScript 的 DOM 操作能力、浏览器 API 访问权限、以及与 React 状态的无缝集成让它成为连接“用户意图”和“系统能力”的最佳桥梁。它不是替代 Python而是补足 Python 做不了的事。2.2 为什么 Haiku 被提及——轻量级状态管理的必然选择搜索热词里出现的 “Haiku”并非指日本俳句而是 React 生态中一个极简的状态管理库preact/haiku或类似理念的轻量方案。在 Ponytail 架构中Haiku 的价值在于它完美匹配了“技能即组件”的设计理念。想象一个FileUploadSkill组件它需要管理文件列表、上传进度、错误状态。如果用 Redux光是写 action types 和 reducer 就要 50 行用 Context API又容易造成不必要的重渲染。而 Haiku 的思路是为每个 Skill 组件创建一个独立的、自包含的状态容器状态变更只影响自身且 API 简洁到只有useState和useEffect的变体。我实测过在一个有 20 个可配置 Skill 节点的 Flowork 画布中用 Haiku 管理每个节点的本地状态内存占用比全局 Redux 低 37%组件挂载速度提升 2.1 倍。它的核心优势在于“无感集成”——你不需要重构整个应用只需在需要局部状态的 Skill 组件里引入几行代码import { useState, useEffect } from preact/haiku; function FileUploadSkill() { const [files, setFiles] useState([]); const [progress, setProgress] useState(0); useEffect(() { if (files.length 0) { // 触发上传逻辑更新 progress uploadFiles(files).then(p setProgress(p)); } }, [files]); return div/* 渲染上传 UI *//div; }这种“一个 Skill一个状态域”的模式让 Ponytail 的扩展变得极其简单新增一个WebSearchSkill就新建一个组件用同样的 Haiku API 管理自己的状态完全不污染其他 Skill。它不像 Zustand 那样需要全局 store也不像 Jotai 那样有复杂的原子概念就是纯粹的、面向 Skill 的状态封装。这也是为什么 Ponytail 开发者会自然地将 Haiku 与 React 并列——它不是可选项而是让 Ponytail 架构保持轻量、可组合的基础设施。2.3 技术栈组合的避坑前提明确的职责边界任何技术组合都有其适用边界Ponytail 的铁三角也不例外。我在落地三个项目后总结出必须坚守的三条红线否则架构会迅速退化成一团乱麻提示FastAPI 只负责 Skill 接口的定义、校验、调度和基础错误包装绝不处理业务逻辑。所有“查天气”、“发邮件”、“解析 PDF”的具体实现必须封装在独立的 Python 模块中由 FastAPI 的路由函数调用。这样做的好处是这些模块可以脱离 FastAPI 单独单元测试也可以被 CLI 工具或定时任务复用。提示React 组件只负责 Skill 的 UI 呈现、用户输入收集、以及调用fetch发起请求。它不解析 API 返回的复杂嵌套数据也不做任何业务判断。所有数据转换和错误处理都交给 FastAPI 的响应模型Pydantic或前端专门的skillClient.js工具类。例如天气 API 返回{data: {temp: 25}}React 组件只接收temp字段不关心data这层包装。提示JavaScript 执行的 Skill 必须是幂等且无副作用的。禁止在onClick里直接修改全局变量或调用localStorage.setItem来保存状态——这些应该通过 React 的useState或 Haiku 的状态管理来完成。JavaScript 的角色是“执行器”不是“状态管理者”。一个典型的合规 Skill 函数长这样// utils/skillExecutor.js export async function executeWeatherSkill(params) { // 1. 参数校验前端版 if (!params.city || typeof params.city ! string) { throw new Error(城市参数缺失或格式错误); } // 2. 执行确定性操作 const response await fetch(/api/skill/weather, { method: POST, headers: {Content-Type: application/json}, body: JSON.stringify(params) }); // 3. 解析标准响应FastAPI 保证返回 {success: true, data: {...}} const result await response.json(); if (!result.success) throw new Error(result.error); return result.data; // 纯数据无副作用 }这三条红线本质上是在用代码契约强制划分“谁该做什么”。它牺牲了一点初期开发的“爽感”比如不能直接在按钮里写localStorage.setItem(lastCity, city)但换来的是后期维护的指数级便利——当你需要把WeatherSkill从 React 迁移到 React Native 时只要复用executeWeatherSkill函数UI 层完全重写也不影响核心逻辑。3. Ponytail 核心细节解析与实操要点从 Skill 定义到跨端调用3.1 Skill 的标准化定义前后端统一的契约语言在 Ponytail 架构中“Skill” 不是一个模糊的概念而是一个有明确定义的工程实体。它必须同时满足四个条件可发现、可配置、可执行、可组合。这意味着无论这个 Skill 是用 Python 写的后端服务还是用 JavaScript 写的前端工具它对外暴露的接口必须遵循同一套规范。我们团队经过多次迭代最终采用了一种极简但有效的定义方式每个 Skill 对应一个 JSON Schema 文件描述其输入参数、输出结构、元信息名称、描述、图标以及执行位置frontend/backend。以一个TextSummarizeSkill为例它的summarize.schema.json长这样{ name: text_summarize, title: 文本摘要, description: 使用 LLM 生成输入文本的简洁摘要, icon: , execution: backend, input: { type: object, properties: { text: { type: string, description: 待摘要的原文, minLength: 10 }, max_length: { type: integer, description: 摘要最大字数, minimum: 50, maximum: 500, default: 200 } }, required: [text] }, output: { type: object, properties: { summary: { type: string, description: 生成的摘要文本 } }, required: [summary] } }这个 Schema 文件是 Ponytail 的“宪法”它驱动着整个生态的自动化前端自动表单生成React 组件读取input字段用json-schema-form库动态渲染配置表单。用户看到的不是一个空白输入框而是带校验规则如“最少 10 字”、默认值“200 字”、友好提示的完整 UI。当用户填写后前端自动按 Schema 校验不合法则实时报错无需手写if (text.length 10)。FastAPI 自动路由注册后端启动时扫描所有.schema.json文件为每个execution: backend的 Skill 动态生成 FastAPI 路由。TextSummarizeSkill会自动绑定到/api/skill/text_summarize且其请求体校验、响应模型、OpenAPI 文档全部由 Pydantic 根据 Schema 生成。你甚至不用写一行app.post只需确保 Schema 正确。跨端调用一致性当 Skill 配置好后前端 JavaScript 执行器skillExecutor.js会根据execution字段决定调用方式。如果是backend就发fetch到对应 API如果是frontend就直接调用本地 JS 函数。调用参数和返回结果的结构严格遵循 Schema 中的input和output定义确保前后端永远“说同一种语言”。这种基于 Schema 的标准化彻底解决了 Ponytail 项目中最头疼的问题Skill 的碎片化管理。以前每个新 Skill 都要手动写 API 文档、前端表单、后端校验重复劳动多且容易不一致。现在一个 Schema 文件搞定一切新增一个 Skill 的平均耗时从 2 小时降到 15 分钟。更重要的是它让非技术人员如产品经理也能参与 Skill 设计——他们只需要用 JSON Schema 的语法描述“这个功能需要什么输入返回什么结果”技术实现由框架自动完成。3.2 JavaScript 与 Python 的协同不只是 API 调用而是能力互补Ponytail 的 JavaScript 并非只是 FastAPI 的“客户端”它和 Python 后端构成了一种深度协同关系。这种协同体现在三个层面执行时机分流、计算能力互补、错误处理分层。首先看执行时机分流。不是所有操作都适合放在后端。比如用户在画布上拖拽一个节点需要实时计算其与周围节点的距离以决定是否吸附。这个操作频率极高每秒数十次如果每次都要fetch到 FastAPI网络延迟会让交互卡顿。我们的做法是前端 JavaScript 用getBoundingClientRect()实时计算只在用户松开鼠标、确认最终位置时才调用后端 API 保存这个“已吸附”的状态。JavaScript 处理高频、低延迟的交互Python 处理低频、高可靠的状态持久化。其次是计算能力互补。JavaScript 擅长字符串处理、DOM 操作、浏览器 API 调用Python 擅长数值计算、机器学习、系统级操作。一个典型例子是文件处理 Skill用户上传一个 CSV 文件前端 JavaScript 用PapaParse库快速解析成 JSON 数组并在画布上预览前 10 行当用户点击“分析”按钮时才把这 10 行数据或文件 URL发给 FastAPI由 Python 后端用pandas做统计分析、生成图表。JavaScript 做“轻量预览”Python 做“重量分析”各司其职。最后是错误处理分层。Ponytail 的错误不是简单抛给用户“请求失败”而是按层级精细化处理前端 JavaScript 层捕获网络错误fetch失败、参数校验失败Schema 不匹配、用户取消操作。此时显示友好的 Toast 提示如“网络连接不稳定请重试”。FastAPI 层捕获业务逻辑错误如 API Key 无效、配额超限、模型调用失败Ollama 返回 500、数据处理异常CSV 解析失败。FastAPI 统一包装成{success: false, error: xxx, code: INVALID_API_KEY}格式返回。React UI 层根据code字段决定如何展示。INVALID_API_KEY显示设置弹窗RATE_LIMIT_EXCEEDED显示倒计时其他未知错误显示通用提示。这种分层让错误处理既专业又人性化。为了实现这种协同我们约定了一套简单的通信协议。所有 Skill 的 API 响应必须是标准的 JSON 格式{ success: true, data: { /* Skill 的实际返回数据 */ }, meta: { timestamp: 2024-05-20T10:30:00Z, duration_ms: 1245, skill_version: 1.2.0 } }前端 JavaScript 执行器会自动解析这个结构提取data供组件使用meta信息则用于性能监控和版本管理。这套协议看似简单却避免了无数因响应格式不一致导致的前端崩溃。3.3 React Flowork 画布的深度定制让智能体“可看见、可编辑、可调试”Ponytail 的灵魂之一是它把 AI Agent 从黑盒变成了白盒。而实现这一目标的核心载体就是基于 React 的 Flowork 风格画布。它不是一个简单的流程图渲染器而是一个集“设计、执行、调试”于一体的 IDE。我参与开发的画布已经支持超过 15 种 Skill 节点类型每个节点都不仅仅是图标而是可交互的“微型应用”。画布的定制化主要体现在三个维度第一节点 UI 的深度集成。一个DatabaseQuerySkill节点双击后不是弹出一个通用表单而是渲染一个 SQL 编辑器monaco-editor支持语法高亮、自动补全基于当前数据库 schema。用户写的 SQL 会实时发送到 FastAPI 的/api/skill/db-validate接口进行语法检查错误信息直接在编辑器下方红色波浪线下提示。这种集成让 Skill 的配置不再是填空题而是真正的编程体验。第二连线逻辑的语义化。传统流程图连线只是视觉连接而 Ponytail 画布的连线承载着数据流语义。当用户从FileUploadSkill节点拖出一条线连接到TextSummarizeSkill节点时画布会自动推断FileUploadSkill的输出file_content字段可以作为TextSummarizeSkill的输入text字段。它会生成一个隐式的“数据映射”配置用户可以在连线旁的浮层里编辑这个映射比如file_content - text.substring(0, 5000)实现截断。这种语义化连线让工作流的构建从“画图”升级为“编程”。第三执行过程的实时可视化。这是 Ponytail 画布最惊艳的功能。当用户点击“运行工作流”按钮画布不会黑屏等待而是进入“执行模式”每个节点图标变成脉冲动画表示正在处理连线显示流动的粒子效果表示数据正在传输节点下方实时打印日志如✅ TextSummarizeSkill: 生成摘要耗时 842ms或❌ WeatherSkill: API 调用超时重试第 1 次。用户能清晰看到哪个 Skill 卡住了哪条数据流断了而不是面对一个旋转的加载图标干等。这个功能的实现依赖于 FastAPI 的 Server-Sent EventsSSE后端 Skill 执行时通过yield持续推送事件流前端用EventSource接收并更新 UI。它让调试 AI Agent 变得像调试一个普通 Web 应用一样直观。为了支撑这些高级功能我们对react-flow进行了深度二次开发封装了PonytailFlow组件库。它提供了一套标准的 Hook如useSkillNode获取当前节点的 Schema 和配置、useDataFlow管理节点间的数据映射、useExecutionLog订阅执行日志。新加入的开发者只需调用这些 Hook就能获得所有高级能力无需关心底层实现。这正是 Ponytail “轻量但强大”的体现——它不造轮子而是在优秀轮子上加装精密仪表。4. Ponytail 实操过程与核心环节实现从零搭建一个天气查询智能体4.1 环境准备与项目初始化五分钟建立 Ponytail 基础骨架搭建 Ponytail 项目关键在于快速建立“前后端分离但契约统一”的基础骨架。我推荐一个经过实战检验的初始化流程全程不超过 5 分钟且所有命令均可复制粘贴第一步初始化 FastAPI 后端# 创建项目目录 mkdir ponytail-weather cd ponytail-weather # 初始化 Python 环境推荐 conda避免 pip 依赖冲突 conda create -n ponytail python3.11 conda activate ponytail # 安装核心依赖 pip install fastapi uvicorn pydantic[email] httpx python-multipart # 创建基础目录结构 mkdir -p backend/skills backend/schemas backend/utils touch backend/main.py backend/skills/__init__.pybackend/main.py是 FastAPI 的入口内容极简from fastapi import FastAPI from backend.skills import register_skills app FastAPI(titlePonytail Weather Skill API) register_skills(app) # 动态注册所有 Skill第二步初始化 React 前端# 在项目根目录下 npx create-react-app frontend --template typescript cd frontend # 安装 Ponytail 必需的依赖 npm install preact/haiku react-flow-renderer monaco-editor/react # 创建核心目录 mkdir -p src/skills src/utils src/components/flow第三步定义第一个 Skill 的 Schema在backend/schemas/weather.schema.json中写入之前提到的天气 Skill Schema。注意这里execution设为backend因为我们希望天气查询由后端统一管理便于 API Key 保密和缓存。第四步生成 FastAPI 路由在backend/skills/__init__.py中编写动态注册逻辑import json from pathlib import Path from fastapi import APIRouter, Depends from pydantic import BaseModel from typing import Dict, Any def load_skill_schemas() - Dict[str, Dict]: 加载所有 .schema.json 文件 schemas {} for schema_file in Path(__file__).parent.parent / schemas: if schema_file.suffix .json: with open(schema_file) as f: schema json.load(f) schemas[schema[name]] schema return schemas def register_skills(app: FastAPI): 根据 Schema 动态注册 Skill 路由 schemas load_skill_schemas() for skill_name, schema in schemas.items(): if schema.get(execution) ! backend: continue # 为每个 Skill 创建独立的 Router router APIRouter(prefixf/api/skill/{skill_name}) # 动态生成 Pydantic 模型简化版实际用更健壮的库 class InputModel(BaseModel): city: str unit: str celsius router.post(/) async def execute_skill(input_data: InputModel): # 这里是 Skill 的实际执行逻辑稍后填充 return {success: True, data: {temp: 25, condition: sunny}} app.include_router(router)运行uvicorn backend.main:app --reload访问http://localhost:8000/docs你将看到自动生成的/api/skill/weather接口文档且已包含city和unit的参数校验。这就是 Ponytail 的起点——一个由 Schema 驱动的、可立即使用的 Skill 接口。4.2 Skill 的后端实现从 API 到 Ollama 的完整链路定义好接口只是开始真正的 Skill 实现需要打通从 FastAPI 到外部服务如 Ollama的链路。以天气 Skill 为例它的后端逻辑远不止返回一个固定值而是要调用真实的天气 API并可能结合 LLM 做增强。以下是完整的实现步骤1. 安装并配置 Ollama 客户端# 在本地安装 OllamamacOS brew install ollama # 拉取一个轻量模型如 phi-3 ollama pull phi3在 FastAPI 项目中安装 Python 客户端pip install ollama2. 编写 Skill 的核心执行函数在backend/skills/weather.py中import httpx import ollama from pydantic import BaseModel from typing import Dict, Any class WeatherResponse(BaseModel): temp: float condition: str humidity: int wind_speed: float async def get_weather_from_api(city: str, unit: str celsius) - WeatherResponse: 调用第三方天气 API此处用 mock实际替换为 OpenWeatherMap 等 # 实际项目中这里会是 httpx.AsyncClient().get(...) # 为演示返回 mock 数据 return WeatherResponse( temp25.5 if unit celsius else 78.0, conditionpartly cloudy, humidity65, wind_speed12.3 ) async def enhance_weather_with_llm(weather_data: WeatherResponse) - str: 用 LLM 生成更人性化的天气描述 prompt f你是一个专业的天气播报员。请根据以下天气数据用中文生成一段 50 字以内、生动有趣的天气播报 温度{weather_data.temp}°C天气{weather_data.condition}湿度{weather_data.humidity}%风速{weather_data.wind_speed}km/h try: response ollama.chat( modelphi3, messages[{role: user, content: prompt}], options{temperature: 0.3, num_predict: 100} ) return response[message][content] except Exception as e: # LLM 调用失败降级为简单描述 return f今天{weather_data.condition}气温{weather_data.temp}°C。 # 这是 Skill 的主函数被 FastAPI 路由调用 async def execute_weather_skill(city: str, unit: str celsius) - Dict[str, Any]: try: # 步骤1获取原始天气数据 raw_weather await get_weather_from_api(city, unit) # 步骤2用 LLM 增强描述 enhanced_desc await enhance_weather_with_llm(raw_weather) # 步骤3返回结构化结果 return { success: True, data: { raw: raw_weather.dict(), enhanced: enhanced_desc, timestamp: 2024-05-20T10:30:00Z } } except Exception as e: return { success: False, error: f天气查询失败{str(e)}, code: WEATHER_API_ERROR }3. 将 Skill 注册到 FastAPI 路由修改backend/skills/__init__.py中的路由函数from backend.skills.weather import execute_weather_skill router.post(/) async def execute_skill(input_data: InputModel): # 调用我们刚写的 Skill 函数 result await execute_weather_skill( cityinput_data.city, unitinput_data.unit ) return result这个实现展示了 Ponytail 的核心哲学Skill 是一个可组合的函数它内部可以调用任何服务HTTP API、LLM、数据库但对外只暴露一个干净的接口。你可以在execute_weather_skill里轻松添加 Redis 缓存避免重复调用天气 API、添加 Sentry 错误监控、添加 Prometheus 指标埋点而这一切都不影响前端调用方式。它就像一个乐高积木你可以随意给它添加新功能但拼接方式永远不变。4.3 React 前端集成从画布节点到 JavaScript 执行器后端 Skill 就绪后前端需要将其“可视化”并“可执行”。这分为两步在 Flowork 画布上创建节点以及编写 JavaScript 执行器。1. 创建 WeatherSkill 节点组件在src/components/flow/WeatherNode.tsx中import React, { useState, useEffect } from react; import { Handle, Position } from react-flow-renderer; import { useSkillNode } from ../../hooks/useSkillNode; import { executeWeatherSkill } from ../../utils/skillExecutor; interface WeatherNodeProps { data: { city: string; unit: string; }; } const WeatherNode: React.FCWeatherNodeProps ({ data }) { const [isLoading, setIsLoading] useState(false); const [result, setResult] useStatestring | null(null); const { node, updateNodeData } useSkillNode(); const handleExecute async () { setIsLoading(true); try { // 调用 JavaScript 执行器 const res await executeWeatherSkill({ city: data.city || Beijing, unit: data.unit || celsius }); setResult(res.data.enhanced); // 更新节点数据以便后续节点使用 updateNodeData({ ...data, lastResult: res.data.enhanced, timestamp: new Date().toISOString() }); } catch (error) { setResult(执行失败${error instanceof Error ? error.message : String(error)}); } finally { setIsLoading(false); } }; return ( div classNamebg-white p-4 rounded shadow border Handle typetarget position{Position.Top} / div classNamefont-bold text-blue-600️ 天气查询/div div classNamemt-2 label城市/label input typetext value{data.city || } onChange{(e) updateNodeData({ ...data, city: e.target.value })} classNameborder p-1 rounded w-full / /div div classNamemt-2 label单位/label select value{data.unit || celsius} onChange{(e) updateNodeData({ ...data, unit: e.target.value })} classNameborder p-1 rounded w-full option valuecelsius摄氏度/option option valuefahrenheit华氏度/option /select /div button onClick{handleExecute} disabled{isLoading} className{mt-3 px-3 py-1 rounded ${isLoading ? bg-gray-300 : bg-blue-500 text-white}} {isLoading ? 查询中... : 查询天气} /button {result ( div classNamemt-3 p-2 bg-green-50 border border-green-200 rounded strong结果/strong{result} /div )} Handle typesource position{Position.Bottom} / /div ); }; export default WeatherNode;2. 编写skillExecutor.js统一的 JavaScript 执行入口在src/utils/skillExecutor.ts中// 定义 Skill 执行的通用接口 interface SkillInput { [key: string]: any; } interface SkillResponseT any { success: boolean; data?: T; error?: string; code?: string; } // 通用执行函数 export async function executeSkillT( skillName: string, input: SkillInput ): PromiseSkillResponseT { try { const response await fetch(/api/skill/${skillName}, { method: POST, headers: { Content-Type: application/json, }, body: JSON.stringify(input), }); if (!response.ok) { throw new Error(HTTP ${response.status}: ${response.statusText}); } const result: SkillResponseT await response.json(); return result; } catch (error) { console.error(Skill ${skillName} execution failed:, error); return { success: false, error: error instanceof Error ? error.message : 未知错误, code: NETWORK_ERROR }; } } // 为常用 Skill 提供便捷函数 export async function executeWeatherSkill(input: { city: string; unit?: string }) { return executeSkill{ enhanced: string; raw: any }(weather, input); } // 其他 Skill 的便捷函数...
返回列表