@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,142 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "10.1 微内核架构解耦与插件契约设计"
|
|
3
|
+
weight: 10
|
|
4
|
+
description: "探讨微内核解耦架构的必要性,确立 Monorepo workspace 严苛的单向依赖边界规范与契约接口设计。"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# 10.1 微内核架构解耦与插件契约设计
|
|
8
|
+
|
|
9
|
+
在前面的章节中,我们打通了智能体的心智模型、短期记忆治理以及高可靠流式事件流。
|
|
10
|
+
|
|
11
|
+
当我们的智能体系统准备从“单机实验室”走向大规模的“企业级商业化定制”时,我们会面临一个不可回避的工程大考:**如何让底座支持几十种不同的垂直行业 LLM 模型、成百上千种定制化的第三方业务工具(Tools),以及接入各种完全不同的前端通信渠道?**
|
|
12
|
+
|
|
13
|
+
如果我们将这些模型驱动、微信公众号回调、Telegram API 以及具体的业务操作工具,全部堆砌在 `packages/core` 内核源码中:
|
|
14
|
+
* **物理包体积膨胀**:任何一个小工具的微调,都需要全量编译打包 core 服务。
|
|
15
|
+
* **发布高风险**:一个 Telegram 连接插件的 Bug 会直接导致内核的短期记忆管理和 EventBus 彻底瘫痪。
|
|
16
|
+
* **代码所有权混乱**:多个团队在同一个仓库核心代码里高频提交,冲突不断。
|
|
17
|
+
|
|
18
|
+
为了打赢这场扩展性战争,底座必须实行**微内核(Microkernel)解耦设计**,在物理目录和依赖层面划定严苛的**单向边界铁律**。
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## 一、 微内核解耦哲学:内核是骨骼,插件是肌肉
|
|
23
|
+
|
|
24
|
+
微内核架构(又称插件化架构)的核心哲学是:**内核只提供最基础的、与具体业务无关的核心运行规则,而将所有可变能力外包给插件(Plugins)来丰富。**
|
|
25
|
+
|
|
26
|
+
在 Freya 中,这一原则在物理层面上表现为:
|
|
27
|
+
* **内核(Core)负责**:ReAct 自循环逻辑(`agent-executor.ts`)、会话与上下文压缩管理(`session-manager.ts`)、进程内事件调度总线(`event-bus.ts`)以及插件的生命周期装配。
|
|
28
|
+
* **插件(Plugins)负责**:具体大厂模型的网络交互适配(如 `plugin-gemini`)、具体物理工具执行(如 `plugin-tool-fs` 进行文件读取)、以及具体的长链接通信适配(如 `plugin-wecom-channel` 连接企业微信)。
|
|
29
|
+
|
|
30
|
+
内核提供骨架支撑,而插件提供具体的力量触达,两者实现完美的物理分离。
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## 二、 Monorepo 依赖边界的“单向铁律”
|
|
35
|
+
|
|
36
|
+
为了保障微内核架构不退化,Freya 采用 `pnpm workspace` 进行 Monorepo 管理,并在物理层面上划定了**单向依赖红线**:
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
┌──────────────────────────────┐
|
|
40
|
+
│ packages/core (内核) │
|
|
41
|
+
└──────────────┬───────────────┘
|
|
42
|
+
│ 强引用依赖
|
|
43
|
+
▼
|
|
44
|
+
┌──────────────────────────────┐
|
|
45
|
+
│ packages/sdk (抽象契约) │
|
|
46
|
+
└──────────────▲───────────────┘
|
|
47
|
+
│ 强引用依赖
|
|
48
|
+
▼
|
|
49
|
+
┌──────────────────────────────┐
|
|
50
|
+
│ plugins/ (外挂插件) │
|
|
51
|
+
└──────────────────────────────┘
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### 🚨 绝对禁止的物理依赖:
|
|
55
|
+
1. **内核绝对不依赖任何插件**。内核中禁止出现 `import ... from 'plugins/plugin-gemini'` 这样的物理路径导入。内核对插件的加载,只在运行时通过扫描目录执行**动态加载(Dynamic Loading)**。
|
|
56
|
+
2. **插件绝对禁止依赖内核内部实现**。插件的 `package.json` 依赖项中,绝对不允许出现 `@eoasmxd/freya-core`。插件如果试图越界导入 core 内的具体类,会导致循环引用,并导致 Monorepo 编译链瞬间崩溃。
|
|
57
|
+
|
|
58
|
+
### 🌟 唯一的黄金通道:`packages/sdk`
|
|
59
|
+
插件有且仅能依赖轻量级的只读接口包 `@eoasmxd/freya-sdk`。
|
|
60
|
+
SDK 中不包含任何实质运行代码,**只包含 TypeScript 契约抽象接口(Interfaces / Types)**。
|
|
61
|
+
插件实现 SDK 暴露的 `LLMPlugin`、`ToolPlugin` 契约,而内核也通过 SDK 契约去获取插件的实例。大家共同遵守同一本协议书(Contracts),各行其道。
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## 三、 标准插件契约的 TypeScript 接口设计
|
|
66
|
+
|
|
67
|
+
任何一个被内核识别的插件,其入口类在物理上必须严格实现 SDK 中的 `FreyaPlugin` 契约定义:
|
|
68
|
+
|
|
69
|
+
```typescript
|
|
70
|
+
import type { ILLMService } from './llm.js';
|
|
71
|
+
|
|
72
|
+
export interface EventBus {
|
|
73
|
+
on(event: string, listener: (...args: any[]) => void): void;
|
|
74
|
+
off(event: string, listener: (...args: any[]) => void): void;
|
|
75
|
+
emit(event: string, ...args: any[]): void;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface Logger {
|
|
79
|
+
info(message: string, ...args: any[]): void;
|
|
80
|
+
warn(message: string, ...args: any[]): void;
|
|
81
|
+
error(message: string, ...args: any[]): void;
|
|
82
|
+
debug(message: string, ...args: any[]): void;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface FreyaPaths {
|
|
86
|
+
appRoot: string; // 程序物理安装根目录
|
|
87
|
+
projectRoot: string; // 运行态主目录 (默认 ~/.freya)
|
|
88
|
+
dataDir: string; // 运行时数据持久化目录
|
|
89
|
+
workspaceDir: string; // 隔离的读写文件沙箱
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface FreyaContext {
|
|
93
|
+
eventBus: EventBus;
|
|
94
|
+
logger: Logger;
|
|
95
|
+
readonly config: Readonly<Record<string, any>>;
|
|
96
|
+
llm: ILLMService; // 统一的大模型服务接口句柄
|
|
97
|
+
paths: FreyaPaths;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export interface FreyaPlugin {
|
|
101
|
+
type: 'llm' | 'tool' | 'channel' | ('llm' | 'tool' | 'channel')[]; // 插件多态标签,用于逻辑路由
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* 生命周期的黄金钩子。
|
|
105
|
+
* 底座在运行时动态加载插件后,会第一时间拉起此方法,
|
|
106
|
+
* 并将物理上下文句柄 (ctx) 强行注入给插件,作为其安全访问底座的唯一窗口。
|
|
107
|
+
*/
|
|
108
|
+
setup(ctx: FreyaContext): Promise<void>;
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
* **物理注入机制**:在 `setup(ctx)` 期间,底座将核心的 `eventBus` 和沙箱的 `paths` 注入给插件。
|
|
113
|
+
这实现了:插件无需自行读取操作系统的环境变量去判断 `~/.freya/workspace/` 在哪里,直接通过 `ctx.paths.workspaceDir` 即可安全定位工作沙箱,防止插件私自在磁盘上乱建文件夹。
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## 四、 【调试与避坑】Monorepo 下的循环依赖检测与防御
|
|
118
|
+
|
|
119
|
+
在 `pnpm workspace` 的单机多包开发模式下,由于所有代码都在一个物理仓库里,开发者在写插件时,IDE 的自动导包功能极易在你不经意间写出越界导入:
|
|
120
|
+
`import { estimateMessageTokens } from '../../packages/core/src/session/compactor';`
|
|
121
|
+
|
|
122
|
+
### 1. 循环依赖的物理恶果
|
|
123
|
+
这种强行跨域强导入,会让 `pnpm` 构建链将插件与内核强行缝合。
|
|
124
|
+
当你发布编译时,会产生严重的循环引用(Circular Dependency),导致 Node.js 在运行时初始化阶段报 `TypeError: Class extends value undefined is not a constructor or null`,直接瘫痪整个服务启动。
|
|
125
|
+
|
|
126
|
+
### 2. 避坑策略:Madge 依赖静态分析检测
|
|
127
|
+
为了物理防御这一隐患,我们在教程和构建流水线中,必须强制引入依赖静态分析器 **`madge`**:
|
|
128
|
+
|
|
129
|
+
我们可以在根目录的 `package.json` 中配置自检脚本:
|
|
130
|
+
```json
|
|
131
|
+
"scripts": {
|
|
132
|
+
"lint:deps": "madge --circular packages/core/src/index.ts"
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
```
|
|
137
|
+
[运行 madge 检测] ───> 物理扫描 TS 抽象语法树 (AST) ───> 揪出越界相对 import ───> 拒绝构建
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
任何试图跨越 `/core/` 边界的强导入,都会在本地提交的最后一微秒被 CI 静态防线无情打回,逼迫开发人员老老实实将共用类型下沉提取到 `packages/sdk` 中,捍卫了微内核微服务物理架构的极致洁净。
|
|
141
|
+
|
|
142
|
+
本节我们明确了微内核解耦哲学与 Monorepo Workspace 的依赖红线。在下一小节中,我们将重点研究插件 package.json 中下沉的静态元数据规范,看懂底座如何通过静态声明控制插件的安全启停。
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "10.2 依赖边界与安全沙箱"
|
|
3
|
+
weight: 20
|
|
4
|
+
description: "探讨插件静态元数据下沉声明的安全性优势,设计零信任默认禁用安全防守拦截,构建只读配置消费约束。"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# 10.2 依赖边界与安全沙箱
|
|
8
|
+
|
|
9
|
+
在 10.1 节中,我们确立了微内核解耦设计与 Monorepo Workspace 的单向依赖红线。然而,仅仅在代码编译期规定“谁不能引用谁”还不够。在运行时,智能体底座如果必须**无条件加载并运行**所有插件的 JavaScript 逻辑代码,才能获取插件的名称、功能说明和所需配置参数,这会带来极其严重的安全隐患。
|
|
10
|
+
|
|
11
|
+
* **毁灭性的安全崩溃**:如果用户扫描并导入了一个恶意的第三方插件,该插件的 JS 入口文件中包含木马挖矿程序。在底座试图“读取”该插件元数据并实例化它的那一微秒,恶意代码就已经在服务器的内存中悄然运行了,防线瞬间决堤。
|
|
12
|
+
|
|
13
|
+
为此,底座在设计插件系统时,必须引入**静态元数据下沉声明(Metadata Decoupling)**与**零信任默认禁用(Security by Default)**的安全沙箱防护。
|
|
14
|
+
|
|
15
|
+
本节我们将详细拆解这一安全沙箱的设计细节与工程避坑实操。
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## 一、 静态元数据下沉声明的物理优势
|
|
20
|
+
|
|
21
|
+
为了在“不读取执行任何一行 JS 代码”的前提下,静态洞察插件的基本概况,底座强制规定:**所有插件的静态元数据、参数 Schema 定义以及提示词依赖,必须统一下沉声明在 `package.json` 的 `"freya"` 声明块中。**
|
|
22
|
+
|
|
23
|
+
### 1. package.json 静态元数据标本
|
|
24
|
+
```json
|
|
25
|
+
{
|
|
26
|
+
"name": "@eoasmxd/freya-plugin-wechat",
|
|
27
|
+
"version": "1.0.0",
|
|
28
|
+
"main": "dist/index.js",
|
|
29
|
+
"freya": {
|
|
30
|
+
"displayName": "微信通道适配器",
|
|
31
|
+
"defaultEnabled": false,
|
|
32
|
+
"schema": "./schema.json",
|
|
33
|
+
"prompts": ["plugin.prompt.wechat.md"]
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### 2. 物理设计意图与扫描避让
|
|
39
|
+
当底座在启动或扫描插件目录时,加载器(Loader)**绝对不会**去 `require()` 或 `import()` 插件的 `main` 入口文件(`dist/index.js`)。
|
|
40
|
+
|
|
41
|
+
加载器只会通过底层的物理文件探针,读取 `package.json` 的文本内容,通过轻量级、安全的 `JSON.parse` 提取 `"freya"` 字段:
|
|
42
|
+
* **无害读取**:这从物理上杜绝了在初筛阶段触发任何未受信任的 JS 代码执行栈。
|
|
43
|
+
* **提前布防**:在不实例化插件的前提下,底座就已经知道了插件的友好名称(`displayName`)、它所要求的静态配置格式文件路径(`schema`)、以及它运行所需的 Prompt 文件(`prompts`)。
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## 二、 零信任默认启用:安全启停防守拦截 (Security by Default)
|
|
48
|
+
|
|
49
|
+
在微内核系统中,对于插件的启停控制,必须遵循**零信任(Zero Trust)**的默认禁用安全法则。
|
|
50
|
+
|
|
51
|
+
### 🚨 绝对禁止的漏洞逻辑:
|
|
52
|
+
底座扫描到了一个第三方外挂插件,直接读取它 `package.json` 里的 `"defaultEnabled": true`,并盲目将该插件初始化并投入运行。这意味着恶意插件只需在元数据里写一个 `true`,就能大摇大摆地在后台越权运行。
|
|
53
|
+
|
|
54
|
+
### 🌟 物理安全的加载器防御拦截算法:
|
|
55
|
+
为了防御这一安全漏洞,Freya 的 `FreyaPluginManager`(位于 `packages/core/src/plugin/plugin-manager.ts`)在扫描分析插件包时,对**内置包内插件**与**外置第三方插件**进行了严格的安全策略划分:
|
|
56
|
+
|
|
57
|
+
```typescript
|
|
58
|
+
// 摘自 packages/core/src/plugin/plugin-manager.ts
|
|
59
|
+
private async inspectPluginPackage(
|
|
60
|
+
dirPath: string,
|
|
61
|
+
source: 'builtin' | 'runtime' | 'npm'
|
|
62
|
+
): Promise<DiscoveredPluginInfo | null> {
|
|
63
|
+
try {
|
|
64
|
+
const pkgPath = path.join(dirPath, 'package.json');
|
|
65
|
+
const rawPkg = await fs.readFile(pkgPath, 'utf-8');
|
|
66
|
+
const pkg = JSON.parse(rawPkg);
|
|
67
|
+
|
|
68
|
+
const id = String(pkg.name || '').trim();
|
|
69
|
+
if (!id) return null;
|
|
70
|
+
|
|
71
|
+
const displayName = String(pkg.freya?.displayName || pkg.displayName || id);
|
|
72
|
+
const description = String(pkg.description || '');
|
|
73
|
+
const version = String(pkg.version || '0.1.0');
|
|
74
|
+
|
|
75
|
+
// 💡 零信任安全拦截防线:
|
|
76
|
+
// 严格遵循 Security by Default 原则,仅内置物理目录 (source === 'builtin')
|
|
77
|
+
// 且显式置为 true 的插件初始启用;其余环境及外置插件统一默认禁用 (false)!
|
|
78
|
+
const defaultEnabled = (source === 'builtin' && pkg.freya?.defaultEnabled === true);
|
|
79
|
+
|
|
80
|
+
const rawPrompts = pkg.freya?.prompts;
|
|
81
|
+
const prompts = Array.isArray(rawPrompts) ? rawPrompts.map(String) : [];
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
id,
|
|
85
|
+
resolvedDir: dirPath,
|
|
86
|
+
mainEntry: path.join(dirPath, pkg.main || 'dist/index.js'),
|
|
87
|
+
displayName,
|
|
88
|
+
description,
|
|
89
|
+
version,
|
|
90
|
+
source,
|
|
91
|
+
defaultEnabled, // 物理安全判定值
|
|
92
|
+
prompts,
|
|
93
|
+
valid: true
|
|
94
|
+
};
|
|
95
|
+
} catch (err: any) {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### 物理安全效果:
|
|
102
|
+
外置的第三方插件即使在 `package.json` 里自称 `defaultEnabled: true`,也会在底座解析器的 `(source === 'builtin' && pkg.freya?.defaultEnabled === true)` 校验前被安全判定为 `false`。该插件将处于冰冻状态,必须由系统管理员在后台经过严格审核后手动开启,彻底破除了越权启用的风险。
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## 三、 只读配置消费约束
|
|
108
|
+
|
|
109
|
+
插件沙箱的另一道防线是:**插件仅作为全局配置的“只读消费端”,绝对不感知也不执行任何配置文件的落盘写入动作。**
|
|
110
|
+
|
|
111
|
+
* **物理红线**:插件内部不允许调用任何修改全局 `freya.json` 或 `providers.json` 的 I/O 方法。
|
|
112
|
+
* **后果预防**:如果允许插件修改全局配置,一个工具插件一旦被大模型调用,有可能会在后台篡改系统核心授权,把管理员的 API key 发送到黑客服务器上。所有的写配置权限被死死收归底座内核,插件只接受底座分配给它的只读配置副本。
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
## 四、 【调试与避坑】外置插件默认启用导致的主动安全越权排查
|
|
117
|
+
|
|
118
|
+
### 1. 越权事故现场
|
|
119
|
+
在调试阶段,开发人员经常会将自己写的本地插件文件夹路径填入外置加载列表中。由于底座在扫描时,如果漏掉了 `isInternal` 路径检测,该插件就会以 `defaultEnabled: true` 越权进入执行状态。
|
|
120
|
+
此时如果插件带有全局拦截器,它会截获所有 Session 的 EventBus 消息,造成调试状态与生产状态的混乱,发生重大敏感日志外泄。
|
|
121
|
+
|
|
122
|
+
### 2. 避坑策略:严格执行物理相对路径断言
|
|
123
|
+
在加载外部插件时,必须对插件所在的绝对路径执行安全越界断言,确保它无法读取程序主沙箱以外的任何私有文件,并在控制台强制打出黄色安全警告,告知开发人员“当前检测到外置未授权插件,已默认置为冰冻状态,请手动去管理控制台授权开启”,实现了物理安全闭环。
|
|
124
|
+
|
|
125
|
+
本节我们解剖了静态元数据下沉声明的必要性、零信任默认禁用安全防守拦截以及配置只读消费约束。在下一小节中,我们将实际白盒剖析核心通道插件(如 CLI/WebSockets 通道插件)在底座中开发与消息转换的TypeScript实现。
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "10.3 【白盒剖析】通道插件开发与消息转换"
|
|
3
|
+
weight: 30
|
|
4
|
+
description: "白盒解剖微信通道插件 plugin-weixin-channel 源码,拆解多模态文件 AES 解密与标准 connection 事件的双向桥接转换机制。"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# 10.3 【白盒剖析】通道插件开发与消息转换
|
|
8
|
+
|
|
9
|
+
在 10.2 节中,我们建立了插件的静态元数据规范与零信任默认禁用的安全沙箱。当安全装配线就绪后,我们该如何真正开发一个通信适配通道插件,实现外部平台消息与底座事件总线的双向转换?
|
|
10
|
+
|
|
11
|
+
本节我们将实际白盒剖析 Freya 核心微信 iLink 通道插件(位于 `plugins/plugin-weixin-channel/src/index.ts`)的源码。
|
|
12
|
+
|
|
13
|
+
我们将一起拆解它是如何处理微信平台特有的多模态(图片、语音、文件)AES 加密文件解密,并将其翻译为 SDK 标准的 `connection:message` 与 `connection:reply` 双向事件桥接的。
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## 一、 统一连接 ID (Connection ID) 的物理路由绑定
|
|
18
|
+
|
|
19
|
+
底座必须区分消息来自哪一个具体的微端、哪一个具体的群。微信插件将微信官方的机器人 ID `botId` 与微信聊天群/用户 ID `chatId` 拼接为唯一的全局连接标识:
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
private getWeixinConnectionId(botId: string, chatId: string): string {
|
|
23
|
+
return `weixin:${botId}:${chatId}`;
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
* **物理优势**:这让 ConnectionManager 能够在一微秒内完成识别,并将其映射到对应的 `sessionId`,实现了跨平台的流量路由。
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## 二、 微信多模态二进制数据的反向解密与转译
|
|
32
|
+
|
|
33
|
+
微信官方 iLink 服务器出于安全隐私考虑,向机器人推送的图片、视频和文件附件全部是通过**对称加密(AES)保护**的。
|
|
34
|
+
|
|
35
|
+
插件必须先在本地执行物理下载并反向解密,将其组装成 SDK 能够识别的 `FreyaAttachment` 标准附件:
|
|
36
|
+
|
|
37
|
+
```typescript
|
|
38
|
+
const items = weixinMsg.item_list || [];
|
|
39
|
+
for (const item of items) {
|
|
40
|
+
let text = "";
|
|
41
|
+
const attachments: FreyaAttachment[] = [];
|
|
42
|
+
|
|
43
|
+
if (item.type === 1 && item.text_item?.text) {
|
|
44
|
+
text = item.text_item.text; // 1. 文本消息
|
|
45
|
+
} else if (item.type === 2) {
|
|
46
|
+
// 2. 图片消息 (带 AES 加密)
|
|
47
|
+
const imgUrl = item.image_item?.media?.full_url || "";
|
|
48
|
+
const aesKey = item.image_item?.aeskey || "";
|
|
49
|
+
let result;
|
|
50
|
+
if (imgUrl) {
|
|
51
|
+
// 💡 物理防线:调用内部解密器进行 AES 物理还原,并存入工作沙箱目录
|
|
52
|
+
result = await this.downloadAndDecryptWeixinMedia(ctx, imgUrl, aesKey, "image.jpg", true);
|
|
53
|
+
}
|
|
54
|
+
if (result) {
|
|
55
|
+
attachments.push({
|
|
56
|
+
type: "image",
|
|
57
|
+
mimeType: result.mimeType,
|
|
58
|
+
path: result.path // 写入解密后的工作区物理相对路径
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
} else if (item.type === 3) {
|
|
62
|
+
// 3. 语音消息:读取微信官方接口自动语音识别转译的文字内容
|
|
63
|
+
const voiceText = item.voice_item?.text || "";
|
|
64
|
+
text = `[语音转文字: ${voiceText || "未识别"}]`;
|
|
65
|
+
} else if (item.type === 4 && item.file_item) {
|
|
66
|
+
// 4. 文件附件 (带 AES 加密)
|
|
67
|
+
const fileUrl = item.file_item.media?.full_url || "";
|
|
68
|
+
const fileName = item.file_item.file_name || "未命名文件";
|
|
69
|
+
const aesKey = item.file_item.media?.aes_key || "";
|
|
70
|
+
let result = await this.downloadAndDecryptWeixinMedia(ctx, fileUrl, aesKey, fileName, false);
|
|
71
|
+
if (result) {
|
|
72
|
+
attachments.push({ type: "file", mimeType: result.mimeType, path: result.path });
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
经过这一层转译,微信平台复杂的 AES 乱序二进制,被净化为了标准的结构化纯文本与解密文件实体,为接下来的“事件上报”打好了物理基础。
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## 三、 双向事件桥接的物理闭环
|
|
83
|
+
|
|
84
|
+
微信插件在 `setup(ctx)` 期间,拉起与底座核心 EventBus 的桥接。整个通信闭环通过两个方向的异步事件实现解耦:
|
|
85
|
+
|
|
86
|
+
```
|
|
87
|
+
微信官方 API (外部)
|
|
88
|
+
│
|
|
89
|
+
▼ (长轮询 getUpdates 捕获消息)
|
|
90
|
+
[微信通道插件] ── 1. emit("connection:message") ──> [底座 EventBus] ──> (核心 ReAct 推理)
|
|
91
|
+
│ │
|
|
92
|
+
▲ ── 2. on("connection:reply") <── [底座 EventBus] <──────────────┘
|
|
93
|
+
│
|
|
94
|
+
▼ (调用 sendWeixinMessage 推送)
|
|
95
|
+
微信用户屏幕 (用户可见)
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### 1. 输入事件上报(微信 -> 底座内核)
|
|
99
|
+
当微信插件接收到解密后的文本及附件后,向总线上报 `connection:message` 唤醒底座:
|
|
100
|
+
|
|
101
|
+
```typescript
|
|
102
|
+
if (text || attachments.length > 0) {
|
|
103
|
+
// 1. 广播连接已激活,告知 ConnectionManager 该通道当前在线
|
|
104
|
+
ctx.eventBus.emit("connection:active", {
|
|
105
|
+
connectionId,
|
|
106
|
+
defaultSessionId: connectionId,
|
|
107
|
+
staleThresholdMs: 0
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// 2. 将消息与文件上载总线
|
|
111
|
+
ctx.eventBus.emit("connection:message", {
|
|
112
|
+
connectionId,
|
|
113
|
+
content: text,
|
|
114
|
+
attachments
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### 2. 输出事件接管与回传(底座内核 -> 微信)
|
|
120
|
+
插件在 `setup()` 钩子中,订阅 `connection:reply` 答复事件。一旦监听到属于本通道的回复,立刻反向推回微信:
|
|
121
|
+
|
|
122
|
+
```typescript
|
|
123
|
+
async setup(ctx: FreyaContext): Promise<void> {
|
|
124
|
+
this.context = ctx;
|
|
125
|
+
|
|
126
|
+
// 💡 订阅统一消息回复事件
|
|
127
|
+
ctx.eventBus.on("connection:reply", async (payload: { connectionId: string; content: string }) => {
|
|
128
|
+
const prefix = "weixin:";
|
|
129
|
+
// 过滤是否是发往微信通道的消息
|
|
130
|
+
if (payload.connectionId.startsWith(prefix)) {
|
|
131
|
+
// 提取 connectionId 中的 botId 和 chatId
|
|
132
|
+
const parts = payload.connectionId.slice(prefix.length).split(":");
|
|
133
|
+
if (parts.length >= 2) {
|
|
134
|
+
const botId = parts[0];
|
|
135
|
+
const chatId = parts.slice(1).join(":");
|
|
136
|
+
// 🚀 物理回传:将大模型的最终文本发送回微信官方接口
|
|
137
|
+
await this.sendWeixinMessage(botId, chatId, payload.content);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### 物理设计的解耦美学:
|
|
145
|
+
在这套闭环下,微信插件的代码中**找不到任何对 `AgentExecutor` 或 `SessionManager` 的物理强引用**。它只跟 SDK 中的 EventBus 发生通信。
|
|
146
|
+
|
|
147
|
+
内核对微信平台的 API 变化也完全不需要有任何代码层面的感知。双向事件管道在内存中完成了最优雅的抽象对齐。
|
|
148
|
+
|
|
149
|
+
本节我们白盒解剖了微信通道插件的多模态 AES 解密、输入上报与回复监听的 TypeScript 实现。在下一小节中,我们将在本地进行动手调试,解决由于“轮询心跳挂起”导致的通道假死大 Bug。
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "10.4 调试与避坑指南:物理连接假死与指数退避重连"
|
|
3
|
+
weight: 40
|
|
4
|
+
description: "实战调试通道长轮询网络挂起假死,设计基于超时熔断、指数退避重连与陈旧消息风暴物理丢弃防御机制。"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# 10.4 调试与避坑指南:物理连接假死与指数退避重连
|
|
8
|
+
|
|
9
|
+
在前几节中,我们解剖了微内核架构的单向依赖边界、插件 package.json 的静态元数据声明,并白盒剖析了微信 iLink 通道插件的多模态转译与双向事件桥接。
|
|
10
|
+
|
|
11
|
+
当我们的智能体服务脱离了本地完美的 `127.0.0.1` 开发网,进入复杂的移动基站、写字楼局域网或云端多网段环境时,长轮询(Long Polling)与长连接(WebSockets)通道插件会频繁触发一个最为棘手的底层硬件问题 —— **“静默假死(Silent Connection Death)”**:
|
|
12
|
+
* 微信机器人运行一天后,突然不再回复任何消息。
|
|
13
|
+
* 控制台没有打出任何 Error 报错,CPU 占用率为 0%,内存正常,系统没有任何崩溃迹象。
|
|
14
|
+
* **物理本质**:网络链路发生瞬时断开或网关丢包,但操作系统底层未能在 TCP 层面触发 close 状态,导致 Node.js 异步 fetch 线程被**无限期卡死在 await pending 状态**,整个轮询循环彻底失去心跳。
|
|
15
|
+
|
|
16
|
+
本节我们将实际编写脚本复现这一网络黑洞卡死,并设计超时熔断、指数退避重连与防范上线消息洪水的三重物理防线。
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## 一、 网络黑洞下的“静默假死”成因
|
|
21
|
+
|
|
22
|
+
在 10.3 节的微信长轮询中,我们读到:
|
|
23
|
+
`while (state.running) { const res = await this.callWeixinApi(...) }`
|
|
24
|
+
|
|
25
|
+
在标准的 HTTP 请求中,如果底层网络连接发生了断开(例如网线被拔掉、路由器死机或基站切换),而服务器端的 `fetch` 请求中**没有配置显式的超时时间(Timeout Limit)**:
|
|
26
|
+
* Node.js 内部的 Socket 句柄会傻傻地一直等待 TCP 握手确认或数据帧的到来。
|
|
27
|
+
* 因为没有超时触发,这行 `await` 永远不会 resolve 也不会 reject。
|
|
28
|
+
* 轮询 `while` 循环彻底被定格在此处,丧失了继续拉取新消息的能力,表现为无声无息的“静默离线”。
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## 二、 本地调试:复现网络黑洞挂起
|
|
33
|
+
|
|
34
|
+
为了复现这一黑洞挂起,我们在本地模拟一个缺乏超时的轮询器:
|
|
35
|
+
|
|
36
|
+
### 1. 假死复现脚本 (zombie_poll_test.js)
|
|
37
|
+
```javascript
|
|
38
|
+
let running = true;
|
|
39
|
+
|
|
40
|
+
// 模拟向官方接口发起的长轮询
|
|
41
|
+
async function mockLongPollRequest(round) {
|
|
42
|
+
console.log(`[HTTP Request] 正在发起第 ${round} 次消息轮询...`);
|
|
43
|
+
|
|
44
|
+
return new Promise((resolve) => {
|
|
45
|
+
// 💡 模拟糟糕的网络黑洞:接口永远不返回数据,也不报错 (Pending 状态)
|
|
46
|
+
// 如果没有配置 Timeout,这个 Promise 将永远不会结束!
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function runZombieLoop() {
|
|
51
|
+
let round = 0;
|
|
52
|
+
|
|
53
|
+
// 启动死锁超时监控
|
|
54
|
+
setTimeout(() => {
|
|
55
|
+
console.error("🚨🚨🚨 [假死确证] 已经过了 5 秒,轮询循环依然卡死在 pending 状态!");
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}, 5000);
|
|
58
|
+
|
|
59
|
+
while (running) {
|
|
60
|
+
round++;
|
|
61
|
+
try {
|
|
62
|
+
// 💡 物理挂起:没有 timeout 限制的 await 导致整个 while 循环僵死
|
|
63
|
+
const data = await mockLongPollRequest(round);
|
|
64
|
+
console.log(`收到数据: ${data}`);
|
|
65
|
+
} catch (err) {
|
|
66
|
+
console.log(`轮询捕获到异常: ${err.message}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
runZombieLoop();
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
运行上述代码,系统会在打印出第一次轮询后彻底沉默,并在 5 秒后触发超时报警,确证了静默假死的存在。
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## 三、 防御实战:超时熔断与指数退避重连 (Exponential Backoff)
|
|
79
|
+
|
|
80
|
+
为了物理粉碎静默假死,底座通道插件必须装配两道自我唤醒的**高可用防线**:
|
|
81
|
+
|
|
82
|
+
### 防线一:强力超时熔断 (Timeout Abort)
|
|
83
|
+
每一次长轮询或 WS 请求,必须在大底座中包裹 AbortSignal 超时熔断器。强制规定长轮询的最长生命周期为 30 秒(微信 getupdates 默认 20 秒),超期立刻物理掐断,逼迫轮询进入 catch 块以进入下一轮新循环:
|
|
84
|
+
|
|
85
|
+
```typescript
|
|
86
|
+
// 💡 超时熔断器包裹实现
|
|
87
|
+
const timeoutSignal = AbortSignal.timeout(30000); // 30秒强行刹车
|
|
88
|
+
|
|
89
|
+
const res = await this.callWeixinApi(
|
|
90
|
+
state.config,
|
|
91
|
+
"ilink/bot/getupdates",
|
|
92
|
+
body,
|
|
93
|
+
timeoutSignal, // 注入强力超时信号
|
|
94
|
+
state.baseUrl,
|
|
95
|
+
state.token
|
|
96
|
+
);
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### 防线二:指数退避重连算法 (Exponential Backoff)
|
|
100
|
+
当网络由于服务器断网确实瘫痪时,如果我们疯狂地进行秒级重连,会在瞬间被微信/TG 等平台网关判定为恶意 DDOS 攻击,从而导致 API Key 被永久封锁。
|
|
101
|
+
|
|
102
|
+
插件必须引入**指数退避延迟策略**,在网络故障期间逐渐拉长重试时间间隔:
|
|
103
|
+
|
|
104
|
+
```typescript
|
|
105
|
+
// 💡 指数退避重试控制
|
|
106
|
+
let retryDelay = 1000; // 初始延迟 1 秒
|
|
107
|
+
|
|
108
|
+
while (state.running) {
|
|
109
|
+
try {
|
|
110
|
+
const timeoutSignal = AbortSignal.timeout(30000);
|
|
111
|
+
const res = await this.callUpdates(timeoutSignal);
|
|
112
|
+
|
|
113
|
+
// 重置重试延迟
|
|
114
|
+
retryDelay = 1000;
|
|
115
|
+
await this.processMessages(res.msgs);
|
|
116
|
+
} catch (err: any) {
|
|
117
|
+
console.warn(`[ConnectionWarning] 轮询异常: ${err.message},将在 ${retryDelay / 1000}s 后重试...`);
|
|
118
|
+
|
|
119
|
+
// 异步挂起,指数级拉长延迟时间间隔
|
|
120
|
+
await new Promise(r => setTimeout(r, retryDelay));
|
|
121
|
+
|
|
122
|
+
// 指数级翻倍,最大拉长到 32 秒,避免压垮平台 API
|
|
123
|
+
retryDelay = Math.min(retryDelay * 2, 32000);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
## 四、 避坑经验:离线“消息风暴(Message Storm)”防御
|
|
131
|
+
|
|
132
|
+
当智能体离线 2 小时后,通过指数退避成功重新连接上线。此时,微信/TG 服务器的未读缓冲区里**积压了这 2 小时内成百上千条未处理的用户消息**。
|
|
133
|
+
* **物理大坝泄洪**:一旦重连成功,这 1,000 条老消息会在几毫秒内倾泻给底座。
|
|
134
|
+
* **后果**:底座瞬间并发处理 1,000 个 Session 的大模型推理,直接将物理服务器的 CPU 榨干崩溃,并在一分钟内燃烧完价值数千元的 API 账单。
|
|
135
|
+
|
|
136
|
+
### 🌟 物理防御:陈旧消息丢弃闸 (Stale Discarding)
|
|
137
|
+
为了防止被上线消息洪水冲垮,微信插件的消息分发层,**必须执行时间戳审查防线**:
|
|
138
|
+
|
|
139
|
+
```typescript
|
|
140
|
+
for (const weixinMsg of msgs) {
|
|
141
|
+
const msgTimeMs = weixinMsg.create_time * 1000; // 微信时间戳通常是秒,转为毫秒
|
|
142
|
+
const nowMs = Date.now();
|
|
143
|
+
|
|
144
|
+
// 💡 物理丢弃防线:如果消息产生的时间距离当前服务器系统时间超过 5 分钟 (300000ms)
|
|
145
|
+
// 说明是离线期间积压的“陈旧历史垃圾消息”
|
|
146
|
+
if (nowMs - msgTimeMs > 300000) {
|
|
147
|
+
console.warn(`[MessageStormGuard] 物理丢弃离线积压陈旧消息 (发送时间: ${new Date(msgTimeMs).toLocaleString()}, 延迟已达 ${Math.floor((nowMs - msgTimeMs) / 1000)}s)`);
|
|
148
|
+
continue; // 直接物理丢弃过滤,拒绝发送给内核!
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// 正常投递给 EventBus
|
|
152
|
+
ctx.eventBus.emit("connection:message", ...);
|
|
153
|
+
}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
通过这一道“陈旧消息丢弃闸”,原本可能压垮整台服务器的“消息洪水”,被插件层在大脑边界前物理蒸发,换来了智能体系统 100% 绝对安全的平稳重连。
|
|
157
|
+
|
|
158
|
+
本节我们通过超时熔断、指数退避与消息风暴丢弃闸,彻底扫清了通道连接的高可用性障碍。
|
|
159
|
+
|
|
160
|
+
在下一章中,我们将进入插件动态加载的引擎车间,去白盒解剖底座是如何动态扫描 Markdown 提示词并执行双通道合并装配的。
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "第五部分:微内核与插件"
|
|
3
|
+
weight: 60
|
|
4
|
+
bookCollapseSection: true
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# 第五部分:触达物理世界 —— 微内核、插件契约与通道适配
|
|
8
|
+
|
|
9
|
+
本部分将深入解密智能体的骨骼与肌肉系统。我们将剖析微内核架构设计、Monorepo 依赖边界约束、插件的静态元数据与生命周期钩子,以及如何动态扫描加载提示词与组件的源码实现。
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## 🧭 章节导学与阅读清单
|
|
14
|
+
|
|
15
|
+
### 🔌 第 10 章:微内核与通道插件的设计哲学:屏蔽 IM 协议差异
|
|
16
|
+
理解 Core 内核事件分发与会话控制,剖析 Monorepo 中(插件只能依赖 SDK,绝不直接导入 Core 实现)的单向依赖边界,以及编写自定义 Console 插件。
|
|
17
|
+
* 👉 **[10.1 微内核架构解耦与插件契约设计](10.1_microkernel_decoupling.md)**
|
|
18
|
+
* 👉 **[10.2 依赖边界与安全沙箱](10.2_plugin_metadata_security.md)**
|
|
19
|
+
* 👉 **[10.3 【白盒剖析】通道插件开发与消息转换](10.3_channel_plugin_development.md)**
|
|
20
|
+
* 👉 **[10.4 调试与避坑指南:物理连接假死与指数退避重连](10.4_debugging_channel_reconnection.md)**
|
|
21
|
+
|