Files
memind/skills-registry.mjs
john e45c9300bf feat: LLM intent router, direct chat execution, and Memory V2 light intervention
Wire chat intent routing with direct_chat on regular sessions, skill-selected
short-circuit to Agent, memory light/heavy intervention tiers, and fix direct
chat UI stuck streaming after completion.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-04 22:32:57 +08:00

183 lines
5.4 KiB
JavaScript

import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import {
ensurePublishSkillInstalled,
PUBLISH_SKILL_NAME,
renderPublishSkill,
resolvePublicBaseUrl,
} from './user-publish.mjs';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
export const DEFAULT_USER_SKILLS = {
web: true,
search: true,
'schedule-assistant': true,
'service-integration-smoke': true,
'form-builder': true,
'table-viewer': true,
'product-campaign-page': true,
'docx-generate': true,
'long-image-download': true,
[PUBLISH_SKILL_NAME]: false,
};
/** Optional presets for admin role configuration (creator / developer). */
export const USER_ROLE_SKILL_PRESETS = {
user: DEFAULT_USER_SKILLS,
creator: {
...DEFAULT_USER_SKILLS,
[PUBLISH_SKILL_NAME]: true,
kanban: true,
timeline: true,
},
developer: {
...DEFAULT_USER_SKILLS,
git: true,
'diff-viewer': true,
'code-playground': true,
'test-runner': true,
},
};
export function parseSkillFrontmatter(content) {
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
if (!match) return { name: null, description: '' };
const block = match[1];
const name = block.match(/^name:\s*(.+)$/m)?.[1]?.trim() ?? null;
const description = block.match(/^description:\s*(.+)$/m)?.[1]?.trim() ?? '';
return { name, description };
}
export function listPlatformSkillCatalog(h5Root = __dirname) {
const skillsRoot = path.join(h5Root, 'skills');
if (!fs.existsSync(skillsRoot)) return [];
const catalog = [];
for (const entry of fs.readdirSync(skillsRoot, { withFileTypes: true })) {
if (!entry.isDirectory()) continue;
const skillPath = path.join(skillsRoot, entry.name, 'SKILL.md');
if (!fs.existsSync(skillPath)) continue;
const raw = fs.readFileSync(skillPath, 'utf8');
const meta = parseSkillFrontmatter(raw);
const name = meta.name || entry.name;
catalog.push({
name,
dirName: entry.name,
label: name,
description: meta.description || '平台通用技能',
category: 'platform',
requiresPublish: name === PUBLISH_SKILL_NAME,
});
}
return catalog.sort((a, b) => a.name.localeCompare(b.name));
}
export function catalogSkillNames(catalog) {
return catalog.map((item) => item.name);
}
export function isValidSkillName(catalog, name) {
return catalog.some((item) => item.name === name);
}
export function normalizeSkillPatch(catalog, patch) {
const normalized = {};
for (const [key, value] of Object.entries(patch ?? {})) {
if (!isValidSkillName(catalog, key)) continue;
normalized[key] = Boolean(value);
}
return normalized;
}
export function resolveSkillMap(roleGrants, userOverrides, catalog) {
const resolved = {};
for (const item of catalog) {
const key = item.name;
if (key in userOverrides) {
resolved[key] = userOverrides[key];
} else if (key in roleGrants) {
resolved[key] = roleGrants[key];
} else {
resolved[key] = DEFAULT_USER_SKILLS[key] ?? false;
}
}
return resolved;
}
export function grantedSkillNames(skillMap) {
return Object.entries(skillMap)
.filter(([, enabled]) => enabled)
.map(([name]) => name);
}
export function applySkillGrantsToCapabilities(capabilities, skillMap) {
const effective = { ...capabilities };
const enabled = grantedSkillNames(skillMap);
if (enabled.length > 0) {
effective.skills = true;
}
if (enabled.includes(PUBLISH_SKILL_NAME)) {
effective.static_publish = true;
}
return effective;
}
function copySkillTree(srcDir, destDir) {
fs.mkdirSync(destDir, { recursive: true });
for (const entry of fs.readdirSync(srcDir, { withFileTypes: true })) {
const from = path.join(srcDir, entry.name);
const to = path.join(destDir, entry.name);
if (entry.isDirectory()) {
copySkillTree(from, to);
} else {
fs.copyFileSync(from, to);
}
}
}
export function syncSkillsToWorkspace({
h5Root,
publishDir,
skillMap,
catalog,
user,
publicBaseUrl,
}) {
const enabled = new Set(grantedSkillNames(skillMap));
const platformNames = new Set(catalog.map((item) => item.name));
const agentsSkills = path.join(publishDir, '.agents', 'skills');
if (fs.existsSync(agentsSkills)) {
for (const entry of fs.readdirSync(agentsSkills, { withFileTypes: true })) {
if (!entry.isDirectory()) continue;
const skillName = entry.name;
const catalogItem = catalog.find((item) => item.name === skillName || item.dirName === skillName);
const resolvedName = catalogItem?.name ?? skillName;
if (platformNames.has(resolvedName) && !enabled.has(resolvedName)) {
fs.rmSync(path.join(agentsSkills, entry.name), { recursive: true, force: true });
}
}
}
for (const item of catalog) {
if (!enabled.has(item.name)) continue;
const srcDir = path.join(h5Root, 'skills', item.dirName);
const destDir = path.join(agentsSkills, item.name);
if (item.name === PUBLISH_SKILL_NAME && user) {
ensurePublishSkillInstalled(publishDir, {
slug: String(user.id).trim().toLowerCase(),
username: user.username ? String(user.username).trim().toLowerCase() : undefined,
publicBaseUrl,
publishDir,
});
continue;
}
if (fs.existsSync(srcDir)) {
if (fs.existsSync(destDir)) fs.rmSync(destDir, { recursive: true, force: true });
copySkillTree(srcDir, destDir);
}
}
}