Compare commits
6
Commits
f214c862d3
...
ac8f3da9df
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ac8f3da9df | ||
|
|
6649b31e17 | ||
|
|
254fbf4628 | ||
|
|
931e134796 | ||
|
|
63a80694ae | ||
|
|
4506601773 |
No files matched your search
+65
-12
@@ -115,6 +115,7 @@ AI 智能体可以调用以下工具来完成任务(由 `Tool` 基类派生,
|
||||
| `reload_skills` | 热重载扩展技能 |
|
||||
| `create_skill` | 创建新技能(AI 自主编写 Python 脚本) |
|
||||
| `install_skill` | 技能安装指引 |
|
||||
| `ask_user` | 向用户提问(AI 主动交互) |
|
||||
| `update_identity` | 更新用户身份信息(`00_IDENTITY.md`) |
|
||||
| `update_soul` | 更新 AI 自我定义(`99_SOUL.md`,智能体进化) |
|
||||
| `save_memory_and_end_conversation` | 保存记忆并结束对话 |
|
||||
@@ -374,36 +375,88 @@ AI 可以通过 `update_soul` 工具动态更新 `99_SOUL.md`,通过 `update_i
|
||||
{
|
||||
"system": {
|
||||
"save_dir": "~/.aibox",
|
||||
"datetime_format": "%Y-%m-%d %H:%M:%S",
|
||||
"date_format": "%Y%m%d_%H%M%S",
|
||||
"debug_level": 1,
|
||||
"load_history_by_default": true,
|
||||
"log_file": "~/.aibox/ai.log",
|
||||
"log_max_size": 10485760,
|
||||
"log_backup_count": 5,
|
||||
"websocket_host": "localhost",
|
||||
"websocket_port": 8765,
|
||||
"debug_level": 1,
|
||||
"log_file": "~/.aibox/ai.log",
|
||||
"skills_dir": "~/.aibox/skills",
|
||||
"prompts_dir": "~/.aibox/prompts"
|
||||
},
|
||||
"scheduler": {
|
||||
"enabled": true,
|
||||
"check_interval": 1.0,
|
||||
"max_tasks_per_run": 10,
|
||||
"broadcast_enabled": true
|
||||
},
|
||||
"api": {
|
||||
"deepseek_api_url": "https://api.deepseek.com/chat/completions",
|
||||
"deepseek_model": "deepseek-v4-flash",
|
||||
"api_key_env": "DEEPSEEK_API_KEY",
|
||||
"timeout": 30,
|
||||
"temperature": 0.7
|
||||
},
|
||||
"context": {
|
||||
"max_history_messages": 500,
|
||||
"max_context_messages": 120,
|
||||
"max_tokens": 8192,
|
||||
"max_history_conversations": 5,
|
||||
"preserve_system_prompts": true,
|
||||
"compress_enabled": true,
|
||||
"compress_message_threshold": 200,
|
||||
"compress_token_threshold": 120000,
|
||||
"compress_ratio": 0.2,
|
||||
"compress_keep_recent": 8
|
||||
},
|
||||
"scheduler": {
|
||||
"enabled": true,
|
||||
"check_interval": 1.0,
|
||||
"max_tasks_per_run": 10
|
||||
"memo": {
|
||||
"default_reminder_minutes": 60,
|
||||
"max_memos_per_check": 10,
|
||||
"allow_user_tasks": true
|
||||
},
|
||||
"api": {
|
||||
"deepseek_api_url": "https://api.deepseek.com/chat/completions",
|
||||
"deepseek_model": "deepseek-v4-flash",
|
||||
"api_key_env": "DEEPSEEK_API_KEY"
|
||||
"files": {
|
||||
"max_file_size": 10485710
|
||||
},
|
||||
"commands": {
|
||||
"enabled": true
|
||||
},
|
||||
"web": {
|
||||
"timeout": 10,
|
||||
"delay": 0,
|
||||
"user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
|
||||
"respect_robots": false,
|
||||
"max_download_size": 104857600
|
||||
},
|
||||
"memory": {
|
||||
"max_search_results": 100
|
||||
"db_name": "memories.db",
|
||||
"max_search_results": 100,
|
||||
"default_importance": 2
|
||||
},
|
||||
"conversation": {
|
||||
"current_file": "current_conversation.json",
|
||||
"history_file": "conversation_history.json",
|
||||
"archive_dir": "archive"
|
||||
},
|
||||
"tools": {
|
||||
"max_result_length": 10000
|
||||
"max_result_length": 10000,
|
||||
"save_result_to_file": false,
|
||||
"tool_results_dir": "tool_results"
|
||||
},
|
||||
"compressor": {
|
||||
"min_messages_to_compress": 5,
|
||||
"weight_decay_factor": 0.01,
|
||||
"max_input_chars": 6000,
|
||||
"llm_max_tokens": 800
|
||||
},
|
||||
"proxy": {
|
||||
"type": "",
|
||||
"host": "",
|
||||
"port": 0,
|
||||
"username": "",
|
||||
"password": ""
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -0,0 +1,823 @@
|
||||
# AIBOX 模块技术手册
|
||||
|
||||
> 文档版本:2026-07-30
|
||||
> 代码版本:v2.0
|
||||
> 语言:Python 3.8+
|
||||
> 定位:单 AI 智能体系统(独立于微服务体系)
|
||||
|
||||
---
|
||||
|
||||
## 一、模块概述
|
||||
|
||||
### 1.1 定位
|
||||
|
||||
**AIBOX** 是一个基于 DeepSeek API 的单 AI 智能体系统,通过 WebSocket 与客户端双向通信。所有任务(文件操作、网页访问、记忆管理、任务调度等)都由 AI 对话驱动执行。
|
||||
|
||||
### 1.2 与微服务体系的关系
|
||||
|
||||
| 属性 | 说明 |
|
||||
|------|------|
|
||||
| 启动方式 | `core`(PMC) 通过 `init.txt` 拉起 |
|
||||
| 监听端口 | 8765(WebSocket) |
|
||||
| 与主干关系 | **完全独立**,不调用 waf/gateway/dnfs 等服务 |
|
||||
| 数据存储 | 本地 `~/.aibox/`(不使用 db:8005) |
|
||||
| 前端 | 独立 HTML 页面 |
|
||||
|
||||
### 1.3 核心特性
|
||||
|
||||
| 特性 | 说明 |
|
||||
|------|------|
|
||||
| AI 对话 | DeepSeek API + 思考模式(reasoning 流式输出) |
|
||||
| 实时通信 | WebSocket 双向通信 + 流式输出 |
|
||||
| 工具系统 | 19 个内置工具(文件/网页/记忆/任务/命令等) |
|
||||
| 技能扩展 | 热重载 `.py` 脚本和 `.md` 技能 |
|
||||
| 记忆管理 | 自动保存/检索对话记忆 |
|
||||
| 任务调度 | 一次性/重复/即时任务 |
|
||||
| 自我进化 | AI 动态更新角色与用户画像 |
|
||||
| 对话压缩 | 自动压缩长对话历史 |
|
||||
| 多语言 | 中英自动检测 |
|
||||
|
||||
---
|
||||
|
||||
## 二、技术架构
|
||||
|
||||
### 2.1 技术栈
|
||||
|
||||
| 层 | 技术 | 用途 |
|
||||
|----|------|------|
|
||||
| 语言 | Python 3.8+ | 主程序 |
|
||||
| 通信 | websockets | WebSocket 服务器 |
|
||||
| AI | DeepSeek API | 智能对话、工具调用、流式输出 |
|
||||
| 存储 | SQLite | 记忆数据库、备忘录任务 |
|
||||
| HTTP | requests | 网页访问、API 调用 |
|
||||
| HTML 解析 | beautifulsoup4 | 网页内容提取 |
|
||||
| SSH | paramiko | SSH 远程执行(技能) |
|
||||
| 加密 | cryptography | AES 加密(技能) |
|
||||
|
||||
### 2.2 架构图
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ WebSocket 客户端 │
|
||||
│ (entry.html / frontend.html / client.py / voice_client.py) │
|
||||
└────────────────────────┬────────────────────────────────────┘
|
||||
│ ws://localhost:8765
|
||||
┌────────────────────────▼────────────────────────────────────┐
|
||||
│ WebSocket 服务器 (main.py) │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────┐ │
|
||||
│ │ 消息处理器 │ │ 命令处理器 │ │ 任务调度器 │ │
|
||||
│ │ (handle_msg) │ │ (handle_cmd) │ │ (MemoScheduler) │ │
|
||||
│ └──────┬───────┘ └──────┬───────┘ └────────┬─────────┘ │
|
||||
└─────────┼─────────────────┼───────────────────┼─────────────┘
|
||||
│ │ │
|
||||
┌─────────▼─────────────────▼───────────────────▼─────────────┐
|
||||
│ AI 对话引擎 (DeepSeekChat) │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────┐ │
|
||||
│ │ 提示词管理器 │ │ 工具注册器 │ │ 对话压缩器 │ │
|
||||
│ │ (PromptMgr) │ │ (ToolRegistry)│ │ (ConversationComp)│ │
|
||||
│ └──────┬───────┘ └──────┬───────┘ └────────┬─────────┘ │
|
||||
└─────────┼─────────────────┼───────────────────┼─────────────┘
|
||||
│ │ │
|
||||
┌─────────▼─────────────────▼───────────────────▼─────────────┐
|
||||
│ 数据持久层 │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────┐ │
|
||||
│ │ 记忆数据库 │ │ 备忘录 DB │ │ 对话历史管理器 │ │
|
||||
│ │ (memories.db) │ │ (memos.db) │ │ (JSON files) │ │
|
||||
│ └──────────────┘ └──────────────┘ └──────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 2.3 核心数据流
|
||||
|
||||
```
|
||||
客户端请求 → WebSocketHandler.handle_message()
|
||||
↓
|
||||
┌─── 消息类型判断 ───┬─── 命令类型判断 ───┬─── 任务操作判断 ───┐
|
||||
│ type="message" │ type="command" │ type="task" │
|
||||
└─────────┬─────────┴─────────┬─────────┴─────────┬─────────┘
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
AI 对话引擎 命令处理器 任务管理器
|
||||
(DeepSeekChat) (handle_command) (handle_task)
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
工具调用循环 系统命令执行 任务 CRUD 操作
|
||||
(ToolRegistry) (/new, /list...) (add, complete...)
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
流式响应生成 命令结果返回 任务状态广播
|
||||
(chunk → complete) (command_result) (task_status)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 三、代码结构
|
||||
|
||||
### 3.1 文件结构
|
||||
|
||||
```
|
||||
aibox/
|
||||
├── main.py # 核心(~8000 行)
|
||||
│ ├── WebSocketHandler # WebSocket 消息处理
|
||||
│ ├── DeepSeekChat # AI 对话引擎
|
||||
│ ├── PromptManager # 提示词管理
|
||||
│ ├── Tool + 19 个工具类 # 工具系统
|
||||
│ ├── MemoScheduler # 任务调度器
|
||||
│ ├── MemoryManager # 记忆管理
|
||||
│ └── ConfigManager # 配置管理
|
||||
├── client.py # CLI WebSocket 客户端
|
||||
├── voice_client.py # 语音客户端(语音输入/输出)
|
||||
├── entry.html # Web 前端入口
|
||||
├── frontend.html # Web 主页面
|
||||
├── skills/ # 内置扩展技能
|
||||
│ ├── hello-world.py # 示例技能
|
||||
│ ├── bing_search.py # 必应搜索
|
||||
│ ├── ssh-exec.py # SSH 远程执行
|
||||
│ ├── sftp-transfer.py # SFTP 传输
|
||||
│ ├── facerec.py # 人脸识别
|
||||
│ ├── aes-crypto.py # AES 加密
|
||||
│ ├── gitclient.py # Git 操作
|
||||
│ └── *.md # Markdown 技能(prompt)
|
||||
├── tasks/ITIL.md # ITIL 任务模板
|
||||
├── SKILLS_FORMAT.md # 技能格式规范
|
||||
├── requirements.txt
|
||||
├── _run.sh # core 拉起的启动脚本
|
||||
├── _runcli.sh # CLI 启动
|
||||
├── _set.sh # 依赖安装
|
||||
├── _setenv.sh # 环境变量设置
|
||||
└── README.md # 用户文档
|
||||
```
|
||||
|
||||
### 3.2 main.py 核心类
|
||||
|
||||
| 类名 | 行号 | 功能 |
|
||||
|------|------|------|
|
||||
| `Tool` | L3642 | 工具基类(抽象方法:get_name/execute) |
|
||||
| `WebSocketHandler` | ~L1900 | WebSocket 消息处理 |
|
||||
| `DeepSeekChat` | ~L2400 | AI 对话引擎 |
|
||||
| `PromptManager` | ~L1050 | 提示词文件管理 |
|
||||
| `ConfigManager` | ~L1080 | 配置加载/合并 |
|
||||
| `MemoryManager` | ~L5200 | 记忆 CRUD |
|
||||
| `MemoScheduler` | ~L4700 | 任务调度器 |
|
||||
| `ConversationCompressor` | ~L6480 | 对话压缩 |
|
||||
|
||||
### 3.3 运行期数据
|
||||
|
||||
```
|
||||
~/.aibox/
|
||||
├── config.json # 配置(自动生成默认值)
|
||||
├── prompts/
|
||||
│ ├── 00_IDENTITY.md # 用户身份(AI 可更新)
|
||||
│ ├── 1_*.md # 基础技能提示词
|
||||
│ ├── 99_SOUL.md # AI 灵魂定义(AI 可更新)
|
||||
│ └── 100_STATUS.md # AI 状态记录
|
||||
├── skills/ # 用户扩展技能
|
||||
├── memories.db # 记忆库(SQLite)
|
||||
├── memos.db # 备忘录任务(SQLite)
|
||||
├── current_conversation.json # 当前对话上下文
|
||||
├── conversation_history.json # 对话索引
|
||||
├── archive/ # 历史对话归档
|
||||
├── tool_results/ # 工具执行结果缓存
|
||||
└── ai.log # 日志(自动轮转)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、工具系统
|
||||
|
||||
### 4.1 工具基类
|
||||
|
||||
```python
|
||||
class Tool:
|
||||
"""工具基类,所有工具必须继承此类"""
|
||||
|
||||
def get_name(self) -> str:
|
||||
"""返回工具名称(抽象方法,必须实现)"""
|
||||
pass
|
||||
|
||||
def get_description(self) -> str:
|
||||
"""返回工具描述"""
|
||||
return ""
|
||||
|
||||
def get_parameters(self) -> Dict:
|
||||
"""返回工具参数(JSON Schema 格式)"""
|
||||
return {"type": "object", "properties": {}}
|
||||
|
||||
def execute(self, **kwargs) -> str:
|
||||
"""执行工具(抽象方法,必须实现)"""
|
||||
pass
|
||||
|
||||
def execute_streaming(self, **kwargs):
|
||||
"""流式执行(可选实现)"""
|
||||
return None
|
||||
```
|
||||
|
||||
### 4.2 内置工具一览
|
||||
|
||||
| 工具名 | 类名 | 行号 | 功能 |
|
||||
|--------|------|------|------|
|
||||
| `read_file` | `ReadFileTool` | L5762 | 读取文件(支持 head/tail/paging) |
|
||||
| `write_file` | `WriteFileTool` | L5884 | 写入/追加文件 |
|
||||
| `list_files` | `ListFilesTool` | L5961 | 目录列表 |
|
||||
| `web` | `SimpleWebCrawlerTool` | L5637 | 网页抓取(标题/正文/链接) |
|
||||
| `memory` | `MemoryTool` | L5280 | 记忆 CRUD(search/save/delete) |
|
||||
| `memo` | `MemoTool` | L4753 | 任务 CRUD(add/complete/list/search) |
|
||||
| `run_command` | `CommandLineTool` | L6150 | 系统命令执行(支持超时) |
|
||||
| `search_skills` | `SearchSkillsTool` | L4076 | 搜索扩展技能 |
|
||||
| `reload_skills` | `ReloadSkillsTool` | L4199 | 热重载技能 |
|
||||
| `create_skill` | `CreateSkillTool` | L4409 | AI 自主创建技能 |
|
||||
| `install_skill` | `InstallSkillTool` | L4306 | 技能安装指引 |
|
||||
| `ask_user` | `AskUserTool` | L4275 | 向用户提问 |
|
||||
| `update_identity` | `UpdateIdentityTool` | L4518 | 更新用户身份 |
|
||||
| `update_soul` | `UpdateSoulTool` | L4582 | 更新 AI 灵魂 |
|
||||
| `save_memory_and_end_conversation` | `SaveMemoryAndEndConversationTool` | L4646 | 保存记忆并结束对话 |
|
||||
| `get_current_time` | `GetCurrentTimeTool` | L6055 | 获取当前时间 |
|
||||
| `calculator` | `CalculatorTool` | L6095 | 数学计算 |
|
||||
| `set_proxy` | `SetProxyTool` | L6415 | 设置 SOCKS5 代理 |
|
||||
| `get_proxy` | `GetProxyTool` | L6453 | 查看代理配置 |
|
||||
|
||||
### 4.3 工具执行流程
|
||||
|
||||
```
|
||||
AI 决定调用工具 → 生成 JSON 格式工具调用
|
||||
↓
|
||||
DeepSeekChat.process_tool_calls()
|
||||
↓
|
||||
┌─── 解析工具名 ───┬─── 解析参数 ───┬─── 注册工具 ───┐
|
||||
└─────────────────┴───────────────┴─────────────────┘
|
||||
↓
|
||||
ToolRegistry.get_tool(name)
|
||||
↓
|
||||
tool.execute(**params)
|
||||
↓
|
||||
┌─── 结果处理 ───┬─── 错误处理 ───┬─── 长结果截断 ───┐
|
||||
└─────────────────┴───────────────┴─────────────────┘
|
||||
↓
|
||||
返回结果给 AI → AI 继续生成响应
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 五、WebSocket 协议
|
||||
|
||||
### 5.1 客户端 → 服务器
|
||||
|
||||
| 类型 | 字段 | 说明 |
|
||||
|------|------|------|
|
||||
| `message` | `content` | 普通对话文本 |
|
||||
| `command` | `command` | 斜杠命令(如 `/new`, `/help`) |
|
||||
| `task` | `action`, `title`, ... | 任务操作(add/complete/list) |
|
||||
| `ping` | — | 心跳检测 |
|
||||
|
||||
### 5.2 服务器 → 客户端
|
||||
|
||||
| 类型 | 字段 | 说明 |
|
||||
|------|------|------|
|
||||
| `chunk` | `content` | AI 响应片段(流式) |
|
||||
| `complete` | `content` | AI 响应完成 |
|
||||
| `reasoning` | `content` | AI 思考过程 |
|
||||
| `error` | `content` | 错误信息 |
|
||||
| `command_result` | `content` | 命令执行结果 |
|
||||
| `task_status` | `task_id`, `status` | 任务状态更新 |
|
||||
| `task_process` | `task_id`, `content` | 任务执行过程 |
|
||||
| `dialogue_ended` | — | 对话结束 |
|
||||
| `dialogue_reset` | — | 对话重置完成 |
|
||||
| `info` | `content` | 系统信息 |
|
||||
| `pong` | — | Ping 响应 |
|
||||
|
||||
### 5.3 消息流示例
|
||||
|
||||
```
|
||||
// 普通对话
|
||||
Client: {type: "message", content: "你好"}
|
||||
Server: {type: "chunk", content: "你好!"}
|
||||
Server: {type: "complete", content: "你好!有什么可以帮你的吗?"}
|
||||
|
||||
// 工具调用
|
||||
Server: {type: "tool_call", tool: "web", args: {url: "..."}}
|
||||
Server: {type: "chunk", content: "网页内容:..."}
|
||||
Server: {type: "complete", content: "网页已获取"}
|
||||
|
||||
// 任务执行
|
||||
Client: {type: "task", action: "add", title: "提醒", delay: 60}
|
||||
Server: {type: "command_result", content: "任务已添加"}
|
||||
// 60 秒后
|
||||
Server: {type: "task_status", task_id: 1, status: "executing"}
|
||||
Server: {type: "task_process", task_id: "1", content: "处理中..."}
|
||||
Server: {type: "task_status", task_id: 1, status: "complete"}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 六、任务系统
|
||||
|
||||
### 6.1 任务类型
|
||||
|
||||
| 类型 | 字段 | 说明 |
|
||||
|------|------|------|
|
||||
| 一次性 | `reminder_time` | 指定执行时间 |
|
||||
| 重复(daily) | `repeat_type: "daily"` | 每天执行 |
|
||||
| 重复(weekly) | `repeat_type: "weekly"`, `weekday` | 每周指定日 |
|
||||
| 重复(monthly) | `repeat_type: "monthly"`, `day` | 每月指定日 |
|
||||
| 重复(custom) | `repeat_type: "custom"`, `interval`, `unit` | 自定义间隔 |
|
||||
| 即时 | `is_immediate: true` | 立即执行 |
|
||||
|
||||
### 6.2 任务调度流程
|
||||
|
||||
```
|
||||
MemoScheduler._run() # 后台线程
|
||||
↓
|
||||
检查所有未完成任务
|
||||
↓
|
||||
┌─── 到达时间? ───┬─── 未完成? ───┬─── 允许用户任务? ───┐
|
||||
└─────────────────┴───────────────┴─────────────────┘
|
||||
↓
|
||||
创建新的 AI 对话(将任务内容作为用户输入)
|
||||
↓
|
||||
AI 处理任务(可能调用多个工具)
|
||||
↓
|
||||
标记任务已触发
|
||||
↓
|
||||
广播结果到所有客户端
|
||||
↓
|
||||
计算下一次执行时间(重复任务)
|
||||
```
|
||||
|
||||
### 6.3 数据库表结构
|
||||
|
||||
```sql
|
||||
-- memories.db
|
||||
CREATE TABLE memories (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id TEXT,
|
||||
content TEXT,
|
||||
importance INTEGER DEFAULT 2,
|
||||
created_at TEXT,
|
||||
updated_at TEXT,
|
||||
embedding BLOB -- 预留向量嵌入
|
||||
);
|
||||
|
||||
-- memos.db
|
||||
CREATE TABLE memos (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
title TEXT,
|
||||
content TEXT,
|
||||
reminder_time TEXT,
|
||||
repeat_type TEXT,
|
||||
repeat_interval INTEGER,
|
||||
repeat_interval_unit TEXT,
|
||||
is_immediate INTEGER DEFAULT 0,
|
||||
status TEXT DEFAULT 'pending',
|
||||
is_triggered INTEGER DEFAULT 0,
|
||||
created_at TEXT,
|
||||
user_id TEXT
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 七、记忆系统
|
||||
|
||||
### 7.1 记忆操作
|
||||
|
||||
| 操作 | AI 调用 | 说明 |
|
||||
|------|---------|------|
|
||||
| 保存 | `memory(action="save", content=...)` | AI 主动保存重要信息 |
|
||||
| 搜索 | `memory(action="search", query=...)` | 按关键词搜索记忆 |
|
||||
| 列表 | `memory(action="list", page=...)` | 分页浏览所有记忆 |
|
||||
| 删除 | `memory(action="delete", id=...)` | 删除指定记忆 |
|
||||
|
||||
### 7.2 自动记忆
|
||||
|
||||
- AI 在对话结束时(`/exit`)自动保存关键信息
|
||||
- 可配置 `system.auto_save_memory` 开关
|
||||
- 重要度(importance)1-5 级
|
||||
|
||||
---
|
||||
|
||||
## 八、对话压缩
|
||||
|
||||
### 8.1 压缩触发条件
|
||||
|
||||
| 条件 | 阈值(默认) | 配置项 |
|
||||
|------|-------------|--------|
|
||||
| 消息数超过 | 200 条 | `compress_message_threshold` |
|
||||
| Token 数超过 | 120000 | `compress_token_threshold` |
|
||||
|
||||
### 8.2 压缩流程
|
||||
|
||||
```
|
||||
检测到阈值触发
|
||||
↓
|
||||
保留最近 8 条消息(compress_keep_recent)
|
||||
↓
|
||||
对早期消息按时间衰减加权(weight_decay_factor=0.01)
|
||||
↓
|
||||
调用 AI 生成摘要(max_input_chars=6000)
|
||||
↓
|
||||
将摘要作为系统消息插入
|
||||
↓
|
||||
压缩后继续对话
|
||||
```
|
||||
|
||||
### 8.3 压缩配置
|
||||
|
||||
```json
|
||||
{
|
||||
"compressor": {
|
||||
"min_messages_to_compress": 5,
|
||||
"weight_decay_factor": 0.01,
|
||||
"max_input_chars": 6000,
|
||||
"llm_max_tokens": 800
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 九、扩展技能系统
|
||||
|
||||
### 9.1 技能类型
|
||||
|
||||
| 类型 | 格式 | 存储位置 |
|
||||
|------|------|---------|
|
||||
| 可执行脚本 | Python/Shell,支持 `--help` | `~/.aibox/skills/` |
|
||||
| Markdown 技能 | `.md` 文件,首行为简介 | `~/.aibox/skills/` |
|
||||
|
||||
### 9.2 内置技能
|
||||
|
||||
| 技能名 | 文件 | 功能 |
|
||||
|--------|------|------|
|
||||
| hello-world | `skills/hello-world.py` | 示例技能 |
|
||||
| bing_search | `skills/bing_search.py` | 必应搜索 |
|
||||
| ssh-exec | `skills/ssh-exec.py` | SSH 远程执行 |
|
||||
| sftp-transfer | `skills/sftp-transfer.py` | SFTP 文件传输 |
|
||||
| facerec | `skills/facerec.py` | 人脸识别 |
|
||||
| aes-crypto | `skills/aes-crypto.py` | AES 加密/解密 |
|
||||
| gitclient | `skills/gitclient.py` | Git 操作封装 |
|
||||
|
||||
### 9.3 技能热重载
|
||||
|
||||
```python
|
||||
# 新技能放置到 ~/.aibox/skills/
|
||||
# 调用 reload_skills 工具
|
||||
# AI 下次对话时即可使用
|
||||
```
|
||||
|
||||
### 9.4 技能格式规范
|
||||
|
||||
详见 [SKILLS_FORMAT.md](file:///Users/qingsong/micro_servers/aibox/SKILLS_FORMAT.md)
|
||||
|
||||
---
|
||||
|
||||
## 十、配置参考
|
||||
|
||||
### 10.1 完整配置项
|
||||
|
||||
#### system 段
|
||||
|
||||
| 字段 | 类型 | 默认值 | 说明 |
|
||||
|------|------|--------|------|
|
||||
| `save_dir` | str | `~/.aibox` | 数据存储目录 |
|
||||
| `datetime_format` | str | `%Y-%m-%d %H:%M:%S` | 日期时间格式 |
|
||||
| `date_format` | str | `%Y%m%d_%H%M%S` | 文件名日期格式 |
|
||||
| `debug_level` | int | `1` | 调试级别 0-3 |
|
||||
| `load_history_by_default` | bool | `true` | 启动时加载历史对话 |
|
||||
| `log_file` | str | `~/.aibox/ai.log` | 日志文件路径 |
|
||||
| `log_max_size` | int | `10485760` | 日志最大字节数(10MB) |
|
||||
| `log_backup_count` | int | `5` | 日志备份数量 |
|
||||
| `websocket_host` | str | `localhost` | WebSocket 监听地址 |
|
||||
| `websocket_port` | int | `8765` | WebSocket 监听端口 |
|
||||
| `skills_dir` | str | `~/.aibox/skills` | 技能目录 |
|
||||
| `prompts_dir` | str | `~/.aibox/prompts` | 提示词目录 |
|
||||
|
||||
#### scheduler 段
|
||||
|
||||
| 字段 | 类型 | 默认值 | 说明 |
|
||||
|------|------|--------|------|
|
||||
| `enabled` | bool | `true` | 启用任务调度器 |
|
||||
| `check_interval` | float | `1.0` | 检查间隔(秒) |
|
||||
| `max_tasks_per_run` | int | `10` | 单次最大触发任务数 |
|
||||
| `broadcast_enabled` | bool | `true` | 广播任务执行结果 |
|
||||
|
||||
#### api 段
|
||||
|
||||
| 字段 | 类型 | 默认值 | 说明 |
|
||||
|------|------|--------|------|
|
||||
| `deepseek_api_url` | str | `https://api.deepseek.com/chat/completions` | API URL |
|
||||
| `deepseek_model` | str | `deepseek-v4-flash` | 模型名 |
|
||||
| `api_key_env` | str | `DEEPSEEK_API_KEY` | API Key 环境变量名 |
|
||||
| `timeout` | int | `30` | 请求超时(秒) |
|
||||
| `temperature` | float | `0.7` | 生成温度 0-1 |
|
||||
|
||||
#### context 段
|
||||
|
||||
| 字段 | 类型 | 默认值 | 说明 |
|
||||
|------|------|--------|------|
|
||||
| `max_history_messages` | int | `500` | 历史消息上限 |
|
||||
| `max_context_messages` | int | `120` | 上下文窗口 |
|
||||
| `max_tokens` | int | `8192` | 最大生成 Token |
|
||||
| `max_history_conversations` | int | `5` | 保留历史对话数 |
|
||||
| `preserve_system_prompts` | bool | `true` | 保留系统提示词 |
|
||||
| `compress_enabled` | bool | `true` | 启用对话压缩 |
|
||||
| `compress_message_threshold` | int | `200` | 压缩消息阈值 |
|
||||
| `compress_token_threshold` | int | `120000` | 压缩 Token 阈值 |
|
||||
| `compress_ratio` | float | `0.2` | 压缩保留比例 |
|
||||
| `compress_keep_recent` | int | `8` | 保留最近消息数 |
|
||||
|
||||
#### memo 段
|
||||
|
||||
| 字段 | 类型 | 默认值 | 说明 |
|
||||
|------|------|--------|------|
|
||||
| `default_reminder_minutes` | int | `60` | 默认提醒分钟数 |
|
||||
| `max_memos_per_check` | int | `10` | 单次最大触发任务数 |
|
||||
| `allow_user_tasks` | bool | `true` | 允许用户创建任务 |
|
||||
|
||||
#### proxy 段
|
||||
|
||||
| 字段 | 类型 | 默认值 | 说明 |
|
||||
|------|------|--------|------|
|
||||
| `type` | str | `""` | 代理类型(socks5/http) |
|
||||
| `host` | str | `""` | 代理主机 |
|
||||
| `port` | int | `0` | 代理端口 |
|
||||
| `username` | str | `""` | 代理用户名 |
|
||||
| `password` | str | `""` | 代理密码 |
|
||||
|
||||
### 10.2 环境变量
|
||||
|
||||
| 变量 | 说明 |
|
||||
|------|------|
|
||||
| `DEEPSEEK_API_KEY` | DeepSeek API Key(必填) |
|
||||
| `AI_CONFIG_PATH` | 自定义配置文件路径 |
|
||||
|
||||
---
|
||||
|
||||
## 十一、命令行参数
|
||||
|
||||
```bash
|
||||
python main.py [选项]
|
||||
```
|
||||
|
||||
| 参数 | 类型 | 默认值 | 说明 |
|
||||
|------|------|--------|------|
|
||||
| `--verbose`, `-v` | flag | `false` | 显示详细日志 |
|
||||
| `--debug` | int | `None` | 调试级别 0-3 |
|
||||
| `--lang` | str | `None` | 强制语言 zh/en |
|
||||
| `--host` | str | `None` | 覆盖 WebSocket 主机 |
|
||||
| `--port` | int | `None` | 覆盖 WebSocket 端口 |
|
||||
| `--no-scheduler` | flag | `false` | 禁用任务调度器 |
|
||||
| `--scheduler-interval` | float | `1.0` | 调度器检查间隔 |
|
||||
| `--config`, `-c` | str | `None` | 指定配置文件 |
|
||||
| `--check-memo` | flag | `false` | 检查到期任务并退出 |
|
||||
| `--memo-add` | str | `None` | 添加任务(CLI) |
|
||||
| `--memo-list` | flag | `false` | 列出任务 |
|
||||
| `--memo-complete` | int | `None` | 完成指定任务 |
|
||||
|
||||
### 使用示例
|
||||
|
||||
```bash
|
||||
# 启动服务器
|
||||
python main.py
|
||||
|
||||
# 详细日志 + 指定端口
|
||||
python main.py --verbose --port 9000
|
||||
|
||||
# 检查并执行到期任务(cron 场景)
|
||||
python main.py --check-memo
|
||||
|
||||
# 添加任务
|
||||
python main.py --memo-add "每日备份|执行数据库备份|+1h"
|
||||
|
||||
# 列出所有任务
|
||||
python main.py --memo-list
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 十二、前端集成
|
||||
|
||||
### 12.1 前端文件
|
||||
|
||||
| 文件 | 说明 |
|
||||
|------|------|
|
||||
| `entry.html` | 简单入口页(重定向到 frontend.html) |
|
||||
| `frontend.html` | 完整前端界面(对话/任务/设置) |
|
||||
|
||||
### 12.2 WebSocket 连接
|
||||
|
||||
```javascript
|
||||
const ws = new WebSocket('ws://localhost:8765');
|
||||
|
||||
// 发送消息
|
||||
ws.send(JSON.stringify({
|
||||
type: 'message',
|
||||
content: '你好'
|
||||
}));
|
||||
|
||||
// 接收消息
|
||||
ws.onmessage = (event) => {
|
||||
const data = JSON.parse(event.data);
|
||||
switch (data.type) {
|
||||
case 'chunk':
|
||||
appendToChat(data.content);
|
||||
break;
|
||||
case 'complete':
|
||||
finishResponse();
|
||||
break;
|
||||
case 'error':
|
||||
showError(data.content);
|
||||
break;
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 十三、部署
|
||||
|
||||
### 13.1 目录结构
|
||||
|
||||
```
|
||||
/opt/micro_servers/aibox/
|
||||
├── main.py
|
||||
├── client.py
|
||||
├── skills/
|
||||
├── _run.sh # core 拉起入口
|
||||
├── _set.sh # 首次安装
|
||||
└── requirements.txt
|
||||
```
|
||||
|
||||
### 13.2 core init.txt 配置
|
||||
|
||||
```bash
|
||||
# aibox 由 core 拉起
|
||||
bash /opt/micro_servers/aibox/_run.sh
|
||||
```
|
||||
|
||||
### 13.3 systemd 单元(可选)
|
||||
|
||||
```ini
|
||||
[Unit]
|
||||
Description=AIBOX AI Agent
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=deploy
|
||||
WorkingDirectory=/opt/micro_servers/aibox
|
||||
ExecStart=/usr/bin/python3 main.py
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
Environment=DEEPSEEK_API_KEY=xxx
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 十四、已知限制
|
||||
|
||||
| ID | 描述 | 严重等级 |
|
||||
|----|------|---------|
|
||||
| L1 | 单实例运行,不支持多进程 | 🟡 Medium |
|
||||
| L2 | 记忆库无向量搜索,仅关键词匹配 | 🟡 Medium |
|
||||
| L3 | 对话压缩可能丢失早期重要信息 | 🟢 Low |
|
||||
| L4 | 工具执行无超时控制(部分工具) | 🟡 Medium |
|
||||
| L5 | 日志无远程推送,仅本地文件 | 🟢 Low |
|
||||
| L6 | 无用户认证,WebSocket 连接可被滥用 | 🔴 High |
|
||||
| L7 | API Key 以明文存储在环境变量或文件中 | 🟡 Medium |
|
||||
|
||||
### L6 详细说明
|
||||
|
||||
WebSocket 服务器**无任何认证**,任何能连接到 `ws://localhost:8765` 的客户端都可以:
|
||||
- 与 AI 对话(消耗 API 额度)
|
||||
- 执行系统命令(`run_command`)
|
||||
- 操作文件系统(`read_file`/`write_file`)
|
||||
- 创建/删除任务
|
||||
|
||||
**建议**:
|
||||
1. 仅绑定 `localhost`(默认配置已这样做)
|
||||
2. 添加 API Token 认证
|
||||
3. 限制 `run_command` 可执行的命令范围
|
||||
|
||||
---
|
||||
|
||||
## 十五、开发指南
|
||||
|
||||
### 15.1 添加新工具
|
||||
|
||||
1. 继承 `Tool` 基类
|
||||
2. 实现 `get_name()`、`get_description()`、`get_parameters()`、`execute()`
|
||||
3. 在 `DeepSeekChat._register_tools()` 中注册
|
||||
4. 更新 README 文档
|
||||
|
||||
```python
|
||||
class MyTool(Tool):
|
||||
def get_name(self) -> str:
|
||||
return "my_tool"
|
||||
|
||||
def get_description(self) -> str:
|
||||
return "我的自定义工具"
|
||||
|
||||
def get_parameters(self) -> Dict:
|
||||
return {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"param": {"type": "string", "description": "参数说明"}
|
||||
},
|
||||
"required": ["param"]
|
||||
}
|
||||
|
||||
def execute(self, param: str, **kwargs) -> str:
|
||||
return f"执行结果: {param}"
|
||||
```
|
||||
|
||||
### 15.2 添加新命令
|
||||
|
||||
在 `WebSocketHandler.handle_command()` 中添加:
|
||||
|
||||
```python
|
||||
elif command == "/mycommand":
|
||||
result = "命令执行结果"
|
||||
await self.send_message(websocket, {
|
||||
"type": "command_result",
|
||||
"content": result
|
||||
})
|
||||
```
|
||||
|
||||
### 15.3 添加配置项
|
||||
|
||||
1. 在 `ConfigManager._get_default_config()` 中添加默认值
|
||||
2. 在 README 文档中添加说明
|
||||
3. 确保 `_merge_configs()` 能正确处理新增字段
|
||||
|
||||
---
|
||||
|
||||
## 十六、故障排查
|
||||
|
||||
### 16.1 常见问题
|
||||
|
||||
| 问题 | 排查步骤 |
|
||||
|------|---------|
|
||||
| API Key 无效 | 检查 `echo $DEEPSEEK_API_KEY` 或 `~/.cert/deepseek.key` |
|
||||
| 技能不生效 | 确认技能文件在 `~/.aibox/skills/` 且有执行权限 |
|
||||
| 任务不执行 | 检查调度器状态:`/tasks`;检查时间设置 |
|
||||
| 对话压缩丢失信息 | 调整 `compress_message_threshold` 和 `compress_keep_recent` |
|
||||
| WebSocket 断开 | 检查服务器进程:`ps aux | grep main.py`;查看日志 |
|
||||
|
||||
### 16.2 日志位置
|
||||
|
||||
```bash
|
||||
# 查看实时日志
|
||||
tail -f ~/.aibox/ai.log
|
||||
|
||||
# 查看最近错误
|
||||
grep "ERROR" ~/.aibox/ai.log | tail -20
|
||||
```
|
||||
|
||||
### 16.3 数据库检查
|
||||
|
||||
```bash
|
||||
# 检查记忆库
|
||||
sqlite3 ~/.aibox/memories.db "SELECT * FROM memories LIMIT 10;"
|
||||
|
||||
# 检查任务库
|
||||
sqlite3 ~/.aibox/memos.db "SELECT * FROM memos WHERE status='pending';"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 十七、版本历史
|
||||
|
||||
| 版本 | 日期 | 主要变更 |
|
||||
|------|------|---------|
|
||||
| v2.0 | 当前 | 新增 `ask_user`、`update_soul`、`create_skill`、`install_skill`、`set_proxy`/`get_proxy` 等工具;支持 Markdown 技能;对话压缩支持 Token 阈值 |
|
||||
| v1.0 | — | 初始版本:WebSocket 服务器、文件/网页/记忆操作、任务调度、扩展技能、多语言 |
|
||||
|
||||
---
|
||||
|
||||
## 附录
|
||||
|
||||
### A. 配置文件示例
|
||||
|
||||
完整默认配置见 [main.py:1100-1180](file:///Users/qingsong/micro_servers/aibox/main.py#L1100-L1180)
|
||||
|
||||
### B. 数据库 Schema
|
||||
|
||||
完整建表语句见:
|
||||
- 记忆库:[main.py](file:///Users/qingsong/micro_servers/aibox/main.py) 中 `MemoryManager._init_db()`
|
||||
- 任务库:[main.py](file:///Users/qingsong/micro_servers/aibox/main.py) 中 `MemoScheduler._init_db()`
|
||||
|
||||
### C. 参考文档
|
||||
|
||||
- [DeepSeek API 文档](https://platform.deepseek.com/docs)
|
||||
- [websockets 库文档](https://websockets.readthedocs.io/)
|
||||
- [SKILLS_FORMAT.md](file:///Users/qingsong/micro_servers/aibox/SKILLS_FORMAT.md)
|
||||
|
||||
---
|
||||
|
||||
*本文档由代码审查工具生成,供开发团队参考。*
|
||||
@@ -0,0 +1,381 @@
|
||||
# Core 模块代码审查日志
|
||||
|
||||
> 审查日期:2026-07-30 14:30:00 (Asia/Shanghai)
|
||||
> 审查人:Automated Review
|
||||
> 审查范围:`core/` 全模块(C++20,PMC/Nidus 进程托管服务)
|
||||
|
||||
---
|
||||
|
||||
## 一、模块概述
|
||||
|
||||
| 属性 | 值 |
|
||||
|------|-----|
|
||||
| 定位 | 微服务进程托管控制器(Process Machine Controller) |
|
||||
| 语言 | C++20 |
|
||||
| 构建 | Makefile(GCC)+ systemd 部署 |
|
||||
| 入口 | `nidus.out` |
|
||||
| 端口 | HTTP API: 5200(仅本机 `127.0.0.1`) |
|
||||
| 启动列表 | `init.txt`(9 个子服务) |
|
||||
|
||||
### 依赖项
|
||||
|
||||
| 库 | 用途 | 版本要求 |
|
||||
|----|------|---------|
|
||||
| Boost.Process | 子进程创建/管理 | — |
|
||||
| Boost.JSON | JSON 解析与序列化 | — |
|
||||
| Boost.Interprocess | POSIX 消息队列 | — |
|
||||
| Boost.Program.Options | CLI 参数解析 | — |
|
||||
| UniServe_libth | 自定义线程/HTTP 服务器封装 | 自研 |
|
||||
| OpenSSL | TLS(HTTP 服务器证书) | — |
|
||||
|
||||
### 目录结构
|
||||
|
||||
```
|
||||
core/
|
||||
├── include/
|
||||
│ ├── cn/ # 中文代码库(自研)
|
||||
│ │ ├── 中文化.hpp # 保留字映射 + 中文字符串/IO 类
|
||||
│ │ ├── 链串.hpp # 分块链表
|
||||
│ │ ├── 顺序表.hpp # 数组封装
|
||||
│ │ ├── 向量.hpp # 动态数组
|
||||
│ │ └── 异常.hpp # 异常层次
|
||||
│ ├── logs/
|
||||
│ │ └── Logger.hpp # 单例日志(控制台输出)
|
||||
│ ├── main/
|
||||
│ │ ├── HttpTask.hh # HTTP 服务器任务
|
||||
│ │ ├── PPool.hh # 进程池
|
||||
│ │ ├── ProcessTask.hh # 子进程任务
|
||||
│ │ ├── Tttask.hh # 通信任务(消息队列监听)
|
||||
│ │ ├── pmc_mtd.hh # 进程管理方法集
|
||||
│ │ └── subsys_call.hh # 子系统调用封装
|
||||
│ └── tt/
|
||||
│ ├── Mq.h # POSIX 消息队列封装
|
||||
│ └── Pipe.h # FIFO 管道封装
|
||||
├── src/
|
||||
│ ├── main.cpp # 主入口
|
||||
│ └── main/ # 实现文件
|
||||
├── init.txt # 自启动列表
|
||||
├── Makefile # 构建脚本
|
||||
├── Nidus.service # systemd 单元
|
||||
├── README.md
|
||||
├── _set.sh # 编译+安装
|
||||
└── _run.sh # 运行脚本
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 二、审查发现
|
||||
|
||||
### 🔴 严重问题 (P0)
|
||||
|
||||
#### [CORE-S01] 命令注入漏洞
|
||||
|
||||
- **文件**: `src/main/HttpTask.cpp:230-247`
|
||||
- **时间**: 2026-07-30 14:32:15
|
||||
- **描述**: `exec` 路由直接将 URL 查询参数 `command` 传递给 `boost::process::child`,无任何过滤或校验。攻击者可构造恶意请求执行任意系统命令。
|
||||
|
||||
```cpp
|
||||
// 危险代码
|
||||
std::string result = execCommand(command, target);
|
||||
// command 直接来自 HTTP 请求参数
|
||||
```
|
||||
|
||||
- **风险**: 远程代码执行(RCE)
|
||||
- **严重等级**: 🔴 Critical
|
||||
- **修复建议**:
|
||||
1. 实现命令白名单机制
|
||||
2. 使用 `execvp` 而非 `system`/shell 模式
|
||||
3. 限制可执行命令的路径范围
|
||||
4. 添加参数长度和字符集校验
|
||||
|
||||
#### [CORE-S02] HTTP API 无任何鉴权
|
||||
|
||||
- **文件**: `src/main/HttpTask.cpp:130-340`
|
||||
- **时间**: 2026-07-30 14:33:00
|
||||
- **描述**: HTTP API (`/list`, `/exec`, `/kill`) 完全开放,无认证、无授权、无访问控制。任何人(本地)都可以:
|
||||
- 列出所有子进程
|
||||
- 启动任意进程
|
||||
- 终止任意进程
|
||||
|
||||
- **风险**: 权限绕过、未授权操作
|
||||
- **严重等级**: 🔴 Critical
|
||||
- **修复建议**:
|
||||
1. 添加 API Token 认证(请求头 `Authorization: Bearer <token>`)
|
||||
2. 实现 IP 白名单(仅允许 `127.0.0.1`)
|
||||
3. 添加请求速率限制
|
||||
4. 记录所有管理操作审计日志
|
||||
|
||||
#### [CORE-S03] ProcessTask 空指针解引用
|
||||
|
||||
- **文件**: `include/main/ProcessTask.hh:19,36`
|
||||
- **时间**: 2026-07-30 14:35:22
|
||||
- **描述**: `isRunning()` 和 `pid()` 方法未检查 `p` 是否为空。如果在 `start()` 之前调用这些方法,会导致空指针解引用(段错误)。
|
||||
|
||||
```cpp
|
||||
bool isRunning() override { return p && p->running(); } // 应加 p 非空检查
|
||||
pid_t pid() { return p->id(); } // 危险!p 可能为 nullptr
|
||||
```
|
||||
|
||||
- **风险**: 程序崩溃
|
||||
- **严重等级**: 🔴 Critical
|
||||
- **修复建议**:
|
||||
```cpp
|
||||
bool isRunning() override { return p != nullptr && p->running(); }
|
||||
pid_t pid() { return p ? p->id() : -1; }
|
||||
```
|
||||
|
||||
#### [CORE-S04] exec 路由路径穿越
|
||||
|
||||
- **文件**: `src/main/HttpTask.cpp:232-236`
|
||||
- **时间**: 2026-07-30 14:37:10
|
||||
- **描述**: `getExecCommand` 函数从 URL 参数提取命令,允许包含 `..` 路径穿越符,可能执行预期外的脚本。
|
||||
|
||||
- **风险**: 路径遍历
|
||||
- **严重等级**: 🔴 High
|
||||
- **修复建议**:
|
||||
- 白名单校验,仅允许执行 `init.txt` 中列出的命令
|
||||
- 规范化路径后检查是否在允许目录内
|
||||
|
||||
### 🟡 中等问题 (P1)
|
||||
|
||||
#### [CORE-S05] 进程池无线程安全保护
|
||||
|
||||
- **文件**: `src/main/PPool.cpp`
|
||||
- **时间**: 2026-07-30 14:40:05
|
||||
- **描述**: `PPool` 类使用 `std::list<std::shared_ptr<ProcessTask>>` 作为内部存储,但无互斥锁保护。`HttpTask` 和 `Tttask` 可能并发调用 `crtp()`、`kill()`、`clr()` 等方法,导致数据竞争。
|
||||
|
||||
- **风险**: 数据竞争、未定义行为
|
||||
- **严重等级**: 🟡 Medium
|
||||
- **修复建议**: 添加 `std::mutex` 保护所有公共方法
|
||||
|
||||
#### [CORE-S06] 消息队列命名冲突
|
||||
|
||||
- **文件**: `src/main/Tttask.cpp:41-42`
|
||||
- **时间**: 2026-07-30 14:42:30
|
||||
- **描述**: 使用 `getpid()` 作为消息队列名。同一台机器上启动多个 Nidus 实例时,可能发生队列名冲突。
|
||||
|
||||
```cpp
|
||||
name = std::to_string(getpid()); // 仅用 PID,无唯一化
|
||||
```
|
||||
|
||||
- **风险**: 多实例冲突
|
||||
- **严重等级**: 🟡 Medium
|
||||
- **修复建议**: 使用 `PID + 时间戳` 或 `PID + 随机后缀` 组合命名
|
||||
|
||||
#### [CORE-S07] 管道文件残留
|
||||
|
||||
- **文件**: `src/main/subsys_call.cpp`
|
||||
- **时间**: 2026-07-30 14:44:18
|
||||
- **描述**: 在异常路径中,`Mq` 和 `Pipe` 对象的析构器可能未正确执行(依赖栈展开),导致 `/tmp/pst-*` 文件残留,长期运行会占满 `/tmp` 空间。
|
||||
|
||||
- **风险**: 磁盘空间耗尽
|
||||
- **严重等级**: 🟡 Medium
|
||||
- **修复建议**:
|
||||
1. 在 `catch` 块中显式清理管道文件
|
||||
2. 添加定期清理脚本
|
||||
3. 使用 `std::filesystem::remove` 在构造器前清理旧文件
|
||||
|
||||
#### [CORE-S08] kill/exec IPC 路径已废弃
|
||||
|
||||
- **文件**: `src/main.cpp:282-296`
|
||||
- **时间**: 2026-07-30 14:46:00
|
||||
- **描述**: `parse_data_from_mq()` 中 `kill` 和 `exec` 的 IPC 处理路径被注释,仅 `list` 可用。HTTP 路由 `/exec` 和 `/kill` 虽然存在,但远端调用时会失败。
|
||||
|
||||
```cpp
|
||||
// 已注释的代码
|
||||
//else if (typ == "kill") {
|
||||
// int trg = jv.at("kill").as_int64();
|
||||
// auto flag = pmc_mtd::kill(trg);
|
||||
// ...
|
||||
//}
|
||||
```
|
||||
|
||||
- **风险**: 功能缺失
|
||||
- **严重等级**: 🟡 Medium
|
||||
- **修复建议**: 恢复被注释的代码,或在文档中明确说明限制
|
||||
|
||||
#### [CORE-S09] PPool::clr() 边界条件错误
|
||||
|
||||
- **文件**: `src/main/PPool.cpp:20-32`
|
||||
- **时间**: 2026-07-30 14:48:22
|
||||
- **描述**: `clr()` 方法从尾部反向遍历删除已终止的进程,但边界条件处理复杂:
|
||||
- 当 `pool.size() == 1` 时,循环条件 `it != pool.begin()` 直接跳过
|
||||
- 最后的 `if (pool.size() > 0 && !(*it)->isRunning())` 可能导致双删或漏删
|
||||
|
||||
```cpp
|
||||
auto it = --pool.end();
|
||||
for (; it != pool.begin(); --it)
|
||||
if (!(*it)->isRunning()) { /* erase */ }
|
||||
if (pool.size() > 0 && !(*it)->isRunning()) // 可能越界
|
||||
pool.erase(it);
|
||||
```
|
||||
|
||||
- **风险**: 迭代器失效、崩溃
|
||||
- **严重等级**: 🟡 Medium
|
||||
- **修复建议**: 简化为正向前遍历,或使用 `std::list::remove_if`
|
||||
|
||||
#### [CORE-S10] 日志仅输出到控制台
|
||||
|
||||
- **文件**: `include/logs/Logger.hpp`
|
||||
- **时间**: 2026-07-30 14:50:00
|
||||
- **描述**: `Logger` 类仅支持控制台输出,日志不持久化。服务重启后日志丢失,无法进行事后审计或问题追溯。
|
||||
|
||||
- **风险**: 可观测性缺失
|
||||
- **严重等级**: 🟡 Medium
|
||||
- **修复建议**:
|
||||
1. 实现日志文件轮转
|
||||
2. 添加远程日志推送(syslog、Elasticsearch)
|
||||
3. 集成到微服务体系的集中日志(db:8005 Logs 表)
|
||||
|
||||
### 🟢 轻微问题 (P2)
|
||||
|
||||
#### [CORE-Q01] 版本号不一致
|
||||
|
||||
- **文件**: `src/main.cpp:42` vs `Makefile:1`
|
||||
- **时间**: 2026-07-30 14:52:10
|
||||
- **描述**: `PMC_VERSION = "0.0.10"` 与 `Makefile` 中 `VERSION := 0.0.4` 不一致。
|
||||
|
||||
- **严重等级**: 🟢 Low
|
||||
- **修复建议**: 统一版本管理,或在 Makefile 中定义并编译时注入
|
||||
|
||||
#### [CORE-Q02] parse_data_from_mq 语法存疑
|
||||
|
||||
- **文件**: `src/main.cpp:267-304`
|
||||
- **时间**: 2026-07-30 14:53:45
|
||||
- **描述**: 函数声明缺少 `try` 关键字(`void parse_data_from_mq(std::string& json_str) try {`),但代码能编译通过,可能使用了 C++11 的 `function-try-block` 语法。代码可读性较差,建议显式标注。
|
||||
|
||||
- **严重等级**: 🟢 Low
|
||||
- **修复建议**: 添加注释说明使用的是函数 try-catch 语法
|
||||
|
||||
#### [CORE-Q03] PPool::kill 参数类型不匹配
|
||||
|
||||
- **文件**: `include/main/PPool.hh:35` vs `src/main/pmc_mtd.cpp:53`
|
||||
- **时间**: 2026-07-30 14:55:00
|
||||
- **描述**: `PPool::kill(int idx)` 接受下标参数,但 `pmc_mtd::kill(pid)` 传入的是 PID。需要遍历查找,O(n) 复杂度。
|
||||
|
||||
- **严重等级**: 🟢 Low
|
||||
- **修复建议**:
|
||||
1. 改为 `killByPid(pid_t pid)`
|
||||
2. 或维护 PID → 迭代器 的映射表
|
||||
|
||||
#### [CORE-Q04] 缺少配置加载能力
|
||||
|
||||
- **文件**: 全局
|
||||
- **时间**: 2026-07-30 14:56:30
|
||||
- **描述**: 当前无 `config.yaml` 或类似配置文件支持,所有参数硬编码:
|
||||
- HTTP 端口 5200
|
||||
- 消息队列大小 2048/100
|
||||
- 证书路径 `server.crt/server.key`
|
||||
|
||||
- **严重等级**: 🟢 Low
|
||||
- **修复建议**: 引入 `config.yaml` 支持,至少分离端口和证书路径配置
|
||||
|
||||
#### [CORE-Q05] 中文代码库未被实际使用
|
||||
|
||||
- **文件**: `include/cn/`
|
||||
- **时间**: 2026-07-30 14:58:00
|
||||
- **描述**: `中文化.hpp`、`链串.hpp`、`顺序表.hpp`、`向量.hpp`、`异常.hpp` 构成了一套完整的中文 API,但在 `src/` 代码中未被引用。这些文件更像是演示项目,而非生产代码。
|
||||
|
||||
- **严重等级**: 🟢 Low
|
||||
- **修复建议**:
|
||||
1. 删除未使用的中文代码库
|
||||
2. 或在 README 中明确标注为"实验性功能"
|
||||
3. 迁移到独立仓库
|
||||
|
||||
#### [CORE-Q06] 缺少单元测试
|
||||
|
||||
- **文件**: 全局
|
||||
- **时间**: 2026-07-30 14:59:30
|
||||
- **描述**: 项目无任何测试代码。核心功能(进程管理、消息队列、管道通信)完全依赖手工测试。
|
||||
|
||||
- **严重等级**: 🟢 Low
|
||||
- **修复建议**: 引入 Google Test,覆盖:
|
||||
- PPool 的增删查改
|
||||
- Pipe 的读写超时
|
||||
- Mq 的发送接收
|
||||
- 中文代码库的基础功能
|
||||
|
||||
---
|
||||
|
||||
## 三、统计汇总
|
||||
|
||||
| 等级 | 数量 | 问题 ID |
|
||||
|------|------|---------|
|
||||
| 🔴 Critical | 4 | S01, S02, S03, S04 |
|
||||
| 🟡 Medium | 6 | S05, S06, S07, S08, S09, S10 |
|
||||
| 🟢 Low | 6 | Q01, Q02, Q03, Q04, Q05, Q06 |
|
||||
| **合计** | **16** | |
|
||||
|
||||
### 按类别统计
|
||||
|
||||
| 类别 | 问题数 | 占比 |
|
||||
|------|--------|------|
|
||||
| 安全(注入、鉴权) | 3 | 19% |
|
||||
| 稳定性(崩溃、竞态) | 4 | 25% |
|
||||
| 功能缺失 | 2 | 13% |
|
||||
| 代码质量 | 5 | 31% |
|
||||
| 可观测性 | 2 | 12% |
|
||||
|
||||
---
|
||||
|
||||
## 四、修复优先级建议
|
||||
|
||||
### 第一阶段(紧急,预计 1 天)
|
||||
|
||||
1. **CORE-S03**: 修复 `ProcessTask` 空指针检查(最简单,影响启动)
|
||||
2. **CORE-S01**: 实现命令白名单 + 路径校验(核心安全)
|
||||
3. **CORE-S02**: 添加 API Token 鉴权(核心安全)
|
||||
|
||||
### 第二阶段(重要,预计 3 天)
|
||||
|
||||
4. **CORE-S05**: 添加 PPool 互斥锁(并发安全)
|
||||
5. **CORE-S08**: 恢复 kill/exec IPC 路径(功能补全)
|
||||
6. **CORE-S09**: 简化 PPool::clr() 实现(稳定性)
|
||||
7. **CORE-S10**: 添加日志文件持久化(可观测性)
|
||||
|
||||
### 第三阶段(优化,预计 1 周)
|
||||
|
||||
8. **CORE-S04**: 完善路径穿越防护
|
||||
9. **CORE-S06**: 改进消息队列命名
|
||||
10. **CORE-S07**: 添加管道清理机制
|
||||
11. **CORE-Q01 ~ Q06**: 代码质量优化
|
||||
|
||||
---
|
||||
|
||||
## 五、亮点
|
||||
|
||||
尽管问题较多,项目仍有值得肯定之处:
|
||||
|
||||
| 亮点 | 说明 |
|
||||
|------|------|
|
||||
| 清晰的模块划分 | 进程池、线程、通信、HTTP 各司其职 |
|
||||
| 双轨 IPC 设计 | 消息队列 + 管道的请求-响应模式简洁高效 |
|
||||
| 中文代码实验 | 对中文编程的探索有教育意义 |
|
||||
| systemd 集成 | 标准部署方式,便于运维 |
|
||||
| 最小依赖 | 仅依赖 Boost,无过度设计 |
|
||||
|
||||
---
|
||||
|
||||
## 六、附录
|
||||
|
||||
### 审查工具
|
||||
|
||||
- 人工静态分析
|
||||
- grep 模式匹配
|
||||
- 代码路径追踪
|
||||
|
||||
### 参考文档
|
||||
|
||||
- [Boost.Process 文档](https://www.boost.org/doc/libs/release/libs/process/)
|
||||
- [Boost.Interprocess 文档](https://www.boost.org/doc/libs/release/libs/interprocess/)
|
||||
- [POSIX 消息队列](https://man7.org/linux/man-pages/man7/mq_overview.7.html)
|
||||
- [FIFO 管道](https://man7.org/linux/man-pages/man7/fifo.7.html)
|
||||
|
||||
### 审查人
|
||||
|
||||
- 自动化审查助手
|
||||
- 审查时间:2026-07-30
|
||||
|
||||
---
|
||||
|
||||
*本日志由自动化代码审查工具生成,供开发团队参考。修复建议需结合实际场景评估。*
|
||||
+10
-2
@@ -288,11 +288,19 @@ def whoami():
|
||||
if info is None:
|
||||
return jsonify({'login': False}), 200
|
||||
|
||||
return jsonify({
|
||||
resp = {
|
||||
'login': True,
|
||||
'user': info.get('user'),
|
||||
'is_admin': bool(info.get('is_admin'))
|
||||
}), 200
|
||||
}
|
||||
# 附加 Token 生命周期信息,供前端 Token 管理器估算过期时间
|
||||
if info.get('iat'):
|
||||
resp['iat'] = info['iat']
|
||||
if info.get('created_at'):
|
||||
resp['created_at'] = info['created_at']
|
||||
if info.get('keep_time'):
|
||||
resp['keep_time'] = info['keep_time']
|
||||
return jsonify(resp), 200
|
||||
|
||||
|
||||
# eg: {"id": 1}
|
||||
|
||||
+199
-2
@@ -1,2 +1,199 @@
|
||||
# UniServe_libcrypt
|
||||
UniServe Framework Cryption Library
|
||||
# libcrypt — UniServe 加密库
|
||||
|
||||
C++20 轻量级加密库,基于 OpenSSL 3.x EVP API 封装。提供 AES-256-CBC 加解密、Base64 编解码、SHA-256 哈希、RSA 密钥管理与加解密/签名验证。
|
||||
|
||||
命名空间 `qing`,库文件 `libus_crypt.so`,头文件安装至 `/usr/include/us/`。被 `gateway` 的 Token 签发/验证和密码哈希直接依赖。
|
||||
|
||||
## 架构
|
||||
|
||||
```
|
||||
qing::Crypto_Basic (基类: bytes_to_hex / hex_to_bytes / CRYPTO_CHECK)
|
||||
├── AES_CBC AES-256-CBC 对称加密(随机密钥+IV / 外部密钥)
|
||||
├── Base64 Base64 编解码(OpenSSL BIO 链)
|
||||
├── SHA256 SHA-256 哈希与验证
|
||||
└── MyRSA RSA 非对称密钥
|
||||
├── Generator 密钥对生成(2048-bit,PKCS#8 PEM 输出)
|
||||
├── Private_Key 私钥加载、OAEP 解密、PKCS1_v1_5/PSS 签名
|
||||
└── Public_Key 公钥加载、OAEP 加密、PKCS1_v1_5/PSS 验签
|
||||
```
|
||||
|
||||
所有加密类均使用 OpenSSL 3.x EVP 高层 API。`bytes_to_hex` / `hex_to_bytes` 在 `Crypto_Basic` 中实现(查表法),编译进 `libus_crypt.so`。加解密类为纯头文件(header-only),需链接时额外加 `-lssl -lcrypto`。
|
||||
|
||||
## 类与 API
|
||||
|
||||
### Crypto_Basic — 基类
|
||||
|
||||
提供两个静态工具方法和异常类型,不直接实例化。
|
||||
|
||||
| 成员 | 说明 |
|
||||
|------|------|
|
||||
| `bytes_to_hex(data)` | 字节向量 → 十六进制字符串(查表法) |
|
||||
| `hex_to_bytes(hex)` | 十六进制字符串 → 字节向量(大小写兼容) |
|
||||
| `CryptoExp` | 异常类,继承 `std::runtime_error` |
|
||||
| `CRYPTO_CHECK(cond, msg)` | 宏,条件为 false 时抛出 `std::runtime_error` |
|
||||
|
||||
### AES_CBC — AES-256-CBC 对称加密
|
||||
|
||||
```cpp
|
||||
AES_CBC aes; // 自动生成随机 32B 密钥 + 16B IV
|
||||
AES_CBC aes(key_vec, iv_vec); // 使用外部密钥和 IV
|
||||
std::string hex = aes.encrypt(text); // 加密,返回 hex 密文
|
||||
std::string raw = aes.decrypt(hex); // 解密 hex 密文,返回原文
|
||||
std::string k = aes.get_key(); // 获取密钥(hex)
|
||||
std::string v = aes.get_iv(); // 获取 IV(hex)
|
||||
```
|
||||
|
||||
加密输出为 hex 编码的密文,非原始字节。解密接受 hex 输入,返回原始字符串。
|
||||
|
||||
### Base64 — Base64 编解码
|
||||
|
||||
提供实例方法和静态工具方法两种调用方式。
|
||||
|
||||
| 方法 | 说明 |
|
||||
|------|------|
|
||||
| `encode(str)` / `encode(vec)` | 编码为 Base64 字符串 |
|
||||
| `decode_to_string(b64)` | 解码为原始字符串 |
|
||||
| `decode_to_bytes(b64)` | 解码为字节向量 |
|
||||
| `base64_encode(str)` (static) | 静态编码工具 |
|
||||
| `base64_decode_to_string(b64)` (static) | 静态解码工具 |
|
||||
| `is_valid_base64(str)` (static) | 验证字符集合法性(不依赖 OpenSSL) |
|
||||
|
||||
编码不添加换行符(`BIO_FLAGS_BASE64_NO_NL`)。
|
||||
|
||||
### SHA256 — SHA-256 哈希
|
||||
|
||||
```cpp
|
||||
SHA256 h;
|
||||
std::string hex = h.hash("hello"); // 字符串 → hex 哈希
|
||||
auto bytes = h.hash_bytes("hello"); // 字符串 → 字节哈希
|
||||
bool ok = h.verify("hello", expected_hex); // 验证
|
||||
|
||||
// 静态快捷方式
|
||||
std::string h = SHA256::sha256("hello");
|
||||
```
|
||||
|
||||
### MyRSA — RSA 非对称密钥
|
||||
|
||||
**Generator** — 生成 2048-bit RSA 密钥对(OpenSSL 3.x `OSSL_PARAM` API)。
|
||||
|
||||
```cpp
|
||||
MyRSA::Generator gen;
|
||||
gen.save_pri_key("private.pem", "password"); // PKCS#8 + AES-256-CBC 加密
|
||||
gen.save_pub_key("public.pem"); // SubjectPublicKeyInfo
|
||||
std::string pem = gen.get_public_key_pem(); // 导出 PEM 字符串
|
||||
```
|
||||
|
||||
**Private_Key** — 加载私钥(支持 PKCS#1/PKCS#8,加密或明文)。
|
||||
|
||||
```cpp
|
||||
auto pri = MyRSA::Private_Key("private.pem", true, "password");
|
||||
std::string raw = pri.Decrypt(cipher_hex); // OAEP 解密
|
||||
std::string sig = pri.Sign(msg); // PSS 签名(默认)
|
||||
std::string sig = pri.Sign(msg, RSASignaturePadding::PKCS1_v1_5);
|
||||
```
|
||||
|
||||
> 私有继承自 `Crypto_Basic`,内部使用其工具方法,外部不可将 `Private_Key*` 转为 `Crypto_Basic*`。
|
||||
|
||||
**Public_Key** — 加载公钥。
|
||||
|
||||
```cpp
|
||||
auto pub = MyRSA::Public_Key("public.pem");
|
||||
std::string hex = pub.Encrypt(text); // OAEP 加密
|
||||
bool ok = pub.Verify(msg, sig_hex); // 验签(默认 PSS)
|
||||
bool ok = pub.Verify(msg, sig_hex, RSASignaturePadding::PKCS1_v1_5);
|
||||
```
|
||||
|
||||
**签名填充模式**(`RSASignaturePadding` 枚举):
|
||||
|
||||
| 值 | 说明 |
|
||||
|----|------|
|
||||
| `PSS` | 概率签名方案(默认,更安全) |
|
||||
| `PKCS1_v1_5` | 传统填充(兼容旧系统) |
|
||||
|
||||
## 构建与安装
|
||||
|
||||
**前置依赖**:OpenSSL 3.x(开发包 `libssl-dev`),g++ C++20。
|
||||
|
||||
```bash
|
||||
# 一键构建 + 安装(需 sudo)
|
||||
bash _set.sh
|
||||
|
||||
# 或手动
|
||||
make # 编译 libus_crypt.so
|
||||
make install # 安装头文件到 /usr/include/us/ + .so 到 /lib/
|
||||
make clear # 清理构建产物
|
||||
make uninstall # 卸载
|
||||
```
|
||||
|
||||
使用:
|
||||
|
||||
```cpp
|
||||
#include <us/Crypto_Basic.hpp>
|
||||
#include <us/AES_CBC.hpp>
|
||||
#include <us/Base64.hpp>
|
||||
#include <us/SHA256.hpp>
|
||||
#include <us/MyRSA.hpp>
|
||||
```
|
||||
|
||||
链接(OpenSSL 符号在 header-only 类中,需显式链接):
|
||||
|
||||
```bash
|
||||
g++ -std=c++20 ... -lus_crypt -lssl -lcrypto
|
||||
```
|
||||
|
||||
## 使用示例
|
||||
|
||||
```cpp
|
||||
#include <us/SHA256.hpp>
|
||||
#include <us/Base64.hpp>
|
||||
#include <us/AES_CBC.hpp>
|
||||
#include <iostream>
|
||||
|
||||
int main() {
|
||||
// SHA-256
|
||||
std::string hash = qing::SHA256::sha256("hello world");
|
||||
std::cout << "SHA256: " << hash << std::endl;
|
||||
|
||||
// Base64
|
||||
std::string b64 = qing::Base64::base64_encode("hello");
|
||||
std::cout << "Base64: " << b64 << std::endl;
|
||||
|
||||
// AES-256-CBC
|
||||
qing::AES_CBC aes;
|
||||
std::string cipher = aes.encrypt("secret message");
|
||||
std::string plain = aes.decrypt(cipher);
|
||||
std::cout << "AES decrypt: " << plain << std::endl;
|
||||
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
libcrypt/
|
||||
├── inc/us/
|
||||
│ ├── Crypto_Basic.hpp # 基类:hex 转换 + 异常
|
||||
│ ├── AES_CBC.hpp # AES-256-CBC 加解密(header-only)
|
||||
│ ├── Base64.hpp # Base64 编解码(header-only)
|
||||
│ ├── SHA256.hpp # SHA-256 哈希(header-only)
|
||||
│ └── MyRSA.hpp # RSA 密钥/加解密/签名(header-only)
|
||||
├── src/
|
||||
│ └── Crypto_Basic.cpp # bytes_to_hex / hex_to_bytes 实现
|
||||
├── Makefile
|
||||
├── _set.sh
|
||||
└── README.md
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
- **OpenSSL 版本**:MyRSA 的 `Generator` 使用了 `OSSL_PARAM`、`EVP_PKEY_CTX_new_from_name` 等 OpenSSL 3.x 独占 API,无法在 OpenSSL 1.1.x 下编译。AES/Base64/SHA256 兼容 1.1.x。
|
||||
- **链接要求**:`libus_crypt.so` 仅含 `bytes_to_hex` 和 `hex_to_bytes`(无 OpenSSL 调用)。所有加密类为 header-only,链接时必须添加 `-lssl -lcrypto`。
|
||||
- **RSA 密钥长度**:默认 2048-bit,可在 `generate_rsa_key(size_t key_bits)` 中调整。
|
||||
- **AES 密钥和 IV**:默认构造函数随机生成,每次创建 `AES_CBC` 对象产生不同的密钥对。需要长期存储密钥时使用参数化构造函数传入外部密钥。
|
||||
- **hex 编码**:`bytes_to_hex` 使用查表法实现,位于 `Crypto_Basic.cpp` 中编译进 `.so`。`SHA256` 不再有私有副本。
|
||||
- **`Private_Key` 私有继承**:该设计防止 `Private_Key` 被隐式转换为 `Crypto_Basic*`,是刻意为之的安全措施。
|
||||
|
||||
## 许可证
|
||||
|
||||
MIT License
|
||||
@@ -2,6 +2,7 @@
|
||||
#include <openssl/evp.h>
|
||||
#include <openssl/err.h>
|
||||
#include <openssl/rand.h>
|
||||
#include <iomanip>
|
||||
#include <iostream>
|
||||
#include <vector>
|
||||
#include <stdexcept>
|
||||
|
||||
@@ -5,9 +5,7 @@
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <stdexcept>
|
||||
#include <iomanip>
|
||||
#include <sstream>
|
||||
#include "us/Crypto_Basic.hpp"
|
||||
#include "Crypto_Basic.hpp"
|
||||
|
||||
namespace qing {
|
||||
|
||||
@@ -99,15 +97,5 @@ namespace qing {
|
||||
|
||||
return digest;
|
||||
}
|
||||
|
||||
/* 辅助函数:字节向量转十六进制字符串 */
|
||||
static std::string bytes_to_hex(const std::vector<unsigned char>& bytes) {
|
||||
std::stringstream ss;
|
||||
ss << std::hex << std::setfill('0');
|
||||
for (unsigned char byte : bytes) {
|
||||
ss << std::setw(2) << static_cast<int>(byte);
|
||||
}
|
||||
return ss.str();
|
||||
}
|
||||
};
|
||||
}
|
||||
+194
-9
@@ -1,14 +1,199 @@
|
||||
# UniServe_libth
|
||||
UniServe Framework Thread Library.
|
||||
# libth — UniServe 线程库
|
||||
|
||||
# Dependancy Chain
|
||||
It should be C++17.
|
||||
C++20 轻量级多线程控制库,为 UniServe Framework 提供线程抽象与状态机驱动的执行模型。由 `core`(PMC 进程管理器)的 `HttpTask` 和 `ProcessTask` 直接依赖。
|
||||
|
||||
# Install Method
|
||||
Use UniServe Generate Installation Script.
|
||||
命名空间 `qing`,库文件 `libus_th.so`,头文件安装至 `/usr/include/us/`。
|
||||
|
||||
``` bash
|
||||
bash _set.sh
|
||||
## 架构
|
||||
|
||||
```
|
||||
┌──────────┐ ┌───────────────────────────────┐
|
||||
│ ITask │ │ Thread │
|
||||
│ (接口) │ │ 持有 Fsm + std::thread │
|
||||
│ start() │ │ │
|
||||
│ stop() │ │ set_stop / set_wake / │
|
||||
│ isRunning│ │ set_loop / set_clear │
|
||||
└────┬─────┘ │ Activate / WaitStart / │
|
||||
│ 实现 │ WaitClose │
|
||||
▼ │ │
|
||||
HttpTask │ main(): "回字形" 执行循环 │
|
||||
ProcessTask └──────────────┬────────────────┘
|
||||
(位于 core) │ 持有
|
||||
┌─────────────────┼─────────────────┐
|
||||
▼ ▼ ▼
|
||||
┌──────────┐ ┌────────────┐ ┌──────────┐
|
||||
│ Fsm │ │ Cv_wait │ │ Thread │
|
||||
│ 状态机 │ │ 条件等待 │ │ .cpp │
|
||||
│ SHUT/STOP│ │ Wait() │ │ 实现文件 │
|
||||
│ START/ │ │ WakeCv() │ │ │
|
||||
│ RUNNING │ │ │ │ │
|
||||
└──────────┘ └────────────┘ └──────────┘
|
||||
```
|
||||
|
||||
# ENJOY!
|
||||
## 类与 API
|
||||
|
||||
### ITask — 任务接口
|
||||
|
||||
纯虚基类,定义可运行任务的契约。下游模块(core 中的 HttpTask、ProcessTask)继承实现。
|
||||
|
||||
```cpp
|
||||
class ITask {
|
||||
virtual void start() = 0; // 启动任务
|
||||
virtual void stop() = 0; // 停止任务
|
||||
virtual bool isRunning() = 0; // 查询运行状态
|
||||
};
|
||||
```
|
||||
|
||||
### Fsm — 线程安全有限状态机
|
||||
|
||||
4 个状态:`SHUT(-1)` → `STOP(0)` → `START(1)` → `RUNNING(2)`。Thread 通过 Fsm 驱动其执行流程。
|
||||
|
||||
| 方法 | 说明 |
|
||||
|------|------|
|
||||
| `check()` | 线程安全地读取当前状态 |
|
||||
| `set(Stat)` | 加锁设置状态 + 唤醒阻塞线程 |
|
||||
| `set_without_lock(Stat)` | 不加锁设置(用于临界区内) |
|
||||
| `set_without_notify(Stat)` | 加锁但不唤醒条件变量 |
|
||||
| `suspend()` | 阻塞当前线程直至状态变更 |
|
||||
|
||||
初始状态为 `STOP`。
|
||||
|
||||
### Thread — 线程抽象
|
||||
|
||||
基于回调组合(composition over inheritance),持有 `Fsm` 和 `std::thread`。执行模型为"回字形"循环:
|
||||
|
||||
```
|
||||
while (状态 != SHUT) {
|
||||
① STOP: 循环调用 StopEvent 回调(空闲轮询)
|
||||
② START: 单次调用 WakeEvent 回调(启动)
|
||||
③ RUNNING:循环调用 LoopEvent 回调(工作循环)
|
||||
④ 退出后: 调用 ClearEvent 回调(清理)
|
||||
}
|
||||
异常捕获:loop 内抛出 std::exception 自动回退到 STOP,进程不崩溃。
|
||||
```
|
||||
|
||||
**生命周期:两阶段构造。** 构造函数只初始化成员,`Activate()` 才真正创建 `std::thread`——避免线程在对象构造完成前启动的竞态条件。
|
||||
|
||||
| 方法 | 说明 |
|
||||
|------|------|
|
||||
| `set_stop(f_t)` | 设置 STOP 状态回调 |
|
||||
| `set_wake(f_t)` | 设置 START 状态回调 |
|
||||
| `set_loop(f_t)` | 设置 RUNNING 状态回调 |
|
||||
| `set_clear(f_t)` | 设置清理回调 |
|
||||
| `Activate()` | 创建 std::thread 并启动 main() |
|
||||
| `WaitStart()` | 唤醒线程并阻塞至其进入 RUNNING |
|
||||
| `WaitClose()` | 关闭线程并 join 等待退出 |
|
||||
| `wake()` | 状态 → START(不可持有锁,否则死锁) |
|
||||
| `stop()` | 状态 → STOP |
|
||||
| `shut()` | 状态 → SHUT |
|
||||
| `run()` | 状态 → RUNNING |
|
||||
| `check()` | 查询当前状态 |
|
||||
| `suspend()` | 阻塞等待状态变更 |
|
||||
|
||||
回调类型:`using f_t = std::function<void(Thread&)>;`
|
||||
|
||||
### Cv_wait — 条件变量等待工具
|
||||
|
||||
简单的单次等待-唤醒工具,用于线程间同步。
|
||||
|
||||
| 方法 | 说明 |
|
||||
|------|------|
|
||||
| `Wait()` | 阻塞当前线程至 WakeCv() 被调用 |
|
||||
| `WakeCv()` | 唤醒所有等待线程 |
|
||||
|
||||
> **已知限制**:仅支持单次等待-唤醒周期,不可复用。缺少超时机制,可能永久阻塞。代码中标注建议改用 `std::future`/`std::promise`。core 的 `main.cpp` 使用此类。
|
||||
|
||||
## 构建与安装
|
||||
|
||||
```bash
|
||||
# 一键构建 + 安装(需 sudo)
|
||||
bash _set.sh
|
||||
|
||||
# 或手动执行
|
||||
make # 编译 libus_th.so
|
||||
make install # 安装头文件到 /usr/include/us/ + .so 到 /lib/
|
||||
make uninstall # 卸载
|
||||
make clear # 清理构建产物
|
||||
```
|
||||
|
||||
安装后使用:
|
||||
|
||||
```cpp
|
||||
#include <us/Thread.hpp>
|
||||
#include <us/Fsm.hpp>
|
||||
#include <us/ITask.hpp>
|
||||
#include <us/Cv_wait.hpp>
|
||||
```
|
||||
|
||||
链接:`g++ -std=c++20 ... -lus_th`
|
||||
|
||||
## 使用示例
|
||||
|
||||
```cpp
|
||||
#include <us/Thread.hpp>
|
||||
#include <iostream>
|
||||
|
||||
int main() {
|
||||
qing::Thread worker;
|
||||
bool done = false;
|
||||
|
||||
// STOP 状态回调:空闲等待
|
||||
worker.set_stop([&](qing::Thread& t) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
});
|
||||
|
||||
// START 状态回调:一次性初始化
|
||||
worker.set_wake([&](qing::Thread& t) {
|
||||
std::cout << "worker started" << std::endl;
|
||||
t.run(); // 进入 RUNNING
|
||||
});
|
||||
|
||||
// RUNNING 状态回调:工作循环
|
||||
worker.set_loop([&](qing::Thread& t) {
|
||||
std::cout << "working..." << std::endl;
|
||||
std::this_thread::sleep_for(std::chrono::seconds(1));
|
||||
done = true;
|
||||
t.stop(); // 回到 STOP
|
||||
});
|
||||
|
||||
// 清理回调
|
||||
worker.set_clear([](qing::Thread& t) {
|
||||
std::cout << "cycle cleaned up" << std::endl;
|
||||
});
|
||||
|
||||
worker.Activate();
|
||||
worker.WaitStart(); // 阻塞至 RUNNING
|
||||
|
||||
while (!done) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
}
|
||||
|
||||
worker.WaitClose();
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
## 依赖
|
||||
|
||||
- C++20(`g++ --std=c++20`)
|
||||
- 纯标准库,无第三方依赖
|
||||
- 线程库需链接 `-pthread`
|
||||
|
||||
## 设计要点
|
||||
|
||||
- **回调组合优于继承**。2025 年 6 月从派生改为持有 `std::function` 回调,Thread 不再需要子类化。
|
||||
- **两阶段构造**。构造与 `Activate()` 分离,防止 `std::thread` 在对象构造完成前访问未初始化的成员。
|
||||
- **异常安全**。main() 循环捕获 `std::exception` 后回退到 STOP 而非崩溃。注意:catch 后继续运行可能造成内存泄漏。
|
||||
- **wake() 不可加锁**。`WaitStart()` 先持有 `mtx_th` 再调用 `wake()`,若 `wake()` 内部也尝试获取同一锁会造成死锁。
|
||||
- **Fsm 提供两套条件变量**。Fsm 内部有一套 `cv` 用于 `suspend()`,Thread 自身另有一套 `cv` 用于 `WaitStart()`/`WaitClose()`,互不干扰。
|
||||
|
||||
## 注意事项
|
||||
|
||||
- Cv_wait 仅支持单次使用,复用时 `flag` 残留为 `true` 会导致 `Wait()` 立即返回而不阻塞。
|
||||
- Fsm 的 `suspend()` 等待的是 Fsm 内部的 `ready` 标志,由 `set()` 触发;Thread 的 `WaitStart()` 等待的是 Thread 内部的 `ready`,由 `run()`/`stop()`/`shut()` 触发。两者独立。
|
||||
- `Activate()` 不可重复调用(内部有空指针检查),但不会报错,静默返回。
|
||||
- 析构函数为虚函数,允许安全的多态删除。
|
||||
|
||||
## 许可证
|
||||
|
||||
MIT License
|
||||
@@ -28,7 +28,7 @@ public:
|
||||
inline void WakeCv() /* 唤醒线程 */
|
||||
{
|
||||
{
|
||||
std::lock_guard<std::mutex> lcok(mtx);
|
||||
std::lock_guard<std::mutex> lock(mtx);
|
||||
flag = true;
|
||||
}
|
||||
cv.notify_all();
|
||||
|
||||
+165
@@ -0,0 +1,165 @@
|
||||
# smtp — 邮箱验证码服务
|
||||
|
||||
基于自研 [pymsg](../pymsg/) 协议(TCP 长度前缀帧 + JSON)的邮箱验证码服务,通过 SMTP_SSL 发送 6 位数字验证码,支持发送冷却和防暴力枚举。
|
||||
|
||||
服务由 `core`(PMC)的 `init.txt` 拉起,监听 `127.0.0.1:8008`(TCP),仅允许本机调用。`gateway` 在 `/register` 流程中经 pymsg 调用本服务完成邮箱验证。
|
||||
|
||||
## 架构
|
||||
|
||||
```
|
||||
gateway :8443 (注册流程)
|
||||
│ pymsg TCP
|
||||
▼
|
||||
smtp :8008 (MsgServer)
|
||||
│ SMTP_SSL
|
||||
▼
|
||||
外部 SMTP 服务器 (163/QQ 等)
|
||||
│
|
||||
▼
|
||||
用户邮箱(收验证码)
|
||||
```
|
||||
|
||||
## 路由
|
||||
|
||||
服务基于 pymsg 协议,每条消息为完整 JSON,`_route` 字段指定路由。
|
||||
|
||||
| _route | 说明 |
|
||||
|--------|------|
|
||||
| `send_email` | 生成 6 位数字验证码,通过 SMTP_SSL 发送至指定邮箱 |
|
||||
| `verify_email` | 校验用户输入的验证码,一次性消费 |
|
||||
| `health` | 健康检查 |
|
||||
|
||||
响应格式:
|
||||
|
||||
```json
|
||||
{"success": true, "data": {...}}
|
||||
{"success": false, "error": "..."}
|
||||
```
|
||||
|
||||
### 1. send_email — 发送验证码
|
||||
|
||||
**请求**:
|
||||
|
||||
```json
|
||||
{"_route": "send_email", "email": "user@example.com"}
|
||||
```
|
||||
|
||||
可选字段 `subject` / `content` 覆盖默认标题与正文,`content` 中 `{code}` 会被替换为验证码。
|
||||
|
||||
**响应**:
|
||||
|
||||
| 响应 data | 触发条件 |
|
||||
|-----------|----------|
|
||||
| `{"sent": true, "ttl": 600}` | 发送成功,TTL 为有效期(秒) |
|
||||
| `{"sent": false, "error": "invalid email"}` | 邮箱格式无效 |
|
||||
| `{"sent": false, "error": "SMTP 服务未配置"}` | .env 缺少 SMTP 配置 |
|
||||
| `{"sent": false, "retry_after": N, "error": "发送过于频繁,请 N 秒后重试"}` | 同一邮箱在冷却期内 |
|
||||
|
||||
**安全机制**:
|
||||
- 发送冷却:同一邮箱两次成功发送的最小间隔 `SEND_INTERVAL`(60 秒)。冷却期内请求直接拒绝,不覆盖已有验证码。
|
||||
- 发送失败回滚:SMTP 发送失败时立即删除刚写入的验证码,避免"码已存但邮件没到"的不可用状态。
|
||||
|
||||
### 2. verify_email — 校验验证码
|
||||
|
||||
**请求**:
|
||||
|
||||
```json
|
||||
{"_route": "verify_email", "email": "user@example.com", "code": "123456"}
|
||||
```
|
||||
|
||||
兼容旧字段名 `input_captcha`。
|
||||
|
||||
**响应**(均为 `success: true`,业务结果在 `data` 中区分):
|
||||
|
||||
| 响应 data | 触发条件 |
|
||||
|-----------|----------|
|
||||
| `{"valid": true}` | 验证码正确(已消费) |
|
||||
| `{"valid": false, "error": "email and code required"}` | 缺少字段 |
|
||||
| `{"valid": false, "error": "invalid or expired code"}` | 验证码错误、过期或不存在 |
|
||||
| `{"valid": false, "invalidated": true, "error": "验证码连续错误 5 次,已失效,请重新获取"}` | 连续校验失败达上限 |
|
||||
|
||||
> 验证码一经正确校验即从内存中删除(一次性消费),防重放。同一验证码连续校验失败达 `MAX_VERIFY_ATTEMPTS`(5 次)即作废,防止对 6 位数字码的在线枚举。
|
||||
|
||||
### 3. health — 健康检查
|
||||
|
||||
**请求**:`{"_route": "health"}`
|
||||
**响应**:`{"success": true, "data": {"status": "ok", "service": "smtp"}}`
|
||||
|
||||
## 配置
|
||||
|
||||
通过 `.env` 文件配置 SMTP 连接信息,支持 4 种格式:
|
||||
|
||||
```bash
|
||||
# KEY=VALUE(推荐)
|
||||
SMTP_SERVER=smtp.163.com
|
||||
SMTP_PORT=465
|
||||
SMTP_USER=your@163.com
|
||||
__PASSWORD=your_smtp_password
|
||||
|
||||
# 也支持 KEY: VALUE / KEY VALUE / export KEY=...
|
||||
MAIL_FROM=dnfs@qsont.me # 可选,发件人身份;默认跟随 SMTP_USER
|
||||
```
|
||||
|
||||
| 变量 | 必填 | 说明 |
|
||||
|------|------|------|
|
||||
| `SMTP_SERVER` | 是 | SMTP 服务器地址 |
|
||||
| `SMTP_PORT` | 否 | SMTP 端口,默认 465 |
|
||||
| `SMTP_USER` | 是 | 邮箱登录账号 |
|
||||
| `__PASSWORD` | 是 | 邮箱密码/授权码(兼容别名 `SMTP_PASSWORD`) |
|
||||
| `MAIL_FROM` | 否 | 发件人身份,默认同 SMTP_USER。多数服务商强制 From 头与登录账号一致 |
|
||||
|
||||
服务启动时检查必填配置项,缺失时打印警告日志但不阻止启动(`send_email` 路由会返回配置错误)。
|
||||
|
||||
## 数据模型与验证码生命周期
|
||||
|
||||
- **存储**:进程内存字典 `code_store`,键为 email,值为 `{code, expires_at, attempts}`。
|
||||
- **冷却记录**:`send_history` 字典,键为 email,值为上次成功发送的时间戳。
|
||||
- **生成**:`send_email` 成功时写入 `code_store`,记录 `expires_at = now + CODE_TTL`(600 秒)。
|
||||
- **消费**:`verify_email` 校验通过时 `del code_store[email]`,不可再次使用。
|
||||
- **过期**:超时后 `verify_email` 自动删除并返回 `invalid or expired code`。`send_email` 时惰性清理过期项。
|
||||
- **作废**:连续校验失败达 5 次后删除验证码,返回 `invalidated: true`。
|
||||
- **发送失败回滚**:SMTP 异常时立即 `pop` 刚写入的 `code_store` 条目。
|
||||
|
||||
## 运行
|
||||
|
||||
```bash
|
||||
# 初始化(创建 venv + 安装依赖)
|
||||
bash _set.sh
|
||||
|
||||
# 启动
|
||||
bash _run.sh
|
||||
|
||||
# 或直接运行
|
||||
python3 smtp_v2.py --addr 127.0.0.1 --port 8008
|
||||
```
|
||||
|
||||
## 集成关系
|
||||
|
||||
- 由 `core` 通过 `init.txt` 拉起的 `bash _run.sh` 守护运行。
|
||||
- `gateway` 在 `/register` 时经 pymsg 调 `send_email` 发送验证码,调 `verify_email` 校验。需在 gateway 的 `example.ini` 中启用邮箱验证功能。
|
||||
- 日志通过 `log_client.py` 异步批量推送至 db:8005 的 Logs 表(缓冲 50 条 / 2 秒)。
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
smtp/
|
||||
├── smtp_v2.py # 服务主程序(pymsg 协议,当前有效版本)
|
||||
├── smtp.py # 旧版 Flask HTTP 实现(已废弃,仅作参考)
|
||||
├── log_client.py # 集中日志推送 SDK
|
||||
├── _set.sh # 初始化脚本(创建 venv + 安装依赖)
|
||||
├── _run.sh # 启动脚本(exec python3 smtp_v2.py)
|
||||
├── requirements.txt # Python 依赖(pymsg、nuitka)
|
||||
└── .gitignore # 忽略 __pycache__ / .env / venv
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
- 验证码存储在进程内存中,服务重启后全部失效;不支持多实例水平扩展。
|
||||
- 默认仅监听 `127.0.0.1`,仅供本机调用;浏览器侧不直接访问本服务。
|
||||
- 旧版 `smtp.py`(Flask HTTP 实现)已废弃,`_run.sh` 当前启动的是 `smtp_v2.py`。旧版存在无冷却、无失败回滚、无防枚举等安全问题,且 `verify_email` 在 email 不存在时有 KeyError 风险,不应在生产环境使用。
|
||||
- 多数邮箱服务商(163/QQ 等)强制 From 头与登录账号一致,`MAIL_FROM` 仅当服务商允许自定义发件身份时才起作用。
|
||||
- 验证码为纯 6 位数字,有效期 600 秒。
|
||||
|
||||
## 许可证
|
||||
|
||||
MIT License
|
||||
+1
-1
@@ -4,4 +4,4 @@ python3 -m venv venv
|
||||
#sudo chmod +x .env/bin/activate
|
||||
source venv/bin/activate
|
||||
pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple
|
||||
#nuitka --standalone smtp.py
|
||||
#nuitka --standalone smtp_v2.py
|
||||
@@ -26,6 +26,7 @@
|
||||
<!--<script src="https://cdnjs.cloudflare.com/ajax/libs/markdown-it/12.0.0/markdown-it.min.js"></script>-->
|
||||
<script src="js/markdown-it-12_0_0.js"></script>
|
||||
<script src="js/validation.js"></script>
|
||||
<script src="js/token_manager.js"></script>
|
||||
<script src="js/dnfs.js"></script>
|
||||
</div>
|
||||
<footer class="footer">
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
/**
|
||||
* Token 生命周期管理器
|
||||
* - 定时静默刷新:在 Token 80% 生命周期时提前调 /refresh_token
|
||||
* - 全局 fetch 拦截器:收到 401 先尝试刷新一次,成功则重试原请求,失败则原样返回 401
|
||||
* - 依赖:gateway POST /refresh_token(Cookie q4_token 自动携带)
|
||||
* - 无侵入:与 dnfs.js 完全解耦
|
||||
*/
|
||||
|
||||
let _tokenExpireAt = null;
|
||||
let _refreshTimer = null;
|
||||
|
||||
const REFRESH_ADVANCE = 0.8; // 在 80% 生命周期时提前刷新
|
||||
const REFRESH_MIN_GAP = 60; // 两次刷新最少间隔(秒)
|
||||
const RETRY_COOLDOWN = 15; // 刷新失败后冷却期(秒)
|
||||
|
||||
/* ---------- 过期时间估算:通过 /whoami 获取 iat + keep_time ---------- */
|
||||
|
||||
async function estimateExpiry() {
|
||||
try {
|
||||
const res = await _rawFetch('/whoami', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: '{}'
|
||||
});
|
||||
if (!res.ok) return false;
|
||||
const data = await res.json();
|
||||
if (!data.login) return false;
|
||||
|
||||
// 优先 iat(首次签发时间,刷新不变),回退 created_at(兼容旧 Token)
|
||||
const issueStr = data.iat || data.created_at;
|
||||
const keepTime = data.keep_time;
|
||||
if (!issueStr || !keepTime) return false;
|
||||
|
||||
const issueDate = new Date(issueStr.replace(' ', 'T') + 'Z');
|
||||
if (isNaN(issueDate.getTime())) return false;
|
||||
|
||||
_tokenExpireAt = issueDate.getTime() + keepTime * 1000;
|
||||
scheduleRefresh();
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.warn('TokenManager: 过期时间估算失败', e.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------- 定时刷新 ---------- */
|
||||
|
||||
function scheduleRefresh() {
|
||||
clearTimeout(_refreshTimer);
|
||||
if (!_tokenExpireAt) return;
|
||||
|
||||
const remainingSec = (_tokenExpireAt - Date.now()) / 1000;
|
||||
if (remainingSec <= 0) {
|
||||
// Token 已过期,立即尝试刷新
|
||||
tryRefresh();
|
||||
return;
|
||||
}
|
||||
|
||||
// 80% 位置触发,但至少等 REFRESH_MIN_GAP 秒
|
||||
const delaySec = Math.max(remainingSec * (1 - REFRESH_ADVANCE), REFRESH_MIN_GAP);
|
||||
_refreshTimer = setTimeout(tryRefresh, delaySec * 1000);
|
||||
}
|
||||
|
||||
async function tryRefresh() {
|
||||
try {
|
||||
const res = await _rawFetch('/refresh_token', { method: 'POST' });
|
||||
if (!res.ok) {
|
||||
const data = await res.json().catch(() => ({}));
|
||||
if (data.message) console.warn('TokenManager: 刷新被拒 -', data.message);
|
||||
// 冷却后重新估算
|
||||
_refreshTimer = setTimeout(() => estimateExpiry(), RETRY_COOLDOWN * 1000);
|
||||
return;
|
||||
}
|
||||
const data = await res.json();
|
||||
if (data.success && data.data) {
|
||||
console.log(`TokenManager: 已刷新 (第${data.data.refresh_count}次, 剩余${data.data.refreshes_remaining}次)`);
|
||||
}
|
||||
await estimateExpiry();
|
||||
} catch (e) {
|
||||
console.warn('TokenManager: 刷新网络异常', e.message);
|
||||
_refreshTimer = setTimeout(() => estimateExpiry(), RETRY_COOLDOWN * 1000);
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------- 全局 fetch 401 拦截器 ---------- */
|
||||
|
||||
const _rawFetch = window.fetch;
|
||||
|
||||
window.fetch = async function (url, options) {
|
||||
let resp = await _rawFetch(url, options);
|
||||
|
||||
// 仅处理 401;/refresh_token 自身不重试防循环
|
||||
if (resp.status !== 401 || (typeof url === 'string' && url.includes('/refresh_token'))) {
|
||||
return resp;
|
||||
}
|
||||
|
||||
try {
|
||||
const refreshRes = await _rawFetch('/refresh_token', { method: 'POST' });
|
||||
if (!refreshRes.ok) return resp;
|
||||
|
||||
const data = await refreshRes.json();
|
||||
if (!data.success) return resp;
|
||||
|
||||
console.log('TokenManager: 401 拦截 → 刷新成功,重试原请求');
|
||||
await estimateExpiry();
|
||||
|
||||
// Cookie 已由 gateway Set-Cookie 自动更新,直接重试
|
||||
return await _rawFetch(url, options);
|
||||
} catch (e) {
|
||||
console.warn('TokenManager: 401 刷新异常', e.message);
|
||||
}
|
||||
|
||||
return resp;
|
||||
};
|
||||
|
||||
/* ---------- 启动 ---------- */
|
||||
|
||||
estimateExpiry();
|
||||
Reference in new issue
Block a user