e45c9300bf
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>
183 lines
5.4 KiB
JavaScript
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);
|
|
}
|
|
}
|
|
}
|