Commit a9534530 authored by AI-甘富林's avatar AI-甘富林

feat(desktop): add curated skill catalog plumbing

Wire desktop skill catalog resolution through the desktop runtime and packaged app paths.
Add the supporting service layer and trigger plumbing needed for curated skill integration.
Co-Authored-By: 's avatarClaude Sonnet 4.6 <noreply@anthropic.com>
parent d3ac333b
export interface CuratedGenericSkillDefinition {
id: string;
description: string;
zhName: string;
zhDescription: string;
searchAliases: string[];
}
export const CURATED_GENERIC_SKILLS: CuratedGenericSkillDefinition[] = [
{
id: "xlsx",
description: "Handle spreadsheets, calculations, and data summaries.",
zhName: "表格助手",
zhDescription: "适合处理 Excel、表格计算、数据汇总和清单整理。",
searchAliases: ["表格", "excel", "xlsx", "csv", "数据", "统计", "台账"]
},
{
id: "docx",
description: "Draft Word documents, reports, and polished written materials.",
zhName: "文档助手",
zhDescription: "适合起草 Word 文档、报告、通知和正式材料。",
searchAliases: ["文档", "word", "docx", "报告", "方案", "正文", "材料"]
},
{
id: "pdf",
description: "Read, extract, merge, and organize PDF content.",
zhName: "PDF 助手",
zhDescription: "适合查看、提取、合并和整理 PDF 文件内容。",
searchAliases: ["pdf", "合同", "文件", "扫描件", "提取", "合并"]
},
{
id: "pptx",
description: "Prepare slides, outlines, and presentation materials.",
zhName: "演示助手",
zhDescription: "适合整理 PPT、大纲、汇报页和演示材料。",
searchAliases: ["ppt", "pptx", "演示", "汇报", "课件", "幻灯片"]
},
{
id: "file-skill",
description: "Organize files, folders, batches, and archives safely.",
zhName: "文件整理助手",
zhDescription: "适合做文件分类、批量整理、归档和目录收纳。",
searchAliases: ["文件", "归档", "整理", "重命名", "目录", "批量"]
},
{
id: "note-organizer",
description: "Organize notes, tags, duplicate entries, and knowledge snippets.",
zhName: "笔记整理助手",
zhDescription: "适合梳理会议纪要、学习笔记、标签分类和内容归并。",
searchAliases: ["笔记", "纪要", "标签", "整理", "知识库", "归并"]
},
{
id: "email-skill",
description: "Draft, read, and manage email tasks from one place.",
zhName: "邮件助手",
zhDescription: "适合写邮件、查邮件、整理收件箱和邮件沟通。",
searchAliases: ["邮件", "邮箱", "email", "发信", "收件箱", "沟通"]
},
{
id: "market-researcher",
description: "Collect and summarize market, competitor, and trend signals.",
zhName: "调研助手",
zhDescription: "适合收集市场信息、竞品动态和趋势摘要。",
searchAliases: ["市场", "竞品", "调研", "行业", "研究", "趋势"]
}
];
export const CURATED_GENERIC_SKILL_IDS = new Set(CURATED_GENERIC_SKILLS.map((skill) => skill.id));
export function getCuratedGenericSkillDefinition(skillId: string): CuratedGenericSkillDefinition | undefined {
return CURATED_GENERIC_SKILLS.find((skill) => skill.id === skillId);
}
import { existsSync } from "node:fs";
import path from "node:path";
import type { SystemSummary } from "@qjclaw/shared-types";
function uniquePaths(paths: Array<string | undefined>): string[] {
return [...new Set(paths.map((value) => value?.trim()).filter((value): value is string => Boolean(value)))];
}
export function resolveGenericSkillsRoot(systemSummary: SystemSummary): string {
const envRoot = process.env.QJCLAW_SKILL_CATALOG_ROOT?.trim();
const devRepoSkillsRoot = path.resolve(systemSummary.appPath, "..", "..", "skills");
const legacyRepoSkillsRoot = path.resolve(systemSummary.appPath, "..", "..", "q-skills", "skills");
const cwdSkillsRoot = path.resolve(process.cwd(), "skills");
const packagedSkillsRoot = systemSummary.isPackaged
? path.join(systemSummary.resourcesPath, "skills")
: undefined;
const candidates = uniquePaths([
envRoot,
packagedSkillsRoot,
devRepoSkillsRoot,
cwdSkillsRoot,
legacyRepoSkillsRoot
]);
return candidates.find((candidate) => existsSync(candidate)) ?? candidates[0] ?? devRepoSkillsRoot;
}
This diff is collapsed.
import type { SkillCatalogItem } from "@qjclaw/shared-types";
export interface SkillTriggerMatch {
query: string;
start: number;
end: number;
}
export function normalizeSkillSearch(value: string): string {
return value
.normalize("NFKC")
.toLowerCase()
.replace(/[^\p{L}\p{N}\s-]+/gu, " ")
.replace(/\s+/g, " ")
.trim();
}
export function findSkillTriggerMatch(value: string, cursor: number): SkillTriggerMatch | null {
const normalizedCursor = Math.max(0, Math.min(cursor, value.length));
const prefix = value.slice(0, normalizedCursor);
const match = prefix.match(/(?:^|[\s((【\[{'""'])@([^\s@]*)$/u);
if (!match) {
return null;
}
const raw = match[0];
const atOffset = raw.lastIndexOf("@");
if (atOffset < 0) {
return null;
}
return {
query: match[1] ?? "",
start: prefix.length - raw.length + atOffset,
end: normalizedCursor
};
}
export function stripSkillTriggerFromPrompt(value: string, trigger: SkillTriggerMatch | null): string {
if (!trigger) {
return value;
}
const next = `${value.slice(0, trigger.start)}${value.slice(trigger.end)}`;
return next.replace(/[ \t]{2,}/g, " ").replace(/\n{3,}/g, "\n\n");
}
export interface ResolvePromptSkillResult {
cleanPrompt: string;
resolvedSkillId: string | undefined;
error?: string;
}
function findSkillTriggerInFullText(value: string): SkillTriggerMatch | null {
const match = value.match(/(?:^|[\s((【\[{'""'])@([^\s@]+)/u);
if (!match || !match[1]) {
return null;
}
const raw = match[0];
const atOffset = raw.lastIndexOf("@");
const start = (match.index ?? 0) + atOffset;
return {
query: match[1],
start,
end: start + 1 + match[1].length
};
}
export function resolvePromptSkill(
promptText: string,
catalog: SkillCatalogItem[]
): ResolvePromptSkillResult {
const trigger = findSkillTriggerInFullText(promptText);
if (!trigger || !trigger.query) {
return { cleanPrompt: promptText, resolvedSkillId: undefined };
}
const q = trigger.query.toLowerCase();
const selectables = catalog.filter((s) => s.selectable);
const matches = selectables.filter(
(s) =>
s.zhName.toLowerCase() === q ||
s.name.toLowerCase() === q ||
normalizeSkillSearch(s.searchText).split(/\s+/).includes(q)
);
if (matches.length === 0) {
return {
cleanPrompt: promptText,
resolvedSkillId: undefined,
error: `未找到可用技能「${trigger.query}」,请从弹层中选择`
};
}
if (matches.length > 1) {
return {
cleanPrompt: promptText,
resolvedSkillId: undefined,
error: `「${trigger.query}」匹配到多个技能,请从弹层中明确选择`
};
}
return {
cleanPrompt: stripSkillTriggerFromPrompt(promptText, trigger),
resolvedSkillId: matches[0]!.id
};
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment