@eoasmxd/freya 0.4.1 → 0.4.3

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 (31) hide show
  1. package/README.md +22 -1
  2. package/core/dist/agent/agent-executor.js +5 -0
  3. package/core/dist/command/command-executor.js +7 -0
  4. package/core/dist/command/command-registry.d.ts +6 -1
  5. package/core/dist/command/command-registry.js +31 -1
  6. package/core/dist/config/config-manager.js +35 -0
  7. package/core/dist/kernel.js +2 -2
  8. package/core/dist/tools/tool-registry.d.ts +12 -7
  9. package/core/dist/tools/tool-registry.js +44 -8
  10. package/core/package.json +2 -2
  11. package/doc/_index.md +1 -1
  12. package/doc/installation-guide.md +41 -6
  13. package/freya.js +16 -1
  14. package/package.json +3 -3
  15. package/plugins/plugin-gemini/package.json +1 -1
  16. package/plugins/plugin-openai/package.json +1 -1
  17. package/plugins/plugin-telegram-channel/package.json +1 -1
  18. package/plugins/plugin-tool-fs/package.json +1 -1
  19. package/plugins/plugin-tool-memory/package.json +1 -1
  20. package/plugins/plugin-tool-mysql/package.json +1 -1
  21. package/plugins/plugin-tool-web/package.json +1 -1
  22. package/plugins/plugin-wecom-channel/package.json +1 -1
  23. package/plugins/plugin-weixin-channel/dist/index.js +10 -36
  24. package/plugins/plugin-weixin-channel/package.json +3 -2
  25. package/src/packages/core/src/agent/agent-executor.ts +5 -0
  26. package/src/packages/core/src/command/command-executor.ts +8 -0
  27. package/src/packages/core/src/command/command-registry.ts +35 -1
  28. package/src/packages/core/src/config/config-manager.ts +35 -0
  29. package/src/packages/core/src/kernel.ts +2 -2
  30. package/src/packages/core/src/tools/tool-registry.ts +49 -9
  31. package/src/plugins/plugin-weixin-channel/src/index.ts +12 -37
package/README.md CHANGED
@@ -33,9 +33,30 @@ freya --no-cli
33
33
  freya stop
34
34
  ```
35
35
 
36
+ ### 方式二:通过 Docker 运行(推荐,免环境配置)
37
+
38
+ 无需安装 Node.js 与包管理环境,直接通过容器一键启动(支持配置与会话数据持久化):
39
+
40
+ ```bash
41
+ # 启动并挂载持久化数据目录(宿主机 ./freya-data 映射至容器 /data)
42
+ docker run -d \
43
+ --name freya \
44
+ -p 3000:3000 \
45
+ -v $(pwd)/freya-data:/data \
46
+ --restart unless-stopped \
47
+ ghcr.io/eoasmxd/freya:latest
48
+ ```
49
+
50
+ 或使用本地源码构建镜像运行:
51
+ ```bash
52
+ # 本地构建并启动
53
+ docker build -t freya .
54
+ docker run -d --name freya -p 3000:3000 -v $(pwd)/freya-data:/data freya
55
+ ```
56
+
36
57
  ---
37
58
 
38
- ### 方式二:从源码构建运行
59
+ ### 方式三:从源码构建运行
39
60
 
40
61
  运行环境要求:**Node.js** (>= 22.0.0) 和 **pnpm** (9.x)。
41
62
 
@@ -187,6 +187,11 @@ export class FreyaAgentExecutor {
187
187
  const nextToolboxIdleRounds = {};
188
188
  const deactivatedIds = [];
189
189
  for (const id of activeToolboxIds) {
190
+ if (!this.toolRegistry.isToolboxEnabled(id)) {
191
+ deactivatedIds.push(id);
192
+ this.context.logger.info(`[FreyaAgentExecutor] 工具箱 [${id}] 已被全局禁用,自动从当前会话卸载。`);
193
+ continue;
194
+ }
190
195
  const boxTools = this.toolRegistry.getToolsInBox(id);
191
196
  const isUsed = boxTools.some((tool) => executedToolNames.has(tool.getDefinition().name));
192
197
  if (isUsed) {
@@ -23,6 +23,13 @@ export class FreyaCommandExecutor {
23
23
  });
24
24
  return true;
25
25
  }
26
+ if (!this.registry.isCommandEnabled(commandName)) {
27
+ this.context.eventBus.emit('session:reply:error', {
28
+ sessionId,
29
+ message: `❌ 权限拒绝:系统指令 "/${commandName}" 已被系统管理员全局禁用。`
30
+ });
31
+ return true;
32
+ }
26
33
  try {
27
34
  const replyContent = await cmd.execute(args, sessionId, this.context, connectionId);
28
35
  if (replyContent) {
@@ -1,9 +1,14 @@
1
- import type { FreyaCommand } from '@eoasmxd/freya-sdk';
1
+ import type { FreyaCommand, FreyaContext } from '@eoasmxd/freya-sdk';
2
2
  /** 系统指令注册表:维护主指令与别名的映射字典 */
3
3
  export declare class FreyaCommandRegistry {
4
+ private context?;
4
5
  private commands;
5
6
  private aliasMap;
6
7
  private pluginCommandsMap;
8
+ constructor(context?: FreyaContext | undefined);
9
+ setContext(context: FreyaContext): void;
10
+ /** 判断指定指令当前是否已注册且处于可用启用状态 */
11
+ isCommandEnabled(name: string): boolean;
7
12
  /** 注册新的系统指令并建立别名路由 */
8
13
  register(command: FreyaCommand, pluginId?: string): void;
9
14
  /** 注销指定插件注册的所有指令与别名 */
@@ -1,8 +1,38 @@
1
+ const BUILTIN_COMMAND_CATEGORY_MAP = {
2
+ approve: 'auth',
3
+ reject: 'auth',
4
+ session: 'session',
5
+ model: 'model',
6
+ models: 'model'
7
+ };
1
8
  /** 系统指令注册表:维护主指令与别名的映射字典 */
2
9
  export class FreyaCommandRegistry {
10
+ context;
3
11
  commands = new Map();
4
12
  aliasMap = new Map();
5
13
  pluginCommandsMap = new Map();
14
+ constructor(context) {
15
+ this.context = context;
16
+ }
17
+ setContext(context) {
18
+ this.context = context;
19
+ }
20
+ /** 判断指定指令当前是否已注册且处于可用启用状态 */
21
+ isCommandEnabled(name) {
22
+ const lowerName = name.toLowerCase();
23
+ const primaryName = this.aliasMap.get(lowerName) || lowerName;
24
+ if (!this.commands.has(primaryName)) {
25
+ return false;
26
+ }
27
+ const category = BUILTIN_COMMAND_CATEGORY_MAP[primaryName];
28
+ if (category) {
29
+ const commandsConfig = this.context?.config?.commands?.builtin;
30
+ if (commandsConfig && typeof commandsConfig[category]?.enabled === 'boolean') {
31
+ return commandsConfig[category].enabled;
32
+ }
33
+ }
34
+ return true;
35
+ }
6
36
  /** 注册新的系统指令并建立别名路由 */
7
37
  register(command, pluginId) {
8
38
  const name = command.name.toLowerCase();
@@ -54,6 +84,6 @@ export class FreyaCommandRegistry {
54
84
  return undefined;
55
85
  }
56
86
  list() {
57
- return Array.from(this.commands.values());
87
+ return Array.from(this.commands.values()).filter((cmd) => this.isCommandEnabled(cmd.name));
58
88
  }
59
89
  }
@@ -688,6 +688,41 @@ export class FreyaConfigManager {
688
688
  min: 10,
689
689
  max: 300,
690
690
  category: '安全'
691
+ },
692
+ {
693
+ key: 'tools.builtin.config.enabled',
694
+ defaultValue: true,
695
+ description: '是否启用系统核心配置工具箱(允许大模型查看与修改系统配置)',
696
+ type: 'boolean',
697
+ category: '系统工具'
698
+ },
699
+ {
700
+ key: 'tools.builtin.session.enabled',
701
+ defaultValue: true,
702
+ description: '是否启用会话与子任务管理工具箱(允许大模型查阅会话历史与派生子任务)',
703
+ type: 'boolean',
704
+ category: '系统工具'
705
+ },
706
+ {
707
+ key: 'commands.builtin.auth.enabled',
708
+ defaultValue: true,
709
+ description: '是否启用敏感操作授权审批指令(/approve 与 /reject)',
710
+ type: 'boolean',
711
+ category: '系统指令'
712
+ },
713
+ {
714
+ key: 'commands.builtin.session.enabled',
715
+ defaultValue: true,
716
+ description: '是否启用会话管理与路由指令(/session 及其子命令)',
717
+ type: 'boolean',
718
+ category: '系统指令'
719
+ },
720
+ {
721
+ key: 'commands.builtin.model.enabled',
722
+ defaultValue: true,
723
+ description: '是否启用模型查看与切换指令(/model 及其子命令)',
724
+ type: 'boolean',
725
+ category: '系统指令'
691
726
  }
692
727
  ];
693
728
  this.schemaRegistry.register('core', coreFields);
@@ -44,10 +44,10 @@ export class FreyaKernel {
44
44
  ctx.logger.info('Freya 核心服务正在启动...');
45
45
  ctx.eventBus = new FreyaEventBus();
46
46
  const configSchemaRegistry = new FreyaConfigSchemaRegistry();
47
- const toolRegistry = new FreyaToolRegistry();
47
+ const toolRegistry = new FreyaToolRegistry(ctx);
48
48
  const llmRegistry = new FreyaLLMRegistry();
49
49
  const promptRegistry = new FreyaPromptRegistry();
50
- const commandRegistry = new FreyaCommandRegistry();
50
+ const commandRegistry = new FreyaCommandRegistry(ctx);
51
51
  this.channelRegistry = new FreyaChannelRegistry();
52
52
  const pluginRegistry = new FreyaPluginRegistry(toolRegistry, llmRegistry, this.channelRegistry);
53
53
  const skillRegistry = new FreyaSkillRegistry();
@@ -1,22 +1,27 @@
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
  export declare class FreyaToolRegistry {
8
+ private context?;
8
9
  private toolboxes;
9
- /** 注册一个工具箱(内置或外部插件)。 */
10
+ constructor(context?: FreyaContext | undefined);
11
+ setContext(context: FreyaContext): void;
12
+ /** 判断指定工具箱当前是否已注册且处于可用启用状态 */
13
+ isToolboxEnabled(toolboxId: string): boolean;
14
+ /** 注册工具箱 */
10
15
  registerToolbox(toolbox: FreyaToolbox): void;
11
16
  /** 注销工具箱 */
12
17
  unregisterToolbox(id: string): void;
13
- /** 获取指定 ID 的工具箱中的所有原子工具。 */
18
+ /** 获取指定 ID 的工具箱中的所有原子工具 */
14
19
  getToolsInBox(id: string): FreyaTool[];
15
- /** 聚合所有来源的工具,返回完整工具字典。 */
20
+ /** 聚合所有来源的已启用工具 */
16
21
  getAllTools(): Map<string, FreyaTool>;
17
- /** 根据当前会话已激活的工具箱列表,动态过滤获取所需的工具字典 */
22
+ /** 根据当前会话已激活的工具箱列表,过滤获取所需的工具字典 */
18
23
  getFilteredTools(activeToolboxIds: string[]): Map<string, FreyaTool>;
19
- /** 聚合所有来源的工具提示词引导说明,延迟解析 key → 内容 */
24
+ /** 聚合所有已启用的工具箱提示词引导说明 */
20
25
  getToolInstructions(promptRegistry: FreyaPromptRegistry): string[];
21
26
  getRegisteredToolboxIds(): string[];
22
27
  }
@@ -1,10 +1,32 @@
1
1
  /**
2
2
  * 内核工具注册表。
3
- * 统一聚合内置工具箱与来自插件体系的外部工具/工具箱。
3
+ * 统一聚合内置工具箱与来自插件体系的外部工具箱。
4
4
  */
5
5
  export class FreyaToolRegistry {
6
+ context;
6
7
  toolboxes = [];
7
- /** 注册一个工具箱(内置或外部插件)。 */
8
+ constructor(context) {
9
+ this.context = context;
10
+ }
11
+ setContext(context) {
12
+ this.context = context;
13
+ }
14
+ /** 判断指定工具箱当前是否已注册且处于可用启用状态 */
15
+ isToolboxEnabled(toolboxId) {
16
+ if (toolboxId === 'meta') {
17
+ return true;
18
+ }
19
+ const isRegistered = this.toolboxes.some((tb) => tb.getId() === toolboxId);
20
+ if (!isRegistered) {
21
+ return false;
22
+ }
23
+ const toolsConfig = this.context?.config?.tools?.builtin;
24
+ if (toolsConfig && typeof toolsConfig[toolboxId]?.enabled === 'boolean') {
25
+ return toolsConfig[toolboxId].enabled;
26
+ }
27
+ return true;
28
+ }
29
+ /** 注册工具箱 */
8
30
  registerToolbox(toolbox) {
9
31
  const newId = toolbox.getId();
10
32
  const existingIndex = this.toolboxes.findIndex((tb) => tb.getId() === newId || tb === toolbox);
@@ -19,27 +41,36 @@ export class FreyaToolRegistry {
19
41
  unregisterToolbox(id) {
20
42
  this.toolboxes = this.toolboxes.filter((tb) => tb.getId() !== id);
21
43
  }
22
- /** 获取指定 ID 的工具箱中的所有原子工具。 */
44
+ /** 获取指定 ID 的工具箱中的所有原子工具 */
23
45
  getToolsInBox(id) {
46
+ if (!this.isToolboxEnabled(id)) {
47
+ return [];
48
+ }
24
49
  const tb = this.toolboxes.find((t) => t.getId() === id);
25
50
  return tb ? tb.getTools() : [];
26
51
  }
27
- /** 聚合所有来源的工具,返回完整工具字典。 */
52
+ /** 聚合所有来源的已启用工具 */
28
53
  getAllTools() {
29
54
  const tools = new Map();
30
55
  for (const toolbox of this.toolboxes) {
56
+ if (!this.isToolboxEnabled(toolbox.getId())) {
57
+ continue;
58
+ }
31
59
  for (const tool of toolbox.getTools()) {
32
60
  tools.set(tool.getDefinition().name, tool);
33
61
  }
34
62
  }
35
63
  return tools;
36
64
  }
37
- /** 根据当前会话已激活的工具箱列表,动态过滤获取所需的工具字典 */
65
+ /** 根据当前会话已激活的工具箱列表,过滤获取所需的工具字典 */
38
66
  getFilteredTools(activeToolboxIds) {
39
67
  const activeSet = new Set(activeToolboxIds || []);
40
68
  const tools = new Map();
41
69
  for (const toolbox of this.toolboxes) {
42
70
  const toolboxId = toolbox.getId();
71
+ if (!this.isToolboxEnabled(toolboxId)) {
72
+ continue;
73
+ }
43
74
  if (toolboxId === 'meta' || activeSet.has(toolboxId)) {
44
75
  for (const tool of toolbox.getTools()) {
45
76
  tools.set(tool.getDefinition().name, tool);
@@ -48,22 +79,27 @@ export class FreyaToolRegistry {
48
79
  }
49
80
  return tools;
50
81
  }
51
- /** 聚合所有来源的工具提示词引导说明,延迟解析 key → 内容 */
82
+ /** 聚合所有已启用的工具箱提示词引导说明 */
52
83
  getToolInstructions(promptRegistry) {
53
84
  const instructions = [];
54
85
  for (const toolbox of this.toolboxes) {
86
+ const toolboxId = toolbox.getId();
87
+ if (!this.isToolboxEnabled(toolboxId)) {
88
+ continue;
89
+ }
55
90
  const key = toolbox.getInstructionPrompt?.();
56
91
  if (!key)
57
92
  continue;
58
93
  const resolved = promptRegistry.get(key);
59
94
  if (resolved) {
60
- const toolboxId = toolbox.getId();
61
95
  instructions.push(`### 工具箱能力说明 [激活ID: "${toolboxId}"]\n${resolved}`);
62
96
  }
63
97
  }
64
98
  return instructions;
65
99
  }
66
100
  getRegisteredToolboxIds() {
67
- return this.toolboxes.map((tb) => tb.getId());
101
+ return this.toolboxes
102
+ .map((tb) => tb.getId())
103
+ .filter((id) => this.isToolboxEnabled(id));
68
104
  }
69
105
  }
package/core/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eoasmxd/freya-core",
3
- "version": "0.4.1",
3
+ "version": "0.4.3",
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.1",
19
+ "@eoasmxd/freya-sdk": "^0.4.3",
20
20
  "ws": "^8.18.0"
21
21
  },
22
22
  "devDependencies": {
package/doc/_index.md CHANGED
@@ -16,7 +16,7 @@ description: "Freya 项目技术文档与开发使用指南。"
16
16
  提供系统首次启动配置、插件管理与日常操作快捷指令。
17
17
 
18
18
  - 🛠️ **[安装与构建运行](installation-guide.md)**
19
- 提供开发环境要求、本地依赖安装、项目编译打包与服务运行调试指南。
19
+ 提供 NPM 全局安装、Docker 容器化部署、源码编译打包与服务运行维护指南。
20
20
 
21
21
  ### 🏗️ 技术规范与设计参考
22
22
 
@@ -49,24 +49,59 @@ freya stop
49
49
 
50
50
  ---
51
51
 
52
- ## 3. 方式二:从源码克隆编译与维护(开发模式)
52
+ ## 3. 方式二:通过 Docker 容器化部署与维护(推荐)
53
+
54
+ 适合希望免去 Node.js 环境配置、开箱即用、或在云服务器/NAS 上实现轻量化私有部署的用户。
55
+
56
+ ### 3.1 启动服务并持久化存储
57
+ 在终端执行以下命令拉取官方最新镜像并常驻启动(Web 服务映射至 3000 端口,并将宿主机当前目录下的 `freya-data` 挂载到容器内持久化数据目录):
58
+
59
+ ```bash
60
+ docker run -d \
61
+ --name freya \
62
+ -p 3000:3000 \
63
+ -v $(pwd)/freya-data:/data \
64
+ --restart unless-stopped \
65
+ ghcr.io/eoasmxd/freya:latest
66
+ ```
67
+
68
+ *也可以使用本地源码直接构建镜像并运行:*
69
+ ```bash
70
+ docker build -t freya .
71
+ docker run -d --name freya -p 3000:3000 -v $(pwd)/freya-data:/data freya
72
+ ```
73
+
74
+ ### 3.2 停止与重启服务
75
+ * **临时停止**:`docker stop freya`
76
+ * **重新唤醒**:`docker start freya`
77
+ * **重启服务**:`docker restart freya`
78
+
79
+ ### 3.3 容器版本平滑更新
80
+ 当发布了新的镜像版本时,执行以下三步即可无损升级:
81
+ 1. **拉取最新镜像**:`docker pull ghcr.io/eoasmxd/freya:latest`
82
+ 2. **销毁旧容器**:`docker rm -f freya`(用户配置与会话记忆均保留在 `./freya-data` 挂载目录中,数据安全无损)
83
+ 3. **重新拉起服务**:重新执行 3.1 中的 `docker run` 命令即可。
84
+
85
+ ---
86
+
87
+ ## 4. 方式三:从源码克隆编译与维护(开发模式)
53
88
 
54
89
  适合需要进行二次开发、编写自定义插件或深入学习 Freya 微内核架构的开发者。
55
90
 
56
- ### 3.1 安装依赖
91
+ ### 4.1 安装依赖
57
92
  克隆项目后,在根目录下安装全量 workspace 依赖:
58
93
  ```bash
59
94
  pnpm install
60
95
  ```
61
96
 
62
- ### 3.2 编译打包
97
+ ### 4.2 编译打包
63
98
  执行代码级编译与物理打包汇总:
64
99
  ```bash
65
100
  pnpm build
66
101
  ```
67
102
  *该命令会编译 core/sdk/ui 与所有插件,并在根目录下生成最终分发包 `dist/`。*
68
103
 
69
- ### 3.3 启动服务
104
+ ### 4.3 启动服务
70
105
  根据开发测试需求选择启动模式:
71
106
  * **前台开发运行**(带 CLI 终端交互):
72
107
  ```bash
@@ -77,13 +112,13 @@ pnpm build
77
112
  pnpm freya --no-cli
78
113
  ```
79
114
 
80
- ### 3.4 停止服务
115
+ ### 4.4 停止服务
81
116
  若需要结束后台常驻的子进程,在根目录下执行:
82
117
  ```bash
83
118
  pnpm stop
84
119
  ```
85
120
 
86
- ### 3.5 手动版本更新与代码同步
121
+ ### 4.5 手动版本更新与代码同步
87
122
  当拉取代码仓库最新修改时,在根目录下执行:
88
123
  1. **停止后台进程**:`pnpm stop`
89
124
  2. **拉取最新源码**:`git pull origin main`
package/freya.js CHANGED
@@ -97,11 +97,16 @@ async function getCliEnabled(args) {
97
97
  return true;
98
98
  }
99
99
 
100
+ function isForegroundMode(args) {
101
+ return args.includes('--foreground') || process.env.FREYA_FOREGROUND === 'true';
102
+ }
103
+
100
104
  await checkSingleInstance();
101
105
 
102
106
  const cliEnabled = await getCliEnabled(process.argv);
107
+ const isForeground = isForegroundMode(process.argv);
103
108
 
104
- if (!cliEnabled) {
109
+ if (!cliEnabled && !isForeground) {
105
110
  const child = fork(coreIndex, process.argv.slice(2), {
106
111
  detached: true,
107
112
  stdio: 'ignore',
@@ -129,6 +134,16 @@ if (!cliEnabled) {
129
134
  }
130
135
  });
131
136
 
137
+ const forwardSignal = (signal) => {
138
+ if (child.pid) {
139
+ try {
140
+ process.kill(child.pid, signal);
141
+ } catch { }
142
+ }
143
+ };
144
+ process.on('SIGTERM', () => forwardSignal('SIGTERM'));
145
+ process.on('SIGINT', () => forwardSignal('SIGINT'));
146
+
132
147
  child.on('exit', (code) => {
133
148
  process.exit(code ?? 0);
134
149
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eoasmxd/freya",
3
- "version": "0.4.1",
3
+ "version": "0.4.3",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "description": "Freya - 微内核智能体系统",
@@ -35,10 +35,10 @@
35
35
  "src"
36
36
  ],
37
37
  "dependencies": {
38
- "@eoasmxd/freya-sdk": "^0.4.1",
38
+ "@eoasmxd/freya-sdk": "^0.4.3",
39
39
  "ws": "^8.18.0",
40
40
  "mysql2": "^3.11.0",
41
- "qrcode-terminal": "^0.12.0"
41
+ "qrcode": "^1.5.4"
42
42
  },
43
43
  "scripts": {
44
44
  "start": "node freya.js",
@@ -18,7 +18,7 @@
18
18
  "clean": "rm -rf dist"
19
19
  },
20
20
  "dependencies": {
21
- "@eoasmxd/freya-sdk": "^0.4.1"
21
+ "@eoasmxd/freya-sdk": "^0.4.3"
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.1"
21
+ "@eoasmxd/freya-sdk": "^0.4.3"
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.1"
22
+ "@eoasmxd/freya-sdk": "^0.4.3"
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.1"
24
+ "@eoasmxd/freya-sdk": "^0.4.3"
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.1"
24
+ "@eoasmxd/freya-sdk": "^0.4.3"
25
25
  },
26
26
  "devDependencies": {
27
27
  "@types/node": "^22.0.0",
@@ -23,7 +23,7 @@
23
23
  "clean": "rm -rf dist"
24
24
  },
25
25
  "dependencies": {
26
- "@eoasmxd/freya-sdk": "^0.4.1",
26
+ "@eoasmxd/freya-sdk": "^0.4.3",
27
27
  "mysql2": "^3.11.0"
28
28
  },
29
29
  "devDependencies": {
@@ -22,7 +22,7 @@
22
22
  "clean": "rm -rf dist"
23
23
  },
24
24
  "dependencies": {
25
- "@eoasmxd/freya-sdk": "^0.4.1"
25
+ "@eoasmxd/freya-sdk": "^0.4.3"
26
26
  },
27
27
  "devDependencies": {
28
28
  "@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.1",
22
+ "@eoasmxd/freya-sdk": "^0.4.3",
23
23
  "ws": "^8.18.0"
24
24
  },
25
25
  "devDependencies": {
@@ -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
- // @ts-ignore
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 matrix = [];
232
- qrcodeTerminal.generate(scanUrl, { small: false }, (code) => {
233
- const lines = code.split("\n");
234
- for (const line of lines) {
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
- `![微信登录二维码](${qrDataUri})\n\n` +
269
- `*(提示:若二维码未能正常显示,您可以直接点击 [打开微信二维码网页](${scanUrl}) 扫码绑定)*`;
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.1",
22
- "qrcode-terminal": "^0.12.0"
21
+ "@eoasmxd/freya-sdk": "^0.4.3",
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
  }
@@ -697,6 +697,41 @@ export class FreyaConfigManager {
697
697
  min: 10,
698
698
  max: 300,
699
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: '系统指令'
700
735
  }
701
736
  ];
702
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();
@@ -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
- /** 聚合所有来源的工具提示词引导说明,延迟解析 key → 内容 */
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.map((tb) => tb.getId());
115
+ return this.toolboxes
116
+ .map((tb) => tb.getId())
117
+ .filter((id) => this.isToolboxEnabled(id));
79
118
  }
80
119
  }
120
+
@@ -2,8 +2,7 @@ import type { ChannelPlugin, FreyaAttachment, FreyaCommand, FreyaContext } from
2
2
  import crypto from "node:crypto";
3
3
  import fs from "node:fs/promises";
4
4
  import path from "node:path";
5
- // @ts-ignore
6
- import qrcodeTerminal from "qrcode-terminal";
5
+ import QRCode from "qrcode";
7
6
 
8
7
  interface WeixinBotConfig {
9
8
  id: string;
@@ -279,48 +278,24 @@ export default class FreyaWeixinChannelPlugin implements ChannelPlugin {
279
278
  throw new Error("获取微信登录二维码失败:服务端未返回 qrcode");
280
279
  }
281
280
 
282
- const matrix: number[][] = [];
283
- qrcodeTerminal.generate(scanUrl, { small: false }, (code: string) => {
284
- const lines = code.split("\n");
285
- for (const line of lines) {
286
- const cleanLine = line.replace(/[\r\n]/g, "");
287
- if (!cleanLine) continue;
288
- const row: number[] = [];
289
- const re = /\x1b\[(40|47)m \x1b\[0m/g;
290
- let match;
291
- while ((match = re.exec(cleanLine)) !== null) {
292
- row.push(match[1] === "40" ? 1 : 0);
293
- }
294
- if (row.length > 0) {
295
- matrix.push(row);
296
- }
297
- }
281
+ const qrAscii = await QRCode.toString(scanUrl, {
282
+ type: "utf8",
283
+ errorCorrectionLevel: "M",
284
+ margin: 2
298
285
  });
299
286
 
300
- const size = matrix.length;
301
- let svgContent = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${size} ${size}" width="180" height="180" style="shape-rendering:crispEdges;">`;
302
- svgContent += `<rect width="${size}" height="${size}" fill="#ffffff"/>`;
303
- for (let y = 0; y < size; y++) {
304
- const row = matrix[y];
305
- for (let x = 0; x < row.length; x++) {
306
- if (row[x] === 1) {
307
- svgContent += `<rect x="${x}" y="${y}" width="1" height="1" fill="#000000"/>`;
308
- }
309
- }
310
- }
311
- svgContent += `</svg>`;
312
-
313
- const base64Svg = Buffer.from(svgContent).toString("base64");
314
- const qrDataUri = `data:image/svg+xml;base64,${base64Svg}`;
315
-
316
287
  this.pollWeixinQrStatus(ctx, accountId, config, qrcode).catch((err) => {
317
288
  ctx.logger.error(`微信账号 [${accountId}] 后台监听扫码绑定失败:`, err.message);
318
289
  });
319
290
 
320
- return `⚠️ **微信账号 [${accountId}] 登录二维码已成功生成!**\n\n` +
291
+ return (
292
+ `⚠️ **微信账号 [${accountId}] 登录二维码已成功生成!**\n\n` +
321
293
  `**[微信扫码] 请使用微信扫描下方二维码绑定账号 [${accountId}]**:\n\n` +
322
- `![微信登录二维码](${qrDataUri})\n\n` +
323
- `*(提示:若二维码未能正常显示,您可以直接点击 [打开微信二维码网页](${scanUrl}) 扫码绑定)*`;
294
+ "```text\n" +
295
+ qrAscii +
296
+ "\n```\n\n" +
297
+ `*(提示:若字符二维码未能正常显示或无法扫描,您可以直接点击 [打开微信二维码网页](${scanUrl}) 扫码绑定)*`
298
+ );
324
299
  } catch (err: any) {
325
300
  ctx.logger.error(`微信账号 [${accountId}] 拉取扫码登录失败:`, err.message);
326
301
  return `❌ 拉取微信登录二维码失败: ${err.message}`;