skills.ts 2.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748
  1. // Codex 技能的展示 ID 与按 Profile 选择(与 Claude 侧的 providers/claude/skills.ts 对应)
  2. import fs from 'node:fs'
  3. import path from 'node:path'
  4. import { COMMUNITY_SKILL_PLUGIN, CUSTOM_SKILL_PLUGIN } from '@agent-server/shared'
  5. import type { SkillMetadata } from './protocol/v2'
  6. import { NO_CODEX_PLUGINS, type CodexPluginGrant } from '../types'
  7. /** 服务场景下始终禁用的系统技能:会联网下载或写入 Codex 自身目录 */
  8. export const ALWAYS_DISABLED = new Set(['skill-installer', 'skill-creator', 'plugin-creator'])
  9. /**
  10. * 展示 ID:
  11. * - 系统技能 → codex-system:<name>
  12. * - 从 openai/skills 安装的技能(CODEX_HOME/skills 下有标记文件)→ openai-skills:<name>
  13. * - 本地技能目录与第三方技能目录(extraRoots,Codex 按 plugin.json 加了前缀)→ 原样,如 agent-skills:report-conventions
  14. * - 其他用户技能 → codex-user:<name>
  15. */
  16. export function skillDisplayId(s: Pick<SkillMetadata, 'name' | 'scope' | 'path'>, codexHome: string): string {
  17. if (s.scope === 'system') return `codex-system:${s.name}`
  18. // Codex 用 SKILL.md 的 name 命名(可能是 "Excel Analysis"),Claude 用目录名;第三方技能统一按目录名,两个引擎 ID 一致
  19. for (const plugin of [COMMUNITY_SKILL_PLUGIN, CUSTOM_SKILL_PLUGIN]) {
  20. if (s.name.startsWith(`${plugin}:`)) return `${plugin}:${path.basename(path.dirname(s.path))}`
  21. }
  22. if (s.name.includes(':')) return s.name
  23. const managedRoot = path.join(codexHome, 'skills')
  24. const dir = path.dirname(s.path)
  25. if (path.dirname(dir) === managedRoot && fs.existsSync(path.join(dir, '.agent-server.json'))) return `openai-skills:${s.name}`
  26. return `codex-user:${s.name}`
  27. }
  28. /**
  29. * 计算需要在本次对话中禁用的技能(按路径选择);选择"全部技能"时第三方技能仍需单独勾选。
  30. * 远程插件带来的技能(pluginId 非空)跟随插件授权:全部开放时(Playground)全部保留,否则只保留开放的插件的技能
  31. */
  32. export function skillsToDisable(
  33. all: SkillMetadata[], selected: string[] | 'all', codexHome: string, localPlugin: string, plugins: CodexPluginGrant = NO_CODEX_PLUGINS, excludedFromAll: string[] = [],
  34. ): { path: string; enabled: false }[] {
  35. const excluded = new Set(excludedFromAll)
  36. const wanted = selected === 'all' ? null : new Set(selected.map(s => (s.includes(':') ? s : `${localPlugin}:${s}`)))
  37. return all
  38. .filter(s => {
  39. if (ALWAYS_DISABLED.has(s.name)) return true
  40. if (s.pluginId) return plugins !== 'all' && !plugins.pluginIds.includes(s.pluginId)
  41. const id = skillDisplayId(s, codexHome)
  42. return wanted === null ? id.startsWith(`${COMMUNITY_SKILL_PLUGIN}:`) || excluded.has(id) : !wanted.has(id)
  43. })
  44. .map(s => ({ path: s.path, enabled: false as const }))
  45. }