feat(skill-runtime): restore admin config and manifest keyword routing
Recover skill runtime policy/admin services and Skill Router v2 helpers so admins can toggle manifest routing and inspect the platform skill catalog. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -18,6 +18,7 @@ import { createUserAuth } from './user-auth.mjs';
|
||||
import { createLlmProviderService } from './llm-providers.mjs';
|
||||
import { createAssetGatewayConfigService } from './asset-gateway.mjs';
|
||||
import { createMemoryV2AdminConfigService } from './memory-v2-admin-config.mjs';
|
||||
import { createSkillRuntimeAdminConfigService } from './skill-runtime-admin-config.mjs';
|
||||
import { createWechatScheduleLlmConfigService } from './wechat-schedule-llm-config.mjs';
|
||||
import { createPlazaPostService, formatPostRow } from './plaza-posts.mjs';
|
||||
import { createPlazaInteractionService } from './plaza-interactions.mjs';
|
||||
@@ -100,6 +101,7 @@ export async function createAdminServices(env = {}) {
|
||||
const llmProviderService = createLlmProviderService(pool, { apiTarget, apiSecret });
|
||||
const assetGatewayConfigService = createAssetGatewayConfigService(pool, { llmProviderService });
|
||||
const memoryV2ConfigService = createMemoryV2AdminConfigService(pool);
|
||||
const skillRuntimeConfigService = createSkillRuntimeAdminConfigService(pool, { h5Root });
|
||||
const wechatScheduleLlmConfigService = createWechatScheduleLlmConfigService(pool);
|
||||
const adminSystemTestService = createAdminSystemTestService({
|
||||
pool,
|
||||
@@ -138,6 +140,7 @@ export async function createAdminServices(env = {}) {
|
||||
llmProviderService,
|
||||
assetGatewayConfigService,
|
||||
memoryV2ConfigService,
|
||||
skillRuntimeConfigService,
|
||||
wechatScheduleLlmConfigService,
|
||||
adminSystemTestService,
|
||||
plazaPosts,
|
||||
|
||||
@@ -31,6 +31,7 @@ function plazaRouteError(res, req, error) {
|
||||
* @param {object} deps.userAuth
|
||||
* @param {object|null} deps.llmProviderService
|
||||
* @param {object|null} deps.memoryV2ConfigService
|
||||
* @param {object|null} deps.skillRuntimeConfigService
|
||||
* @param {object|null} deps.adminSystemTestService
|
||||
* @param {object|null} deps.plazaPosts
|
||||
* @param {object|null} deps.plazaOps
|
||||
@@ -44,6 +45,7 @@ export function createAdminApi({
|
||||
llmProviderService,
|
||||
assetGatewayConfigService,
|
||||
memoryV2ConfigService,
|
||||
skillRuntimeConfigService,
|
||||
adminSystemTestService,
|
||||
plazaPosts,
|
||||
plazaOps,
|
||||
@@ -148,6 +150,40 @@ export function createAdminApi({
|
||||
return res.json(result);
|
||||
});
|
||||
|
||||
adminApi.get('/skill-runtime/config', requireAdmin, async (_req, res) => {
|
||||
if (!skillRuntimeConfigService?.getAdminConfig) {
|
||||
return res.status(503).json({ message: 'Skill Runtime 配置服务未启用' });
|
||||
}
|
||||
return res.json(await skillRuntimeConfigService.getAdminConfig());
|
||||
});
|
||||
|
||||
const updateSkillRuntimeConfig = async (req, res) => {
|
||||
if (!skillRuntimeConfigService?.updateAdminConfig) {
|
||||
return res.status(503).json({ message: 'Skill Runtime 配置服务未启用' });
|
||||
}
|
||||
const result = await skillRuntimeConfigService.updateAdminConfig(req.body?.config ?? req.body ?? {}, {
|
||||
updatedBy: req.currentUser.id,
|
||||
});
|
||||
return res.json(result);
|
||||
};
|
||||
|
||||
adminApi.put('/skill-runtime/config', requireAdmin, updateSkillRuntimeConfig);
|
||||
adminApi.patch('/skill-runtime/config', requireAdmin, updateSkillRuntimeConfig);
|
||||
|
||||
adminApi.get('/skill-runtime/catalog', requireAdmin, async (_req, res) => {
|
||||
if (!skillRuntimeConfigService?.listCatalogSummary) {
|
||||
return res.status(503).json({ message: 'Skill Runtime 配置服务未启用' });
|
||||
}
|
||||
return res.json({ catalog: await skillRuntimeConfigService.listCatalogSummary() });
|
||||
});
|
||||
|
||||
adminApi.get('/skill-runtime/runtime', requireAdmin, async (_req, res) => {
|
||||
if (!skillRuntimeConfigService?.getPublicRuntimeConfig) {
|
||||
return res.status(503).json({ message: 'Skill Runtime 配置服务未启用' });
|
||||
}
|
||||
return res.json(await skillRuntimeConfigService.getPublicRuntimeConfig());
|
||||
});
|
||||
|
||||
adminApi.post('/system-tests/skill-validation', requireAdmin, async (req, res) => {
|
||||
if (!adminSystemTestService?.runSkillValidation) {
|
||||
return res.status(503).json({ message: '系统测试服务未启用' });
|
||||
|
||||
+92
-4
@@ -1,6 +1,7 @@
|
||||
// Keep browser-safe: do not import user-publish.mjs (uses node:fs/path/url).
|
||||
const PUBLISH_SKILL_NAME = 'static-page-publish';
|
||||
export const PAGE_DATA_COLLECT_SKILL_NAME = 'page-data-collect';
|
||||
export const SKILL_ROUTER_V2_ENV = 'TKMIND_SKILL_ROUTER_V2';
|
||||
|
||||
const WEB_INTENT_PATTERNS = [
|
||||
/(?:今天|今日|最新|热点|热搜|新闻|头条|头条新闻|最新消息|发生了什么|最近发生)/u,
|
||||
@@ -37,6 +38,9 @@ const PAGE_DATA_INTENT_PATTERNS = [
|
||||
/(?:密码|口令).{0,12}(?:查看|后台|管理|进入)/u,
|
||||
/(?:sqlite|数据库|page[\s-]?data)/i,
|
||||
/(?:每个用户|各用户).{0,12}(?:提交|记录)/u,
|
||||
/(?:记账|账本|收支|流水|日记|习惯打卡|每日打卡).{0,40}(?:页面|记录|管理|汇总|统计)/u,
|
||||
/(?:管理页面|管理页).{0,30}(?:查看|记录|汇总|统计|明细|删除|修改)/u,
|
||||
/(?:页面|网页|H5|h5).{0,80}(?:每天|每日|新增|添加|填写|记录).{0,80}(?:所有记录|历史记录|管理|汇总|统计)/u,
|
||||
];
|
||||
|
||||
export function isPageDataIntent(text) {
|
||||
@@ -155,7 +159,7 @@ export function buildChatSkillPrompt(promptKey, skillName) {
|
||||
case 'page-data-collect':
|
||||
return (
|
||||
`请使用 ${skillName ?? PAGE_DATA_COLLECT_SKILL_NAME} 技能:在 MindSpace 页面中实现可提交、可持久化的数据收集(问卷/报名/台账等),必须使用 Page Data API。` +
|
||||
'先匹配技能内能力分支(默认 A:匿名前台 public insert + 独立后台 password read,口令默认 88888888);展示方案摘要确认后再开工。' +
|
||||
'先匹配技能内能力分支(默认 A:匿名前台 public insert + 独立后台 password read,口令默认 88888888);“帮我做/创建”本身就是创建授权,展示简短方案摘要后必须同一轮继续执行,只有互斥需求或非法口令才追问。' +
|
||||
'流程:load_skill → private_data_execute 建表 → private_data_register_dataset → write_file/edit_file 写 public/*.html(含 /assets/page-data-client.js)→ private_data_bind_workspace_page 发布并写策略。' +
|
||||
'禁止 localStorage / 浏览器本地存储 fallback;禁止自建 Express/独立端口(如 8899)、禁止 HTML 硬编码 127.0.0.1 API、禁止连续空转不调用工具。HTML 视觉规范参照 static-page-publish。完成后只返回 workspaceUrl(/MindSpace/<用户ID>/public/...),禁止给用户 /u/用户名/pages/... 链接,并说明后台入口与口令。'
|
||||
);
|
||||
@@ -206,9 +210,65 @@ export function filterChatSkills(options, ctx) {
|
||||
});
|
||||
}
|
||||
|
||||
export function buildAutoChatSkillPrefix(text, grantedSkills = []) {
|
||||
const trimmed = String(text ?? '').trim();
|
||||
if (!trimmed) return '';
|
||||
const PROMPT_KEY_BY_SKILL_NAME = new Map(
|
||||
CHAT_SKILL_DEFINITIONS.filter((item) => item.skillName && item.promptKey).map((item) => [
|
||||
item.skillName,
|
||||
item.promptKey,
|
||||
]),
|
||||
);
|
||||
|
||||
/** Opt-in Skill Router v2. Default off unless TKMIND_SKILL_ROUTER_V2=1 or options.skillRouterV2=true. */
|
||||
export function isSkillRouterV2Enabled(explicit) {
|
||||
if (explicit === true) return true;
|
||||
if (explicit === false) return false;
|
||||
const raw =
|
||||
typeof process !== 'undefined' && process?.env ? process.env[SKILL_ROUTER_V2_ENV] : undefined;
|
||||
return /^(1|true|yes)$/i.test(String(raw ?? ''));
|
||||
}
|
||||
|
||||
/** Build manifest routes from platform catalog entries that declare trigger.keywords. */
|
||||
export function buildManifestRoutesFromCatalog(catalog = []) {
|
||||
return catalog
|
||||
.filter((item) => Array.isArray(item?.manifest?.trigger?.keywords) && item.manifest.trigger.keywords.length > 0)
|
||||
.map((item) => ({
|
||||
skillName: item.name,
|
||||
keywords: item.manifest.trigger.keywords,
|
||||
priority: Number(item.manifest?.router?.priority ?? 0),
|
||||
promptKey:
|
||||
item.manifest?.router?.promptKey ??
|
||||
PROMPT_KEY_BY_SKILL_NAME.get(item.name) ??
|
||||
null,
|
||||
promptVariant: item.manifest?.router?.promptVariant ?? null,
|
||||
}))
|
||||
.sort((a, b) => b.priority - a.priority || a.skillName.localeCompare(b.skillName));
|
||||
}
|
||||
|
||||
export function matchManifestSkillRoute(text, routes = [], grantedSkills = []) {
|
||||
const normalized = String(text ?? '').trim().toLowerCase();
|
||||
if (!normalized || !Array.isArray(routes) || routes.length === 0) return null;
|
||||
const granted = new Set(grantedSkills);
|
||||
for (const route of routes) {
|
||||
if (!route?.skillName || !granted.has(route.skillName)) continue;
|
||||
const keywords = Array.isArray(route.keywords) ? route.keywords : [];
|
||||
if (keywords.some((keyword) => normalized.includes(String(keyword).trim().toLowerCase()))) {
|
||||
return route;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function buildManifestSkillPrompt(route) {
|
||||
if (!route) return '';
|
||||
if (route.promptVariant === 'web-news') {
|
||||
return buildWebNewsSkillPrompt(route.skillName);
|
||||
}
|
||||
if (route.promptKey) {
|
||||
return buildChatSkillPrompt(route.promptKey, route.skillName);
|
||||
}
|
||||
return `请使用 ${route.skillName} 技能:`;
|
||||
}
|
||||
|
||||
function buildLegacyAutoChatSkillPrefix(trimmed, grantedSkills) {
|
||||
if (
|
||||
grantedSkills.includes(PAGE_DATA_COLLECT_SKILL_NAME) &&
|
||||
isPageDataIntent(trimmed)
|
||||
@@ -226,6 +286,34 @@ export function buildAutoChatSkillPrefix(text, grantedSkills = []) {
|
||||
return buildChatSkillPrompt('web', 'web');
|
||||
}
|
||||
|
||||
export function buildAutoChatSkillPrefixOptions(publicConfig) {
|
||||
if (!publicConfig?.routerV2Enabled || !publicConfig?.manifestRoutingEnabled) {
|
||||
return {};
|
||||
}
|
||||
return {
|
||||
skillRouterV2: true,
|
||||
manifestRoutes: publicConfig.manifestRoutes ?? [],
|
||||
};
|
||||
}
|
||||
|
||||
export function buildAutoChatSkillPrefix(text, grantedSkills = [], options = {}) {
|
||||
const trimmed = String(text ?? '').trim();
|
||||
if (!trimmed) return '';
|
||||
|
||||
if (
|
||||
isSkillRouterV2Enabled(options.skillRouterV2) &&
|
||||
Array.isArray(options.manifestRoutes) &&
|
||||
options.manifestRoutes.length > 0
|
||||
) {
|
||||
const matched = matchManifestSkillRoute(trimmed, options.manifestRoutes, grantedSkills);
|
||||
if (matched) {
|
||||
return buildManifestSkillPrompt(matched);
|
||||
}
|
||||
}
|
||||
|
||||
return buildLegacyAutoChatSkillPrefix(trimmed, grantedSkills);
|
||||
}
|
||||
|
||||
export function stripKnownChatSkillPrompt(text) {
|
||||
let next = String(text ?? '').trimStart();
|
||||
const candidates = [];
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
import { SKILL_ROUTER_V2_ENV } from './chat-skills.mjs';
|
||||
import {
|
||||
buildPublicSkillRuntimeConfig,
|
||||
buildSkillRuntimeCatalogSummary,
|
||||
} from './skill-runtime-policy.mjs';
|
||||
|
||||
const CONFIG_TABLE = 'h5_skill_runtime_config';
|
||||
const CONFIG_SCOPE = 'global';
|
||||
|
||||
function defaultConfigShape() {
|
||||
return {
|
||||
router: {
|
||||
v2Enabled: false,
|
||||
manifestRoutingEnabled: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeBoolean(value, fallback = false) {
|
||||
if (value == null || value === '') return fallback;
|
||||
if (typeof value === 'boolean') return value;
|
||||
const normalized = String(value).trim().toLowerCase();
|
||||
if (['1', 'true', 'yes', 'on'].includes(normalized)) return true;
|
||||
if (['0', 'false', 'no', 'off'].includes(normalized)) return false;
|
||||
return fallback;
|
||||
}
|
||||
|
||||
function cloneConfig(config = null) {
|
||||
return structuredClone?.(config ?? defaultConfigShape())
|
||||
?? JSON.parse(JSON.stringify(config ?? defaultConfigShape()));
|
||||
}
|
||||
|
||||
function parseJsonLike(value, fallback) {
|
||||
if (value == null || value === '') return fallback;
|
||||
if (typeof value === 'string') {
|
||||
try {
|
||||
return JSON.parse(value);
|
||||
} catch {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
if (typeof value === 'object') return value;
|
||||
return fallback;
|
||||
}
|
||||
|
||||
function envRouterEnabled(env = process.env) {
|
||||
return /^(1|true|yes)$/i.test(String(env?.[SKILL_ROUTER_V2_ENV] ?? ''));
|
||||
}
|
||||
|
||||
function applyEnv(config, env = process.env) {
|
||||
const next = cloneConfig(config);
|
||||
if (next.router.v2Enabled === false && envRouterEnabled(env)) {
|
||||
next.router.v2Enabled = true;
|
||||
}
|
||||
if (next.router.manifestRoutingEnabled === false && next.router.v2Enabled) {
|
||||
next.router.manifestRoutingEnabled = envRouterEnabled(env);
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
function mergePatch(currentConfig, patch = {}) {
|
||||
const next = cloneConfig(currentConfig);
|
||||
if (patch?.router && 'v2Enabled' in patch.router) {
|
||||
next.router.v2Enabled = normalizeBoolean(patch.router.v2Enabled, false);
|
||||
}
|
||||
if (patch?.router && 'manifestRoutingEnabled' in patch.router) {
|
||||
next.router.manifestRoutingEnabled = normalizeBoolean(patch.router.manifestRoutingEnabled, false);
|
||||
}
|
||||
if (!next.router.v2Enabled) {
|
||||
next.router.manifestRoutingEnabled = false;
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
async function ensureConfigTable(pool) {
|
||||
await pool.query(`
|
||||
CREATE TABLE IF NOT EXISTS ${CONFIG_TABLE} (
|
||||
config_scope VARCHAR(32) PRIMARY KEY,
|
||||
config_json JSON NOT NULL,
|
||||
updated_by CHAR(36) NULL,
|
||||
updated_at BIGINT NOT NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||
`);
|
||||
}
|
||||
|
||||
async function loadStoredState(pool) {
|
||||
await ensureConfigTable(pool);
|
||||
const [rows] = await pool.query(
|
||||
`SELECT config_json, updated_by, updated_at
|
||||
FROM ${CONFIG_TABLE}
|
||||
WHERE config_scope = ?
|
||||
LIMIT 1`,
|
||||
[CONFIG_SCOPE],
|
||||
);
|
||||
const row = rows[0];
|
||||
if (!row) return null;
|
||||
return {
|
||||
config: { ...defaultConfigShape(), ...parseJsonLike(row.config_json, {}) },
|
||||
updatedAt: Number(row.updated_at ?? 0) || null,
|
||||
updatedBy: row.updated_by ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
export function createSkillRuntimeAdminConfigService(pool, { env = process.env, h5Root = process.cwd() } = {}) {
|
||||
async function loadEffectiveConfig() {
|
||||
const stored = await loadStoredState(pool);
|
||||
if (!stored) {
|
||||
const config = applyEnv(defaultConfigShape(), env);
|
||||
return {
|
||||
config,
|
||||
updatedAt: null,
|
||||
updatedBy: null,
|
||||
source: envRouterEnabled(env) ? 'env' : 'default',
|
||||
};
|
||||
}
|
||||
return {
|
||||
config: cloneConfig(stored.config),
|
||||
updatedAt: stored.updatedAt,
|
||||
updatedBy: stored.updatedBy,
|
||||
source: 'admin-db',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
async getAdminConfig() {
|
||||
const state = await loadEffectiveConfig();
|
||||
return {
|
||||
config: state.config,
|
||||
updatedAt: state.updatedAt,
|
||||
updatedBy: state.updatedBy,
|
||||
source: state.source,
|
||||
};
|
||||
},
|
||||
|
||||
async updateAdminConfig(patch = {}, { updatedBy = null } = {}) {
|
||||
const stored = await loadStoredState(pool);
|
||||
const base = stored?.config ?? defaultConfigShape();
|
||||
const nextConfig = mergePatch(base, patch.config ?? patch);
|
||||
await ensureConfigTable(pool);
|
||||
const now = Date.now();
|
||||
await pool.query(
|
||||
`INSERT INTO ${CONFIG_TABLE}
|
||||
(config_scope, config_json, updated_by, updated_at)
|
||||
VALUES (?, ?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
config_json = VALUES(config_json),
|
||||
updated_by = VALUES(updated_by),
|
||||
updated_at = VALUES(updated_at)`,
|
||||
[CONFIG_SCOPE, JSON.stringify(nextConfig), updatedBy, now],
|
||||
);
|
||||
return this.getAdminConfig();
|
||||
},
|
||||
|
||||
async getRuntimeState() {
|
||||
const state = await loadEffectiveConfig();
|
||||
return {
|
||||
source: state.source,
|
||||
updatedAt: state.updatedAt,
|
||||
updatedBy: state.updatedBy,
|
||||
config: state.config,
|
||||
};
|
||||
},
|
||||
|
||||
async getPublicRuntimeConfig() {
|
||||
const state = await loadEffectiveConfig();
|
||||
return buildPublicSkillRuntimeConfig(state.config, h5Root);
|
||||
},
|
||||
|
||||
listCatalogSummary() {
|
||||
return buildSkillRuntimeCatalogSummary(h5Root);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export const skillRuntimeAdminConfigInternals = {
|
||||
CONFIG_SCOPE,
|
||||
CONFIG_TABLE,
|
||||
defaultConfigShape,
|
||||
applyEnv,
|
||||
mergePatch,
|
||||
};
|
||||
@@ -0,0 +1,74 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import {
|
||||
createSkillRuntimeAdminConfigService,
|
||||
skillRuntimeAdminConfigInternals,
|
||||
} from './skill-runtime-admin-config.mjs';
|
||||
|
||||
function createPool(seedRow = null) {
|
||||
const state = { row: seedRow };
|
||||
return {
|
||||
state,
|
||||
async query(sql, params) {
|
||||
if (sql.includes('CREATE TABLE')) return [[], []];
|
||||
if (sql.includes('SELECT config_json')) {
|
||||
return [state.row ? [state.row] : [], []];
|
||||
}
|
||||
if (sql.includes('INSERT INTO h5_skill_runtime_config')) {
|
||||
state.row = {
|
||||
config_json: params[1],
|
||||
updated_by: params[2],
|
||||
updated_at: params[3],
|
||||
};
|
||||
return [[], []];
|
||||
}
|
||||
throw new Error(`Unexpected query: ${sql}`);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
test('skill runtime admin config defaults to disabled router', async () => {
|
||||
const service = createSkillRuntimeAdminConfigService(createPool(), { env: {} });
|
||||
const result = await service.getAdminConfig();
|
||||
assert.equal(result.config.router.v2Enabled, false);
|
||||
assert.equal(result.config.router.manifestRoutingEnabled, false);
|
||||
});
|
||||
|
||||
test('skill runtime admin config falls back to env when db row is absent', async () => {
|
||||
const service = createSkillRuntimeAdminConfigService(createPool(), {
|
||||
env: { TKMIND_SKILL_ROUTER_V2: '1' },
|
||||
});
|
||||
const result = await service.getAdminConfig();
|
||||
assert.equal(result.config.router.v2Enabled, true);
|
||||
assert.equal(result.config.router.manifestRoutingEnabled, true);
|
||||
assert.equal(result.source, 'env');
|
||||
});
|
||||
|
||||
test('skill runtime admin config persists router toggles', async () => {
|
||||
const pool = createPool();
|
||||
const service = createSkillRuntimeAdminConfigService(pool, { env: {} });
|
||||
const updated = await service.updateAdminConfig(
|
||||
{ router: { v2Enabled: true, manifestRoutingEnabled: true } },
|
||||
{ updatedBy: 'admin-1' },
|
||||
);
|
||||
assert.equal(updated.config.router.v2Enabled, true);
|
||||
assert.equal(updated.config.router.manifestRoutingEnabled, true);
|
||||
const runtime = await service.getPublicRuntimeConfig();
|
||||
assert.equal(runtime.routerV2Enabled, true);
|
||||
assert.equal(runtime.manifestRoutingEnabled, true);
|
||||
assert.ok(Array.isArray(runtime.manifestRoutes));
|
||||
});
|
||||
|
||||
test('skill runtime admin config keeps db authoritative over env', async () => {
|
||||
const pool = createPool({
|
||||
config_json: JSON.stringify(skillRuntimeAdminConfigInternals.defaultConfigShape()),
|
||||
updated_by: 'admin-1',
|
||||
updated_at: 123,
|
||||
});
|
||||
const service = createSkillRuntimeAdminConfigService(pool, {
|
||||
env: { TKMIND_SKILL_ROUTER_V2: '1' },
|
||||
});
|
||||
const result = await service.getAdminConfig();
|
||||
assert.equal(result.config.router.v2Enabled, false);
|
||||
assert.equal(result.source, 'admin-db');
|
||||
});
|
||||
@@ -0,0 +1,30 @@
|
||||
import { buildManifestRoutesFromCatalog } from './chat-skills.mjs';
|
||||
import { listPlatformSkillCatalog } from './skills-registry.mjs';
|
||||
|
||||
export function buildPublicSkillRuntimeConfig(config, h5Root = process.cwd()) {
|
||||
const routerV2Enabled = Boolean(config?.router?.v2Enabled);
|
||||
const manifestRoutingEnabled = routerV2Enabled && Boolean(config?.router?.manifestRoutingEnabled);
|
||||
const catalog = listPlatformSkillCatalog(h5Root);
|
||||
const manifestRoutes = manifestRoutingEnabled ? buildManifestRoutesFromCatalog(catalog) : [];
|
||||
return {
|
||||
routerV2Enabled,
|
||||
manifestRoutingEnabled,
|
||||
manifestRoutes,
|
||||
manifestSkillCount: catalog.filter((item) => item.manifest?.trigger?.keywords?.length).length,
|
||||
};
|
||||
}
|
||||
|
||||
export function buildSkillRuntimeCatalogSummary(h5Root = process.cwd()) {
|
||||
const catalog = listPlatformSkillCatalog(h5Root);
|
||||
return catalog.map((item) => ({
|
||||
name: item.name,
|
||||
dirName: item.dirName,
|
||||
description: item.description,
|
||||
version: item.version ?? null,
|
||||
executors: item.executors ?? ['goose'],
|
||||
hasManifest: Boolean(item.manifest),
|
||||
triggerKeywords: item.manifest?.trigger?.keywords ?? [],
|
||||
routerPromptKey: item.manifest?.router?.promptKey ?? null,
|
||||
routerPriority: item.manifest?.router?.priority ?? 0,
|
||||
}));
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import path from 'node:path';
|
||||
import test from 'node:test';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import {
|
||||
buildPublicSkillRuntimeConfig,
|
||||
buildSkillRuntimeCatalogSummary,
|
||||
} from './skill-runtime-policy.mjs';
|
||||
import { buildAutoChatSkillPrefixOptions } from './chat-skills.mjs';
|
||||
|
||||
const h5Root = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
test('buildPublicSkillRuntimeConfig omits manifest routes when disabled', () => {
|
||||
const config = buildPublicSkillRuntimeConfig(
|
||||
{ router: { v2Enabled: false, manifestRoutingEnabled: false } },
|
||||
h5Root,
|
||||
);
|
||||
assert.equal(config.routerV2Enabled, false);
|
||||
assert.equal(config.manifestRoutingEnabled, false);
|
||||
assert.deepEqual(config.manifestRoutes, []);
|
||||
});
|
||||
|
||||
test('buildSkillRuntimeCatalogSummary includes product-campaign-page manifest', () => {
|
||||
const catalog = buildSkillRuntimeCatalogSummary(h5Root);
|
||||
const product = catalog.find((item) => item.name === 'product-campaign-page');
|
||||
assert.ok(product);
|
||||
assert.equal(product.hasManifest, true);
|
||||
assert.ok(product.triggerKeywords.includes('带货'));
|
||||
});
|
||||
|
||||
test('buildAutoChatSkillPrefixOptions returns empty object when router is disabled', () => {
|
||||
assert.deepEqual(
|
||||
buildAutoChatSkillPrefixOptions({
|
||||
routerV2Enabled: false,
|
||||
manifestRoutingEnabled: false,
|
||||
manifestRoutes: [{ skillName: 'web', keywords: ['新闻'], priority: 1, promptKey: 'web', promptVariant: null }],
|
||||
}),
|
||||
{},
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,14 @@
|
||||
name: product-campaign-page
|
||||
version: 1.0.0
|
||||
description: 商品宣传 / 活动页
|
||||
trigger:
|
||||
keywords:
|
||||
- 带货
|
||||
- 商品页
|
||||
- 种草
|
||||
- 电商页
|
||||
router:
|
||||
promptKey: product-campaign-page
|
||||
priority: 20
|
||||
executors:
|
||||
- goose
|
||||
Reference in New Issue
Block a user