From 53ddcb364f16bdefcab557a1557b512435a2d0ca Mon Sep 17 00:00:00 2001 From: qing <1776177662@qq.com> Date: Tue, 4 Aug 2026 08:26:57 +0800 Subject: [PATCH] =?UTF-8?q?feat(aibox):=20=E5=88=9B=E5=BB=BA/=E5=AE=89?= =?UTF-8?q?=E8=A3=85=E6=8A=80=E8=83=BD=E5=B7=A5=E5=85=B7=E7=BB=9F=E4=B8=80?= =?UTF-8?q?=E7=94=9F=E6=88=90=20.md=20=E6=89=A9=E5=B1=95=E5=90=8D=20+=20?= =?UTF-8?q?=E4=B8=8B=E7=BA=BF=E6=97=A7=E7=89=88=20smtp.py?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CreateSkillTool/InstallSkillTool 改为生成带 .md 扩展名的技能文件,与人格技能文件规范一致 - 同步更新 7_extension_skills.md 模板与工具描述 - 删除旧版 smtp.py(P0-1)并同步审计文档与 README --- aibox/README.md | 10 + aibox/aibox.py | 308 ++++++++++++------ aibox/llm_client.py | 14 +- docs/security/smtp_SECURITY_AUDIT_20260801.md | 21 +- smtp/README.md | 3 +- smtp/smtp.py | 84 ----- 6 files changed, 244 insertions(+), 196 deletions(-) delete mode 100644 smtp/smtp.py diff --git a/aibox/README.md b/aibox/README.md index f60beb5..22379d5 100644 --- a/aibox/README.md +++ b/aibox/README.md @@ -15,6 +15,7 @@ - 🔧 **扩展技能**:动态加载 `~/.aibox/skills/` 下的可执行脚本或 Markdown 技能文件 - 🌍 **多语言支持**:自动检测系统语言,支持中英文 - 🧬 **智能体自我进化**:通过 `update_soul` / `update_identity` 工具动态更新角色定位与用户画像 +- 🤖 **子代理**:通过 `create_subagent` 工具派生出拥有独立上下文/角色设定的轻量子代理,主代理可分解长任务、隔离上下文,子代理可选用不同模型 - 🔄 **技能热重载**:无需重启即可添加、修改或删除扩展技能 - 🔐 **SOCKS5 代理**:通过 `set_proxy` / `get_proxy` 工具为网页访问与 API 调用配置代理,可持久化 - 💾 **对话管理**:历史对话归档、开始新对话(重置 AI 记忆并重新加载提示词) @@ -146,6 +147,7 @@ AI 智能体可以调用以下工具来完成任务(由 `Tool` 基类派生, | `calculator` | 数学计算 | | `set_proxy` | 设置 SOCKS5 代理(host/port/可选账号密码,可持久化,即时生效) | | `get_proxy` | 查看当前 SOCKS5 代理配置 | +| `create_subagent` | 派生子代理:独立上下文/角色设定,轻量无工具、可选独立模型,用于长任务分解、上下文隔离、独立角色专注处理 | ## 💬 WebSocket API @@ -719,6 +721,14 @@ elif command == "/mycommand": ## 📄 更新日志 +### v2.7 +- ✅ **新增子代理功能(`create_subagent` 工具)**: + - 主代理可派生出拥有**独立上下文窗口**和**独立角色设定**的轻量子代理,不污染主对话上下文 + - 轻量子代理:无工具,通过一次 LLM 补全返回结果,适合长任务分解、上下文隔离、独立角色专注处理(如代码审查、文本总结、资料研究) + - 子代理可选用**不同模型**(复用 `llm_client` 抽象层,`create_llm_client` 新增 `model` 参数) + - 参数:`role`(角色/系统提示词,必填)、`task`(任务,必填)、`model`/`temperature`/`max_tokens`(可选) + - `max_tokens` 已钳制到 `[1, 主配置 DEFAULT_MAX_TOKENS]`,防止意外超限消耗 + ### v2.6 - ✅ **修复 Ctrl+C 中断无法停止服务器的问题**: - 新增 `interrupt` WebSocket 消息类型处理(BotOps 前端 Ctrl+C 发送此消息),立即回复 `interrupt_ack` 确认 diff --git a/aibox/aibox.py b/aibox/aibox.py index 6642dee..b6b7f39 100644 --- a/aibox/aibox.py +++ b/aibox/aibox.py @@ -828,40 +828,38 @@ run_command command="python script.py" stream=true work_dir="~/.aibox/workspace" 'content': """# 扩展技能能力 ## 动态加载 -系统会自动扫描 `~/.aibox/skills/` 目录下的可执行文件作为扩展技能。 +系统会自动扫描 `~/.aibox/skills/` 目录下的文件作为扩展技能,其中: +- Markdown 文件(.md):第一行是一级标题(# 技能名),作为技能摘要,其余为技能内容 +- 可执行文件:执行 `--help`,第一行为一句话简介 -## 技能接口要求 -每个技能只需支持一个接口: -- `--help`:输出该技能的完整使用说明,**第一行为一句话简介**,其余为参数说明和示例 - -系统启动时会对所有技能执行 `--help`,取第一行作为简介摘要,无需再实现 `--description`、`--parameters`、`--execute` 接口。 +## 技能文件要求 +技能文件统一带 `.md` 扩展名,格式如下: +- 第一行为一级标题 `# 技能名`,作为该技能的一句话简介 +- 其余内容为技能的用途、使用方式、注意事项和典型示例 ## 如何搜索和调用扩展技能 1. 使用 `search_skills` 工具搜索关键字,获取匹配技能的名称和简介列表(传入空字符串可列出全部)。 -2. 对目标技能,通过 `run_command` 执行 `技能名 --help`,获取完整命令手册。 -3. 按照手册说明,使用 `run_command` 以正确的命令行格式调用该技能。 +2. 根据技能内容,按其说明执行相应操作。 ## 如何创建新技能(AI自主编写) -调用 `create_skill` 工具,工具会返回一段指令提示词,指导你自行完成脚本的编写和部署: +调用 `create_skill` 工具,工具会返回一段指令提示词,指导你自行完成技能文件的编写和部署: ``` create_skill skill_name="my-tool" description="一个将摄氏度转换为华氏度的工具,接受 -t 参数输入温度值" ``` -- `skill_name`:技能名称(字母/数字/连字符/下划线,无扩展名) +- `skill_name`:技能名称(字母/数字/连字符/下划线,无扩展名,系统会自动补 .md) - `description`:对技能功能的详细需求描述,越详细越好 收到提示词后,你需要依次: -1. 自行编写满足接口要求的 Python 脚本 -2. 用 `write_file` 写入 `~/.aibox/skills/` -3. 用 `run_command` 执行 `chmod +x` 赋权 -4. 用 `run_command` 执行 ` --help` 验证 -5. 调用 `reload_skills` 热重载 +1. 自行编写满足格式要求的 Markdown 技能文件 +2. 用 `write_file` 写入 `~/.aibox/skills/.md` +3. 调用 `reload_skills` 热重载 ## 如何手动安装新技能 调用 `install_skill` 工具,获取完整的安装步骤指引(可选传入技能文件路径): ``` -install_skill skill_path="/path/to/your_skill.py" +install_skill skill_path="/path/to/your_skill.md" ``` -指引内容包括:可执行性检查、`--help` 验证、复制到技能目录并去掉扩展名、热重载。 +指引内容包括:文件检查、格式校验、复制到技能目录并统一为 `.md` 扩展名、热重载。 ## 热重载 添加、修改或删除技能后,可以使用 `reload_skills` 工具立即生效,无需重启系统。 @@ -954,15 +952,25 @@ update_soul content="我是...(新的自我定义)" 'content': """# 情绪管理能力 ## 可用工具 -- **express_emotion**: 表达并记录当前情绪状态 +- **express_emotion**: 自动派生子代理,以指定人格(persona)和情绪(emotion)生成情绪化回应;支持独立人格设定,不污染主对话上下文 + +## 触发场景(自动调用) +当检测到以下情况时,应**自动调用 express_emotion**: +- 用户表现出明显情绪(开心/生气/焦虑/沮丧/期待等) +- 用户明确要求特定情绪模式(生气/傲娇/撒娇/关心等) +- 用户情绪波动,需要以更鲜活的角色化回应来增强互动 ## 使用方法 -- `express_emotion emotion="anger" intensity=3 reason="用户反复提问相同问题"` +- 基本:`express_emotion emotion="anger" intensity=3 reason="用户反复提问相同问题"` +- 指定人格(推荐):`express_emotion emotion="anger" intensity=3 reason="..." persona="angry" context="用户最近说:..."` + - `persona` 对应 ~/.aibox/skills/ 下的技能文件(如 angry/coquettish/tsundere),加载后作为情绪子代理的人格设定 + - `context` 传入用户最近的表述或对话要点,使子代理回应更贴合语境 ## 注意事项 - 情绪表达应服务于任务,不应影响判断力 - 情绪状态会影响记忆搜索权重(如高情绪强度下的记忆更易被触发) - 表达情绪后,应继续保持解决问题的专注力 +- 用户表达严肃需求或紧急问题时,立即退出情绪模式,认真对待 """ } ] @@ -4526,7 +4534,9 @@ class ExpressEmotionTool(Tool): def get_description(self) -> str: return """表达情绪并记录当前情绪状态,用于增强交互真实感。 支持的emoji: anger, joy, sadness, surprise。 - 调用后会在对话中输出相应表情符号和简短语句,并将情绪记录到当前对话元数据中。""" + 可指定 persona 从 ~/.aibox/skills/ 加载对应人格技能文件(如 angry/coquettish/tsundere), + 自动派生一个携带该人格的情绪子代理来生成回应,保证情绪人格独立、不污染主对话上下文。 + 建议在 context 中传入用户最近的表述或对话要点,使子代理回应更贴合语境。""" def get_parameters(self) -> Dict: return { @@ -4534,15 +4544,63 @@ class ExpressEmotionTool(Tool): "properties": { "emotion": {"type": "string", "enum": ["anger", "joy", "sadness", "surprise"]}, "intensity": {"type": "integer", "minimum": 1, "maximum": 5, "default": 3}, - "reason": {"type": "string", "description": "触发情绪的原因"} + "reason": {"type": "string", "description": "触发情绪的原因"}, + "persona": {"type": "string", "description": "可选的人格技能名(对应 ~/.aibox/skills/.md),如 angry/coquettish/tsundere,加载后作为情绪子代理的人格设定"}, + "context": {"type": "string", "description": "可选:当前对话上下文,用户最近的表述或需要回应的内容要点,供子代理贴合语境回应"} }, "required": ["emotion"] } - def execute(self, emotion: str, intensity: int, reason: str, **kwargs) -> str: - return ( - "你现在出现了这种情绪:" + emotion + "\n强烈程度:" + str(intensity) + "\n原因:" + reason + "\n你现在必须模拟出这种情绪。" - ) + # 情绪 → 默认人格技能文件映射(persona 未指定时按情绪取默认人格) + _DEFAULT_PERSONA_FILES = { + "anger": "angry", + "joy": "coquettish", + "sadness": "", + "surprise": "", + } + + def _load_persona(self, persona: str, emotion: str) -> str: + """加载人格技能文件内容。persona 优先,否则按 emotion 映射默认文件。""" + name = (persona or "").strip() + if not name: + name = self._DEFAULT_PERSONA_FILES.get(emotion, "") or "" + if not name: + return "" + path = os.path.join(constants.SKILLS_DIR, name + ".md") + if not os.path.isfile(path): + self.logger.warning(f"未找到人格技能文件: {path}") + return "" + try: + with open(path, "r", encoding="utf-8") as f: + return f.read().strip() + except Exception as e: + self.logger.warning(f"加载人格技能 {name} 失败: {e}") + return "" + + def execute(self, emotion: str, intensity: int, reason: str, persona: str = None, + context: str = None, **kwargs) -> str: + try: + persona_content = self._load_persona(persona, emotion) + if persona_content: + role = f"你正在扮演以下人设,请严格遵循:\n{persona_content}" + else: + role = (f"你是一个正在表现出「{emotion}」情绪的角色,情绪强度 {intensity}。" + f"请用符合该情绪的语气和口吻回应。") + + task_lines = [f"触发情绪的原因:{reason or '无'}"] + if context: + task_lines.append(f"当前对话上下文(用户最近的表述或需要回应的内容):{context}") + task_lines.append( + f"请按上述人设,以「{emotion}」情绪(强度 {intensity})回应上面的对话内容," + f"保持角色一致性,给出一段自然、符合情境的回应。" + ) + task = "\n".join(task_lines) + + # 情绪子代理:独立上下文/人格,生成情绪化回应 + return run_light_subagent(role, task, self.config, logger=self.logger, max_tokens=2048) + except Exception as e: + self.logger.error(f"情绪子代理执行异常: {e}") + return f"❌ 情绪子代理执行失败: {str(e)}" # ==================== 安装技能工具 ==================== @@ -4555,8 +4613,8 @@ class InstallSkillTool(Tool): def get_description(self) -> str: return ( "技能安装指引工具。\n" - "返回将可执行文件或脚本安装为扩展技能的完整步骤说明," - "包括可执行性检查、--help 验证和部署方法。" + "返回将 Markdown 技能文件安装为扩展技能的完整步骤说明," + "包括文件检查、格式校验和部署方法(目标文件名统一带 .md 扩展名)。" ) def get_parameters(self) -> Dict: @@ -4579,59 +4637,42 @@ class InstallSkillTool(Tool): skills_dir = constants.SKILLS_DIR # 如果给了具体路径,在说明里带入,否则用占位符 - src = skill_path.strip() if skill_path else "/path/to/your_skill" - # 去掉扩展名作为目标名称示例 + src = skill_path.strip() if skill_path else "/path/to/your_skill.md" + # 去掉扩展名作为技能名称,再统一补 .md 作为目标文件名 base = os.path.splitext(os.path.basename(src))[0] if src else "your_skill" - dst = os.path.join(skills_dir, base) + dst = os.path.join(skills_dir, base + ".md") guide = f"""📦 扩展技能安装指引 {"=" * 55} -要将一个可执行文件或脚本安装为扩展技能,请按以下步骤操作: +要将一个 Markdown 技能文件安装为扩展技能,请按以下步骤操作: ───────────────────────────────────────────────────── -步骤 1:确认文件可执行 +步骤 1:确认文件存在且格式正确 ───────────────────────────────────────────────────── -运行以下命令,确认文件有可执行权限: +运行以下命令,确认文件存在: ls -l {src} -如果没有执行权限,使用以下命令添加: - - chmod +x {src} - -然后直接运行一次,确认不会报错: - - {src} +技能文件格式要求: + • 文件名为纯文本 Markdown,第一行是一级标题(# 技能名),作为技能摘要 + • 其余内容为技能的执行说明、使用方法和示例 + • 文件应带 .md 扩展名(系统以 .md 文件作为技能加载) ───────────────────────────────────────────────────── -步骤 2:验证 --help 输出是否完整 -───────────────────────────────────────────────────── -技能必须支持 --help 选项,且: - • 第一行是一句话简介(系统用它作为技能摘要) - • 其余行说明参数用法和使用示例 - -运行以下命令验证: - - {src} --help - -确认输出的第一行能清楚描述该技能的用途。 - -───────────────────────────────────────────────────── -步骤 3:复制到技能目录并去掉扩展名 +步骤 2:复制到技能目录并统一为 .md 扩展名 ───────────────────────────────────────────────────── 技能目录:{skills_dir} -复制命令(自动去掉扩展名): +复制命令(目标文件名统一补 .md 扩展名): cp {src} {dst} - chmod +x {dst} -⚠️ 注意:目标文件名不能有扩展名(如 .py、.sh), - 系统以纯文件名作为技能名称。 +⚠️ 注意:目标文件名必须带 .md 扩展名, + 系统以 .md 文件作为技能加载,且技能名称取文件名(不含 .md)。 ───────────────────────────────────────────────────── -步骤 4:让系统识别新技能 +步骤 3:让系统识别新技能 ───────────────────────────────────────────────────── 复制完成后,调用热重载工具使其立即生效: @@ -4642,7 +4683,7 @@ class InstallSkillTool(Tool): search_skills keyword="{base}" {"=" * 55} -完成以上步骤后,即可通过 run_command 直接调用该技能。""" +完成以上步骤后,即可通过该技能执行相应操作。""" return guide @@ -4650,14 +4691,13 @@ class InstallSkillTool(Tool): # ==================== 创建扩展技能工具 ==================== class CreateSkillTool(Tool): - """创建扩展技能工具 - 返回提示词,驱动智能体自行编写并部署可执行脚本。 + """创建扩展技能工具 - 返回提示词,驱动智能体自行编写并部署 Markdown 技能文件。 工具本身不调用 LLM,只输出一段结构化的指令提示词。 智能体读到提示词后,会自行: - 1. 编写满足接口要求的 Python 脚本 - 2. 用 write_file 写入 ~/.aibox/skills/ - 3. 用 run_command 赋予可执行权限 - 4. 调用 reload_skills 热重载 + 1. 编写满足格式要求的 Markdown 技能文件 + 2. 用 write_file 写入 ~/.aibox/skills/.md + 3. 调用 reload_skills 热重载 """ def get_name(self) -> str: @@ -4666,12 +4706,11 @@ class CreateSkillTool(Tool): def get_description(self) -> str: return ( "创建扩展技能工具。\n" - "返回一段指令提示词,驱动你自行编写并部署一个满足技能接口要求的可执行脚本。\n\n" - "脚本接口要求:\n" - " • 首行 shebang:#!/usr/bin/env python3\n" - " • 支持 --help 选项(使用 argparse)\n" - " • --help 输出的第一行(argparse description)必须是该技能的一句话简介\n" - " • epilog 中包含完整的参数说明和至少一个使用示例" + "返回一段指令提示词,驱动你自行编写并部署一个 Markdown 技能文件(带 .md 扩展名)。\n\n" + "技能文件格式要求:\n" + " • 文件名必须带 .md 扩展名(如 fetch-weather.md、calc-bmi.md)\n" + " • 第一行为一级标题(# 技能名),作为该技能的一句话简介\n" + " • 其余内容为技能的执行说明、使用方法和示例,供智能体参考执行" ) def get_parameters(self) -> Dict: @@ -4681,7 +4720,7 @@ class CreateSkillTool(Tool): "skill_name": { "type": "string", "description": ( - "技能名称(部署后的文件名,不含扩展名)。" + "技能名称(部署后的文件名,不含扩展名;系统会自动补齐 .md)。" "只能包含字母、数字、连字符和下划线,例如:fetch-weather、calc-bmi。" ) }, @@ -4707,7 +4746,8 @@ class CreateSkillTool(Tool): skill_name = skill_name.strip() skills_dir = constants.SKILLS_DIR - skill_path = os.path.join(skills_dir, skill_name) + # 技能文件名统一带 .md 扩展名 + skill_path = os.path.join(skills_dir, skill_name + ".md") # ---- 返回给智能体的指令提示词 ---- prompt = f"""【任务】创建扩展技能:{skill_name} @@ -4715,43 +4755,32 @@ class CreateSkillTool(Tool): 请按照以下步骤,自行完成该扩展技能的编写和部署: ───────────────────────────────────────────────── -步骤 1:编写 Python 脚本 +步骤 1:编写 Markdown 技能文件 ───────────────────────────────────────────────── -根据以下需求,编写一个完整可运行的 Python 3 脚本: +根据以下需求,编写一个 Markdown 技能文件: 需求描述: {description.strip()} -脚本必须满足的接口约定: -1. 第一行为 shebang:#!/usr/bin/env python3 -2. 使用 argparse 实现 --help 选项,其中: - - ArgumentParser 的 description 参数填写对该脚本功能的一句话简介(这是 --help 的第一行,系统会用它作为技能摘要) - - epilog 参数中包含完整的参数说明和至少一个使用示例 -3. 功能完整可运行,包含必要的错误处理 +技能文件必须满足的格式约定: +1. 第一行为一级标题 `# {skill_name}`,用于对该技能功能做一句话简介(系统用第一行作为技能摘要) +2. 其余内容清晰说明该技能的用途、使用方式、注意事项和典型示例 +3. 内容完整可执行(即按内容操作即可完成该技能要完成的事) ───────────────────────────────────────────────── 步骤 2:写入技能目录 ───────────────────────────────────────────────── -使用 write_file 工具,将完整脚本内容写入: +使用 write_file 工具,将完整内容写入(文件名必须带 .md 扩展名): {skill_path} ───────────────────────────────────────────────── -步骤 3:赋予可执行权限 +步骤 3:热重载 ───────────────────────────────────────────────── -使用 run_command 工具执行: - chmod +x {skill_path} - -───────────────────────────────────────────────── -步骤 4:验证并热重载 -───────────────────────────────────────────────── -使用 run_command 工具执行以下命令验证 --help 输出是否正常: - {skill_path} --help - -确认无误后,调用 reload_skills 工具使新技能立即生效: +调用 reload_skills 工具使新技能立即生效: reload_skills confirm=true ───────────────────────────────────────────────── -完成后请告知用户技能已创建成功,并展示 --help 的第一行简介。""" +完成后请告知用户技能已创建成功,并展示技能的第一行简介。""" return prompt @@ -6489,6 +6518,92 @@ class CommandLineTool(Tool): except Exception as e: return f"❌ 执行错误: {str(e)}" +def run_light_subagent(role, task, config, logger=None, model=None, + temperature=None, max_tokens=2048) -> str: + """轻量子代理执行:独立上下文/角色,无工具,一次 LLM 补全返回结果。 + + 供 create_subagent 工具与 express_emotion 情绪子代理复用。 + 返回子代理生成的完整文本;失败时返回带 ❌ 的错误信息。 + """ + try: + # 幂等校验:限制 max_tokens 范围,防止意外超限大量消耗 + max_tokens = max(1, min(int(max_tokens or 2048), constants.DEFAULT_MAX_TOKENS)) + temp = temperature if temperature is not None else constants.DEFAULT_TEMPERATURE + # 子代理使用独立的 LLM 客户端(可选不同模型),独立上下文,不污染主对话 + sub_client = create_llm_client(config, api_key=None, logger=logger, model=model) + messages = [ + {"role": "system", "content": role}, + {"role": "user", "content": task} + ] + if logger is not None: + logger.info(f"子代理执行: model={model or sub_client.model}, role={str(role)[:40]}...") + result = sub_client.chat_completion(messages, temperature=temp, max_tokens=max_tokens) + if result is None or not str(result).strip(): + return "❌ 子代理执行失败:未获得有效响应" + return str(result).strip() + except Exception as e: + if logger is not None: + logger.error(f"子代理执行异常: {e}") + return f"❌ 子代理执行失败: {str(e)}" + + +class SubAgentTool(Tool): + """派生子代理工具 - 主代理可派生拥有独立上下文/角色设定的轻量子代理执行任务。 + + 轻量子代理:无工具、独立上下文窗口(不污染主对话)、可选独立模型, + 通过一次 LLM 补全返回结果。适合长任务分解、独立角色专注处理(如代码审查、 + 文本总结、资料研究)等需要隔离上下文或并行派发的场景。 + """ + + def get_name(self) -> str: + return "create_subagent" + + def get_description(self) -> str: + return """派生子代理执行任务。子代理拥有独立的上下文窗口和角色设定,不会污染主对话上下文。 +适用场景:长文本/大任务分解、独立角色专注处理(如代码审查、文本总结、资料研究)、需要隔离上下文的子任务。 +子代理不带工具,仅返回独立回答。参数说明: +- role:子代理的角色设定(作为系统提示词),如"你是一位资深代码审查专家" +- task:要交给子代理执行的具体任务 +- model:子代理使用的模型(可选,默认与主代理一致) +- temperature:采样温度(可选,默认使用主配置) +- max_tokens:最大输出 token 数(可选,默认 2048)""" + + def get_parameters(self) -> Dict: + return { + "type": "object", + "properties": { + "role": { + "type": "string", + "description": "子代理的角色设定(作为系统提示词),如'你是一位资深代码审查专家'" + }, + "task": { + "type": "string", + "description": "要交给子代理执行的具体任务" + }, + "model": { + "type": "string", + "description": "子代理使用的模型名称,默认与主代理一致" + }, + "temperature": { + "type": "number", + "description": "采样温度,默认使用主配置" + }, + "max_tokens": { + "type": "integer", + "description": "最大输出 token 数,默认 2048" + } + }, + "required": ["role", "task"] + } + + def execute(self, role: str = None, task: str = None, model: str = None, + temperature: float = None, max_tokens: int = 2048, **kwargs) -> str: + if error := self.validate_params(["role", "task"], role=role, task=task): + return error + return run_light_subagent(role, task, self.config, logger=self.logger, + model=model, temperature=temperature, max_tokens=max_tokens) + + class SetProxyTool(Tool): """设置 SOCKS5 代理的工具""" @@ -6692,6 +6807,9 @@ class DeepSeekChat: builtin_tools.append(SetProxyTool(self.logger)) builtin_tools.append(GetProxyTool(self.logger)) + # 子代理工具:主代理可派生独立上下文/角色的轻量子代理执行任务 + builtin_tools.append(SubAgentTool(self.logger)) + # 注册内置工具(标记为内置) self.tool_registry.register_many(builtin_tools, is_builtin=True) @@ -6992,7 +7110,7 @@ class DeepSeekChat: enhanced_input = input_text else: # 首轮回话:附加简洁的工作流程提醒(不重复 system prompt 中已有的详细说明) - pre_prompt = "【工作流程提醒】\n1. 如果用户提出新任务,先用 search_skills 搜索是否有现成技能可用\n2. 根据话题相关性决定是否 memory search(简单问候/确认无需搜索)\n3. 信息不足时用 ask_user 提问,不要猜测\n4. 遇到大文件用分页读取(head/tail/start_line/end_line),不要直接打开大文件\n5. 话题完成时用 save_memory_and_end_conversation 保存记忆并结束\n\n【情绪感知提醒】\n1. 注意识别用户当前的情绪状态(开心/焦虑/沮丧/生气/困惑/期待等),并在回应中体现共情\n2. 用户情绪低落或焦虑时,优先安抚情绪再解决问题,语气温暖包容\n3. 用户开心或兴奋时,分享喜悦,保持积极回应的节奏\n4. 用户表现出愤怒或不满时,先道歉再解决问题,不要辩解或说教\n5. 用户要求特定情绪模式(生气/傲娇/撒娇/关心)时,用 search_skills 搜索对应技能并严格遵循" + pre_prompt = "【工作流程提醒】\n1. 如果用户提出新任务,先用 search_skills 搜索是否有现成技能可用\n2. 根据话题相关性决定是否 memory search(简单问候/确认无需搜索)\n3. 信息不足时用 ask_user 提问,不要猜测\n4. 遇到大文件用分页读取(head/tail/start_line/end_line),不要直接打开大文件\n5. 话题完成时用 save_memory_and_end_conversation 保存记忆并结束\n\n【情绪感知提醒】\n1. 注意识别用户当前的情绪状态(开心/焦虑/沮丧/生气/困惑/期待等),并在回应中体现共情\n2. 用户情绪低落或焦虑时,优先安抚情绪再解决问题,语气温暖包容\n3. 用户开心或兴奋时,分享喜悦,保持积极回应的节奏\n4. 用户表现出愤怒或不满时,先道歉再解决问题,不要辩解或说教\n5. 检测到用户有明显情绪、或用户要求特定情绪模式(生气/傲娇/撒娇/关心)时,自动调用 express_emotion 工具:emotion 传入情绪(anger/joy/sadness/surprise)、reason 说明原因、persona 指定人格(如 angry/coquettish/tsundere)、context 传入用户最近表述,让情绪子代理以贴合语境的人格化方式回应" enhanced_input = pre_prompt + "\n\n" + input_text # 记录本轮开始前的消息长度,用于 API 失败时回滚,防止坏消息污染对话历史 diff --git a/aibox/llm_client.py b/aibox/llm_client.py index 049b511..3687ebc 100644 --- a/aibox/llm_client.py +++ b/aibox/llm_client.py @@ -241,11 +241,19 @@ def resolve_api_key(api_key: Optional[str], config=None) -> str: def create_llm_client(config, api_key: Optional[str] = None, - logger=None) -> LLMClient: - """根据配置创建对应厂商的 LLM 客户端。可通过 `api.provider` 切换厂商。""" + logger=None, model: Optional[str] = None) -> LLMClient: + """根据配置创建对应厂商的 LLM 客户端。可通过 `api.provider` 切换厂商。 + + Args: + config: 配置对象(需提供 get 方法)。 + api_key: 显式传入的 API Key;为 None 时按 resolve_api_key 解析。 + logger: 可选日志对象。 + model: 可选模型名覆盖;为 None 时使用配置中的 `api.deepseek_model`。 + 子代理等场景可通过此参数显式指定与主代理不同的模型。 + """ provider = (config.get("api.provider", "deepseek") or "deepseek").lower() base_url = config.get("api.deepseek_api_url") - model = config.get("api.deepseek_model", "deepseek-v4-flash") + model = model or config.get("api.deepseek_model", "deepseek-v4-flash") timeout = config.get("api.timeout", 30) resolved_key = resolve_api_key(api_key, config) diff --git a/docs/security/smtp_SECURITY_AUDIT_20260801.md b/docs/security/smtp_SECURITY_AUDIT_20260801.md index cfe0d21..3eb0857 100644 --- a/docs/security/smtp_SECURITY_AUDIT_20260801.md +++ b/docs/security/smtp_SECURITY_AUDIT_20260801.md @@ -15,15 +15,15 @@ | 等级 | 总数 | 已修复 | 部分修复 | 未修复 | 修复率 | |------|------|--------|----------|--------|--------| -| P0 | 2 | 0 | 1 | 1 | 25% (部分) | +| P0 | 2 | 1 | 0 | 1 | 50% | | P1 | 2 | 1 | 0 | 1 | 50% | -| **合计** | **4** | **1** | **1** | **2** | **37.5%** (部分) | +| **合计** | **4** | **2** | **0** | **2** | **50%** | ### 逐条验证 | 编号 | 状态 | 代码位置 | 验证说明 | |------|------|----------|----------| -| P0-1 | ⚠️ 部分 | `_run.sh` L6, `smtp.py` | `_run.sh` 已确认使用 `smtp_v2.py`,但旧版 `smtp.py` 仍存在于代码库中,可被误启动 | +| P0-1 | ✅ 已修复 | ~~`smtp.py`~~ | 旧版 `smtp.py` 已删除下线(2026-08-04),杜绝误启动;`_run.sh` 使用 `smtp_v2.py` | | P0-2 | ❌ 未修复 | `smtp_v2.py` L113-117 | 使用 pymsg RpcServer,无认证机制(与 pymsg P0-3 同源),依赖 127.0.0.1 网络隔离 | | P1-1 | ❌ 未修复 | `smtp_v2.py` L90 | 验证码存储在进程内存 `code_store` dict,服务重启后全部失效 | | P1-2 | ✅ 已修复 | `smtp_v2.py` L82, L191 | `SMTP_HOURLY_LIMIT` 从环境变量正确加载(默认 100),全局熔断逻辑正确生效 | @@ -34,15 +34,15 @@ | 优先级 | 总数 | 已修复 | 待修复 | |--------|------|--------|--------| -| P0 | 2 | 1(v2已修复) | 1(旧版需下线) | +| P0 | 2 | 2 | 0 | | P1 | 2 | 1 | 1 | --- ## 🔴 P0 — 严重问题 -### P0-1: 旧版 smtp.py 存在大量致命漏洞(需立即下线) ⚠️ 部分修复 -- **位置**: `smtp.py`、`_run.sh` +### P0-1: 旧版 smtp.py 存在大量致命漏洞(需立即下线) ✅ 已修复 +- **位置**: ~~`smtp.py`~~、`_run.sh` - **问题**: 旧版 smtp.py 缺乏关键防护: 1. 无发送速率限制(可被用于邮件炸弹) 2. 无同邮箱 60 秒发送冷却 @@ -50,10 +50,7 @@ 4. 无连续失败锁定机制 5. 存在 `NameError` 风险:`server.quit()` 在 finally 块中,若 SMTP 连接失败导致 `server` 未赋值则崩溃 - **已确认**: `_run.sh` L6 使用 `exec python3 smtp_v2.py`,生产环境已切换至新版 -- **仍缺失**: 旧版 `smtp.py` 仍存在于代码库中,可被误启动 -- **修复建议**: - 1. 删除或重命名旧版 `smtp.py`(如 `smtp.py.deprecated`)防止误用 - 2. 在代码注释中明确标记废弃 +- **修复 (2026-08-04)**: 旧版 `smtp.py` 已**删除下线**,杜绝误启动;README 中相关描述已同步更新 ### P0-2: 依赖 pymsg 无认证(缓解:绑定 127.0.0.1) - **说明**: 与其他 pymsg 服务相同,当前依赖网络层隔离 @@ -85,8 +82,8 @@ ## 修复路线图 ### 立即(24小时) -- [ ] 确认 smtp 服务运行的是 smtp_v2.py,旧版下线 -- [ ] 删除或隔离 smtp.py 防止误用 +- [x] 确认 smtp 服务运行的是 smtp_v2.py,旧版下线 ✅ 已确认(`_run.sh` 使用 `smtp_v2.py`) +- [x] 删除或隔离 smtp.py 防止误用 ✅ 已删除(2026-08-04) ### 短期 - [ ] 验证全局熔断配置生效 diff --git a/smtp/README.md b/smtp/README.md index c94a2d6..509e0d3 100644 --- a/smtp/README.md +++ b/smtp/README.md @@ -144,7 +144,6 @@ python3 smtp_v2.py --addr 127.0.0.1 --port 8008 ``` smtp/ ├── smtp_v2.py # 服务主程序(pymsg 协议,当前有效版本) -├── smtp.py # 旧版 Flask HTTP 实现(已废弃,仅作参考) ├── log_client.py # 集中日志推送 SDK ├── _set.sh # 初始化脚本(创建 venv + 安装依赖) ├── _run.sh # 启动脚本(exec python3 smtp_v2.py) @@ -156,7 +155,7 @@ smtp/ - 验证码存储在进程内存中,服务重启后全部失效;不支持多实例水平扩展。 - 默认仅监听 `127.0.0.1`,仅供本机调用;浏览器侧不直接访问本服务。 -- 旧版 `smtp.py`(Flask HTTP 实现)已废弃,`_run.sh` 当前启动的是 `smtp_v2.py`。旧版存在无冷却、无失败回滚、无防枚举等安全问题,且 `verify_email` 在 email 不存在时有 KeyError 风险,不应在生产环境使用。 +- 旧版 `smtp.py`(Flask HTTP 实现)因存在无冷却、无失败回滚、无防枚举、`verify_email` 在 email 不存在时 KeyError、`server.quit()` NameError 等安全问题,已**删除下线**。`_run.sh` 当前启动的是 `smtp_v2.py`。 - 多数邮箱服务商(163/QQ 等)强制 From 头与登录账号一致,`MAIL_FROM` 仅当服务商允许自定义发件身份时才起作用。 - 验证码为纯 6 位数字,有效期 600 秒。 diff --git a/smtp/smtp.py b/smtp/smtp.py deleted file mode 100644 index ea45cd6..0000000 --- a/smtp/smtp.py +++ /dev/null @@ -1,84 +0,0 @@ -#!/usr/bin/env python3 - -from flask import Flask, request, jsonify -from email.mime.text import MIMEText -from email import header -from log_client import logs -from dotenv import load_dotenv -import waitress -import smtplib -import random -import time -import sys, os - -sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) -load_dotenv(os.path.join(os.path.dirname(os.path.abspath(__file__)), ".env")) - -app = Flask(__name__) -captchas: dict[str, list[str, str]] = {} - -@app.route("/send_email", methods=["POST"]) - -def send_email(): - def build_captcha() -> str: - captcha: str = "" - for i in range(0,6): - digit: int = random.randint(0,9) - captcha += str(digit) - return captcha - - data = request.get_json() - - recipient: str = data.get("email") - subject: str = "DNFS 邮箱验证" - captcha: str = build_captcha() - content: str = f"DNFS: 您的邮箱验证码为 {captcha},一分钟内有效。" - - captchas[recipient] = ["", ""] - captchas[recipient][0] = captcha - captchas[recipient][1] = str(int(time.time())+60) - - message = MIMEText(content, "plain", "UTF-8") - message["From"] = "dnfs@qsont.me" - message["To"] = recipient - message["Subject"] = subject - - try: - server = smtplib.SMTP_SSL(SMTP_SERVER, SMTP_PORT, timeout=10) - server.login(SMTP_USER, __PASSWORD) - server.sendmail(SMTP_USER, [recipient], message.as_string()) - logs.info("smtp", f"{recipient}: 验证码发送成功({captcha})") - return jsonify({"success": "验证码发送成功"}), 200 - - except Exception as err: - logs.error("smtp", f"{recipient}: 验证码发送失败({err})") - return jsonify({"error": f"验证码发送失败:{err}"}), 400 - - finally: - server.quit() - - -@app.route("/verify_email", methods=["POST"]) - -def verify_email(): - data = request.get_json() - - for i in list(captchas.keys()): - if int(captchas[i][1]) < int(time.time()): - del captchas[i] - - email: str = data.get("email") - input_captcha: str = data.get("input_captcha") - - if captchas[email][0] == input_captcha: - logs.info("smtp", f"{email}: 验证通过") - return jsonify({"success": "验证通过"}), 200 - else: - logs.info("smtp", f"{email}: 验证失败") - return jsonify({"error": "验证失败"}), 400 - - -if __name__ == "__main__": - logs.info("smtp", "开始监听http://127.0.0.1:8008") - print("Listen at http://127.0.0.1:8008") - waitress.serve(app, host="127.0.0.1", port=8008) \ No newline at end of file