PostHog Desktop Skills Tab v2 设计与实现:用七层 PR 栈把只读技能列表升级为完整技能管理器

PostHog Desktop Skills Tab v2 设计与实现:用七层 PR 栈把只读技能列表升级为完整技能管理器 PostHog Desktop Skills Tab v2 设计与实现用七层 PR 栈把只读技能列表升级为完整技能管理器【免费下载链接】posthog:hedgehog: PostHog is the leading platform for building self-driving products. Our developer tools – AI observability, analytics, session replay, flags, experiments, error tracking, logs, and more – capture all the context agents need to diagnose problems, uncover opportunities, and ship fixes. Steer it all from Slack, web, desktop, or the MCP.项目地址: https://gitcode.com/GitHub_Trending/po/posthogPostHog Desktop 的 Skills Tab 原本只是一个只读列表SkillsService.listSkills()扫描 bundled、user、repo、marketplace 四个来源一个 tRPC 查询喂给SkillsView详情面板只渲染SKILL.md单个文件。本文基于仓库中的实现计划文档 skills-tab.mdOwner: Peter Kirkham状态 Ready to build完整还原这套七层 PR 栈的设计决策——从多文件目录渲染、CRUD、实时刷新、校验信号到 skills.sh 市场安装、PostHog 云端团队技能与 Codex 统一——并结合workspace-server、host-router、core、api-client中已落地的源码逐条印证每个 PR 的实现形态与安全守卫帮助读者掌握桌面端技能即目录架构的分层规则与写路径防护设计。一、背景只读列表的五个缺口与本次改造目标改造前的 Skills Tab 存在明确的体验缺口无 CRUD不能创建、编辑、删除技能无实时刷新外部编辑agent 会话、终端touch不会反映到界面无市场浏览不能浏览/安装社区技能无云端协作不能发布/消费团队技能渲染不完整技能目录中的references/、scripts/等伴随文件完全不可见。v2 计划将 Skills Tab 升级为跨七个叠加 PR 的完整技能管理器目标Goals为在应用内完整创建、编辑、删除用户技能~/.claude/skills与仓库技能{repo}/.claude/skills按技能的本来面目渲染它目录SKILL.mdreferences/scripts/ 资源文件而不是单个 markdown 文件不离开应用即可浏览并安装 skills.sh 上的社区技能通过既有 PostHog 云端LLMSkillAPI 发布团队技能并消费团队技能与 Codex 统一读取用户 Codex 技能、支持导入、并把用户技能同步出去带上你的技能在任何 agent 中使用。明确不做的事Non-goals计划文档特意列出了四条硬性排除项它们是理解整个设计边界的关键不改官方 PostHog 技能管道agent-skills-latest发布物、skills.zip、context-mill omnibus zips、update-skills-saga.ts 以及 30 分钟刷新周期原封不动不为该通道增加清单manifest或版本管理。当前代码中该 saga 依旧采用staging 目录 分步下载 overlay 已下载技能的可回滚结构bundled 与插件市场技能永远只读且在服务端强制而非靠 UI 隐藏按钮不做上游版本跟踪安装即拷贝install is a copy。一旦安装技能就是本地用户技能除市场浏览里的 Installed 徽章外系统忘记它的来源——无 hash、无 diff、无更新通知重装确认 覆盖就是升级路径不引入新 npm 依赖、不影响 bundle 体积市场浏览是运行时 HTTP解压复用既有的基于fflate的 extract-zip.ts。二、五条设计原则计划文档给出了贯穿全部 PR 的五条设计原则值得逐条对照源码理解技能是目录不是文件。SKILL.md是清单manifest目录才是安装、编辑、共享、渲染的单位。可编辑性跟随目录所有权。我们代表用户持有的目录~/.claude/skills、{repo}/.claude/skills可写其他系统持有的目录运行时插件目录、插件管理器安装路径、我们并未写入的 Codex 目录只读。安装即拷贝拷贝归你。不与上游绑定。写路径守卫位于 workspace-server不在 UI。每一次变更都要验证目标解析在可写技能根之下。标准分层遵循桌面端 AGENTS.md 的放置规则fs/zip/watcher/HTTP 下载放在workspace-server的一行 tRPC 转发背后PostHog 云端 API 调用放在经api-client注入的core服务中纯决策逻辑校验、遮蔽判断、列表合并放在coreUI 每个 query/mutation 只调一个 hook。计划文档中还有一张可复用接缝reusable seams清单指出每个能力都已有现成实现可挂靠需求既有接缝仓库内路径Zip 解压packages/workspace-server/src/services/posthog-plugin/extract-zip.tsfflate实际位于 extract-zip.ts文件写入packages/workspace-server/src/services/fs/service.tswriteRepoFile模式文件监听packages/workspace-server/src/services/watcher/service.tsparcel/watcher带 debouncetRPC 订阅模式packages/host-router/src/routers/workspace.router.tsasync-generatorsubscriptionMutation 模式packages/host-router/src/routers/fs.router.ts编辑器packages/ui/src/features/code-editor/CodeMirroruseCodeMirror版本化 JSON 状态文件skill-discovery.ts中读取~/.claude/plugins/installed_plugins.json的逻辑API 资源客户端模式packages/api-client/src/posthog-client.ts中 MCP 安装方法约 L2671–2798 一带说明上表路径沿用原文档写法相对桌面产品根在完整仓库中实际位于products/desktop/之下。三、技能来源矩阵改造后的六个来源计划文档给出了改造后的完整来源矩阵它直接对应今天源码中getSkillRoots()的构造顺序来源路径列出可编辑说明bundled运行时插件目录是否不动的官方管道user~/.claude/skills是是包含市场安装repo{repo}/.claude/skills是是每个工作区文件夹一份marketplace插件安装路径是否由 Claude 插件管理器持有codex新增~/.agents/skills是否可导入后编辑与 bundled 及镜像名去重team新增PostHog 云端LLMSkill是通过 publishInstall 物化一份本地拷贝在 skills.ts 中getSkillRoots()恰好按此顺序组装bundled运行时插件目录下的skills/、user~/.claude/skills、每个打开的工作区文件夹的.claude/skills、从installed_plugins.json解析出的 marketplace 安装路径、以及~/.agents/skills的 codex 来源——与计划文档PR 7 之后共六个来源完全一致。可编辑性跟随所有权原则 2落在 skill-discovery.ts 的isEditableSource()source user || source repo才返回 true并在readSkillMetadataFromDir()里写入每个SkillInfo.editable字段——可编辑性在服务端计算UI 不自行判断。四、PR 栈Graphite 叠加布局与工作流计划要求把七块工作建成 Graphite 叠加栈stack使后续 PR 在途时前面的可以先行评审main └── skills-01-full-rendering # PR 1 └── skills-02-crud # PR 2 └── skills-03-live-refresh # PR 3 └── skills-04-validation # PR 4 └── skills-05-marketplace # PR 5 └── skills-07-codex # PR 7 └── skills-06a-team-read # PR 6a独立于 main 的第二栈若 01 先落地则 rebase 其上 └── skills-06b-team-publish # PR 6b配套的工作流命令gt create skills-01-full-rendering -m feat(skills): render full skill directories # ...work, commit... gt create skills-02-crud -m feat(skills): create/edit/delete user and repo skills # ... gt submit --stack # 为整个栈打开/刷新 PR gt sync gt restack # PR 落地或 main 前移之后组织约束PR 1→5→7 是真实依赖链每一层复用上一层的 schema 与组件必须留在一个栈内PR 6a/6b 只触碰api-client、core与一个 UI 分组可并行开发、择机 restack。每个 PR 必须独立通过pnpm typecheck、pnpm lint、pnpm test和node scripts/check-host-boundaries.mjs——每个 PR 单独可发布shippable on its own。五、逐 PR 拆解实现细节与源码印证PR 1 — 渲染完整技能目录只读地基计划要点SkillsService.getSkillContents(skillPath)返回文件树相对路径 大小readSkillFile(skillPath, relPath)读取单个文件两者都必须验证路径解析在技能发现skill discovery返回的目录之内——绝不能让端点变成任意文件系统读取器。源码印证skills.tsgetSkillContents()先过resolveKnownSkillDir()再调listSkillFiles()文件上限MAX_SKILL_FILES 500readSkillFile()在解析后额外做了符号链接逃逸防护对文件与技能目录都取realpath若真实路径不在目录真实路径之下则返回null注释明言realpath 还能抓住经符号链接中间目录逃逸的情形单文件上限MAX_SKILL_FILE_BYTES 2 * 1024 * 1024。resolveKnownSkillDir()本身是只读端点守卫skills.ts#L479-L496目标必须是某个发现根的直接子目录且目录下必须存在SKILL.md否则抛Access denied: not a known skill directory。文件树的遍历在 skill-discovery.ts#L145-L181 的listSkillFiles()中跳过符号链接精心构造的技能不能借此暴露目录外文件、过滤被忽略条目点目录、node_modules等、SKILL.md恒定排首。SkillInfo上新增的派生字段editable: booleanuser/repo为 true正是计划中在SkillsService而非 UI 计算的落点。SkillsView、SkillDetailPanel等 UI 位于 packages/ui/src/features/skills/非可编辑技能显示锁定徽章其他文件经useCodeMirror只读打开。PR 2 — 用户/仓库技能的创建、编辑、删除计划要点createSkill({scope, repoPath?, name})脚手架化目录 模板SKILL.mdsaveSkillFile、deleteSkillFile、deleteSkill、renameSkill每次 mutation 的硬守卫——目标必须解析在~/.claude/skills或某个工作区文件夹的.claude/skills之下bundled/插件安装路径/运行时插件目录/Codex 目录一律在服务端拒绝。源码印证resolveWritableSkillDir()skills.ts#L529-L543即这条硬守卫父目录必须精确等于某个可写根且目录中必须存在SKILL.md或SKILL.md.disabled否则Access denied: skill is not in a writable location。createSkill()校验目录名后mkdir -p并写入模板。命名规则由validateSkillDirName()强制/^[a-z0-9][a-z0-9._-]*$/且长度 ≤ 64。saveSkillManifest()实现了一个关键的正确性闭环先serializeSkillMarkdown()序列化 frontmatter再用parseSkillFrontmatter()把写出的内容回解析一遍失败即抛错——注释写道writer 和 parser 必须一致否则技能会从列表里消失。这正是计划验收标准frontmatter 写出物能被 parse-skill-frontmatter.ts 再解析的落地。文件级操作有细粒度不变量SKILL.md不可重命名、不可删除写入目标必须通过assertVisibleSkillFilePath()拒绝点目录等文件树永远不显示的位置否则文件落盘后在 UI 中不可见、也不可发布路径含反斜杠直接拒绝防止云沙箱解压时\→/归一化导致的静默碰撞。计划中的renameSkill在最终实现里被拆分为文件级renameFilehost-router的 skills.router.ts 完整暴露了create/saveManifest/saveFile/renameFile/deleteFile/delete/setEnabled等一行转发 mutation。PR 3 — 实时刷新计划要点用WatcherService监听~/.claude/skills与每个工作区文件夹的.claude/skills暴露skills.watchtRPC 订阅沿用workspace.router.ts的 async-generator 模式发出防抖后的 skills changed 事件UI 订阅一次事件到达即失效skills.list/skills.contents查询摆脱对 30 秒 stale-time 的依赖。源码印证skills.ts#L388-L451 的watchSkills()与watchSkillDirs()是这段计划的完整实现SKILLS_WATCH_DEBOUNCE_MS 300把事件爆发折叠成单次通知尚不存在的仓库技能目录以MISSING_DIR_POLL_MS 2000轮询等待出现单个根上 watcher 失败不影响其他根每个根在独立 async 闭包中运行失败只被吞掉并继续计数。订阅端即计划所称的一行转发skills.router.ts#L144-L149watch: publicProcedure.subscription(async function* (opts) { const service opts.ctx.container.getSkillsService(SKILLS_SERVICE); for await (const event of service.watchSkills(opts.signal)) { yield event; } }),验收标准对应从终端touch ~/.claude/skills/foo/SKILL.md打开的标签页约 1 秒内更新agent 会话的编辑无需手动刷新即可出现。PR 4 — 校验与遮蔽shadowing信号计划要点analyzeSkills(skills: SkillInfo[])作为posthog/core中的纯函数无 I/O检查缺失/空 description、frontmatter 名与目录名不一致、SKILL.md过大上下文成本警告、跨来源同名冲突并给出谁赢的显式解析UI 在SkillCard上显示健康徽章、在详情面板给出被同名用户技能遮蔽之类的提示。从当前源码结构看这类信号已内化在数据模型中SkillInfo携带skillMdBytesskill-discovery.ts中按 UTF-8 字节计算支撑上下文成本判断、enabled对应实现中的禁用机制——SKILL.md与SKILL.md.disabled互转见setSkillEnabled()、以及disableModelInvocation标志遮蔽解析的一种形态即dedupeCodexSkills()见 PR 7 节它显式回答这个 codex 来源的副本是否已在别处存在、该以谁为准。analyzeSkills的逐规则单测仍是该 PR 的验收项。PR 5 — skills.sh 市场浏览 安装copy-and-forget计划把安装定义为拷贝后忘掉下载、解压到~/.claude/skills/name完成。此后它就是一般的可编辑用户技能installed.json沿用installed_plugins.json的版本化 JSON 模式{ version, installed: { [name]: { repo } } }唯一用途是浏览结果里的 Installed 徽章。SkillsMarketplaceService 的落地比计划更具体值得细看的常量skills-marketplace.ts#L17-L31常量值作用MAX_ARCHIVE_BYTES100 MiB仓库压缩包下载上限先查content-length流式读取时再兜底MAX_UNZIPPED_BYTES500 MiB解压字节预算防 zip-bomb在unzipWithLimit的 filter 中累加originalSize并中止MAX_PREVIEW_FILE_BYTES256 KiB预览时单文件内容读取上限超出只给大小不给内容ARCHIVE_CACHE_TTL_MS/ARCHIVE_CACHE_MAX_ENTRIES5 min / 4内存级 LRU 归档缓存命中刷新新鲜度SEARCH_TIMEOUT_MS10 sskills.sh 搜索接口超时POPULAR_SEED_QUERIES[code,git,review,test,docs,web]热门聚合的种子查询合并后按 installs 排序取 40 条缓存 30 min安装路径install()skills-marketplace.ts#L206-L234校验skillId目录名 → 目标存在且未传overwrite: true时抛错UI 借此确认将替换你的本地版本→ 从 codeload 拉取仓库 tarballsource必须匹配owner/repo模式、ref必须匹配 SHA/tag/无斜杠分支名模式防止改写 URL 路径→findSkillDirPrefix()在归档中定位含SKILL.md的最浅技能目录前缀 →collectSkillFiles()收集文件逐条isSafeRelativePath()拒绝 zip-slip、过滤被忽略路径、强制必须有SKILL.md→ 写入~/.claude/skills/skillId→ 更新installed.json。preview()复用 PR 1 的文件树 UI 展示全部文件在安装前并输出hasScripts标志——对应计划中的安全要求技能含scripts/时给出显式警告芯片安装的技能能执行代码——绝不盲装。验收链条搜索 → 预览所有文件可见→ 安装 → 出现在 Your skills 下且可编辑 → 浏览显示 Installed → 本地改过后重装先提示再覆盖。另有一项验收不新增 package.json 依赖bundle 体积不变。PR 6a — 云端团队技能读路径计划断言且经当前仓库核实云端已有完整的技能模型与 API无需新 Django 模型。需要说明的是文档写作时模型位于products/ai_observability/backend/models/skills.py在当前仓库中它已随产品拆分迁至 products/skills/backend/models/skills.py模型本体与文档描述一致并有所增强LLMSkill表llm_analytics_llmskillname≤64Agent Skills 规格要求、description列宽 4096新写入在序列化层按规格限 1024、bodySKILL.md markdown 正文、allowed_tools、metadata外加license/compatibility/category版本化沿用LLMPrompt模式——versionis_latestversion_description软删除deleted。约束即文档所述(team, name, version)在未删除行上唯一(team, name)在is_latestTrue行上唯一skills.py#L24-L38。LLMSkillFile伴随文件path/content/content_type(skill, path)唯一——多文件技能目录在云端原生受支持。当前还多了一个LLMSkillOwner以逻辑技能(team, skill_name)为键而非版本行保证编辑正文不会改变所有权。计划中 posthog 侧唯一的工作是 PR 6a 的标志位/权限滚动为 PostHog 使用场景放开LLM_ANALYTICS_SKILLS并确认桌面 OAuth token 携带llm_skill:read/llm_skill:write范围——不动模型、不动端点。桌面侧三层api-clientposthog-client.ts 中的listLlmSkills()约 L7047、getLlmSkillByName(name)约 L7074、createLlmSkill()约 L7124、publishLlmSkillVersion()约 L7156Zod schema 随行——正是计划所指照 MCP 安装方法的资源客户端模式。coreteamSkillsService.ts 的TeamSkillsService按放置规则注入 api-client拥有可用性决策listTeamSkills()在组织未启用该功能时返回{ available: false, skills: [] }——UI 的 Team 分组直接消失不报错。这正是 PR 6a 验收标准标志关闭时分组缺席且无任何错误的落点。UISkillsView 的 Team 分组新team来源只读详情视图经 PR 1 组件渲染bodyLLMSkillFiles相关文件见 TeamSkillsTab.tsx、TeamSkillDetailPanel.tsx。PR 6b — 团队技能发布 本地安装发布任何 user/repo 技能上的 Publish to team——workspace-server 把目录读出exportSkill()拆出 frontmatter、剥离正文、收集全部文本伴随文件二进制与超限文件跳过并报告于skippedTeamSkillsService.publishSkill()首发布走createLlmSkill再发布对当前 latest 调publishLlmSkillVersion并传base_version——版本管理从模型的version/is_latest免费获得再发布即版本号 1。计划中还隐含两条发布前校验已实现为硬性抛错无名不可发布、无 description 不可发布队友的 agent 依赖它。本地安装Team 技能上的 Install 把技能物化到~/.claude/skillsagent 需要磁盘文件此后同样遵循 copy-and-forget通过 Team 分组重装即升级覆盖前确认。源码中这一步比计划更讲究——installTeamSkill()skills.ts#L315-L381采用staging 目录 原子替换先mkdtemp落一份暂存写SKILL.md与全部伴随文件解析先行穿越路径会响亮失败小写skill.md变体不会覆盖正文被忽略条目落盘前剔除再把原目录 rename 备份、暂存目录 rename 就位、失败则回滚备份。注释点明动机坏载荷不得损坏或删除既有技能。云端侧还有一道传输完整性防线fetchSkillForInstall()比对body_total_length与实收body.length正文截断即拒绝安装而不是让半截说明静默覆盖已装技能。验收链条发布多文件技能 → 队友在 Team 分组看到 → 安装 → 磁盘目录完全一致 → 原作者编辑再发布 → 队友重装得到新版本。PR 7 — Codex 统一带上你的技能在任何地方使用读方向Codex → 标签页计划要求把~/.agents/skills加为发现来源并按名去重——跳过 bundled 技能saga 曾把副本同步到那里与已镜像的用户技能剩下的才是用户真正的 Codex 专属技能UI 提供Import动作把目录复制进~/.claude/skills此后即为普通可编辑用户技能。源码印证codex-mirror.ts#L21-L23 定义getCodexSkillsDir()~/.agents/skillsSkillsService.listSkills()末尾调用dedupeCodexSkills()skills.ts#L758-L779bundled 按 frontmatter 名匹配、镜像状态与用户自有技能按目录名匹配三张集合任一命中即从列表隐藏——与计划什么留下才是真 Codex 技能的去重规则一一对应importCodexSkill()的守卫很严格目标父目录必须精确等于codex 根且含SKILL.mdAccess denied: not a Codex skill directory目标已存在且未overwrite时抛错提示将替换你的本地版本复制用cp(recursive, dereference)原 Codex 副本原封不动。写方向你的技能 → Codex原计划是扩展 bundled→Codex 同步、把~/.claude/skills单向镜像进~/.agents/skills并附安全规则永不覆盖我们没放进去的技能用小型状态文件记录镜像名冲突时跳过并在标签页展示冲突。从当前源码看这一方向经历了设计演进codex-mirror.ts#L14-L19 的注释说明现在的取向是PostHog 从不向该目录写技能——只读它bundled 与 Claude 技能通过私有CODEX_HOME进入 Codex 会话让~/.agents/skills保持用户自有。为此还实现了cleanupLegacyCodexMirror()一次性清掉早期版本拷贝进去的遗留副本且只删除能证明是自己写的镜像状态文件记录的名称bundled 残留需同名 SKILL.md逐字节相同双证据——计划中的安全规则在清理逻辑中得到了最严格的表达。readCodexMirrorState()读取.posthog-mirror.json时对每个名称再过一遍isSafePathSegment()确保状态文件里的值永远无法把path.join扩到兄弟或父目录。验收链条Codex 里写的技能出现在 Codex 分组 → Import 后在 Your skills 下可编辑 → 同步机制把导入副本带回而不错杀、不重复标签页里新建的用户技能在下次同步后对 Codex 可见。六、安全模型三层防线的统一视角计划文档的 Security notes 与源码共同构成一条清晰的安全线核心判断是市场安装是代码执行不是内容下载scripts/会被 agent 执行、SKILL.md内容注入 agent 上下文。由此推出安装前全文件预览PR 5 的preview() 含scripts/时的警告芯片 全计划无静默/自动安装或自动更新读写端点分设守卫且都有含../穿越在内的直接单测写端点resolveWritableSkillDir()对照可写根逐条验证PR 2读端点resolveKnownSkillDir()对照发现目录验证PR 1readSkillFile()再加realpath符号链接逃逸检查技能内部相对路径resolveSkillFilePath()拒绝越界与反斜杠assertVisibleSkillFilePath()拒绝文件树不可见的位置归档侧zip-slip 逐段校验、REPO_SOURCE_PATTERN/GIT_REF_PATTERN防 URL 改写、下载与解压双重字节预算状态文件侧isSafePathSegment()skill-discovery.ts#L35-L44确保来自 JSON 状态的值无法加宽path.join团队技能继承 PostHog 既有访问控制AccessControlPermission、API 范围桌面侧零自定义权限逻辑。七、测试策略与发布节奏计划给出的测试策略与仓库中已存在的测试文件对应单元Vitest与源码同目录frontmatter 往返write-skill-frontmatter.test.ts、路径守卫与 skills.test.ts、skill-discovery.test.ts、市场服务的 skills-marketplace.test.ts、团队技能纯决策的 teamSkillsService.test.ts 等依赖一律 fake 注入E2EPlaywright每个 PR 一条流——渲染多文件技能创建/编辑/删除外部编辑触发的实时刷新市场 搜索→预览→安装团队 发布→安装Codex 导入边界检查posthog/shared/posthog/platform变更后重建/typecheck distcore变更后biome lint packages/core且noRestrictedImports零违规。发布节奏Rollout order阶段PR成果11, 2, 3标签页成为真正的技能管理器看全所有文件、CRUD、实时24, 5质量信号 社区市场36a, 6b, 7团队共享 Codex 统一其中 PR 1–4 无外部依赖、可立即开工PR 5 只依赖 skills.sh 的公开索引PR 6 需要 posthog 侧标志位/权限 PR6a 启动时提交PR 7 无外部依赖。八、延伸阅读关键文件索引实现计划全文products/desktop/docs/plans/skills-tab.md技能核心服务与守卫skills.ts技能发现、市场插件路径解析skill-discovery.tsfrontmatter 解析/序列化parse-skill-frontmatter.ts、write-skill-frontmatter.tstRPC 路由全部一行转发skills.router.ts市场服务搜索/预览/安装/缓存skills-marketplace.tsCodex 目录与镜像状态codex-mirror.ts官方技能管道计划声明不动的部分update-skills-saga.ts云端模型LLMSkill/LLMSkillFile/LLMSkillOwnerproducts/skills/backend/models/skills.py团队技能纯决策层teamSkillsService.tsUI 入口与 Team 分组SkillsView.tsx、TeamSkillsTab.tsx适用前提本文所述实现对应计划文档2026-06-11 更新之后的仓库状态文中计划要点与源码印证分属两个证据层级——计划描述意图含 PR 4 的analyzeSkills纯函数等尚未逐条对应到独立文件的条目源码部分均给出可核对的路径与行为。云端技能 API 依赖组织侧LLM_ANALYTICS_SKILLS能力开启未开启时 Team 分组按设计缺席而非报错。【免费下载链接】posthog:hedgehog: PostHog is the leading platform for building self-driving products. Our developer tools – AI observability, analytics, session replay, flags, experiments, error tracking, logs, and more – capture all the context agents need to diagnose problems, uncover opportunities, and ship fixes. Steer it all from Slack, web, desktop, or the MCP.项目地址: https://gitcode.com/GitHub_Trending/po/posthog创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考