@eoasmxd/freya 0.4.0 → 0.4.1

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.
Files changed (45) hide show
  1. package/core/dist/command/commands/skill-commands.js +5 -4
  2. package/core/dist/config/config-manager.d.ts +5 -1
  3. package/core/dist/config/config-manager.js +13 -1
  4. package/core/dist/kernel.js +2 -2
  5. package/core/dist/skill/skill-registry.d.ts +12 -2
  6. package/core/dist/skill/skill-registry.js +89 -8
  7. package/core/dist/tools/meta/index.d.ts +3 -1
  8. package/core/dist/tools/meta/index.js +9 -1
  9. package/core/dist/web/config-api.js +18 -0
  10. package/core/package.json +2 -2
  11. package/package.json +3 -2
  12. package/plugins/plugin-gemini/package.json +1 -1
  13. package/plugins/plugin-openai/package.json +1 -1
  14. package/plugins/plugin-telegram-channel/package.json +1 -1
  15. package/plugins/plugin-tool-fs/package.json +1 -1
  16. package/plugins/plugin-tool-memory/package.json +1 -1
  17. package/plugins/plugin-tool-mysql/config/prompts/plugin.prompt.mysql.md +9 -0
  18. package/plugins/plugin-tool-mysql/config/prompts/plugin.prompt.mysql.select.audit.md +26 -0
  19. package/plugins/plugin-tool-mysql/dist/audit.d.ts +13 -0
  20. package/plugins/plugin-tool-mysql/dist/audit.js +87 -0
  21. package/plugins/plugin-tool-mysql/dist/index.d.ts +14 -0
  22. package/plugins/plugin-tool-mysql/dist/index.js +34 -0
  23. package/plugins/plugin-tool-mysql/dist/pool-manager.d.ts +32 -0
  24. package/plugins/plugin-tool-mysql/dist/pool-manager.js +113 -0
  25. package/plugins/plugin-tool-mysql/dist/tools.d.ts +11 -0
  26. package/plugins/plugin-tool-mysql/dist/tools.js +88 -0
  27. package/plugins/plugin-tool-mysql/package.json +33 -0
  28. package/plugins/plugin-tool-mysql/schema.json +83 -0
  29. package/plugins/plugin-tool-web/package.json +1 -1
  30. package/plugins/plugin-wecom-channel/package.json +1 -1
  31. package/plugins/plugin-weixin-channel/package.json +1 -1
  32. package/src/packages/core/src/command/commands/skill-commands.ts +6 -5
  33. package/src/packages/core/src/config/config-manager.ts +15 -1
  34. package/src/packages/core/src/kernel.ts +3 -2
  35. package/src/packages/core/src/skill/skill-registry.ts +100 -8
  36. package/src/packages/core/src/tools/meta/index.ts +9 -1
  37. package/src/packages/core/src/web/config-api.ts +20 -0
  38. package/src/packages/ui/src/features/config/ConfigModal.tsx +13 -1
  39. package/src/packages/ui/src/features/config/panels/SkillConfigPanel.tsx +137 -0
  40. package/src/plugins/plugin-tool-mysql/src/audit.ts +103 -0
  41. package/src/plugins/plugin-tool-mysql/src/index.ts +44 -0
  42. package/src/plugins/plugin-tool-mysql/src/pool-manager.ts +132 -0
  43. package/src/plugins/plugin-tool-mysql/src/tools.ts +100 -0
  44. package/ui/assets/{index-Be0cAgdB.js → index-BqPQMflk.js} +14 -14
  45. package/ui/index.html +1 -1
@@ -10,10 +10,11 @@ export function registerSkillCommands(deps) {
10
10
  return '🔧 当前未激活任何技能。';
11
11
  };
12
12
  const handleList = async () => {
13
- if (skills.size === 0) {
13
+ const activeSkills = Array.from(skills.values()).filter((s) => s.enabled !== false);
14
+ if (activeSkills.length === 0) {
14
15
  return '📋 暂无可用技能。';
15
16
  }
16
- const lines = Array.from(skills.values()).map((s) => `- **${s.name}** — ${s.description || '无描述'}`).join('\n');
17
+ const lines = activeSkills.map((s) => `- **${s.name}** \`${s.id}\` — ${s.description || '无描述'}`).join('\n');
17
18
  return `### 📋 可用技能\n\n${lines}`;
18
19
  };
19
20
  const handleSet = async (skillId, sessionId) => {
@@ -21,8 +22,8 @@ export function registerSkillCommands(deps) {
21
22
  return '❌ 用法:`\`/skill set <skillId>\``。';
22
23
  }
23
24
  const skill = skills.get(skillId);
24
- if (!skill) {
25
- return `❌ 技能 \`${skillId}\` 不存在。请使用 \`/skill list\` 查看可用技能。`;
25
+ if (!skill || skill.enabled === false) {
26
+ return `❌ 技能 \`${skillId}\` 不存在或未启用。请使用 \`/skill list\` 查看可用技能。`;
26
27
  }
27
28
  await sessionManager.updateSession(sessionId, { activeSkillId: skillId });
28
29
  return `✅ 已激活技能: **${skill.name}** \`${skillId}\`。`;
@@ -3,6 +3,7 @@ import type { FreyaLLMRegistry } from '../llm/llm-registry.js';
3
3
  import type { FreyaPluginManager } from '../plugin/plugin-manager.js';
4
4
  import type { FreyaPromptManager } from '../prompt/prompt-manager.js';
5
5
  import { FreyaConfigSchemaRegistry } from './schema-registry.js';
6
+ import type { FreyaSkillRegistry, FreyaSkill } from '../skill/skill-registry.js';
6
7
  /** 核心统一配置管理器 */
7
8
  export declare class FreyaConfigManager {
8
9
  private context;
@@ -11,7 +12,8 @@ export declare class FreyaConfigManager {
11
12
  private llmRegistry;
12
13
  private pluginManager;
13
14
  private promptManager;
14
- constructor(context: FreyaContext, schemaRegistry: FreyaConfigSchemaRegistry, promptManager: FreyaPromptManager, llmRegistry: FreyaLLMRegistry, pluginManager: FreyaPluginManager);
15
+ private skillRegistry?;
16
+ constructor(context: FreyaContext, schemaRegistry: FreyaConfigSchemaRegistry, promptManager: FreyaPromptManager, llmRegistry: FreyaLLMRegistry, pluginManager: FreyaPluginManager, skillRegistry?: FreyaSkillRegistry);
15
17
  /** 获取全部敏感字段的 keyPath 列表 */
16
18
  getSensitiveKeys(): string[];
17
19
  loadAndInit(): Promise<void>;
@@ -38,6 +40,8 @@ export declare class FreyaConfigManager {
38
40
  removeModel(providerId: string, modelId: string): Promise<string>;
39
41
  listPlugins(): Promise<any[]>;
40
42
  togglePlugin(pluginId: string, enabled: boolean): Promise<string>;
43
+ listSkills(): FreyaSkill[];
44
+ toggleSkill(skillId: string, enabled: boolean): Promise<string>;
41
45
  readPrompt(name: string): Promise<string>;
42
46
  writePrompt(name: string, content: string): Promise<string>;
43
47
  editPrompt(name: string, targetContent: string, replacementContent: string): Promise<string>;
@@ -138,12 +138,14 @@ export class FreyaConfigManager {
138
138
  llmRegistry;
139
139
  pluginManager;
140
140
  promptManager;
141
- constructor(context, schemaRegistry, promptManager, llmRegistry, pluginManager) {
141
+ skillRegistry;
142
+ constructor(context, schemaRegistry, promptManager, llmRegistry, pluginManager, skillRegistry) {
142
143
  this.context = context;
143
144
  this.schemaRegistry = schemaRegistry;
144
145
  this.promptManager = promptManager;
145
146
  this.llmRegistry = llmRegistry;
146
147
  this.pluginManager = pluginManager;
148
+ this.skillRegistry = skillRegistry;
147
149
  }
148
150
  /** 获取全部敏感字段的 keyPath 列表 */
149
151
  getSensitiveKeys() {
@@ -475,6 +477,16 @@ export class FreyaConfigManager {
475
477
  return '❌ 插件服务未初始化。';
476
478
  return await this.pluginManager.togglePlugin(pluginId, enabled);
477
479
  }
480
+ listSkills() {
481
+ if (!this.skillRegistry)
482
+ return [];
483
+ return this.skillRegistry.getAllSkills();
484
+ }
485
+ async toggleSkill(skillId, enabled) {
486
+ if (!this.skillRegistry)
487
+ return '❌ 技能注册表服务未初始化。';
488
+ return await this.skillRegistry.toggleSkill(skillId, enabled);
489
+ }
478
490
  async readPrompt(name) {
479
491
  const promptName = String(name).trim().toUpperCase();
480
492
  if (!ALLOWED_PROMPTS.has(promptName)) {
@@ -54,7 +54,7 @@ export class FreyaKernel {
54
54
  const promptManager = new FreyaPromptManager(promptRegistry, ctx.logger);
55
55
  this.pluginManager = new FreyaPluginManager(configSchemaRegistry, commandRegistry, promptRegistry);
56
56
  await this.pluginManager.loadConfiguredPlugins(pluginRegistry, ctx);
57
- const configManager = new FreyaConfigManager(ctx, configSchemaRegistry, promptManager, llmRegistry, this.pluginManager);
57
+ const configManager = new FreyaConfigManager(ctx, configSchemaRegistry, promptManager, llmRegistry, this.pluginManager, skillRegistry);
58
58
  configManager.registerCoreSchema();
59
59
  await configManager.loadAndInit();
60
60
  await configManager.resolveAndFreeze();
@@ -72,7 +72,7 @@ export class FreyaKernel {
72
72
  this.connectionManager = new FreyaConnectionManager(ctx.eventBus, ctx.logger);
73
73
  const configToolbox = new ConfigToolbox(configManager, ctx);
74
74
  const sessionToolbox = new SessionToolbox(this.sessionManager);
75
- const metaToolbox = new FreyaMetaToolbox(this.sessionManager, toolRegistry);
75
+ const metaToolbox = new FreyaMetaToolbox(this.sessionManager, toolRegistry, skillRegistry);
76
76
  toolRegistry.registerToolbox(configToolbox);
77
77
  toolRegistry.registerToolbox(sessionToolbox);
78
78
  toolRegistry.registerToolbox(metaToolbox);
@@ -4,14 +4,24 @@ export interface FreyaSkill {
4
4
  name: string;
5
5
  description: string;
6
6
  content: string;
7
+ enabled: boolean;
8
+ source: 'builtin' | 'runtime';
7
9
  }
8
- /** 技能注册表,从 skills/ 目录加载 Markdown 格式技能 */
10
+ /** 技能注册表,从 skills/ 目录加载 Markdown 格式技能并管理软开关状态 */
9
11
  export declare class FreyaSkillRegistry {
10
12
  private skills;
13
+ private context?;
11
14
  loadSkills(context: FreyaContext): Promise<void>;
12
15
  /** 从指定目录加载技能到内存注册表中 */
13
16
  private loadSkillsFromDirectory;
14
- getSkills(): Map<string, FreyaSkill>;
17
+ /** 获取技能列表(默认只返回启用状态的技能) */
18
+ getSkills(onlyEnabled?: boolean): Map<string, FreyaSkill>;
19
+ /** 获取全量技能数组(供管理控制台使用) */
20
+ getAllSkills(): FreyaSkill[];
21
+ /** 切换技能的启用/禁用状态并持久化 */
22
+ toggleSkill(skillId: string, enabled: boolean): Promise<string>;
23
+ /** 持久化当前所有技能启停状态至 skills.json */
24
+ private persistSkillsConfig;
15
25
  get(id: string): FreyaSkill | undefined;
16
26
  has(id: string): boolean;
17
27
  /** 解析技能文件的 YAML Frontmatter */
@@ -1,24 +1,57 @@
1
1
  import fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import { APP_ROOT, PROJECT_ROOT } from '../utils/paths.js';
4
- /** 技能注册表,从 skills/ 目录加载 Markdown 格式技能 */
4
+ /** 技能注册表,从 skills/ 目录加载 Markdown 格式技能并管理软开关状态 */
5
5
  export class FreyaSkillRegistry {
6
6
  skills = new Map();
7
+ context;
7
8
  async loadSkills(context) {
9
+ this.context = context;
8
10
  const runtimeSkillsDir = path.join(PROJECT_ROOT, 'skills');
9
11
  const defaultSkillsDir = path.join(APP_ROOT, 'skills');
12
+ const configSkillsPath = path.join(PROJECT_ROOT, 'config', 'skills.json');
10
13
  try {
11
14
  await fs.mkdir(runtimeSkillsDir, { recursive: true });
12
- await this.loadSkillsFromDirectory(defaultSkillsDir, context);
13
- await this.loadSkillsFromDirectory(runtimeSkillsDir, context);
14
- context.logger.info(`动态技能扫描完成。共加载了 ${this.skills.size} 个物理技能。`);
15
+ await this.loadSkillsFromDirectory(defaultSkillsDir, 'builtin', context);
16
+ await this.loadSkillsFromDirectory(runtimeSkillsDir, 'runtime', context);
17
+ let configList = [];
18
+ try {
19
+ const raw = await fs.readFile(configSkillsPath, 'utf-8');
20
+ const parsed = JSON.parse(raw);
21
+ if (Array.isArray(parsed)) {
22
+ configList = parsed;
23
+ }
24
+ }
25
+ catch {
26
+ configList = [];
27
+ }
28
+ const configMap = new Map();
29
+ for (const item of configList) {
30
+ if (item && typeof item.id === 'string' && typeof item.enabled === 'boolean') {
31
+ configMap.set(item.id, item.enabled);
32
+ }
33
+ }
34
+ let configChanged = false;
35
+ for (const skill of this.skills.values()) {
36
+ if (configMap.has(skill.id)) {
37
+ skill.enabled = configMap.get(skill.id);
38
+ }
39
+ else {
40
+ configChanged = true;
41
+ }
42
+ }
43
+ if (configChanged || configList.length !== this.skills.size) {
44
+ await this.persistSkillsConfig(configSkillsPath);
45
+ }
46
+ const enabledCount = Array.from(this.skills.values()).filter((s) => s.enabled).length;
47
+ context.logger.info(`动态技能扫描完成。共加载 ${this.skills.size} 个物理技能 (已启用: ${enabledCount})。`);
15
48
  }
16
49
  catch (err) {
17
50
  context.logger.error('扫描物理技能 skills 目录遭遇故障:', err);
18
51
  }
19
52
  }
20
53
  /** 从指定目录加载技能到内存注册表中 */
21
- async loadSkillsFromDirectory(dirPath, context) {
54
+ async loadSkillsFromDirectory(dirPath, source, context) {
22
55
  try {
23
56
  const files = await fs.readdir(dirPath);
24
57
  for (const file of files) {
@@ -26,11 +59,16 @@ export class FreyaSkillRegistry {
26
59
  const rawContent = await fs.readFile(path.join(dirPath, file), 'utf-8');
27
60
  const { metadata, content } = this.parseFrontmatter(rawContent);
28
61
  if (metadata.id) {
62
+ const defaultEnabled = metadata.defaultEnabled !== undefined
63
+ ? metadata.defaultEnabled !== 'false'
64
+ : source === 'builtin';
29
65
  this.skills.set(metadata.id, {
30
66
  id: metadata.id,
31
67
  name: metadata.name || file.replace('.md', ''),
32
68
  description: metadata.description || '',
33
- content: content.trim()
69
+ content: content.trim(),
70
+ enabled: defaultEnabled,
71
+ source
34
72
  });
35
73
  }
36
74
  }
@@ -40,8 +78,51 @@ export class FreyaSkillRegistry {
40
78
  context.logger.warn(`扫描技能目录失败: ${dirPath}`, err);
41
79
  }
42
80
  }
43
- getSkills() {
44
- return this.skills;
81
+ /** 获取技能列表(默认只返回启用状态的技能) */
82
+ getSkills(onlyEnabled = true) {
83
+ if (!onlyEnabled) {
84
+ return this.skills;
85
+ }
86
+ const filtered = new Map();
87
+ for (const [id, skill] of this.skills.entries()) {
88
+ if (skill.enabled) {
89
+ filtered.set(id, skill);
90
+ }
91
+ }
92
+ return filtered;
93
+ }
94
+ /** 获取全量技能数组(供管理控制台使用) */
95
+ getAllSkills() {
96
+ return Array.from(this.skills.values());
97
+ }
98
+ /** 切换技能的启用/禁用状态并持久化 */
99
+ async toggleSkill(skillId, enabled) {
100
+ const skill = this.skills.get(skillId);
101
+ if (!skill) {
102
+ return `❌ 未找到 ID 为 "${skillId}" 的技能,请检查名称是否正确。`;
103
+ }
104
+ if (skill.enabled === enabled) {
105
+ return `ℹ️ 技能 "${skill.name || skillId}" 状态已是 ${enabled ? '启用' : '禁用'}。`;
106
+ }
107
+ skill.enabled = enabled;
108
+ const configSkillsPath = path.join(PROJECT_ROOT, 'config', 'skills.json');
109
+ try {
110
+ await this.persistSkillsConfig(configSkillsPath);
111
+ this.context?.logger.info(`技能 "${skill.name || skillId}" 已切换为: ${enabled ? '启用' : '禁用'}`);
112
+ return `✅ 技能 "${skill.name || skillId}" 已成功${enabled ? '启用' : '禁用'}。`;
113
+ }
114
+ catch (err) {
115
+ return `❌ 技能状态变更成功,但写入 skills.json 失败: ${err.message}`;
116
+ }
117
+ }
118
+ /** 持久化当前所有技能启停状态至 skills.json */
119
+ async persistSkillsConfig(configSkillsPath) {
120
+ await fs.mkdir(path.dirname(configSkillsPath), { recursive: true });
121
+ const payload = Array.from(this.skills.values()).map((s) => ({
122
+ id: s.id,
123
+ enabled: s.enabled
124
+ }));
125
+ await fs.writeFile(configSkillsPath, JSON.stringify(payload, null, 2) + '\n', 'utf-8');
45
126
  }
46
127
  get(id) {
47
128
  return this.skills.get(id);
@@ -1,10 +1,12 @@
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
  export declare class FreyaMetaToolbox implements FreyaToolbox {
5
6
  private sessionManager;
6
7
  private toolRegistry?;
7
- constructor(sessionManager: FreyaSessionManager, toolRegistry?: FreyaToolRegistry | undefined);
8
+ private skillRegistry?;
9
+ constructor(sessionManager: FreyaSessionManager, toolRegistry?: FreyaToolRegistry | undefined, skillRegistry?: FreyaSkillRegistry | undefined);
8
10
  getId(): string;
9
11
  getInstructionPrompt(): string;
10
12
  getTools(): FreyaTool[];
@@ -1,9 +1,11 @@
1
1
  export class FreyaMetaToolbox {
2
2
  sessionManager;
3
3
  toolRegistry;
4
- constructor(sessionManager, toolRegistry) {
4
+ skillRegistry;
5
+ constructor(sessionManager, toolRegistry, skillRegistry) {
5
6
  this.sessionManager = sessionManager;
6
7
  this.toolRegistry = toolRegistry;
8
+ this.skillRegistry = skillRegistry;
7
9
  }
8
10
  getId() {
9
11
  return 'meta';
@@ -99,6 +101,12 @@ export class FreyaMetaToolbox {
99
101
  if (!sessionId) {
100
102
  return '❌ 错误:无法从执行上下文中提取当前会话ID。';
101
103
  }
104
+ if (this.skillRegistry) {
105
+ const skill = this.skillRegistry.get(skillId);
106
+ if (!skill || !skill.enabled) {
107
+ return `❌ 错误:技能 [${skillId}] 不存在或已被系统管理员禁用,无法切入该模式。`;
108
+ }
109
+ }
102
110
  await this.sessionManager.updateSession(sessionId, { activeSkillId: skillId });
103
111
  return `已成功切入 [${skillId}] 技能特长工作模式。`;
104
112
  }
@@ -177,6 +177,24 @@ export class FreyaConfigApi {
177
177
  res.end(JSON.stringify({ success: !msg.startsWith('❌'), message: msg }));
178
178
  return true;
179
179
  }
180
+ if (pathname === '/api/config/skills' && req.method === 'GET') {
181
+ const skills = this.configManager.listSkills();
182
+ res.writeHead(200, this.headers);
183
+ res.end(JSON.stringify({ success: true, data: skills }));
184
+ return true;
185
+ }
186
+ if (pathname === '/api/config/skills/toggle' && req.method === 'POST') {
187
+ const { skillId, enabled } = await this.getBody(req);
188
+ if (!skillId) {
189
+ res.writeHead(200, this.headers);
190
+ res.end(JSON.stringify({ success: false, error: '缺少必要参数: skillId' }));
191
+ return true;
192
+ }
193
+ const msg = await this.configManager.toggleSkill(skillId, enabled);
194
+ res.writeHead(200, this.headers);
195
+ res.end(JSON.stringify({ success: !msg.startsWith('❌'), message: msg }));
196
+ return true;
197
+ }
180
198
  const promptMatch = pathname.match(/^\/api\/config\/prompts\/([^/]+)$/);
181
199
  if (promptMatch && req.method === 'GET') {
182
200
  const promptName = decodeURIComponent(promptMatch[1]);
package/core/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eoasmxd/freya-core",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "private": false,
5
5
  "license": "MIT",
6
6
  "publishConfig": {
@@ -16,7 +16,7 @@
16
16
  "clean": "rm -rf dist"
17
17
  },
18
18
  "dependencies": {
19
- "@eoasmxd/freya-sdk": "^0.4.0",
19
+ "@eoasmxd/freya-sdk": "^0.4.1",
20
20
  "ws": "^8.18.0"
21
21
  },
22
22
  "devDependencies": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eoasmxd/freya",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "description": "Freya - 微内核智能体系统",
@@ -35,8 +35,9 @@
35
35
  "src"
36
36
  ],
37
37
  "dependencies": {
38
- "@eoasmxd/freya-sdk": "^0.4.0",
38
+ "@eoasmxd/freya-sdk": "^0.4.1",
39
39
  "ws": "^8.18.0",
40
+ "mysql2": "^3.11.0",
40
41
  "qrcode-terminal": "^0.12.0"
41
42
  },
42
43
  "scripts": {
@@ -18,7 +18,7 @@
18
18
  "clean": "rm -rf dist"
19
19
  },
20
20
  "dependencies": {
21
- "@eoasmxd/freya-sdk": "^0.4.0"
21
+ "@eoasmxd/freya-sdk": "^0.4.1"
22
22
  },
23
23
  "devDependencies": {
24
24
  "@types/node": "^22.0.0",
@@ -18,7 +18,7 @@
18
18
  "clean": "rm -rf dist"
19
19
  },
20
20
  "dependencies": {
21
- "@eoasmxd/freya-sdk": "^0.4.0"
21
+ "@eoasmxd/freya-sdk": "^0.4.1"
22
22
  },
23
23
  "devDependencies": {
24
24
  "@types/node": "^22.0.0",
@@ -19,7 +19,7 @@
19
19
  "clean": "rm -rf dist"
20
20
  },
21
21
  "dependencies": {
22
- "@eoasmxd/freya-sdk": "^0.4.0"
22
+ "@eoasmxd/freya-sdk": "^0.4.1"
23
23
  },
24
24
  "devDependencies": {
25
25
  "@types/node": "^22.0.0",
@@ -21,7 +21,7 @@
21
21
  "clean": "rm -rf dist"
22
22
  },
23
23
  "dependencies": {
24
- "@eoasmxd/freya-sdk": "^0.4.0"
24
+ "@eoasmxd/freya-sdk": "^0.4.1"
25
25
  },
26
26
  "devDependencies": {
27
27
  "@types/node": "^22.0.0",
@@ -21,7 +21,7 @@
21
21
  "clean": "rm -rf dist"
22
22
  },
23
23
  "dependencies": {
24
- "@eoasmxd/freya-sdk": "^0.4.0"
24
+ "@eoasmxd/freya-sdk": "^0.4.1"
25
25
  },
26
26
  "devDependencies": {
27
27
  "@types/node": "^22.0.0",
@@ -0,0 +1,9 @@
1
+ MySQL 数据库查询工具箱。
2
+
3
+ 提供执行标准 SQL 只读查询语句并返回结构化 JSON 数据结果的能力。
4
+
5
+ 使用规范:
6
+ 1. 本工具仅支持执行 SELECT 查询语句,严禁执行任何非 SELECT 语句,禁止执行任何增删改或 DDL 破坏性语句。
7
+ 2. 严禁主观猜测表名与字段名,必须严格基于上下文或业务 Skill 指南中确定、已知的数据结构编写 SQL。
8
+ 3. 支持通过 `connection` 参数指定目标数据库连接名称,若不传则自动使用默认连接。
9
+ 4. 单次查询返回结果条数受到系统安全限制截断,编写查询时建议显式使用合理的 `LIMIT` 子句。
@@ -0,0 +1,26 @@
1
+ 你是一个严格的 SQL 安全与完整性审查审计员。你的唯一职责是对待执行的 SQL 查询语句进行安全与完整性审查。
2
+
3
+ 审查准则:
4
+ 1. **完整性审查**:
5
+ - SQL 语句必须语法结构完整、语义明确。
6
+ - 严禁包含伪代码、截断残缺语句或未闭合的括号/引号。
7
+
8
+ 2. **只读安全约束**:
9
+ - 必须仅为 SELECT 查询操作(仅允许 SELECT 查询语句,严禁执行 SHOW、EXPLAIN、DESCRIBE 等其他语句)。
10
+ - 严禁包含任何数据修改语句(如 INSERT、UPDATE、DELETE、REPLACE 等)。
11
+ - 严禁包含任何数据定义与删除语句(如 DROP、TRUNCATE、ALTER、CREATE 等)。
12
+ - 严禁包含权限管理、事务控制等系统级命令(如 GRANT、REVOKE、LOCK TABLES 等)。
13
+ - 严禁利用分号 `;` 拼接多条执行语句进行批处理或 SQL 注入攻击。
14
+ - 严禁包含高危文件读写或外部系统交互指令(如 INTO OUTFILE、INTO DUMPFILE、LOAD DATA、LOAD_FILE 等)。
15
+
16
+ 输出规范:
17
+ 请直接返回合法且不包含 Markdown 代码块标记(如 ```json)的纯 JSON 字符串,格式如下:
18
+ {
19
+ "passed": true,
20
+ "reason": "审核通过,为合法的只读查询语句。"
21
+ }
22
+
23
+ {
24
+ "passed": false,
25
+ "reason": "具体的拦截阻断原因"
26
+ }
@@ -0,0 +1,13 @@
1
+ import type { FreyaContext } from '@eoasmxd/freya-sdk';
2
+ export interface AuditResult {
3
+ passed: boolean;
4
+ reason: string;
5
+ }
6
+ /** 前置 SQL 安全与完整性审查审计服务 */
7
+ export declare class SqlAuditService {
8
+ private cachedPrompt;
9
+ /** 双通道探针加载审计提示词模板 */
10
+ private loadAuditPrompt;
11
+ /** 执行前置独立 LLM 分析审查 */
12
+ audit(sql: string, connectionName: string, ctx: FreyaContext): Promise<AuditResult>;
13
+ }
@@ -0,0 +1,87 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ /** 前置 SQL 安全与完整性审查审计服务 */
5
+ export class SqlAuditService {
6
+ cachedPrompt = null;
7
+ /** 双通道探针加载审计提示词模板 */
8
+ async loadAuditPrompt(ctx) {
9
+ if (this.cachedPrompt) {
10
+ return this.cachedPrompt;
11
+ }
12
+ const promptFileName = 'plugin.prompt.mysql.select.audit.md';
13
+ const runtimeOverridePath = path.join(ctx.paths.projectRoot, 'config', 'prompts', promptFileName);
14
+ try {
15
+ const content = await fs.readFile(runtimeOverridePath, 'utf-8');
16
+ if (content.trim()) {
17
+ this.cachedPrompt = content;
18
+ return content;
19
+ }
20
+ }
21
+ catch {
22
+ // 运行时覆盖不存在,继续降级读取内置模板
23
+ }
24
+ const currentDir = path.dirname(fileURLToPath(import.meta.url));
25
+ const packageDefaultPath = path.resolve(currentDir, '..', 'config', 'prompts', promptFileName);
26
+ try {
27
+ const content = await fs.readFile(packageDefaultPath, 'utf-8');
28
+ this.cachedPrompt = content;
29
+ return content;
30
+ }
31
+ catch (err) {
32
+ ctx.logger.error(`加载内置 SQL 审计提示词模板失败: ${packageDefaultPath}`, err);
33
+ return '';
34
+ }
35
+ }
36
+ /** 执行前置独立 LLM 分析审查 */
37
+ async audit(sql, connectionName, ctx) {
38
+ const trimmedSql = sql.trim();
39
+ if (!trimmedSql) {
40
+ return { passed: false, reason: 'SQL 语句内容不能为空。' };
41
+ }
42
+ const auditPrompt = await this.loadAuditPrompt(ctx);
43
+ if (!auditPrompt) {
44
+ return { passed: false, reason: 'SQL 审计提示词模板未就绪,安全拒绝执行。' };
45
+ }
46
+ const messages = [
47
+ {
48
+ role: 'system',
49
+ content: auditPrompt
50
+ },
51
+ {
52
+ role: 'user',
53
+ content: JSON.stringify({
54
+ connection: connectionName,
55
+ sql: trimmedSql
56
+ }, null, 2)
57
+ }
58
+ ];
59
+ try {
60
+ const response = await ctx.llm.chat(messages);
61
+ const rawOutput = response.message?.content?.trim() || '';
62
+ const cleaned = rawOutput
63
+ .replace(/^```json\s*/i, '')
64
+ .replace(/^```\s*/i, '')
65
+ .replace(/\s*```$/i, '')
66
+ .trim();
67
+ const parsed = JSON.parse(cleaned);
68
+ if (typeof parsed.passed === 'boolean') {
69
+ return {
70
+ passed: parsed.passed,
71
+ reason: parsed.reason ? String(parsed.reason) : (parsed.passed ? '审查通过' : '未提供拦截原因')
72
+ };
73
+ }
74
+ return {
75
+ passed: false,
76
+ reason: `审查响应结构不符合预期: ${rawOutput}`
77
+ };
78
+ }
79
+ catch (err) {
80
+ ctx.logger.error('前置 LLM SQL 审计过程发生异常', err);
81
+ return {
82
+ passed: false,
83
+ reason: `SQL 安全审查服务调用失败: ${err?.message || err}`
84
+ };
85
+ }
86
+ }
87
+ }
@@ -0,0 +1,14 @@
1
+ import type { FreyaContext, ToolPlugin, FreyaTool } from '@eoasmxd/freya-sdk';
2
+ /** MySQL 数据库查询工具箱插件 */
3
+ export default class MysqlToolsPlugin implements ToolPlugin {
4
+ type: "tool";
5
+ private poolManager?;
6
+ private auditService;
7
+ private tools;
8
+ setup(ctx: FreyaContext): Promise<void>;
9
+ getId(): string;
10
+ getInstructionPrompt(): string;
11
+ getTools(): FreyaTool[];
12
+ stop(ctx: FreyaContext): Promise<void>;
13
+ }
14
+ export declare const Plugin: typeof MysqlToolsPlugin;
@@ -0,0 +1,34 @@
1
+ import { MysqlPoolManager } from './pool-manager.js';
2
+ import { SqlAuditService } from './audit.js';
3
+ import { MysqlQueryTool } from './tools.js';
4
+ /** MySQL 数据库查询工具箱插件 */
5
+ export default class MysqlToolsPlugin {
6
+ type = 'tool';
7
+ poolManager;
8
+ auditService = new SqlAuditService();
9
+ tools = [];
10
+ async setup(ctx) {
11
+ this.poolManager = new MysqlPoolManager(ctx);
12
+ this.tools = [
13
+ new MysqlQueryTool(this.poolManager, this.auditService)
14
+ ];
15
+ const available = this.poolManager.getAvailableConnectionNames();
16
+ ctx.logger.info(`MySQL 工具箱插件初始化就绪,已注册连接: [${available.join(', ') || '暂未配置'}]`);
17
+ }
18
+ getId() {
19
+ return 'mysql';
20
+ }
21
+ getInstructionPrompt() {
22
+ return 'plugin.prompt.mysql';
23
+ }
24
+ getTools() {
25
+ return this.tools;
26
+ }
27
+ async stop(ctx) {
28
+ if (this.poolManager) {
29
+ await this.poolManager.closeAll();
30
+ ctx.logger.info('MySQL 工具箱连接资源已全部释放。');
31
+ }
32
+ }
33
+ }
34
+ export const Plugin = MysqlToolsPlugin;
@@ -0,0 +1,32 @@
1
+ import mysql from 'mysql2/promise';
2
+ import type { FreyaContext } from '@eoasmxd/freya-sdk';
3
+ /** MySQL 连接配置契约 */
4
+ export interface MysqlConnectionConfig {
5
+ name: string;
6
+ host?: string;
7
+ port?: number;
8
+ user?: string;
9
+ password?: string;
10
+ database?: string;
11
+ connectionLimit?: number;
12
+ connectTimeout?: number;
13
+ }
14
+ /** MySQL 多命名连接池管理器 */
15
+ export declare class MysqlPoolManager {
16
+ private ctx;
17
+ private pools;
18
+ private connectionConfigs;
19
+ private defaultConnectionName;
20
+ constructor(ctx: FreyaContext);
21
+ /** 加载与同步配置 */
22
+ reloadConfigs(): void;
23
+ /** 获取指定名称的数据库连接池 */
24
+ getPool(targetName?: string): {
25
+ pool: mysql.Pool;
26
+ config: MysqlConnectionConfig;
27
+ };
28
+ /** 获取所有可用连接名称 */
29
+ getAvailableConnectionNames(): string[];
30
+ /** 释放所有已建立的连接池资源 */
31
+ closeAll(): Promise<void>;
32
+ }