// Codex 技能的展示 ID 与按 Profile 选择(与 Claude 侧的 providers/claude/skills.ts 对应) import fs from 'node:fs' import path from 'node:path' import { COMMUNITY_SKILL_PLUGIN } from '@agent-server/shared' import type { SkillMetadata } from './protocol/v2' /** 服务场景下始终禁用的系统技能:会联网下载或写入 Codex 自身目录 */ export const ALWAYS_DISABLED = new Set(['skill-installer', 'skill-creator', 'plugin-creator']) /** * 展示 ID: * - 系统技能 → codex-system: * - 从 openai/skills 安装的技能(CODEX_HOME/skills 下有标记文件)→ openai-skills: * - 本地技能目录与第三方技能目录(extraRoots,Codex 按 plugin.json 加了前缀)→ 原样,如 agent-skills:report-conventions * - 其他用户技能 → codex-user: */ export function skillDisplayId(s: Pick, codexHome: string): string { if (s.scope === 'system') return `codex-system:${s.name}` // Codex 用 SKILL.md 的 name 命名(可能是 "Excel Analysis"),Claude 用目录名;第三方技能统一按目录名,两个引擎 ID 一致 if (s.name.startsWith(`${COMMUNITY_SKILL_PLUGIN}:`)) return `${COMMUNITY_SKILL_PLUGIN}:${path.basename(path.dirname(s.path))}` if (s.name.includes(':')) return s.name const managedRoot = path.join(codexHome, 'skills') const dir = path.dirname(s.path) if (path.dirname(dir) === managedRoot && fs.existsSync(path.join(dir, '.agent-server.json'))) return `openai-skills:${s.name}` return `codex-user:${s.name}` } /** 计算需要在本次对话中禁用的技能(按路径选择);选择"全部技能"时第三方技能仍需单独勾选 */ export function skillsToDisable(all: SkillMetadata[], selected: string[] | 'all', codexHome: string, localPlugin: string): { path: string; enabled: false }[] { const wanted = selected === 'all' ? null : new Set(selected.map(s => (s.includes(':') ? s : `${localPlugin}:${s}`))) return all .filter(s => { if (ALWAYS_DISABLED.has(s.name)) return true const id = skillDisplayId(s, codexHome) return wanted === null ? id.startsWith(`${COMMUNITY_SKILL_PLUGIN}:`) : !wanted.has(id) }) .map(s => ({ path: s.path, enabled: false as const })) }