@eoasmxd/freya 0.4.0 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/core/dist/agent/agent-executor.js +5 -0
- package/core/dist/command/command-executor.js +7 -0
- package/core/dist/command/command-registry.d.ts +6 -1
- package/core/dist/command/command-registry.js +31 -1
- package/core/dist/command/commands/skill-commands.js +5 -4
- package/core/dist/config/config-manager.d.ts +5 -1
- package/core/dist/config/config-manager.js +48 -1
- package/core/dist/kernel.js +4 -4
- package/core/dist/skill/skill-registry.d.ts +12 -2
- package/core/dist/skill/skill-registry.js +89 -8
- package/core/dist/tools/meta/index.d.ts +3 -1
- package/core/dist/tools/meta/index.js +9 -1
- package/core/dist/tools/tool-registry.d.ts +12 -7
- package/core/dist/tools/tool-registry.js +44 -8
- package/core/dist/web/config-api.js +18 -0
- package/core/package.json +2 -2
- package/package.json +4 -3
- package/plugins/plugin-gemini/package.json +1 -1
- package/plugins/plugin-openai/package.json +1 -1
- package/plugins/plugin-telegram-channel/package.json +1 -1
- package/plugins/plugin-tool-fs/package.json +1 -1
- package/plugins/plugin-tool-memory/package.json +1 -1
- package/plugins/plugin-tool-mysql/config/prompts/plugin.prompt.mysql.md +9 -0
- package/plugins/plugin-tool-mysql/config/prompts/plugin.prompt.mysql.select.audit.md +26 -0
- package/plugins/plugin-tool-mysql/dist/audit.d.ts +13 -0
- package/plugins/plugin-tool-mysql/dist/audit.js +87 -0
- package/plugins/plugin-tool-mysql/dist/index.d.ts +14 -0
- package/plugins/plugin-tool-mysql/dist/index.js +34 -0
- package/plugins/plugin-tool-mysql/dist/pool-manager.d.ts +32 -0
- package/plugins/plugin-tool-mysql/dist/pool-manager.js +113 -0
- package/plugins/plugin-tool-mysql/dist/tools.d.ts +11 -0
- package/plugins/plugin-tool-mysql/dist/tools.js +88 -0
- package/plugins/plugin-tool-mysql/package.json +33 -0
- package/plugins/plugin-tool-mysql/schema.json +83 -0
- package/plugins/plugin-tool-web/package.json +1 -1
- package/plugins/plugin-wecom-channel/package.json +1 -1
- package/plugins/plugin-weixin-channel/dist/index.js +10 -36
- package/plugins/plugin-weixin-channel/package.json +3 -2
- package/src/packages/core/src/agent/agent-executor.ts +5 -0
- package/src/packages/core/src/command/command-executor.ts +8 -0
- package/src/packages/core/src/command/command-registry.ts +35 -1
- package/src/packages/core/src/command/commands/skill-commands.ts +6 -5
- package/src/packages/core/src/config/config-manager.ts +50 -1
- package/src/packages/core/src/kernel.ts +5 -4
- package/src/packages/core/src/skill/skill-registry.ts +100 -8
- package/src/packages/core/src/tools/meta/index.ts +9 -1
- package/src/packages/core/src/tools/tool-registry.ts +49 -9
- package/src/packages/core/src/web/config-api.ts +20 -0
- package/src/packages/ui/src/features/config/ConfigModal.tsx +13 -1
- package/src/packages/ui/src/features/config/panels/SkillConfigPanel.tsx +137 -0
- package/src/plugins/plugin-tool-mysql/src/audit.ts +103 -0
- package/src/plugins/plugin-tool-mysql/src/index.ts +44 -0
- package/src/plugins/plugin-tool-mysql/src/pool-manager.ts +132 -0
- package/src/plugins/plugin-tool-mysql/src/tools.ts +100 -0
- package/src/plugins/plugin-weixin-channel/src/index.ts +12 -37
- package/ui/assets/{index-Be0cAgdB.js → index-BqPQMflk.js} +14 -14
- package/ui/index.html +1 -1
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
2
|
import fs from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
|
|
5
|
-
import qrcodeTerminal from "qrcode-terminal";
|
|
4
|
+
import QRCode from "qrcode";
|
|
6
5
|
const WEIXIN_MIME_MAP = {
|
|
7
6
|
".pdf": "application/pdf",
|
|
8
7
|
".txt": "text/plain",
|
|
@@ -228,45 +227,20 @@ export default class FreyaWeixinChannelPlugin {
|
|
|
228
227
|
if (!qrcode) {
|
|
229
228
|
throw new Error("获取微信登录二维码失败:服务端未返回 qrcode");
|
|
230
229
|
}
|
|
231
|
-
const
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
const cleanLine = line.replace(/[\r\n]/g, "");
|
|
236
|
-
if (!cleanLine)
|
|
237
|
-
continue;
|
|
238
|
-
const row = [];
|
|
239
|
-
const re = /\x1b\[(40|47)m \x1b\[0m/g;
|
|
240
|
-
let match;
|
|
241
|
-
while ((match = re.exec(cleanLine)) !== null) {
|
|
242
|
-
row.push(match[1] === "40" ? 1 : 0);
|
|
243
|
-
}
|
|
244
|
-
if (row.length > 0) {
|
|
245
|
-
matrix.push(row);
|
|
246
|
-
}
|
|
247
|
-
}
|
|
230
|
+
const qrAscii = await QRCode.toString(scanUrl, {
|
|
231
|
+
type: "utf8",
|
|
232
|
+
errorCorrectionLevel: "M",
|
|
233
|
+
margin: 2
|
|
248
234
|
});
|
|
249
|
-
const size = matrix.length;
|
|
250
|
-
let svgContent = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${size} ${size}" width="180" height="180" style="shape-rendering:crispEdges;">`;
|
|
251
|
-
svgContent += `<rect width="${size}" height="${size}" fill="#ffffff"/>`;
|
|
252
|
-
for (let y = 0; y < size; y++) {
|
|
253
|
-
const row = matrix[y];
|
|
254
|
-
for (let x = 0; x < row.length; x++) {
|
|
255
|
-
if (row[x] === 1) {
|
|
256
|
-
svgContent += `<rect x="${x}" y="${y}" width="1" height="1" fill="#000000"/>`;
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
svgContent += `</svg>`;
|
|
261
|
-
const base64Svg = Buffer.from(svgContent).toString("base64");
|
|
262
|
-
const qrDataUri = `data:image/svg+xml;base64,${base64Svg}`;
|
|
263
235
|
this.pollWeixinQrStatus(ctx, accountId, config, qrcode).catch((err) => {
|
|
264
236
|
ctx.logger.error(`微信账号 [${accountId}] 后台监听扫码绑定失败:`, err.message);
|
|
265
237
|
});
|
|
266
|
-
return `⚠️ **微信账号 [${accountId}] 登录二维码已成功生成!**\n\n` +
|
|
238
|
+
return (`⚠️ **微信账号 [${accountId}] 登录二维码已成功生成!**\n\n` +
|
|
267
239
|
`**[微信扫码] 请使用微信扫描下方二维码绑定账号 [${accountId}]**:\n\n` +
|
|
268
|
-
|
|
269
|
-
|
|
240
|
+
"```text\n" +
|
|
241
|
+
qrAscii +
|
|
242
|
+
"\n```\n\n" +
|
|
243
|
+
`*(提示:若字符二维码未能正常显示或无法扫描,您可以直接点击 [打开微信二维码网页](${scanUrl}) 扫码绑定)*`);
|
|
270
244
|
}
|
|
271
245
|
catch (err) {
|
|
272
246
|
ctx.logger.error(`微信账号 [${accountId}] 拉取扫码登录失败:`, err.message);
|
|
@@ -18,11 +18,12 @@
|
|
|
18
18
|
"clean": "rm -rf dist"
|
|
19
19
|
},
|
|
20
20
|
"dependencies": {
|
|
21
|
-
"@eoasmxd/freya-sdk": "^0.4.
|
|
22
|
-
"qrcode
|
|
21
|
+
"@eoasmxd/freya-sdk": "^0.4.2",
|
|
22
|
+
"qrcode": "^1.5.4"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
25
|
"@types/node": "^22.0.0",
|
|
26
|
+
"@types/qrcode": "^1.5.5",
|
|
26
27
|
"typescript": "^5.5.0"
|
|
27
28
|
}
|
|
28
29
|
}
|
|
@@ -216,6 +216,11 @@ export class FreyaAgentExecutor {
|
|
|
216
216
|
const deactivatedIds: string[] = [];
|
|
217
217
|
|
|
218
218
|
for (const id of activeToolboxIds) {
|
|
219
|
+
if (!this.toolRegistry.isToolboxEnabled(id)) {
|
|
220
|
+
deactivatedIds.push(id);
|
|
221
|
+
this.context.logger.info(`[FreyaAgentExecutor] 工具箱 [${id}] 已被全局禁用,自动从当前会话卸载。`);
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
219
224
|
const boxTools = this.toolRegistry.getToolsInBox(id);
|
|
220
225
|
const isUsed = boxTools.some((tool) => executedToolNames.has(tool.getDefinition().name));
|
|
221
226
|
|
|
@@ -32,6 +32,14 @@ export class FreyaCommandExecutor {
|
|
|
32
32
|
return true;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
if (!this.registry.isCommandEnabled(commandName)) {
|
|
36
|
+
this.context.eventBus.emit('session:reply:error', {
|
|
37
|
+
sessionId,
|
|
38
|
+
message: `❌ 权限拒绝:系统指令 "/${commandName}" 已被系统管理员全局禁用。`
|
|
39
|
+
});
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
|
|
35
43
|
try {
|
|
36
44
|
const replyContent = await cmd.execute(args, sessionId, this.context, connectionId);
|
|
37
45
|
if (replyContent) {
|
|
@@ -1,11 +1,45 @@
|
|
|
1
1
|
import type { FreyaCommand, FreyaContext } from '@eoasmxd/freya-sdk';
|
|
2
2
|
|
|
3
|
+
const BUILTIN_COMMAND_CATEGORY_MAP: Record<string, string> = {
|
|
4
|
+
approve: 'auth',
|
|
5
|
+
reject: 'auth',
|
|
6
|
+
session: 'session',
|
|
7
|
+
model: 'model',
|
|
8
|
+
models: 'model'
|
|
9
|
+
};
|
|
10
|
+
|
|
3
11
|
/** 系统指令注册表:维护主指令与别名的映射字典 */
|
|
4
12
|
export class FreyaCommandRegistry {
|
|
5
13
|
private commands = new Map<string, FreyaCommand>();
|
|
6
14
|
private aliasMap = new Map<string, string>();
|
|
7
15
|
private pluginCommandsMap = new Map<string, string[]>();
|
|
8
16
|
|
|
17
|
+
constructor(private context?: FreyaContext) {}
|
|
18
|
+
|
|
19
|
+
setContext(context: FreyaContext): void {
|
|
20
|
+
this.context = context;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** 判断指定指令当前是否已注册且处于可用启用状态 */
|
|
24
|
+
isCommandEnabled(name: string): boolean {
|
|
25
|
+
const lowerName = name.toLowerCase();
|
|
26
|
+
const primaryName = this.aliasMap.get(lowerName) || lowerName;
|
|
27
|
+
|
|
28
|
+
if (!this.commands.has(primaryName)) {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const category = BUILTIN_COMMAND_CATEGORY_MAP[primaryName];
|
|
33
|
+
if (category) {
|
|
34
|
+
const commandsConfig = (this.context?.config as any)?.commands?.builtin;
|
|
35
|
+
if (commandsConfig && typeof commandsConfig[category]?.enabled === 'boolean') {
|
|
36
|
+
return commandsConfig[category].enabled;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
|
|
9
43
|
/** 注册新的系统指令并建立别名路由 */
|
|
10
44
|
register(command: FreyaCommand, pluginId?: string): void {
|
|
11
45
|
const name = command.name.toLowerCase();
|
|
@@ -63,6 +97,6 @@ export class FreyaCommandRegistry {
|
|
|
63
97
|
}
|
|
64
98
|
|
|
65
99
|
list(): FreyaCommand[] {
|
|
66
|
-
return Array.from(this.commands.values());
|
|
100
|
+
return Array.from(this.commands.values()).filter((cmd) => this.isCommandEnabled(cmd.name));
|
|
67
101
|
}
|
|
68
102
|
}
|
|
@@ -22,11 +22,12 @@ export function registerSkillCommands(deps: SkillCommandDeps): void {
|
|
|
22
22
|
};
|
|
23
23
|
|
|
24
24
|
const handleList = async (): Promise<string> => {
|
|
25
|
-
|
|
25
|
+
const activeSkills = Array.from(skills.values()).filter((s) => s.enabled !== false);
|
|
26
|
+
if (activeSkills.length === 0) {
|
|
26
27
|
return '📋 暂无可用技能。';
|
|
27
28
|
}
|
|
28
|
-
const lines =
|
|
29
|
-
`- **${s.name}** — ${s.description || '无描述'}`
|
|
29
|
+
const lines = activeSkills.map((s) =>
|
|
30
|
+
`- **${s.name}** \`${s.id}\` — ${s.description || '无描述'}`
|
|
30
31
|
).join('\n');
|
|
31
32
|
return `### 📋 可用技能\n\n${lines}`;
|
|
32
33
|
};
|
|
@@ -36,8 +37,8 @@ export function registerSkillCommands(deps: SkillCommandDeps): void {
|
|
|
36
37
|
return '❌ 用法:`\`/skill set <skillId>\``。';
|
|
37
38
|
}
|
|
38
39
|
const skill = skills.get(skillId);
|
|
39
|
-
if (!skill) {
|
|
40
|
-
return `❌ 技能 \`${skillId}\`
|
|
40
|
+
if (!skill || skill.enabled === false) {
|
|
41
|
+
return `❌ 技能 \`${skillId}\` 不存在或未启用。请使用 \`/skill list\` 查看可用技能。`;
|
|
41
42
|
}
|
|
42
43
|
await sessionManager.updateSession(sessionId, { activeSkillId: skillId });
|
|
43
44
|
return `✅ 已激活技能: **${skill.name}** \`${skillId}\`。`;
|
|
@@ -4,6 +4,7 @@ import type { FreyaPluginManager } from '../plugin/plugin-manager.js';
|
|
|
4
4
|
import type { FreyaPromptManager } from '../prompt/prompt-manager.js';
|
|
5
5
|
import { FreyaConfigFileHandler } from './file-handler.js';
|
|
6
6
|
import { FreyaConfigSchemaRegistry } from './schema-registry.js';
|
|
7
|
+
import type { FreyaSkillRegistry, FreyaSkill } from '../skill/skill-registry.js';
|
|
7
8
|
import path from 'node:path';
|
|
8
9
|
import { PROJECT_ROOT } from '../utils/paths.js';
|
|
9
10
|
|
|
@@ -155,19 +156,22 @@ export class FreyaConfigManager {
|
|
|
155
156
|
private llmRegistry: FreyaLLMRegistry;
|
|
156
157
|
private pluginManager: FreyaPluginManager;
|
|
157
158
|
private promptManager: FreyaPromptManager;
|
|
159
|
+
private skillRegistry?: FreyaSkillRegistry;
|
|
158
160
|
|
|
159
161
|
constructor(
|
|
160
162
|
context: FreyaContext,
|
|
161
163
|
schemaRegistry: FreyaConfigSchemaRegistry,
|
|
162
164
|
promptManager: FreyaPromptManager,
|
|
163
165
|
llmRegistry: FreyaLLMRegistry,
|
|
164
|
-
pluginManager: FreyaPluginManager
|
|
166
|
+
pluginManager: FreyaPluginManager,
|
|
167
|
+
skillRegistry?: FreyaSkillRegistry
|
|
165
168
|
) {
|
|
166
169
|
this.context = context;
|
|
167
170
|
this.schemaRegistry = schemaRegistry;
|
|
168
171
|
this.promptManager = promptManager;
|
|
169
172
|
this.llmRegistry = llmRegistry;
|
|
170
173
|
this.pluginManager = pluginManager;
|
|
174
|
+
this.skillRegistry = skillRegistry;
|
|
171
175
|
}
|
|
172
176
|
|
|
173
177
|
/** 获取全部敏感字段的 keyPath 列表 */
|
|
@@ -480,6 +484,16 @@ export class FreyaConfigManager {
|
|
|
480
484
|
return await this.pluginManager.togglePlugin(pluginId, enabled);
|
|
481
485
|
}
|
|
482
486
|
|
|
487
|
+
listSkills(): FreyaSkill[] {
|
|
488
|
+
if (!this.skillRegistry) return [];
|
|
489
|
+
return this.skillRegistry.getAllSkills();
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
async toggleSkill(skillId: string, enabled: boolean): Promise<string> {
|
|
493
|
+
if (!this.skillRegistry) return '❌ 技能注册表服务未初始化。';
|
|
494
|
+
return await this.skillRegistry.toggleSkill(skillId, enabled);
|
|
495
|
+
}
|
|
496
|
+
|
|
483
497
|
async readPrompt(name: string): Promise<string> {
|
|
484
498
|
const promptName = String(name).trim().toUpperCase();
|
|
485
499
|
if (!ALLOWED_PROMPTS.has(promptName)) {
|
|
@@ -683,6 +697,41 @@ export class FreyaConfigManager {
|
|
|
683
697
|
min: 10,
|
|
684
698
|
max: 300,
|
|
685
699
|
category: '安全'
|
|
700
|
+
},
|
|
701
|
+
{
|
|
702
|
+
key: 'tools.builtin.config.enabled',
|
|
703
|
+
defaultValue: true,
|
|
704
|
+
description: '是否启用系统核心配置工具箱(允许大模型查看与修改系统配置)',
|
|
705
|
+
type: 'boolean',
|
|
706
|
+
category: '系统工具'
|
|
707
|
+
},
|
|
708
|
+
{
|
|
709
|
+
key: 'tools.builtin.session.enabled',
|
|
710
|
+
defaultValue: true,
|
|
711
|
+
description: '是否启用会话与子任务管理工具箱(允许大模型查阅会话历史与派生子任务)',
|
|
712
|
+
type: 'boolean',
|
|
713
|
+
category: '系统工具'
|
|
714
|
+
},
|
|
715
|
+
{
|
|
716
|
+
key: 'commands.builtin.auth.enabled',
|
|
717
|
+
defaultValue: true,
|
|
718
|
+
description: '是否启用敏感操作授权审批指令(/approve 与 /reject)',
|
|
719
|
+
type: 'boolean',
|
|
720
|
+
category: '系统指令'
|
|
721
|
+
},
|
|
722
|
+
{
|
|
723
|
+
key: 'commands.builtin.session.enabled',
|
|
724
|
+
defaultValue: true,
|
|
725
|
+
description: '是否启用会话管理与路由指令(/session 及其子命令)',
|
|
726
|
+
type: 'boolean',
|
|
727
|
+
category: '系统指令'
|
|
728
|
+
},
|
|
729
|
+
{
|
|
730
|
+
key: 'commands.builtin.model.enabled',
|
|
731
|
+
defaultValue: true,
|
|
732
|
+
description: '是否启用模型查看与切换指令(/model 及其子命令)',
|
|
733
|
+
type: 'boolean',
|
|
734
|
+
category: '系统指令'
|
|
686
735
|
}
|
|
687
736
|
];
|
|
688
737
|
|
|
@@ -49,11 +49,11 @@ export class FreyaKernel {
|
|
|
49
49
|
ctx.eventBus = new FreyaEventBus();
|
|
50
50
|
|
|
51
51
|
const configSchemaRegistry = new FreyaConfigSchemaRegistry();
|
|
52
|
-
const toolRegistry = new FreyaToolRegistry();
|
|
52
|
+
const toolRegistry = new FreyaToolRegistry(ctx);
|
|
53
53
|
const llmRegistry = new FreyaLLMRegistry();
|
|
54
54
|
const promptRegistry = new FreyaPromptRegistry();
|
|
55
55
|
|
|
56
|
-
const commandRegistry = new FreyaCommandRegistry();
|
|
56
|
+
const commandRegistry = new FreyaCommandRegistry(ctx);
|
|
57
57
|
this.channelRegistry = new FreyaChannelRegistry();
|
|
58
58
|
const pluginRegistry = new FreyaPluginRegistry(toolRegistry, llmRegistry, this.channelRegistry);
|
|
59
59
|
const skillRegistry = new FreyaSkillRegistry();
|
|
@@ -68,7 +68,8 @@ export class FreyaKernel {
|
|
|
68
68
|
configSchemaRegistry,
|
|
69
69
|
promptManager,
|
|
70
70
|
llmRegistry,
|
|
71
|
-
this.pluginManager
|
|
71
|
+
this.pluginManager,
|
|
72
|
+
skillRegistry
|
|
72
73
|
);
|
|
73
74
|
|
|
74
75
|
configManager.registerCoreSchema();
|
|
@@ -96,7 +97,7 @@ export class FreyaKernel {
|
|
|
96
97
|
|
|
97
98
|
const configToolbox = new ConfigToolbox(configManager, ctx);
|
|
98
99
|
const sessionToolbox = new SessionToolbox(this.sessionManager);
|
|
99
|
-
const metaToolbox = new FreyaMetaToolbox(this.sessionManager, toolRegistry);
|
|
100
|
+
const metaToolbox = new FreyaMetaToolbox(this.sessionManager, toolRegistry, skillRegistry);
|
|
100
101
|
toolRegistry.registerToolbox(configToolbox);
|
|
101
102
|
toolRegistry.registerToolbox(sessionToolbox);
|
|
102
103
|
toolRegistry.registerToolbox(metaToolbox);
|
|
@@ -8,27 +8,66 @@ export interface FreyaSkill {
|
|
|
8
8
|
name: string;
|
|
9
9
|
description: string;
|
|
10
10
|
content: string;
|
|
11
|
+
enabled: boolean;
|
|
12
|
+
source: 'builtin' | 'runtime';
|
|
11
13
|
}
|
|
12
14
|
|
|
13
|
-
/** 技能注册表,从 skills/ 目录加载 Markdown
|
|
15
|
+
/** 技能注册表,从 skills/ 目录加载 Markdown 格式技能并管理软开关状态 */
|
|
14
16
|
export class FreyaSkillRegistry {
|
|
15
17
|
private skills = new Map<string, FreyaSkill>();
|
|
18
|
+
private context?: FreyaContext;
|
|
16
19
|
|
|
17
20
|
async loadSkills(context: FreyaContext): Promise<void> {
|
|
21
|
+
this.context = context;
|
|
18
22
|
const runtimeSkillsDir = path.join(PROJECT_ROOT, 'skills');
|
|
19
23
|
const defaultSkillsDir = path.join(APP_ROOT, 'skills');
|
|
24
|
+
const configSkillsPath = path.join(PROJECT_ROOT, 'config', 'skills.json');
|
|
25
|
+
|
|
20
26
|
try {
|
|
21
27
|
await fs.mkdir(runtimeSkillsDir, { recursive: true });
|
|
22
|
-
await this.loadSkillsFromDirectory(defaultSkillsDir, context);
|
|
23
|
-
await this.loadSkillsFromDirectory(runtimeSkillsDir, context);
|
|
24
|
-
|
|
28
|
+
await this.loadSkillsFromDirectory(defaultSkillsDir, 'builtin', context);
|
|
29
|
+
await this.loadSkillsFromDirectory(runtimeSkillsDir, 'runtime', context);
|
|
30
|
+
|
|
31
|
+
let configList: Array<{ id: string; enabled: boolean }> = [];
|
|
32
|
+
try {
|
|
33
|
+
const raw = await fs.readFile(configSkillsPath, 'utf-8');
|
|
34
|
+
const parsed = JSON.parse(raw);
|
|
35
|
+
if (Array.isArray(parsed)) {
|
|
36
|
+
configList = parsed;
|
|
37
|
+
}
|
|
38
|
+
} catch {
|
|
39
|
+
configList = [];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const configMap = new Map<string, boolean>();
|
|
43
|
+
for (const item of configList) {
|
|
44
|
+
if (item && typeof item.id === 'string' && typeof item.enabled === 'boolean') {
|
|
45
|
+
configMap.set(item.id, item.enabled);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
let configChanged = false;
|
|
50
|
+
for (const skill of this.skills.values()) {
|
|
51
|
+
if (configMap.has(skill.id)) {
|
|
52
|
+
skill.enabled = configMap.get(skill.id)!;
|
|
53
|
+
} else {
|
|
54
|
+
configChanged = true;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (configChanged || configList.length !== this.skills.size) {
|
|
59
|
+
await this.persistSkillsConfig(configSkillsPath);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const enabledCount = Array.from(this.skills.values()).filter((s) => s.enabled).length;
|
|
63
|
+
context.logger.info(`动态技能扫描完成。共加载 ${this.skills.size} 个物理技能 (已启用: ${enabledCount})。`);
|
|
25
64
|
} catch (err: any) {
|
|
26
65
|
context.logger.error('扫描物理技能 skills 目录遭遇故障:', err);
|
|
27
66
|
}
|
|
28
67
|
}
|
|
29
68
|
|
|
30
69
|
/** 从指定目录加载技能到内存注册表中 */
|
|
31
|
-
private async loadSkillsFromDirectory(dirPath: string, context: FreyaContext): Promise<void> {
|
|
70
|
+
private async loadSkillsFromDirectory(dirPath: string, source: 'builtin' | 'runtime', context: FreyaContext): Promise<void> {
|
|
32
71
|
try {
|
|
33
72
|
const files = await fs.readdir(dirPath);
|
|
34
73
|
for (const file of files) {
|
|
@@ -36,11 +75,17 @@ export class FreyaSkillRegistry {
|
|
|
36
75
|
const rawContent = await fs.readFile(path.join(dirPath, file), 'utf-8');
|
|
37
76
|
const { metadata, content } = this.parseFrontmatter(rawContent);
|
|
38
77
|
if (metadata.id) {
|
|
78
|
+
const defaultEnabled = metadata.defaultEnabled !== undefined
|
|
79
|
+
? metadata.defaultEnabled !== 'false'
|
|
80
|
+
: source === 'builtin';
|
|
81
|
+
|
|
39
82
|
this.skills.set(metadata.id, {
|
|
40
83
|
id: metadata.id,
|
|
41
84
|
name: metadata.name || file.replace('.md', ''),
|
|
42
85
|
description: metadata.description || '',
|
|
43
|
-
content: content.trim()
|
|
86
|
+
content: content.trim(),
|
|
87
|
+
enabled: defaultEnabled,
|
|
88
|
+
source
|
|
44
89
|
});
|
|
45
90
|
}
|
|
46
91
|
}
|
|
@@ -50,8 +95,55 @@ export class FreyaSkillRegistry {
|
|
|
50
95
|
}
|
|
51
96
|
}
|
|
52
97
|
|
|
53
|
-
|
|
54
|
-
|
|
98
|
+
/** 获取技能列表(默认只返回启用状态的技能) */
|
|
99
|
+
getSkills(onlyEnabled: boolean = true): Map<string, FreyaSkill> {
|
|
100
|
+
if (!onlyEnabled) {
|
|
101
|
+
return this.skills;
|
|
102
|
+
}
|
|
103
|
+
const filtered = new Map<string, FreyaSkill>();
|
|
104
|
+
for (const [id, skill] of this.skills.entries()) {
|
|
105
|
+
if (skill.enabled) {
|
|
106
|
+
filtered.set(id, skill);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return filtered;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** 获取全量技能数组(供管理控制台使用) */
|
|
113
|
+
getAllSkills(): FreyaSkill[] {
|
|
114
|
+
return Array.from(this.skills.values());
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** 切换技能的启用/禁用状态并持久化 */
|
|
118
|
+
async toggleSkill(skillId: string, enabled: boolean): Promise<string> {
|
|
119
|
+
const skill = this.skills.get(skillId);
|
|
120
|
+
if (!skill) {
|
|
121
|
+
return `❌ 未找到 ID 为 "${skillId}" 的技能,请检查名称是否正确。`;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (skill.enabled === enabled) {
|
|
125
|
+
return `ℹ️ 技能 "${skill.name || skillId}" 状态已是 ${enabled ? '启用' : '禁用'}。`;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
skill.enabled = enabled;
|
|
129
|
+
const configSkillsPath = path.join(PROJECT_ROOT, 'config', 'skills.json');
|
|
130
|
+
try {
|
|
131
|
+
await this.persistSkillsConfig(configSkillsPath);
|
|
132
|
+
this.context?.logger.info(`技能 "${skill.name || skillId}" 已切换为: ${enabled ? '启用' : '禁用'}`);
|
|
133
|
+
return `✅ 技能 "${skill.name || skillId}" 已成功${enabled ? '启用' : '禁用'}。`;
|
|
134
|
+
} catch (err: any) {
|
|
135
|
+
return `❌ 技能状态变更成功,但写入 skills.json 失败: ${err.message}`;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** 持久化当前所有技能启停状态至 skills.json */
|
|
140
|
+
private async persistSkillsConfig(configSkillsPath: string): Promise<void> {
|
|
141
|
+
await fs.mkdir(path.dirname(configSkillsPath), { recursive: true });
|
|
142
|
+
const payload = Array.from(this.skills.values()).map((s) => ({
|
|
143
|
+
id: s.id,
|
|
144
|
+
enabled: s.enabled
|
|
145
|
+
}));
|
|
146
|
+
await fs.writeFile(configSkillsPath, JSON.stringify(payload, null, 2) + '\n', 'utf-8');
|
|
55
147
|
}
|
|
56
148
|
|
|
57
149
|
get(id: string): FreyaSkill | undefined {
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import type { FreyaTool, FreyaToolbox } from '@eoasmxd/freya-sdk';
|
|
2
2
|
import type { FreyaSessionManager } from '../../session/session-manager.js';
|
|
3
3
|
import type { FreyaToolRegistry } from '../tool-registry.js';
|
|
4
|
+
import type { FreyaSkillRegistry } from '../../skill/skill-registry.js';
|
|
4
5
|
|
|
5
6
|
export class FreyaMetaToolbox implements FreyaToolbox {
|
|
6
7
|
constructor(
|
|
7
8
|
private sessionManager: FreyaSessionManager,
|
|
8
|
-
private toolRegistry?: FreyaToolRegistry
|
|
9
|
+
private toolRegistry?: FreyaToolRegistry,
|
|
10
|
+
private skillRegistry?: FreyaSkillRegistry
|
|
9
11
|
) {}
|
|
10
12
|
|
|
11
13
|
getId(): string {
|
|
@@ -106,6 +108,12 @@ export class FreyaMetaToolbox implements FreyaToolbox {
|
|
|
106
108
|
if (!sessionId) {
|
|
107
109
|
return '❌ 错误:无法从执行上下文中提取当前会话ID。';
|
|
108
110
|
}
|
|
111
|
+
if (this.skillRegistry) {
|
|
112
|
+
const skill = this.skillRegistry.get(skillId);
|
|
113
|
+
if (!skill || !skill.enabled) {
|
|
114
|
+
return `❌ 错误:技能 [${skillId}] 不存在或已被系统管理员禁用,无法切入该模式。`;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
109
117
|
await this.sessionManager.updateSession(sessionId, { activeSkillId: skillId });
|
|
110
118
|
return `已成功切入 [${skillId}] 技能特长工作模式。`;
|
|
111
119
|
}
|
|
@@ -1,14 +1,39 @@
|
|
|
1
|
-
import type { FreyaTool, FreyaToolbox } from '@eoasmxd/freya-sdk';
|
|
1
|
+
import type { FreyaContext, FreyaTool, FreyaToolbox } from '@eoasmxd/freya-sdk';
|
|
2
2
|
import type { FreyaPromptRegistry } from '../prompt/prompt-registry.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* 内核工具注册表。
|
|
6
|
-
*
|
|
6
|
+
* 统一聚合内置工具箱与来自插件体系的外部工具箱。
|
|
7
7
|
*/
|
|
8
8
|
export class FreyaToolRegistry {
|
|
9
9
|
private toolboxes: FreyaToolbox[] = [];
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
constructor(private context?: FreyaContext) {}
|
|
12
|
+
|
|
13
|
+
setContext(context: FreyaContext): void {
|
|
14
|
+
this.context = context;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** 判断指定工具箱当前是否已注册且处于可用启用状态 */
|
|
18
|
+
isToolboxEnabled(toolboxId: string): boolean {
|
|
19
|
+
if (toolboxId === 'meta') {
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const isRegistered = this.toolboxes.some((tb) => tb.getId() === toolboxId);
|
|
24
|
+
if (!isRegistered) {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const toolsConfig = (this.context?.config as any)?.tools?.builtin;
|
|
29
|
+
if (toolsConfig && typeof toolsConfig[toolboxId]?.enabled === 'boolean') {
|
|
30
|
+
return toolsConfig[toolboxId].enabled;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** 注册工具箱 */
|
|
12
37
|
registerToolbox(toolbox: FreyaToolbox): void {
|
|
13
38
|
const newId = toolbox.getId();
|
|
14
39
|
const existingIndex = this.toolboxes.findIndex((tb) => tb.getId() === newId || tb === toolbox);
|
|
@@ -25,16 +50,22 @@ export class FreyaToolRegistry {
|
|
|
25
50
|
this.toolboxes = this.toolboxes.filter((tb) => tb.getId() !== id);
|
|
26
51
|
}
|
|
27
52
|
|
|
28
|
-
/** 获取指定 ID
|
|
53
|
+
/** 获取指定 ID 的工具箱中的所有原子工具 */
|
|
29
54
|
getToolsInBox(id: string): FreyaTool[] {
|
|
55
|
+
if (!this.isToolboxEnabled(id)) {
|
|
56
|
+
return [];
|
|
57
|
+
}
|
|
30
58
|
const tb = this.toolboxes.find((t) => t.getId() === id);
|
|
31
59
|
return tb ? tb.getTools() : [];
|
|
32
60
|
}
|
|
33
61
|
|
|
34
|
-
/**
|
|
62
|
+
/** 聚合所有来源的已启用工具 */
|
|
35
63
|
getAllTools(): Map<string, FreyaTool> {
|
|
36
64
|
const tools = new Map<string, FreyaTool>();
|
|
37
65
|
for (const toolbox of this.toolboxes) {
|
|
66
|
+
if (!this.isToolboxEnabled(toolbox.getId())) {
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
38
69
|
for (const tool of toolbox.getTools()) {
|
|
39
70
|
tools.set(tool.getDefinition().name, tool);
|
|
40
71
|
}
|
|
@@ -42,13 +73,16 @@ export class FreyaToolRegistry {
|
|
|
42
73
|
return tools;
|
|
43
74
|
}
|
|
44
75
|
|
|
45
|
-
/**
|
|
76
|
+
/** 根据当前会话已激活的工具箱列表,过滤获取所需的工具字典 */
|
|
46
77
|
getFilteredTools(activeToolboxIds: string[]): Map<string, FreyaTool> {
|
|
47
78
|
const activeSet = new Set(activeToolboxIds || []);
|
|
48
79
|
const tools = new Map<string, FreyaTool>();
|
|
49
80
|
|
|
50
81
|
for (const toolbox of this.toolboxes) {
|
|
51
82
|
const toolboxId = toolbox.getId();
|
|
83
|
+
if (!this.isToolboxEnabled(toolboxId)) {
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
52
86
|
if (toolboxId === 'meta' || activeSet.has(toolboxId)) {
|
|
53
87
|
for (const tool of toolbox.getTools()) {
|
|
54
88
|
tools.set(tool.getDefinition().name, tool);
|
|
@@ -58,16 +92,19 @@ export class FreyaToolRegistry {
|
|
|
58
92
|
return tools;
|
|
59
93
|
}
|
|
60
94
|
|
|
61
|
-
/**
|
|
95
|
+
/** 聚合所有已启用的工具箱提示词引导说明 */
|
|
62
96
|
getToolInstructions(promptRegistry: FreyaPromptRegistry): string[] {
|
|
63
97
|
const instructions: string[] = [];
|
|
64
98
|
for (const toolbox of this.toolboxes) {
|
|
99
|
+
const toolboxId = toolbox.getId();
|
|
100
|
+
if (!this.isToolboxEnabled(toolboxId)) {
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
65
103
|
const key = toolbox.getInstructionPrompt?.();
|
|
66
104
|
if (!key) continue;
|
|
67
105
|
|
|
68
106
|
const resolved = promptRegistry.get(key);
|
|
69
107
|
if (resolved) {
|
|
70
|
-
const toolboxId = toolbox.getId();
|
|
71
108
|
instructions.push(`### 工具箱能力说明 [激活ID: "${toolboxId}"]\n${resolved}`);
|
|
72
109
|
}
|
|
73
110
|
}
|
|
@@ -75,6 +112,9 @@ export class FreyaToolRegistry {
|
|
|
75
112
|
}
|
|
76
113
|
|
|
77
114
|
getRegisteredToolboxIds(): string[] {
|
|
78
|
-
return this.toolboxes
|
|
115
|
+
return this.toolboxes
|
|
116
|
+
.map((tb) => tb.getId())
|
|
117
|
+
.filter((id) => this.isToolboxEnabled(id));
|
|
79
118
|
}
|
|
80
119
|
}
|
|
120
|
+
|
|
@@ -199,6 +199,26 @@ export class FreyaConfigApi {
|
|
|
199
199
|
return true;
|
|
200
200
|
}
|
|
201
201
|
|
|
202
|
+
if (pathname === '/api/config/skills' && req.method === 'GET') {
|
|
203
|
+
const skills = this.configManager.listSkills();
|
|
204
|
+
res.writeHead(200, this.headers);
|
|
205
|
+
res.end(JSON.stringify({ success: true, data: skills }));
|
|
206
|
+
return true;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (pathname === '/api/config/skills/toggle' && req.method === 'POST') {
|
|
210
|
+
const { skillId, enabled } = await this.getBody(req);
|
|
211
|
+
if (!skillId) {
|
|
212
|
+
res.writeHead(200, this.headers);
|
|
213
|
+
res.end(JSON.stringify({ success: false, error: '缺少必要参数: skillId' }));
|
|
214
|
+
return true;
|
|
215
|
+
}
|
|
216
|
+
const msg = await this.configManager.toggleSkill(skillId, enabled);
|
|
217
|
+
res.writeHead(200, this.headers);
|
|
218
|
+
res.end(JSON.stringify({ success: !msg.startsWith('❌'), message: msg }));
|
|
219
|
+
return true;
|
|
220
|
+
}
|
|
221
|
+
|
|
202
222
|
const promptMatch = pathname.match(/^\/api\/config\/prompts\/([^/]+)$/);
|
|
203
223
|
if (promptMatch && req.method === 'GET') {
|
|
204
224
|
const promptName = decodeURIComponent(promptMatch[1]);
|