5e32375f0e
架构变更: - 采用 pnpm workspaces 实现 Monorepo 结构 - 将现有代码迁移到 packages/core - 新增 packages/server HTTP 服务层 Server 功能: - REST API: 会话管理、工具管理、配置管理 - WebSocket: 实时双向通信支持 - SSE: 服务端事件推送 - Hono + Bun 作为运行时 API 端点: - GET/POST /api/sessions - 会话 CRUD - GET/POST /api/sessions/:id/messages - 消息管理 - GET /api/sessions/:id/events - SSE 事件流 - WS /api/ws/:sessionId - WebSocket 连接 - GET/POST /api/tools - 工具管理 - GET/PUT /api/config - 配置管理
359 lines
8.2 KiB
TypeScript
359 lines
8.2 KiB
TypeScript
/**
|
|
* Skill 注册表
|
|
*
|
|
* 管理所有可用的 Skills,支持:
|
|
* - 注册/注销 Skills
|
|
* - 按名称/分类/关键词查询
|
|
* - 渲染 Skill 提示模板
|
|
*/
|
|
|
|
import type {
|
|
Skill,
|
|
SkillContext,
|
|
SkillExecutionResult,
|
|
SkillSearchResult,
|
|
SkillRegistryConfig,
|
|
} from './types.js';
|
|
import { skillLoader } from './loader.js';
|
|
import { builtinSkills } from './builtin/index.js';
|
|
|
|
/**
|
|
* Skill 注册表
|
|
*/
|
|
export class SkillRegistry {
|
|
private skills = new Map<string, Skill>();
|
|
private config: SkillRegistryConfig;
|
|
private initialized = false;
|
|
|
|
constructor(config: SkillRegistryConfig = {}) {
|
|
this.config = {
|
|
autoLoad: true,
|
|
...config,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* 初始化注册表
|
|
*/
|
|
async initialize(workdir: string = process.cwd()): Promise<void> {
|
|
if (this.initialized) {
|
|
return;
|
|
}
|
|
|
|
// 1. 注册内置 Skills
|
|
for (const skill of builtinSkills) {
|
|
this.register(skill);
|
|
}
|
|
|
|
// 2. 加载用户 Skills
|
|
if (this.config.autoLoad) {
|
|
const userDir =
|
|
this.config.userSkillsDir || skillLoader.getUserSkillsDir();
|
|
const userSkills = await skillLoader.loadFromDirectory(userDir, 'user');
|
|
for (const skill of userSkills) {
|
|
this.register(skill);
|
|
}
|
|
}
|
|
|
|
// 3. 加载项目 Skills
|
|
if (this.config.autoLoad) {
|
|
const projectDir =
|
|
this.config.projectSkillsDir || skillLoader.getProjectSkillsDir(workdir);
|
|
const projectSkills = await skillLoader.loadFromDirectory(
|
|
projectDir,
|
|
'project'
|
|
);
|
|
for (const skill of projectSkills) {
|
|
this.register(skill);
|
|
}
|
|
}
|
|
|
|
this.initialized = true;
|
|
}
|
|
|
|
/**
|
|
* 注册 Skill
|
|
*/
|
|
register(skill: Skill): void {
|
|
// 项目 Skills 优先级最高,可以覆盖同名的内置/用户 Skills
|
|
const existing = this.skills.get(skill.name);
|
|
if (existing) {
|
|
// 优先级: project > user > builtin
|
|
const priority = { project: 3, user: 2, builtin: 1 };
|
|
if (priority[skill.source] < priority[existing.source]) {
|
|
return; // 不覆盖更高优先级的 Skill
|
|
}
|
|
}
|
|
this.skills.set(skill.name, skill);
|
|
}
|
|
|
|
/**
|
|
* 注销 Skill
|
|
*/
|
|
unregister(name: string): boolean {
|
|
return this.skills.delete(name);
|
|
}
|
|
|
|
/**
|
|
* 获取 Skill
|
|
*/
|
|
get(name: string): Skill | undefined {
|
|
return this.skills.get(name);
|
|
}
|
|
|
|
/**
|
|
* 检查 Skill 是否存在
|
|
*/
|
|
has(name: string): boolean {
|
|
return this.skills.has(name);
|
|
}
|
|
|
|
/**
|
|
* 获取所有 Skills
|
|
*/
|
|
getAll(): Skill[] {
|
|
return Array.from(this.skills.values());
|
|
}
|
|
|
|
/**
|
|
* 获取启用的 Skills
|
|
*/
|
|
getEnabled(): Skill[] {
|
|
return this.getAll().filter((s) => s.enabled !== false);
|
|
}
|
|
|
|
/**
|
|
* 按分类获取 Skills
|
|
*/
|
|
getByCategory(category: string): Skill[] {
|
|
return this.getEnabled().filter((s) => s.category === category);
|
|
}
|
|
|
|
/**
|
|
* 获取所有分类
|
|
*/
|
|
getCategories(): string[] {
|
|
const categories = new Set<string>();
|
|
for (const skill of this.getEnabled()) {
|
|
if (skill.category) {
|
|
categories.add(skill.category);
|
|
}
|
|
}
|
|
return Array.from(categories).sort();
|
|
}
|
|
|
|
/**
|
|
* 搜索 Skills
|
|
*/
|
|
search(query: string, limit: number = 10): SkillSearchResult[] {
|
|
const queryLower = query.toLowerCase();
|
|
const results: SkillSearchResult[] = [];
|
|
|
|
for (const skill of this.getEnabled()) {
|
|
let score = 0;
|
|
let matchReason = '';
|
|
|
|
// 精确名称匹配
|
|
if (skill.name.toLowerCase() === queryLower) {
|
|
score = 100;
|
|
matchReason = '名称精确匹配';
|
|
}
|
|
// 名称前缀匹配
|
|
else if (skill.name.toLowerCase().startsWith(queryLower)) {
|
|
score = 80;
|
|
matchReason = '名称前缀匹配';
|
|
}
|
|
// 名称包含匹配
|
|
else if (skill.name.toLowerCase().includes(queryLower)) {
|
|
score = 60;
|
|
matchReason = '名称包含匹配';
|
|
}
|
|
// 描述匹配
|
|
else if (skill.description.toLowerCase().includes(queryLower)) {
|
|
score = 40;
|
|
matchReason = '描述匹配';
|
|
}
|
|
// 关键词匹配
|
|
else if (
|
|
skill.keywords?.some((k) => k.toLowerCase().includes(queryLower))
|
|
) {
|
|
score = 30;
|
|
matchReason = '关键词匹配';
|
|
}
|
|
// 分类匹配
|
|
else if (skill.category?.toLowerCase().includes(queryLower)) {
|
|
score = 20;
|
|
matchReason = '分类匹配';
|
|
}
|
|
|
|
if (score > 0) {
|
|
results.push({ skill, score, matchReason });
|
|
}
|
|
}
|
|
|
|
// 按分数降序排序
|
|
results.sort((a, b) => b.score - a.score);
|
|
|
|
return results.slice(0, limit);
|
|
}
|
|
|
|
/**
|
|
* 渲染 Skill 提示模板
|
|
*/
|
|
renderPrompt(
|
|
skill: Skill,
|
|
params: Record<string, unknown>,
|
|
context?: SkillContext
|
|
): SkillExecutionResult {
|
|
try {
|
|
// 验证必需参数
|
|
if (skill.parameters) {
|
|
for (const [name, param] of Object.entries(skill.parameters)) {
|
|
if (param.required && !(name in params)) {
|
|
return {
|
|
success: false,
|
|
error: `缺少必需参数: ${name}`,
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
// 构建变量映射
|
|
const variables: Record<string, string> = {};
|
|
|
|
// 添加参数值
|
|
for (const [key, value] of Object.entries(params)) {
|
|
variables[key] = String(value);
|
|
}
|
|
|
|
// 添加上下文变量
|
|
if (context?.variables) {
|
|
for (const [key, value] of Object.entries(context.variables)) {
|
|
if (!(key in variables)) {
|
|
variables[key] = value;
|
|
}
|
|
}
|
|
}
|
|
|
|
// 添加默认值
|
|
if (skill.parameters) {
|
|
for (const [name, param] of Object.entries(skill.parameters)) {
|
|
if (!(name in variables) && param.default !== undefined) {
|
|
variables[name] = String(param.default);
|
|
}
|
|
}
|
|
}
|
|
|
|
// 渲染模板
|
|
let prompt = skill.promptTemplate;
|
|
|
|
// 替换 {{variable}} 格式的变量
|
|
prompt = prompt.replace(/\{\{(\w+)\}\}/g, (match, varName) => {
|
|
if (varName in variables) {
|
|
return variables[varName];
|
|
}
|
|
// 保留未匹配的变量(可能是用户意图保留)
|
|
return match;
|
|
});
|
|
|
|
return {
|
|
success: true,
|
|
prompt,
|
|
};
|
|
} catch (error) {
|
|
return {
|
|
success: false,
|
|
error: `渲染 Skill 提示失败: ${error instanceof Error ? error.message : String(error)}`,
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 执行 Skill(渲染模板并返回提示)
|
|
*/
|
|
execute(
|
|
name: string,
|
|
params: Record<string, unknown>,
|
|
context?: SkillContext
|
|
): SkillExecutionResult {
|
|
const skill = this.get(name);
|
|
|
|
if (!skill) {
|
|
return {
|
|
success: false,
|
|
error: `Skill 不存在: ${name}`,
|
|
};
|
|
}
|
|
|
|
if (skill.enabled === false) {
|
|
return {
|
|
success: false,
|
|
error: `Skill 已禁用: ${name}`,
|
|
};
|
|
}
|
|
|
|
return this.renderPrompt(skill, params, context);
|
|
}
|
|
|
|
/**
|
|
* 重新加载 Skills
|
|
*/
|
|
async reload(workdir: string = process.cwd()): Promise<void> {
|
|
this.skills.clear();
|
|
this.initialized = false;
|
|
await this.initialize(workdir);
|
|
}
|
|
|
|
/**
|
|
* 获取 Skill 统计信息
|
|
*/
|
|
getStats(): {
|
|
total: number;
|
|
enabled: number;
|
|
bySource: Record<string, number>;
|
|
byCategory: Record<string, number>;
|
|
} {
|
|
const skills = this.getAll();
|
|
const enabled = this.getEnabled();
|
|
|
|
const bySource: Record<string, number> = {};
|
|
const byCategory: Record<string, number> = {};
|
|
|
|
for (const skill of skills) {
|
|
bySource[skill.source] = (bySource[skill.source] || 0) + 1;
|
|
if (skill.category) {
|
|
byCategory[skill.category] = (byCategory[skill.category] || 0) + 1;
|
|
}
|
|
}
|
|
|
|
return {
|
|
total: skills.length,
|
|
enabled: enabled.length,
|
|
bySource,
|
|
byCategory,
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 全局 Skill 注册表实例
|
|
*/
|
|
let skillRegistryInstance: SkillRegistry | null = null;
|
|
|
|
/**
|
|
* 获取全局 Skill 注册表
|
|
*/
|
|
export function getSkillRegistry(): SkillRegistry {
|
|
if (!skillRegistryInstance) {
|
|
skillRegistryInstance = new SkillRegistry();
|
|
}
|
|
return skillRegistryInstance;
|
|
}
|
|
|
|
/**
|
|
* 重置全局 Skill 注册表(用于测试)
|
|
*/
|
|
export function resetSkillRegistry(): void {
|
|
skillRegistryInstance = null;
|
|
}
|