目标读者:希望在 VSCode/Zed 编辑器中直接复用 Claude Code 强大本地能力的开发者、DevOps 工程师、AI 工具流搭建者。
核心价值:通过自动化映射机制,打破 Claude Code CLI 与编辑器 Copilot 之间的"生殖隔离",实现一套 Skills/Agents 双端复用。
阅读时间:8 分钟
你是否遇到过这种割裂的体验:在终端里,Claude Code 配置了强大的 tech-blog 技能,能一键生成高质量博客;配置了 code-review Agent,能深度审查代码。但在 VSCode 编辑器里,面对 GitHub Copilot,你却只能用最基础的自然语言对话,Copilot 对你精心调教的那些本地技能一无所知。
这就像是你拥有一本绝世武功秘籍(Claude Code Skills),但你的随身保镖(Copilot)却是个只会打直拳的门外汉。
如果我们能建立一种机制,自动将 Claude Code 的所有能力"注册"给 Copilot,会发生什么?
本文将深度解析如何通过一个 Node.js 扫描脚本,自动生成"能力映射表",让 Copilot 瞬间"读取"并掌握你所有的本地 Skills、Agents 和 Commands。
Claude Code 的核心优势在于其高度可定制的 Local Skills(本地技能)和 Agents(智能体)。这些定义通常以 Markdown 文件的形式存储在 ~/.claude/skills 或 ~/.claude/agents 目录中。
然而,GitHub Copilot 运行在编辑器的上下文中,它无法直接通过系统路径去"扫描"和"理解"这些散落在文件系统中的技能定义。
我们需要一个"中间层"——Mapping Files(映射文件)。
这就像是给 Copilot 准备的一份"技能菜单"。菜单上不仅列出了有什么菜(Skill Name),还写明了这道菜是什么味道(Description),以及大厨在哪里(File Path)。Copilot 拿到这份菜单,就能根据你的需求点菜了。
为了实现这一目标,我 Vibe 了一个名为 scan-and-generate.mjs 的自动化脚本。它的核心职责是:遍历目录 -> 提取元数据 -> 生成 Markdown。
1. 灵活的配置策略
脚本的设计必须足够通用,以支持 Skills、Agents、Commands 以及插件(Plugins)中的各种资源。我们在代码中定义了一个强大的 CONFIG 对象:
constCONFIG=,},// ... 其他映射配置(Agents, Commands等)],};
这段配置定义了"去哪找"(sourceDir)、“找什么”(sourcePattern)以及"怎么展示"(groupBy)。特别是 frontmatterFields,它直接从 Markdown 的头部元数据(Frontmatter)中提取技能描述,这是 Copilot 理解技能用途的关键。
2. 智能提取与分组
脚本不仅是简单的列表生成,还包含了智能的分类逻辑。例如,getSkillCategory 函数维护了一个映射表,将杂乱的技能归类为 “Content & Writing”、“Development”、“Project Management” 等类别。
// 示例:将技能映射到类别const categories ={"tech-blog":"Content & Writing","code-review":"Code Analysis",zustand:"Development",// ...};
这种结构化的输出对于 LLM(大语言模型)非常友好。当 Copilot 阅读这份文档时,它能建立起结构化的认知:“哦,如果用户要写文章,我应该去 Content & Writing 分类下找找。”
3. 生成 Copilot 可读的指令
仅仅列出文件是不够的,我们还需要告诉 Copilot 如何使用 这些技能。脚本会读取 templates 目录下的指令模版,并将其嵌入到生成的 Markdown 头部。
以 skills-mapping.md 为例,生成的头部包含这样的指令:
“当用户激活本地技能时… 1. 识别技能引用… 3. 使用 Read 工具读取 SKILL.md 文件… 4. 将技能规则应用到当前会话…”
这相当于给 Copilot 植入了一段"元指令"(Meta-Prompt),教它如何加载和执行外部技能。
4. 完整实现
1. scan-and-generate.mjs
#!/usr/bin/env node/ * Scan and Generate Mapping Documents * 扫描 ~/.claude/ 目录并生成映射文档 */import{ readFileSync, writeFileSync, mkdirSync, existsSync, lstatSync, readdirSync,}from"fs";import{ globSync }from"glob";import{ dirname, basename, join }from"path";import{ fileURLToPath }from"url";const __dirname =dirname(fileURLToPath(import.meta.url));constROOT_DIR= process.env.HOME+"/.claude";constOUTPUT_DIR=join(__dirname,"output");constTEMPLATES_DIR=join(__dirname,"templates");// 确保输出目录存在if(!existsSync(OUTPUT_DIR)){mkdirSync(OUTPUT_DIR,{recursive:true});}// 扫描配置constCONFIG={mappings:[{id:"commands",name:"Local Commands",outputFile:"commands-mapping.md",sourceDir:ROOT_DIR+"/commands/",sourcePattern:"/*.md",exclude:["CLAUDE.md"],frontmatterFields:["description","argument-hint","allowed-tools"],groupBy:(file)=>{const relative = file.replace(ROOT_DIR+"/commands/","");const parts = relative.split("/");return parts.length >1?capitalize(parts[0]):"General";},getShortcut:(file, frontmatter)=>{const relative = file .replace(ROOT_DIR+"/commands/","").replace(".md","").replace(///g,":");return relative ?"/"+ relative :"/";},getName:(frontmatter, title, file)=>{return frontmatter.name || title ||basename(file,".md");},},return"Unknown";},getShortcut:(file, frontmatter)=>{const cmdMatch = file.match(/[/.](claude/)commands/([^/]+).md$/);const cmdName = cmdMatch ? cmdMatch[2]:basename(file,".md");const orgMatch = file.match(/plugins/cache/([^/]+)//);const org = orgMatch ? orgMatch[1]:"";return"/"+(org ? org.replace("-plugins","")+":":"")+ cmdName;},getName:(frontmatter, title, file)=>{const match = file.match(/[/.](claude/)commands/([^/]+).md$/);return frontmatter.name || match?.[2]||basename(file,".md");},},return"Unknown";},subgroupBy:(file)=>,getShortcut:null,getName:(frontmatter, title, file)=>{return frontmatter.name || title ||basename(file,".md");},},return"Unknown";},getShortcut:null,getName:(frontmatter, title, file)=>{return frontmatter.name || title ||basename(file,".md");},},,getShortcut:null,getName:(frontmatter, title, file)=>{return frontmatter.name || title ||basename(file,".md");},},{id:"agents",name:"Local Agents",outputFile:"agents-mapping.md",sourceDir:ROOT_DIR+"/agents/",sourcePattern:"/*.md",exclude:["CLAUDE.md"],frontmatterFields:["description","name"],groupBy:(file)=>{const relative = file.replace(ROOT_DIR+"/agents/","");const parts = relative.split("/");return parts.length >1?capitalize(parts[0]):"General";},getShortcut:null,getName:(frontmatter, title, file)=>{return frontmatter.name || title ||basename(file,".md");},},],};// 工具函数functionformatPluginName(org, plugin){const displayNames ={thedotmack:"Claude Mem","nyldn-plugins":"Claude Octopus","claude-plugins-official":"Official Plugins","planning-with-files":"Planning With Files",};const orgDisplay = displayNames[org]|| org;const pluginDisplay = plugin === org ?"":` (${org})`;return orgDisplay + pluginDisplay;}functioncapitalize(str)functiongetSkillCategory(skillName);return categories[skillName]||"Other";}functionparseFrontmatter(content){const result ={};const match = content.match(/^---
([sS]?) —/);if(!match)return result;const yamlContent = match[1];const lines = yamlContent.split(” “);let currentKey =null;let multilineValue =[];let isMultiline =false;for(let i =0; i < lines.length; i++)const key = line.slice(0, colonIndex).trim();const value = line.slice(colonIndex +1).trim();// 检查是否是多行格式 (> 或 |)if(value ===”>”|| value ===“|”){ currentKey = key; isMultiline =true; multilineValue =[];}else{// 单行值 currentKey = key; result[key]= value.replace(/^[|]$/g,“”).replace(/,s/g,“, “);}}elseif(isMultiline && line.trim()){// 多行值的内容行(忽略空行) multilineValue.push(line.trim());}}// 保存最后一个多行值if(isMultiline && currentKey){ result[currentKey]= multilineValue.join(” “).trim();}return result;}functiongetTitle(content)}returnnull;}functionscanFiles(pattern, exclude =[]){const patterns = Array.isArray(pattern)? pattern :[pattern];let files =[];for(const p of patterns){ files = files.concat(globSync(p,{ignore: exclude }));}return[…newSet(files.filter((f)=>!lstatSync(f).isDirectory()))];}// 获取插件目录下的最新版本号functiongetLatestVersion(versions)else{ nonSemverVersions.push(v);}}// 如果有语义版本,使用 semver 比较if(semverVersions.length >0){// 简单的 semver 比较函数constcompareSemver=(a, b)=>{constparse=(v)=>{const parts = v.split(”-“)[0].split(”.“).map(Number);const preRelease = v.split(”-“)[1]||”“;return{major: parts[0]||0,minor: parts[1]||0,patch: parts[2]||0, preRelease,};};const pa =parse(a);const pb =parse(b);if(pa.major !== pb.major)return pb.major - pa.major;if(pa.minor !== pb.minor)return pb.minor - pa.minor;if(pa.patch !== pb.patch)return pb.patch - pa.patch;// 处理 pre-release: 正式版 > pre-releaseif(!pa.preRelease && pb.preRelease)return-1;if(pa.preRelease &&!pb.preRelease)return1;return0;};return semverVersions.sort(compareSemver)[0];}// 否则使用字母序最后一个return nonSemverVersions.sort().pop();}// 获取插件目录下所有版本目录functiongetPluginVersions(pluginPath));return entries .filter((entry)=> entry.isDirectory()).map((entry)=> entry.name);}// 过滤文件,只保留每个插件最新版本的内容functionfilterLatestVersionFiles(files, patterns, exclude =[])/{plugin}/{version}/…const pluginVersions =newMap();// key: “org/plugin”, value: versionfor(const file of files)/\({plugin}`;if(!pluginVersions.has(key))else}}// 重新扫描获取最新版本的实际文件const latestFiles =newSet();const sourceDir =ROOT_DIR+"/plugins/cache/";for(const[key, version]of pluginVersions)); matched.forEach((f)=> latestFiles.add(f));}}}return[...latestFiles];}functiongroupFiles(files, groupBy, subgroupBy =null){const groups ={};for(const file of files):[];}if(subgroupBy) groups[category][subcategory].push(file);}else{ groups[category].push(file);}}return groups;}functiongetAgentUsageInstructions(mappingId){// 模板文件映射const templateFiles ={commands:"commands-usage.md","plugins-commands":"plugins-commands-usage.md","plugins-agents":"plugins-agents-usage.md","plugins-skills":"plugins-skills-usage.md",skills:"skills-usage.md",agents:"agents-usage.md",};const templateFile = templateFiles[mappingId];if(!templateFile){return"";}const templatePath =join(TEMPLATES_DIR, templateFile);if(!existsSync(templatePath)){ console.warn(`Warning: Template file not found: \){templatePath});return"";}try{returnreadFileSync(templatePath,"utf-8");}catch(error){ console.warn(Warning: Failed to read template file: ${templatePath}, error,);return"";}}functiongenerateMarkdown(mapping, groups){let md =— version: 1.0
lastUpdated: ${newDate().toISOString().split(“T”)[0]}
; md +=# ${mapping.name} 映射表
; md +=本文件从 ${mapping.sourceDir} 目录自动扫描生成。
;// 添加 Agent 使用流程说明const usageInstructions =getAgentUsageInstructions(mapping.id);if(usageInstructions){ md += usageInstructions;} md +=—
;const categories = Object.keys(groups).sort();let totalCount =0;for(const category of categories){const group = groups[category]; md += ${category}
;if(mapping.subgroupBy &&typeof group ==="object"){const subcategories = Object.keys(group).sort();for(const subcategory of subcategories){const files = group[subcategory]; md += ${subcategory}
; md +=generateTable(mapping, files, totalCount); totalCount += files.length;}}else{const files = Array.isArray(group)? group : group[category]||[]; md +=generateTable(mapping, files, totalCount); totalCount += files.length;}} md +=—
; md +=最后更新:${newDate().toLocaleDateString(“zh-CN”)} ;return md;}functiongenerateTable(mapping, files, startIndex)else{ md +=| 名称 | 描述 | 完整路径 | ; md +=|——|——|———-| ;}for(const file of files.sort()){ md +=generateRow(mapping, file)+" ";} md +=" ";return md;}functiongenerateRow(mapping, filePath)if(shortcut){return| \({shortcut} | \){name} | \({description} | `\){shortPath}|;}return| ${name} | ${description} |\({shortPath}` |`;}functionrun(){ console.log("Scanning and generating mapping documents... ");for(const mapping ofCONFIG.mappings){ console.log(` Processing: \){mapping.name}…);const patterns = Array.isArray(mapping.sourcePattern)? mapping.sourcePattern :[mapping.sourcePattern];let files =[];for(const pattern of patterns){// Prepend sourceDir to all patternsconst fullPattern =join(mapping.sourceDir, pattern); files = files.concat(globSync(fullPattern,{ignore: mapping.exclude }));} files =[...newSet(files.filter((f)=>!lstatSync(f).isDirectory()))];// 插件 mapping 需要过滤只保留最新版本if(mapping.id.startsWith("plugins-")){ files =filterLatestVersionFiles(files, patterns, mapping.exclude);}if(files.length ===0){ console.log( Warning: No files found for \({mapping.name}`);continue;}const groups =groupFiles(files, mapping.groupBy, mapping.subgroupBy);const markdown =generateMarkdown(mapping, groups);const outputPath =join(OUTPUT_DIR, mapping.outputFile);writeFileSync(outputPath, markdown,"utf-8");const totalItems = Object.values(groups).reduce((sum, group)=>return sum +(Array.isArray(group)? group.length :0);},0); console.log(` Generated: \){mapping.outputFile} (${totalItems} items));} console.log(" All mapping documents generated successfully!");}run();
2. Agent Template 格式
Agent 使用流程 当用户输入命令时,按以下步骤执行: 1. 解析命令快捷方式 - 顶层命令:直接查找表格(格式:/command) - 嵌套命令:解析 category:command 格式(格式:/category:command`) 2. 查找映射表 - 在对应分类表格中查找快捷方式列 - 获取完整路径字段 3. 读取命令文件 - 使用 Read 工具读取完整路径对应的 .md 文件 - 解析 frontmatter 获取 allowed-tools 和其他元数据 4. 执行命令 - 按照命令文件中的指令执行 - 严格使用 frontmatter 中指定的 allowed-tools - 如果未指定 allowed-tools,使用默认工具集 —
万事俱备,现在的核心问题是:体验如何?
假设你已经运行了脚本,生成了 skills-mapping.md。
- 加载上下文:在 VSCode Copilot Chat 中,通过
@workspace或直接打开skills-mapping.md文件,让 Copilot 读取这个文件。 - 下达指令:输入 “我想写一篇关于 Zustand 状态管理的博客,使用 tech-blog 技能的深度风格。”
- Copilot 的思考链:
- 扫描
skills-mapping.md。 - 发现
tech-blog条目,描述匹配 “技术博客文章创作工具”。 - 获取路径
~/.claude/skills/tech-blog/SKILL.md。 - (关键一步) Copilot 会读取该路径下的文件内容(前提是你允许它读取,或者你将内容复制到了 Context 中)。
- Copilot 学习到
tech-blog的 Prompt 规则(如 3W 框架、金句要求)。 - 执行输出:Copilot 按照
tech-blog的深度版风格,生成了一篇结构完美的文章。
- 扫描
这通过一次简单的映射,打破了工具间的壁垒。 你在 Claude Code 里沉淀的每一次 Prompt 优化、每一个 Agent 调教,现在都能无缝同步给编辑器里的 Copilot。
“工欲善其事,必先利其器”。但在 AI 时代,我们面临的问题往往不是器不够利,而是”器”太多且互不相通。
通过 scan-and-generate.mjs 这样一个小巧的胶水脚本,我们不仅仅是生成了一份文档,更是建立了一座桥梁。它连接了 CLI 的灵活性与 IDE 的便捷性,连接了系统级的能力与编辑器级的交互。
现在,去运行你的扫描脚本,把你的 Claude Code 变成 Copilot 的最强外脑吧。
- Claude Code 官方文档
- VSCode GitHub Copilot 扩展指南
- 项目源码:
@mappings/scan-and-generate.mjs
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容,请联系我们,一经查实,本站将立刻删除。
如需转载请保留出处:https://51itzy.com/kjqy/257044.html