feat(aibox): 创建/安装技能工具统一生成 .md 扩展名 + 下线旧版 smtp.py
- CreateSkillTool/InstallSkillTool 改为生成带 .md 扩展名的技能文件,与人格技能文件规范一致 - 同步更新 7_extension_skills.md 模板与工具描述 - 删除旧版 smtp.py(P0-1)并同步审计文档与 README
This commit is contained in:
1 parent
fb485ebac4
commit
53ddcb364f
6 files changed
+243
-195
No files matched your search
@@ -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` 确认
|
||||
|
||||
+212
-94
@@ -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/<skill_name>`
|
||||
3. 用 `run_command` 执行 `chmod +x` 赋权
|
||||
4. 用 `run_command` 执行 `<skill_name> --help` 验证
|
||||
5. 调用 `reload_skills` 热重载
|
||||
1. 自行编写满足格式要求的 Markdown 技能文件
|
||||
2. 用 `write_file` 写入 `~/.aibox/skills/<skill_name>.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/<persona>.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/<skill_name>
|
||||
3. 用 run_command 赋予可执行权限
|
||||
4. 调用 reload_skills 热重载
|
||||
1. 编写满足格式要求的 Markdown 技能文件
|
||||
2. 用 write_file 写入 ~/.aibox/skills/<skill_name>.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 失败时回滚,防止坏消息污染对话历史
|
||||
|
||||
+11
-3
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
### 短期
|
||||
- [ ] 验证全局熔断配置生效
|
||||
|
||||
+1
-2
@@ -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 秒。
|
||||
|
||||
|
||||
@@ -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)
|
||||
Reference in new issue
Block a user