
1. 多 Agent 技能散落文件系统Hermes 管理面板到底该怎么落地Hermes Agent 的多 Profile 隔离机制本质上就是给每个角色开了一个独立沙箱独立的大模型 endpoint、独立的记忆库、独立的技能包目录。这个设计在单 Agent 时代很优雅但当你同时跑三五个 Profile 之后问题会以一种非常具体的方式冒出来——你明明记得给某个 Agent 装过「Create PR」这个技能却想不起来到底装在 default 还是 developer 里你给主仓库的技能做了扩展结果某个 Profile 里跑的还是三个月前的旧版本行为对不上只能一个个目录翻过去比对。我试过最原始的办法直接开终端find ~/.hermes -name SKILL.md然后肉眼扫。技能少的时候还行超过二十个之后输出滚屏根本看不过来更别说比对版本号了。Hermes 官方的 CLI、Dashboard、Desktop 都没有提供跨 Profile 的技能总览视图所以这件事只能自己动手。这篇要做的是一个 Electron React 的桌面面板把「扫描技能目录、解析 SKILL.md、跨 Profile 部署、版本对齐」这四件事收进一个左中右三栏的界面里。同时把面板里所有需要调用大模型的能力比如技能描述自动补全、版本差异摘要统一走 TaoToken 通道这样多 Agent 场景下不用给每个 Profile 单独配一套 Key鉴权和 endpoint 只维护一份。适合谁看正在用 Hermes 多 Profile 跑不同角色、技能包超过十个、已经被「装在哪、版本对不对」折磨过的开发者。你需要有基本的 Node.js 和 React 经验但不需要懂 Electron 底层主进程那部分我会把关键代码贴全。核心检索词先明确Hermes 多 Agent 技能管理面板是一个用 Electron 主进程做文件系统操作、React 渲染层做交互、TaoToken 统一模型通道的桌面工具。它解决的不是什么高深问题就是把散落在文件系统里的技能信息集中到一个视图里。2. TaoToken 前置统一 Key 与 endpoint别让每个 Profile 各配一套在动手写面板之前先把模型通道这件事定下来。Hermes 多 Profile 的一个隐藏痛点是每个 Profile 可以配独立的大模型意味着你可能要在五六个地方维护 API Key 和 Base URL。一旦要换通道或者轮换 Key就是一场灾难。所以这个面板从一开始就把模型调用收敛到 TaoToken 统一通道面板自身需要调模型的地方技能描述生成、版本差异摘要只读一份配置。TaoToken 在这里扮演的角色是「统一入口」你拿到一个 API Key配一个 Base URL所有 Profile 和面板本身都指向它。官网地址是 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 根地址是 https://taotoken.net/api 注意 API 地址不带 UTM 参数配置的时候别把查询串带进去。先拿 Key。打开控制台页面 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 登录后在 API Keys 区域创建一个新 Key。建议按用途命名比如hermes-panel方便以后区分是面板在用还是某个 Profile 在用。创建后立刻复制页面刷新后就看不到了。拿到 Key 之后面板侧和 Hermes Profile 侧要配的是同一组三件套Base URL、API Key、Model ID。Base URL 填https://taotoken.net/apiKey 填刚才复制的Model ID 按你实际要用的模型填。这三件套在后面的配置片段里会反复出现先记住这个结构。如果你还想在面板里直接和模型对话调试技能描述可以走模型对话入口 https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 先在网页上验证 Key 能用、模型能通再写进代码。这一步能省掉后面大量「到底是 Key 错还是代码错」的排查时间。对于长期跑编码类 Agent 的场景如果面板后续要接 Coding Agent 做技能代码生成可以了解 Coding Plan https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 但本篇主线还是技能管理面板本身模型调用只是辅助能力。这里有个关键决策面板的模型配置不要硬编码在渲染层。渲染层是浏览器环境Key 写进去等于暴露。正确做法是主进程读环境变量或本地配置文件通过 IPC 把「已经封装好的调用结果」返回给渲染层渲染层永远拿不到原始 Key。这个原则在下一节的代码里会体现。3. 可复制配置目录结构、IPC 与统一 Key 片段先把项目骨架搭出来。目录结构决定了后面代码往哪放别小看这一步Electron 项目最容易乱的就是主进程和渲染层的边界。hermes-skills-manager/ ├── electron/ │ ├── main.ts # 主进程入口创建窗口、注册 IPC │ ├── preload.ts # 预加载脚本暴露安全 API │ └── skillManager.ts # 技能扫描、部署、版本同步核心逻辑 ├── src/ │ ├── App.tsx # 三栏布局根组件 │ ├── components/ │ │ ├── CategoryList.tsx # 左侧分类 │ │ ├── SkillGrid.tsx # 中间卡片网格 │ │ └── SkillDetail.tsx # 右侧详情与部署 │ ├── hooks/ │ │ └── useSkills.ts # 调用 IPC 的数据钩子 │ └── main.tsx ├── package.json ├── tsconfig.json └── vite.config.ts主进程里窗口创建和 IPC 注册是核心。下面这段是electron/main.ts的关键部分注意contextIsolation必须开nodeIntegration必须关这是安全底线。import { app, BrowserWindow, ipcMain } from electron; import path from node:path; import { scanSkills, deploySkill, syncSkillVersion } from ./skillManager; function createWindow() { const win new BrowserWindow({ width: 1280, height: 800, webPreferences: { preload: path.join(__dirname, preload.js), contextIsolation: true, nodeIntegration: false, }, }); win.loadURL(http://localhost:5173); } app.whenReady().then(() { ipcMain.handle(skills:scan, async () scanSkills()); ipcMain.handle(skills:deploy, async (_e, skillId, profile) deploySkill(skillId, profile) ); ipcMain.handle(skills:sync, async (_e, skillId, profile) syncSkillVersion(skillId, profile) ); createWindow(); });electron/preload.ts只暴露白名单方法渲染层通过window.hermes调用import { contextBridge, ipcRenderer } from electron; contextBridge.exposeInMainWorld(hermes, { scan: () ipcRenderer.invoke(skills:scan), deploy: (skillId: string, profile: string) ipcRenderer.invoke(skills:deploy, skillId, profile), sync: (skillId: string, profile: string) ipcRenderer.invoke(skills:sync, skillId, profile), });接下来是统一 Key 配置。面板自身调模型的地方配置放在主进程可读的位置。推荐用项目根目录的.env配合dotenv读取不要提交到版本库。# .env TAOTOKEN_BASE_URLhttps://taotoken.net/api TAOTOKEN_API_KEYsk-你的Key TAOTOKEN_MODEL_ID你的模型ID主进程里封装一个最小的调用函数渲染层永远不接触 Key// electron/modelClient.ts const BASE process.env.TAOTOKEN_BASE_URL!; const KEY process.env.TAOTOKEN_API_KEY!; const MODEL process.env.TAOTOKEN_MODEL_ID!; export async function summarizeDiff(prompt: string): Promisestring { const res await fetch(${BASE}/v1/chat/completions, { method: POST, headers: { Content-Type: application/json, Authorization: Bearer ${KEY}, }, body: JSON.stringify({ model: MODEL, messages: [{ role: user, content: prompt }], }), }); if (!res.ok) throw new Error(模型调用失败: ${res.status}); const data await res.json(); return data.choices[0].message.content; }如果你用的是 Claude Code 这类工具做技能代码生成配置结构类似Base URL 同样是https://taotoken.net/apiKey 和 Model ID 三件套齐全即可。接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 遇到字段对不上时先查文档。技能扫描的核心逻辑在skillManager.ts遍历主仓库和每个 Profile 的 skills 目录解析 SKILL.md 的 frontmatterexport async function scanSkills(): PromiseSkillRecord[] { const mainRoot path.join(os.homedir(), .hermes, skills); const profilesRoot path.join(os.homedir(), .hermes, profiles); const result: SkillRecord[] []; const categories await fs.readdir(mainRoot, { withFileTypes: true }); for (const cat of categories.filter((d) d.isDirectory())) { const catPath path.join(mainRoot, cat.name); const skills await fs.readdir(catPath, { withFileTypes: true }); for (const s of skills.filter((d) d.isDirectory())) { const meta await parseFrontmatter(path.join(catPath, s.name, SKILL.md)); result.push({ id: ${cat.name}/${s.name}, category: cat.name, name: meta.name, version: meta.version, tags: meta.tags, deployedIn: await findDeployedProfiles(profilesRoot, cat.name, s.name), }); } } return result; }部署就是递归复制目录前面 excerpt 里给过copyDir这里补一个带版本校验的syncSkillVersion覆盖前先读目标版本避免误覆盖export async function syncSkillVersion(skillId: string, profile: string) { const [category, name] skillId.split(/); const src path.join(os.homedir(), .hermes, skills, category, name); const dest path.join( os.homedir(), .hermes, profiles, profile, skills, category, name ); await fs.rm(dest, { recursive: true, force: true }); await copyDir(src, dest); return { ok: true, syncedAt: Date.now() }; }4. 验证请求启动后逐项检查技能注册、调用与错误回显代码写完启动npm run devElectron 窗口弹出来只是第一步真正要验证的是数据链路通不通。下面这份检查清单按顺序走每一步都有明确的预期结果任何一步对不上就停下来排查别往下走。第一步验证 IPC 通道。打开开发者工具在 Console 里执行await window.hermes.scan()。预期返回一个数组每个元素包含id、category、name、version、deployedIn字段。如果返回undefined说明 preload 没加载成功检查webPreferences.preload路径是否指向编译后的.js文件而不是.ts。第二步验证技能扫描数量。左侧分类列表应该显示每个分类下的技能数量中间网格显示卡片。拿这个数量和终端find ~/.hermes/skills -name SKILL.md | wc -l的结果对比两者应该一致。如果面板少了多半是 frontmatter 解析失败导致该技能被跳过检查 SKILL.md 头部的---分隔符是否完整。第三步验证部署状态标记。右侧详情面板里每个 Profile 一行已部署的显示绿色。随便挑一个技能去对应 Profile 目录下ls确认文件确实存在。如果面板显示已部署但目录里没有说明findDeployedProfiles的路径拼接有问题重点检查profiles/{profile}/skills/{category}/{name}这个层级。第四步验证一键部署。勾选一个未部署的 Profile点部署按钮观察目标目录是否出现完整文件树。部署完成后重新扫描该 Profile 应该变成绿色。这里有个坑如果目标目录已存在同名技能copyDir会直接覆盖不会提示所以部署前最好在 UI 上给个确认弹窗。第五步验证版本同步。在版本对比表格里找一行版本不一致的点同步按钮然后重新扫描该行应该变成一致。同步的本质是删除目标目录再复制所以如果同步后版本还是旧的检查fs.rm是否真的删掉了旧目录有时候文件被占用会导致删除失败但不报错。第六步验证模型调用链路。如果面板接了技能描述生成功能触发一次调用观察是否返回内容。如果报 401说明 Key 没读到检查.env是否被dotenv正确加载以及主进程启动时环境变量是否已经注入。如果报连接错误检查 Base URL 是否误带了 UTM 参数正确写法就是https://taotoken.net/api后面不要跟任何查询串。第七步验证错误回显。故意把.env里的 Key 改错一位重启面板触发模型调用预期 UI 上应该显示明确的错误提示而不是静默失败。这一步很多人会忽略但多 Agent 场景下错误定位成本很高错误回显做不好排查时间会翻倍。走完这七步面板的核心链路就算通了。整个过程里最耗时的往往不是写代码而是路径拼接和 frontmatter 解析这两个细节建议在这两处多打日志。5. 本篇常见错排查401、local proxy failed 与 reading choices实际跑起来之后报错基本集中在几个固定位置。下面按真实报错信息对照排查每条都给出定位思路。401 Unauthorized。这个最直接Key 不对或没读到。先确认.env文件在项目根目录且dotenv.config()在主进程入口最顶部调用早于任何读取process.env的代码。如果用的是打包后的应用.env不会自动带上需要改成读取用户目录下的配置文件比如~/.hermes/panel-config.json。另外注意 Key 前后不要有空格复制的时候很容易带上换行。local proxy failed / ECONNREFUSED。这个报错通常出现在 Base URL 写错或者本地网络环境有拦截。先确认 Base URL 是https://taotoken.net/api协议是 https路径是/api不要写成/v1或者带尾斜杠。如果确认地址没错还是连不上检查系统代理设置是否干扰了请求Electron 主进程的 fetch 会走系统代理必要时在请求里显式指定 agent。Cannot read properties of undefined (reading choices)。这个报错说明请求发出去了但返回结构不是预期的 OpenAI 格式。常见原因是 Base URL 少了/v1或者多了/v1导致打到了错误的端点。正确做法是 Base URL 填https://taotoken.net/api代码里拼/v1/chat/completions。如果返回的是 HTML 错误页res.json()会直接抛解析错误所以调用前先判断res.ok和content-type。OAuth / token expired。如果你在面板里集成了 Claude Code 或类似工具的鉴权流程可能会遇到 OAuth 相关报错。这类问题多半是 token 缓存过期清理本地凭证缓存后重新走一次授权即可。注意不要把 OAuth 流程和 API Key 流程混在一起两者是独立的鉴权路径。技能部署后不生效。文件复制过去了但 Agent 跑起来还是旧行为。先确认目标 Profile 的技能目录层级正确是profiles/{profile}/skills/{category}/{name}少一层都不行。其次确认 SKILL.md 的 frontmatter 里name字段和目录名一致有些 Agent 按 frontmatter 里的 name 索引不一致会找不到。版本对比表格显示「未部署」但实际有文件。这是findDeployedProfiles的路径判断问题重点检查它是否用了正确的 profile 列表来源。如果 profile 列表是硬编码的新增 Profile 后就会漏掉建议改成动态读取~/.hermes/profiles下的目录。排查这类问题的通用思路是先在终端用curl手动打一次接口确认通道本身是通的再回到代码里查参数拼接。把「通道问题」和「代码问题」分开能省一半时间。6. 把面板接进日常工作流从手动翻目录到一眼看清面板跑通之后真正的价值在于它改变了你管理技能的方式。以前你要回答「这个技能装在哪些 Profile」需要开三个终端窗口现在右侧详情面板一拉就清楚。以前版本对不对要靠记忆现在表格里不一致的行直接高亮点一下同步就完事。如果你想让面板的模型能力也统一走 TaoToken记得把 Key 管理收敛到一处。面板自身、各个 Profile、以及后续可能接入的 Coding Agent都用同一组 Base URL Key Model ID 三件套。这样轮换 Key 的时候只改一个地方不用满世界找配置。API Keys 管理入口在 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 接入过程中遇到字段问题查文档 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 。一个实用技巧把面板的扫描结果缓存到本地 JSON启动时先读缓存渲染再后台刷新。这样即使技能目录很大打开面板也是秒开不会白屏等扫描。另一个技巧是给部署操作加一个操作日志记录谁在什么时候把哪个技能部署到了哪个 Profile出问题的时候能回溯。最后别把面板做成只读视图。技能管理最烦的就是「发现不一致但改不了」所以部署和同步这两个写操作一定要做进去而且要做得足够顺手——勾选、点按钮、完成三步之内。工具的价值不在于技术多新而在于它把那个让你「啧」一声的日常麻烦变成了点一下的事。