GitHub Copilot 接入 Claude Code 本地技能的自动化映射方案
引言
你是否遇到过这种割裂的体验:在终端里,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 拿到这份菜单,就能根据你的需求点菜了。
核心实现:自动化扫描脚本
为了实现这一目标,我编写了一个名为 scan-and-generate.mjs 的自动化脚本。它的核心职责是:遍历目录 -> 提取元数据 -> 生成 Markdown。
1. 灵活的配置策略
脚本的设计必须足够通用,以支持 Skills、Agents、Commands 以及插件(Plugins)中的各种资源。我们在代码中定义了一个强大的 CONFIG 对象:
const CONFIG = {
mappings: [
{
id: "skills",
name: "Local Skills",
outputFile: "skills-mapping.md",
sourceDir: ROOT_DIR + "/skills/",
sourcePattern: "*/SKILL.md",
frontmatterFields: ["description", "name"],
groupBy: (file) => {
const relative = file.replace(ROOT_DIR + "/skills/", "");
const skillName = relative.split("/")[0];
return getSkillCategory(skillName);
},
},
// ... 其他映射配置(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));
const ROOT_DIR = process.env.HOME + "/.claude";
const OUTPUT_DIR = join(__dirname, "output");
const TEMPLATES_DIR = join(__dirname, "templates");
// 确保输出目录存在
if (!existsSync(OUTPUT_DIR)) {
mkdirSync(OUTPUT_DIR, { recursive: true });
}
// 扫描配置
const CONFIG = {
mappings: [
{
id: "commands",
name: "Local Commands",
outputFile: "commands-mapping.md",
: + ,
: ,
: [],
: [, , ],
: {
relative = file.( + , );
parts = relative.();
parts. > ? (parts[]) : ;
},
: {
relative = file
.( + , )
.(, )
.(, );
relative ? + relative : ;
},
: {
frontmatter. || title || (file, );
},
},
{
: ,
: ,
: ,
: + ,
: [, ],
: [],
: [, ],
: {
match = file.();
(match) {
(match[], match[]);
}
;
},
: {
cmdMatch = file.();
cmdName = cmdMatch ? cmdMatch[] : (file, );
orgMatch = file.();
org = orgMatch ? orgMatch[] : ;
+ (org ? org.(, ) + : ) + cmdName;
},
: {
match = file.();
frontmatter. || match?.[] || (file, );
},
},
{
: ,
: ,
: ,
: + ,
: [, ],
: [],
: [, ],
: {
match = file.();
(match) {
(match[], match[]);
}
;
},
: {
relative = file.()[];
(!relative) ;
parts = relative.();
parts. > ? (parts[]) : ;
},
: ,
: {
frontmatter. || title || (file, );
},
},
{
: ,
: ,
: ,
: + ,
: ,
: [, ],
: [, ],
: {
match = file.();
(match) {
(match[], match[]);
}
;
},
: ,
: {
frontmatter. || title || (file, );
},
},
{
: ,
: ,
: ,
: + ,
: ,
: [],
: [, ],
: {
relative = file.( + , );
skillName = relative.()[];
(skillName);
},
: ,
: {
frontmatter. || title || (file, );
},
},
{
: ,
: ,
: ,
: + ,
: ,
: [],
: [, ],
: {
relative = file.( + , );
parts = relative.();
parts. > ? (parts[]) : ;
},
: ,
: {
frontmatter. || title || (file, );
},
},
],
};
() {
displayNames = {
: ,
nyldn-: ,
: ,
: ,
};
orgDisplay = displayNames[org] || org;
pluginDisplay = plugin === org ? : ;
orgDisplay + pluginDisplay;
}
() {
(!str) ;
str.().() + str.();
}
() {
categories = {
: ,
: ,
: ,
: ,
: ,
: ,
: ,
: ,
: ,
: ,
: ,
: ,
: ,
: ,
: ,
: ,
: ,
: ,
: ,
: ,
: ,
};
categories[skillName] || ;
}
() {
result = {};
match = content.();
(!match) result;
yamlContent = match[];
lines = yamlContent.();
currentKey = ;
multilineValue = [];
isMultiline = ;
( i = ; i < lines.; i++) {
line = lines[i];
colonIndex = line.();
(colonIndex > && !line.() && !line.()) {
(isMultiline && currentKey) {
result[currentKey] = multilineValue.().();
multilineValue = [];
isMultiline = ;
}
key = line.(, colonIndex).();
value = line.(colonIndex + ).();
(value === || value === ) {
currentKey = key;
isMultiline = ;
multilineValue = [];
} {
currentKey = key;
result[key] = value.(, ).(, );
}
} (isMultiline && line.()) {
multilineValue.(line.());
}
}
(isMultiline && currentKey) {
result[currentKey] = multilineValue.().();
}
result;
}
() {
lines = content.();
( line lines) {
(line.()) {
line.(, ).();
}
}
;
}
() {
patterns = .(pattern) ? pattern : [pattern];
files = [];
( p patterns) {
files = files.((p, { : exclude }));
}
[... (files.( !(f).()))];
}
() {
(versions. === ) ;
semverVersions = [];
nonSemverVersions = [];
( v versions) {
(.(v)) {
semverVersions.(v);
} {
nonSemverVersions.(v);
}
}
(semverVersions. > ) {
= () => {
= () => {
parts = v.()[].().();
preRelease = v.()[] || ;
{ : parts[] || , : parts[] || , : parts[] || , preRelease };
};
pa = (a);
pb = (b);
(pa. !== pb.) pb. - pa.;
(pa. !== pb.) pb. - pa.;
(pa. !== pb.) pb. - pa.;
(!pa. && pb.) -;
(pa. && !pb.) ;
;
};
semverVersions.(compareSemver)[];
}
nonSemverVersions.().();
}
() {
(!(pluginPath)) [];
entries = (pluginPath, { : });
entries
.( entry.())
.( entry.);
}
() {
(files. === ) files;
pluginVersions = ();
( file files) {
match = file.();
(match) {
org = match[];
plugin = match[];
version = match[];
key = ;
(!pluginVersions.(key)) {
pluginVersions.(key, version);
} {
currentLatest = pluginVersions.(key);
candidateVersions = [currentLatest, version];
latest = (candidateVersions);
pluginVersions.(key, latest);
}
}
}
latestFiles = ();
sourceDir = + ;
( [key, version] pluginVersions) {
[org, plugin] = key.();
versionPath = (sourceDir, org, plugin, version);
((versionPath)) {
( pattern patterns) {
fullPattern = (versionPath, pattern);
matched = (fullPattern, { : exclude });
matched.( latestFiles.(f));
}
}
}
[...latestFiles];
}
() {
groups = {};
( file files) {
category = (file);
(!groups[category]) {
groups[category] = subgroupBy ? {} : [];
}
(subgroupBy) {
subcategory = (file);
(!groups[category][subcategory]) {
groups[category][subcategory] = [];
}
groups[category][subcategory].(file);
} {
groups[category].(file);
}
}
groups;
}
() {
templateFiles = {
: ,
: ,
: ,
: ,
: ,
: ,
};
templateFile = templateFiles[mappingId];
(!templateFile) ;
templatePath = (, templateFile);
(!(templatePath)) {
.();
;
}
{
(templatePath, );
} (error) {
.(, error);
;
}
}
() {
md = ;
md += ;
md += ;
usageInstructions = (mapping.);
(usageInstructions) {
md += usageInstructions;
}
md += ;
categories = .(groups).();
totalCount = ;
( category categories) {
group = groups[category];
md += ;
(mapping. && group === ) {
subcategories = .(group).();
( subcategory subcategories) {
files = group[subcategory];
md += ;
md += (mapping, files, totalCount);
totalCount += files.;
}
} {
files = .(group) ? group : group[category] || [];
md += (mapping, files, totalCount);
totalCount += files.;
}
}
md += ;
md += ;
md;
}
() {
(files. === ) ;
md = ;
hasShortcut = mapping. !== ;
(hasShortcut) {
md += ;
md += ;
} {
md += ;
md += ;
}
( file files.()) {
md += (mapping, file) + ;
}
md += ;
md;
}
() {
content = (filePath, );
frontmatter = (content);
title = (content);
shortPath = filePath.(process.., );
name = mapping.(frontmatter, title, filePath);
shortcut = mapping. ? mapping.(filePath, frontmatter) : ;
description = frontmatter. || ;
(description.() && description.()) {
description = description.(, -);
}
(shortcut) {
;
}
;
}
() {
.();
( mapping .) {
.();
patterns = .(mapping.) ? mapping. : [mapping.];
files = [];
( pattern patterns) {
fullPattern = (mapping., pattern);
files = files.((fullPattern, { : mapping. }));
}
files = [... (files.( !(f).()))];
(mapping..()) {
files = (files, patterns, mapping.);
}
(files. === ) {
.();
;
}
groups = (files, mapping., mapping.);
markdown = (mapping, groups);
outputPath = (, mapping.);
(outputPath, markdown, );
totalItems = .(groups).( {
(mapping. && group === ) {
sum + .(group).( s + f., );
}
sum + (.(group) ? group. : );
}, );
.();
}
.();
}
();
2. Agent Template 格式
## Agent 使用流程
当用户输入命令时,按以下步骤执行:
1. **解析命令快捷方式**
- 顶层命令:直接查找表格(格式:`/command`)
- 嵌套命令:解析 category:command 格式(格式:`/category:command`)
2. **查找映射表**
- 在对应分类表格中查找快捷方式列
- 获取完整路径字段
3. **读取命令文件**
- 使用 Read 工具读取完整路径对应的 .md 文件
- 解析 frontmatter 获取 allowed-tools 和其他元数据
4. **执行命令**
- 按照命令文件中的指令执行
- 严格使用 frontmatter 中指定的 allowed-tools
- 如果未指定 allowed-tools,使用默认工具集
实战演练:在 Copilot 中调用 tech-blog
万事俱备,现在的核心问题是:体验如何?
假设你已经运行了脚本,生成了 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 的最强外脑吧。


