@eoasmxd/freya 0.3.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.
- package/README.md +9 -3
- 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 +13 -1
- package/core/dist/kernel.js +2 -2
- 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/web/config-api.js +18 -0
- package/core/package.json +2 -2
- package/doc/_index.md +20 -7
- package/doc/getting-started.md +1 -1
- package/doc/installation-guide.md +2 -2
- package/doc/{architecture-design.md → specifications/architecture-design.md} +7 -4
- package/doc/{config-spec.md → specifications/config-spec.md} +1 -1
- package/doc/{llm-interface-params.md → specifications/llm-interface-params.md} +8 -8
- package/doc/{prompt-system.md → specifications/prompt-system.md} +1 -1
- package/doc/tutorials/_index.md +96 -0
- package/doc/tutorials/part0_basic/0.1_probability_prediction.md +155 -0
- package/doc/tutorials/part0_basic/0.2_attention_and_context.md +145 -0
- package/doc/tutorials/part0_basic/0.3_generation_parameters.md +132 -0
- package/doc/tutorials/part0_basic/0.4_debugging_token.md +99 -0
- package/doc/tutorials/part0_basic/1.1_stateless_and_history.md +143 -0
- package/doc/tutorials/part0_basic/1.2_chat_data_structure.md +179 -0
- package/doc/tutorials/part0_basic/1.3_system_user_assistant.md +147 -0
- package/doc/tutorials/part0_basic/1.4_freya_model_proxy.md +203 -0
- package/doc/tutorials/part0_basic/_index.md +28 -0
- package/doc/tutorials/part1_react/2.1_agency_vs_chatbot.md +127 -0
- package/doc/tutorials/part1_react/2.2_react_mind_model.md +144 -0
- package/doc/tutorials/part1_react/2.3_freya_agent_executor.md +233 -0
- package/doc/tutorials/part1_react/2.4_debugging_loop_deadlock.md +160 -0
- package/doc/tutorials/part1_react/3.1_hardcoded_prompt_pain.md +104 -0
- package/doc/tutorials/part1_react/3.2_decoupled_architecture.md +138 -0
- package/doc/tutorials/part1_react/3.3_freya_dual_read_probe.md +152 -0
- package/doc/tutorials/part1_react/3.4_debugging_composition_placeholder.md +97 -0
- package/doc/tutorials/part1_react/_index.md +28 -0
- package/doc/tutorials/part2_tools/4.1_json_schema_mapping.md +119 -0
- package/doc/tutorials/part2_tools/4.2_tool_call_raw_packet.md +105 -0
- package/doc/tutorials/part2_tools/4.3_freya_tool_execution.md +145 -0
- package/doc/tutorials/part2_tools/4.4_debugging_observation_fix.md +154 -0
- package/doc/tutorials/part2_tools/5.1_observation_injection.md +131 -0
- package/doc/tutorials/part2_tools/5.2_openai_vs_gemini_protocol.md +125 -0
- package/doc/tutorials/part2_tools/5.3_freya_llm_proxy_mapping.md +162 -0
- package/doc/tutorials/part2_tools/5.4_debugging_parallel_call_chaos.md +135 -0
- package/doc/tutorials/part2_tools/_index.md +28 -0
- package/doc/tutorials/part3_memory/6.1_session_state_lifecycle.md +143 -0
- package/doc/tutorials/part3_memory/6.2_physical_sandbox_separation.md +108 -0
- package/doc/tutorials/part3_memory/6.3_freya_session_storage.md +139 -0
- package/doc/tutorials/part3_memory/6.4_debugging_session_concurrency.md +161 -0
- package/doc/tutorials/part3_memory/7.1_context_overflow_loss.md +109 -0
- package/doc/tutorials/part3_memory/7.2_sliding_window_vs_summary.md +85 -0
- package/doc/tutorials/part3_memory/7.3_freya_compactor_impl.md +142 -0
- package/doc/tutorials/part3_memory/7.4_debugging_summarize_deadlock.md +160 -0
- package/doc/tutorials/part3_memory/_index.md +28 -0
- package/doc/tutorials/part4_streaming/8.1_sse_protocol_basics.md +119 -0
- package/doc/tutorials/part4_streaming/8.2_hiding_thoughts_in_stream.md +132 -0
- package/doc/tutorials/part4_streaming/8.3_freya_event_bus.md +104 -0
- package/doc/tutorials/part4_streaming/8.4_debugging_stream_decoder.md +158 -0
- package/doc/tutorials/part4_streaming/9.1_abort_signal_braking.md +162 -0
- package/doc/tutorials/part4_streaming/9.2_async_event_channels.md +142 -0
- package/doc/tutorials/part4_streaming/9.3_freya_abort_billing.md +122 -0
- package/doc/tutorials/part4_streaming/9.4_debugging_abort_lock_deadlock.md +187 -0
- package/doc/tutorials/part4_streaming/_index.md +28 -0
- package/doc/tutorials/part5_plugins/10.1_microkernel_decoupling.md +142 -0
- package/doc/tutorials/part5_plugins/10.2_plugin_metadata_security.md +125 -0
- package/doc/tutorials/part5_plugins/10.3_channel_plugin_development.md +149 -0
- package/doc/tutorials/part5_plugins/10.4_debugging_channel_reconnection.md +160 -0
- package/doc/tutorials/part5_plugins/_index.md +21 -0
- package/doc/tutorials/part6_advanced/11.1_react_model_flaws.md +111 -0
- package/doc/tutorials/part6_advanced/11.2_reflexion_mind_model.md +103 -0
- package/doc/tutorials/part6_advanced/11.3_reflexion_hands_on.md +182 -0
- package/doc/tutorials/part6_advanced/11.4_debugging_reflexion_convergence.md +108 -0
- package/doc/tutorials/part6_advanced/12.1_single_agent_limits.md +100 -0
- package/doc/tutorials/part6_advanced/12.2_multi_agent_patterns.md +121 -0
- package/doc/tutorials/part6_advanced/12.3_freya_multi_agent_routing.md +143 -0
- package/doc/tutorials/part6_advanced/12.4_multi_agent_hands_on.md +176 -0
- package/doc/tutorials/part6_advanced/_index.md +28 -0
- package/doc/tutorials/preface.md +30 -0
- package/package.json +3 -2
- 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/package.json +1 -1
- package/src/packages/core/src/command/commands/skill-commands.ts +6 -5
- package/src/packages/core/src/config/config-manager.ts +15 -1
- package/src/packages/core/src/kernel.ts +3 -2
- 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/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/ui/assets/{index-Be0cAgdB.js → index-BqPQMflk.js} +14 -14
- package/ui/index.html +1 -1
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import mysql from 'mysql2/promise';
|
|
2
|
+
/** 安全解算嵌套或扁平配置值 */
|
|
3
|
+
function getNestedConfig(config, keyPath) {
|
|
4
|
+
if (!config)
|
|
5
|
+
return undefined;
|
|
6
|
+
if (keyPath in config)
|
|
7
|
+
return config[keyPath];
|
|
8
|
+
const parts = keyPath.split('.');
|
|
9
|
+
let current = config;
|
|
10
|
+
for (const part of parts) {
|
|
11
|
+
if (current && typeof current === 'object' && part in current) {
|
|
12
|
+
current = current[part];
|
|
13
|
+
}
|
|
14
|
+
else {
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return current;
|
|
19
|
+
}
|
|
20
|
+
/** MySQL 多命名连接池管理器 */
|
|
21
|
+
export class MysqlPoolManager {
|
|
22
|
+
ctx;
|
|
23
|
+
pools = new Map();
|
|
24
|
+
connectionConfigs = new Map();
|
|
25
|
+
defaultConnectionName = 'default';
|
|
26
|
+
constructor(ctx) {
|
|
27
|
+
this.ctx = ctx;
|
|
28
|
+
this.reloadConfigs();
|
|
29
|
+
}
|
|
30
|
+
/** 加载与同步配置 */
|
|
31
|
+
reloadConfigs() {
|
|
32
|
+
this.connectionConfigs.clear();
|
|
33
|
+
const config = this.ctx.config || {};
|
|
34
|
+
this.defaultConnectionName = getNestedConfig(config, 'mysql.defaultConnection') || 'default';
|
|
35
|
+
const connections = getNestedConfig(config, 'mysql.connections');
|
|
36
|
+
if (Array.isArray(connections)) {
|
|
37
|
+
for (const item of connections) {
|
|
38
|
+
if (item && typeof item === 'object' && item.name) {
|
|
39
|
+
this.connectionConfigs.set(item.name, {
|
|
40
|
+
name: String(item.name),
|
|
41
|
+
host: item.host ? String(item.host) : '127.0.0.1',
|
|
42
|
+
port: item.port ? Number(item.port) : 3306,
|
|
43
|
+
user: item.user ? String(item.user) : 'root',
|
|
44
|
+
password: item.password ? String(item.password) : '',
|
|
45
|
+
database: item.database ? String(item.database) : undefined,
|
|
46
|
+
connectionLimit: item.connectionLimit ? Number(item.connectionLimit) : 5,
|
|
47
|
+
connectTimeout: item.connectTimeout ? Number(item.connectTimeout) : 10000
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (this.connectionConfigs.size === 0) {
|
|
53
|
+
const singleHost = getNestedConfig(config, 'mysql.host');
|
|
54
|
+
if (singleHost) {
|
|
55
|
+
this.connectionConfigs.set(this.defaultConnectionName, {
|
|
56
|
+
name: this.defaultConnectionName,
|
|
57
|
+
host: String(singleHost),
|
|
58
|
+
port: getNestedConfig(config, 'mysql.port') ? Number(getNestedConfig(config, 'mysql.port')) : 3306,
|
|
59
|
+
user: getNestedConfig(config, 'mysql.user') ? String(getNestedConfig(config, 'mysql.user')) : 'root',
|
|
60
|
+
password: getNestedConfig(config, 'mysql.password') ? String(getNestedConfig(config, 'mysql.password')) : '',
|
|
61
|
+
database: getNestedConfig(config, 'mysql.database') ? String(getNestedConfig(config, 'mysql.database')) : undefined,
|
|
62
|
+
connectionLimit: getNestedConfig(config, 'mysql.connectionLimit') ? Number(getNestedConfig(config, 'mysql.connectionLimit')) : 5,
|
|
63
|
+
connectTimeout: getNestedConfig(config, 'mysql.connectTimeout') ? Number(getNestedConfig(config, 'mysql.connectTimeout')) : 10000
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/** 获取指定名称的数据库连接池 */
|
|
69
|
+
getPool(targetName) {
|
|
70
|
+
this.reloadConfigs();
|
|
71
|
+
const connectionName = targetName || this.defaultConnectionName;
|
|
72
|
+
const conf = this.connectionConfigs.get(connectionName);
|
|
73
|
+
if (!conf) {
|
|
74
|
+
const available = Array.from(this.connectionConfigs.keys()).join(', ') || '无配置';
|
|
75
|
+
throw new Error(`未找到名为 "${connectionName}" 的 MySQL 连接配置。当前可用连接: [${available}]`);
|
|
76
|
+
}
|
|
77
|
+
let pool = this.pools.get(connectionName);
|
|
78
|
+
if (!pool) {
|
|
79
|
+
pool = mysql.createPool({
|
|
80
|
+
host: conf.host,
|
|
81
|
+
port: conf.port,
|
|
82
|
+
user: conf.user,
|
|
83
|
+
password: conf.password,
|
|
84
|
+
database: conf.database,
|
|
85
|
+
connectionLimit: conf.connectionLimit,
|
|
86
|
+
connectTimeout: conf.connectTimeout,
|
|
87
|
+
waitForConnections: true,
|
|
88
|
+
queueLimit: 0,
|
|
89
|
+
dateStrings: true
|
|
90
|
+
});
|
|
91
|
+
this.pools.set(connectionName, pool);
|
|
92
|
+
this.ctx.logger.info(`MySQL 连接池已创建: [${connectionName}] -> ${conf.user}@${conf.host}:${conf.port}/${conf.database || ''}`);
|
|
93
|
+
}
|
|
94
|
+
return { pool, config: conf };
|
|
95
|
+
}
|
|
96
|
+
/** 获取所有可用连接名称 */
|
|
97
|
+
getAvailableConnectionNames() {
|
|
98
|
+
return Array.from(this.connectionConfigs.keys());
|
|
99
|
+
}
|
|
100
|
+
/** 释放所有已建立的连接池资源 */
|
|
101
|
+
async closeAll() {
|
|
102
|
+
for (const [name, pool] of this.pools.entries()) {
|
|
103
|
+
try {
|
|
104
|
+
await pool.end();
|
|
105
|
+
this.ctx.logger.info(`MySQL 连接池已释放: [${name}]`);
|
|
106
|
+
}
|
|
107
|
+
catch (err) {
|
|
108
|
+
this.ctx.logger.warn(`关闭 MySQL 连接池 [${name}] 时发生异常: ${err?.message || err}`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
this.pools.clear();
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { FreyaContext, ToolDefinition, FreyaTool } from '@eoasmxd/freya-sdk';
|
|
2
|
+
import type { MysqlPoolManager } from './pool-manager.js';
|
|
3
|
+
import type { SqlAuditService } from './audit.js';
|
|
4
|
+
/** MySQL 数据库查询执行工具 */
|
|
5
|
+
export declare class MysqlQueryTool implements FreyaTool {
|
|
6
|
+
private poolManager;
|
|
7
|
+
private auditService;
|
|
8
|
+
constructor(poolManager: MysqlPoolManager, auditService: SqlAuditService);
|
|
9
|
+
getDefinition(): ToolDefinition;
|
|
10
|
+
execute(args: Record<string, any>, ctx: FreyaContext): Promise<string>;
|
|
11
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/** 脱敏错误信息中的敏感凭据 */
|
|
2
|
+
function sanitizeErrorMessage(err, password) {
|
|
3
|
+
let message = err?.message || String(err);
|
|
4
|
+
if (password) {
|
|
5
|
+
message = message.replaceAll(password, '******');
|
|
6
|
+
}
|
|
7
|
+
return message;
|
|
8
|
+
}
|
|
9
|
+
/** MySQL 数据库查询执行工具 */
|
|
10
|
+
export class MysqlQueryTool {
|
|
11
|
+
poolManager;
|
|
12
|
+
auditService;
|
|
13
|
+
constructor(poolManager, auditService) {
|
|
14
|
+
this.poolManager = poolManager;
|
|
15
|
+
this.auditService = auditService;
|
|
16
|
+
}
|
|
17
|
+
getDefinition() {
|
|
18
|
+
return {
|
|
19
|
+
name: 'mysql_query',
|
|
20
|
+
description: '执行 MySQL SELECT 查询语句并返回结构化数据。执行前会进行独立 LLM 安全与完整性审核,支持通过 connection 指定目标连接。',
|
|
21
|
+
parameters: {
|
|
22
|
+
type: 'object',
|
|
23
|
+
properties: {
|
|
24
|
+
sql: {
|
|
25
|
+
type: 'string',
|
|
26
|
+
description: '待执行的 SQL 查询语句'
|
|
27
|
+
},
|
|
28
|
+
connection: {
|
|
29
|
+
type: 'string',
|
|
30
|
+
description: '目标数据库连接名称(可选,若未指定则使用系统默认连接)'
|
|
31
|
+
},
|
|
32
|
+
params: {
|
|
33
|
+
type: 'array',
|
|
34
|
+
items: {},
|
|
35
|
+
description: '可选的参数化查询参数列表'
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
required: ['sql']
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
async execute(args, ctx) {
|
|
43
|
+
const rawSql = typeof args.sql === 'string' ? args.sql.trim() : '';
|
|
44
|
+
if (!rawSql) {
|
|
45
|
+
return '❌ 参数错误: sql 语句不能为空。';
|
|
46
|
+
}
|
|
47
|
+
const connectionName = typeof args.connection === 'string' && args.connection.trim()
|
|
48
|
+
? args.connection.trim()
|
|
49
|
+
: undefined;
|
|
50
|
+
const queryParams = Array.isArray(args.params) ? args.params : undefined;
|
|
51
|
+
const auditResult = await this.auditService.audit(rawSql, connectionName || 'default', ctx);
|
|
52
|
+
if (!auditResult.passed) {
|
|
53
|
+
return `❌ SQL 审查未通过,已拒绝执行。\n原因: ${auditResult.reason}`;
|
|
54
|
+
}
|
|
55
|
+
let currentPassword = '';
|
|
56
|
+
try {
|
|
57
|
+
const { pool, config } = this.poolManager.getPool(connectionName);
|
|
58
|
+
currentPassword = config.password || '';
|
|
59
|
+
const maxRows = Number(ctx.config?.mysql?.maxRows ?? ctx.config?.['mysql.maxRows']) || 100;
|
|
60
|
+
const [rows] = await pool.query(rawSql, queryParams);
|
|
61
|
+
if (!Array.isArray(rows)) {
|
|
62
|
+
return JSON.stringify({
|
|
63
|
+
connection: config.name,
|
|
64
|
+
database: config.database || null,
|
|
65
|
+
affectedRows: rows.affectedRows ?? 0,
|
|
66
|
+
info: rows.info || '查询已完成'
|
|
67
|
+
}, null, 2);
|
|
68
|
+
}
|
|
69
|
+
const totalCount = rows.length;
|
|
70
|
+
const truncated = totalCount > maxRows;
|
|
71
|
+
const data = truncated ? rows.slice(0, maxRows) : rows;
|
|
72
|
+
return JSON.stringify({
|
|
73
|
+
connection: config.name,
|
|
74
|
+
database: config.database || null,
|
|
75
|
+
totalCount,
|
|
76
|
+
returnedCount: data.length,
|
|
77
|
+
truncated,
|
|
78
|
+
notice: truncated ? `结果集超过最大限制 ${maxRows} 条,已自动截断返回前 ${maxRows} 条记录。` : undefined,
|
|
79
|
+
data
|
|
80
|
+
}, null, 2);
|
|
81
|
+
}
|
|
82
|
+
catch (err) {
|
|
83
|
+
const safeMessage = sanitizeErrorMessage(err, currentPassword);
|
|
84
|
+
ctx.logger.error(`MySQL 查询执行异常: ${safeMessage}`);
|
|
85
|
+
return `❌ MySQL 查询执行失败: ${safeMessage}`;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@eoasmxd/freya-plugin-tool-mysql",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"publishConfig": {
|
|
7
|
+
"access": "public"
|
|
8
|
+
},
|
|
9
|
+
"description": "Freya MySQL 数据库查询工具集插件",
|
|
10
|
+
"freya": {
|
|
11
|
+
"displayName": "MySQL 数据库查询工具集",
|
|
12
|
+
"defaultEnabled": false,
|
|
13
|
+
"schema": "./schema.json",
|
|
14
|
+
"prompts": [
|
|
15
|
+
"plugin.prompt.mysql.md",
|
|
16
|
+
"plugin.prompt.mysql.select.audit.md"
|
|
17
|
+
]
|
|
18
|
+
},
|
|
19
|
+
"type": "module",
|
|
20
|
+
"main": "./dist/index.js",
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "tsc -p tsconfig.json",
|
|
23
|
+
"clean": "rm -rf dist"
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@eoasmxd/freya-sdk": "^0.4.1",
|
|
27
|
+
"mysql2": "^3.11.0"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@types/node": "^22.0.0",
|
|
31
|
+
"typescript": "^5.5.0"
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"key": "mysql.defaultConnection",
|
|
4
|
+
"defaultValue": "default",
|
|
5
|
+
"description": "默认使用的数据库连接名称",
|
|
6
|
+
"type": "string",
|
|
7
|
+
"category": "MySQL 连接"
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"key": "mysql.maxRows",
|
|
11
|
+
"defaultValue": 100,
|
|
12
|
+
"description": "单次查询最大返回记录条数(防止大模型上下文窗口溢出)",
|
|
13
|
+
"type": "number",
|
|
14
|
+
"min": 1,
|
|
15
|
+
"max": 1000,
|
|
16
|
+
"category": "MySQL 安全",
|
|
17
|
+
"uiHint": "slider"
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"key": "mysql.connections",
|
|
21
|
+
"defaultValue": [],
|
|
22
|
+
"description": "MySQL 命名数据库连接配置列表",
|
|
23
|
+
"type": "array",
|
|
24
|
+
"category": "MySQL 连接",
|
|
25
|
+
"children": [
|
|
26
|
+
{
|
|
27
|
+
"key": "name",
|
|
28
|
+
"description": "连接名称 (例如 default、bi_db)",
|
|
29
|
+
"type": "string",
|
|
30
|
+
"required": true
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"key": "host",
|
|
34
|
+
"description": "数据库主机 (例如 127.0.0.1)",
|
|
35
|
+
"type": "string",
|
|
36
|
+
"required": true
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"key": "port",
|
|
40
|
+
"description": "数据库端口 (默认 3306)",
|
|
41
|
+
"type": "number",
|
|
42
|
+
"defaultValue": 3306,
|
|
43
|
+
"required": true
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"key": "user",
|
|
47
|
+
"description": "数据库用户名",
|
|
48
|
+
"type": "string",
|
|
49
|
+
"required": true
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"key": "password",
|
|
53
|
+
"description": "数据库密码",
|
|
54
|
+
"type": "string",
|
|
55
|
+
"required": true,
|
|
56
|
+
"sensitive": true,
|
|
57
|
+
"uiHint": "password"
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
"key": "database",
|
|
61
|
+
"description": "数据库名称",
|
|
62
|
+
"type": "string",
|
|
63
|
+
"required": false
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
"key": "connectionLimit",
|
|
67
|
+
"description": "连接池最大连接数 (默认 5)",
|
|
68
|
+
"type": "number",
|
|
69
|
+
"defaultValue": 5,
|
|
70
|
+
"min": 1,
|
|
71
|
+
"max": 50
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
"key": "connectTimeout",
|
|
75
|
+
"description": "连接超时时间 (毫秒,默认 10000)",
|
|
76
|
+
"type": "number",
|
|
77
|
+
"defaultValue": 10000,
|
|
78
|
+
"min": 1000,
|
|
79
|
+
"max": 60000
|
|
80
|
+
}
|
|
81
|
+
]
|
|
82
|
+
}
|
|
83
|
+
]
|
|
@@ -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)) {
|
|
@@ -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);
|