diff --git a/.features/skill/MEMORY.md b/.features/skill/MEMORY.md new file mode 100644 index 0000000..7d6fc1a --- /dev/null +++ b/.features/skill/MEMORY.md @@ -0,0 +1,121 @@ +# Skill 功能 + +> 负责范围:技能包管理服务 - 核心实现 +> 最后更新:2025-02-11 + +## 当前状态 + +Skill 系统支持两种来源:官方 skills (`./skills/`) 和用户 skills (`projects/uploads/{bot_id}/skills/`)。支持 Hook 系统和 MCP 服务器配置,通过 SKILL.md 或 plugin.json 定义元数据。 + +## 核心文件 + +- `routes/skill_manager.py` - Skill 上传/删除/列表 API +- `agent/plugin_hook_loader.py` - Hook 系统实现 +- `agent/deep_assistant.py` - `CustomSkillsMiddleware` +- `agent/prompt_loader.py` - PrePrompt hooks + MCP 配置合并 +- `skills/` - 官方 skills 目录 +- `skills_developing/` - 开发中 skills + +## 最近重要事项 + +- 2025-02-11: 初始化 skill 功能 memory + +## Gotchas(开发必读) + +- ⚠️ 执行脚本必须使用绝对路径 +- ⚠️ MCP 配置优先级:Skill MCP > 默认 MCP > 用户参数 +- ⚠️ 上传大小限制:50MB(ZIP),解压后最大 500MB +- ⚠️ 压缩比例检查:最大 100:1(防止 zip 炸弹) +- ⚠️ 符号链接检查:禁止解压包含符号链接的文件 + +## Skill 目录结构 + +``` +skill-name/ +├── SKILL.md # 核心指令文档(必需) +├── skill.yaml # 元数据配置(可选) +├── .claude-plugin/ +│ └── plugin.json # Hook 和 MCP 配置(可选) +└── scripts/ # 可执行脚本(可选) + └── script.py +``` + +## Hook 系统 + +| Hook 类型 | 执行时机 | 用途 | +|-----------|---------|------| +| `PrePrompt` | system_prompt 加载时 | 动态注入用户上下文 | +| `PostAgent` | agent 执行后 | 处理响应结果 | +| `PreSave` | 保存消息前 | 内容过滤/修改 | + +## API 接口 + +| 端点 | 方法 | 功能 | +|------|------|------| +| `GET /api/v1/skill/list` | - | 返回官方 + 用户 skills | +| `POST /api/v1/skill/upload` | - | ZIP 上传,解压到用户目录 | +| `DELETE /api/v1/skill/remove` | - | 删除用户 skill | + +## 内置 Skills + +| Skill 名称 | 功能描述 | +|-----------|---------| +| `excel-analysis` | Excel 数据分析、透视表、图表 | +| `managing-scripts` | 管理可复用脚本库 | +| `rag-retrieve` | RAG 知识库检索 | +| `jina-ai` | Jina AI Reader/Search | +| `user-context-loader` | Hook 机制示例 | + +## plugin.json 格式 + +```json +{ + "name": "skill-name", + "description": "描述", + "hooks": { + "PrePrompt": [{"type": "command", "command": "python hooks/pre_prompt.py"}], + "PostAgent": [...], + "PreSave": [...] + }, + "mcpServers": { + "server-name": { + "command": "...", + "args": [...] + } + } +} +``` + +## Skill 加载优先级 + +1. Skill MCP 配置(最高) +2. 默认 MCP 配置 (`mcp/mcp_settings.json`) +3. 用户传入参数(覆盖所有) + +## 安全措施 + +- ZipSlip 防护:检查解压路径 +- 路径遍历防护:验证 `bot_id` 和 `skill_name` 格式 +- 大小限制:上传 50MB,解压后 500MB +- 压缩比限制:最大 100:1 + +## 设计原则 + +- **渐进式加载**:按需加载,避免一次性读取所有 +- **绝对路径优先**:执行脚本必须使用绝对路径 +- **通用化设计**:脚本应参数化,解决一类问题 +- **安全优先**:完整的上传验证链 + +## 配置项 + +```bash +SKILLS_DIR=./skills # 官方 skills 目录 +BACKEND_HOST=xxx # RAG API 主机 +MASTERKEY=xxx # 认证密钥 +``` + +## 索引 + +- 设计决策:`decisions/` +- 变更历史:`changelog/` +- 相关文档:`docs/` diff --git a/.features/skill/changelog/2025-Q4.md b/.features/skill/changelog/2025-Q4.md new file mode 100644 index 0000000..b43c00a --- /dev/null +++ b/.features/skill/changelog/2025-Q4.md @@ -0,0 +1,38 @@ +# 2025-Q4 Skill Changelog + +## 版本 0.1.0 - 初始实现 + +### 2025-10-31 +- **新增**: agent skills 支持,测试阶段代码 +- **文件**: `chat_handler.py`, `knowledge_chat_cc_service.py` +- **作者**: Alex + +### 2025-11-03 +- **新增**: 内置 skills (pptx, docx, pdf, xlsx) +- **新增**: jina skill - 规范 jina 网络搜索 +- **解决**: "prompt too long" 问题 + +### 2025-11-13 +- **新增**: cc agent task 任务添加默认 skills +- **文件**: `task_handler.py`, `knowledge_task_cc_service.py` + +### 2025-11-19 +- **新增**: skill-creator 内置技能 + +### 2025-11-20 +- **新增**: EFS 类型接口,新增上传 skill +- **功能**: 支持 skill 包上传 + +### 2025-11-21 +- **新增**: EFS 删除 skill 接口 +- **移除**: skill 查询接口(暂存) + +### 2025-11-22 +- **新增**: GRPC chat 接口,skills 参数支持 + +### 2025-11-26 +- **新增**: skill 上传支持 `.skill` 后缀(测试) + +### 2025-11-28 +- **优化**: 默认挂载的 skill 改为合并逻辑 +- **优化**: 代码结构优化 diff --git a/.features/skill/changelog/2026-Q1.md b/.features/skill/changelog/2026-Q1.md new file mode 100644 index 0000000..9c0ea7e --- /dev/null +++ b/.features/skill/changelog/2026-Q1.md @@ -0,0 +1,36 @@ +# 2026-Q1 Skill Changelog + +## 版本 0.2.0 - API 完善 + +### 2026-01-07 +- **新增**: Skills 列表查询 API(能力管理页面) +- **新增**: 技能管理 API with authentication +- **文件**: `routes/skill_manager.py` +- **作者**: claude[bot], 朱潮 + +### 2026-01-09 +- **重构**: 移除 catalog agent,合并到 general agent +- **说明**: 简化架构,统一使用 general_agent +- **作者**: 朱潮 + +### 2026-01-10 +- **修复**: SKILL.md 的 name 字段解析逻辑 +- **新增**: 支持非标准 YAML 格式 +- **新增**: 目录名称不匹配时自动重命名 +- **作者**: Alex + +### 2026-01-13 +- **修复**: multipart form data format for catalog service +- **作者**: 朱潮 + +### 2026-01-28 +- **新增**: enable_thinking, enable_memory, skills to agent_bot_config +- **作者**: 朱潮 + +### 2026-01-30 +- **修复**: skill router 正确注册 +- **作者**: 朱潮 + +### 2026-02-11 +- **新增**: 初始化 skill feature memory +- **作者**: 朱潮 diff --git a/.features/skill/decisions/001-architecture.md b/.features/skill/decisions/001-architecture.md new file mode 100644 index 0000000..e899950 --- /dev/null +++ b/.features/skill/decisions/001-architecture.md @@ -0,0 +1,46 @@ +# 001: Skill 架构设计 + +## 状态 +已采纳 (Accepted) + +## 上下文 +需要为 QWEN_AGENT 模式的机器人提供可扩展的技能(插件/工具)支持,允许动态加载自定义功能。 + +## 决策 + +### 目录结构设计 +``` +skill-name/ +├── SKILL.md # 核心指令文档(必需) +├── skill.yaml # 元数据配置(可选) +├── .claude-plugin/ +│ └── plugin.json # Hook 和 MCP 配置(可选) +└── scripts/ # 可执行脚本(可选) +``` + +### Hook 系统 +| Hook 类型 | 执行时机 | 用途 | +|-----------|---------|------| +| `PrePrompt` | system_prompt 加载时 | 动态注入用户上下文 | +| `PostAgent` | agent 执行后 | 处理响应结果 | +| `PreSave` | 保存消息前 | 内容过滤/修改 | + +### 技能来源 +1. **官方 skills**: `./skills/` 目录 +2. **用户 skills**: `projects/uploads/{bot_id}/skills/` + +## 结果 + +### 正面影响 +- 渐进式加载,按需读取 +- 支持多种元数据格式(优先级: plugin.json > SKILL.md) +- 完整的 Hook 扩展机制 +- MCP 服���器配置支持 + +### 负面影响 +- 需要管理文件系统权限 +- 技能包格式验证复杂度增加 + +## 替代方案 +1. 使用数据库存储(拒绝:文件更灵活) +2. 仅支持单一格式(拒绝:用户多样性需求) diff --git a/.features/skill/decisions/002-security.md b/.features/skill/decisions/002-security.md new file mode 100644 index 0000000..dc671c5 --- /dev/null +++ b/.features/skill/decisions/002-security.md @@ -0,0 +1,35 @@ +# 002: Skill 上传安全措施 + +## 状态 +已采纳 (Accepted) + +## 上下文 +用户可以上传 ZIP 格式的技能包,需要防范常见的安全攻击。 + +## 决策 + +### 安全防护措施 + +| 威胁 | 防护措施 | +|------|---------| +| ZipSlip 攻击 | 检查每个文件的解压路径 | +| 路径遍历 | 验证 `bot_id` 和 `skill_name` 格式 | +| Zip 炸弹 | 压缩比检查(最大 100:1) | +| 磁盘空间滥用 | 上传 50MB,解压后最大 500MB | +| 符号链接攻击 | 禁止解压包含符号链接的文件 | + +### 限制规则 +```python +MAX_UPLOAD_SIZE = 50 * 1024 * 1024 # 50MB +MAX_EXTRACTED_SIZE = 500 * 1024 * 1024 # 500MB +MAX_COMPRESSION_RATIO = 100 # 100:1 +``` + +## 结果 +- 完整的上传验证链 +- 防止恶意文件攻击 +- 资源使用可控 + +## 替代方案 +1. 使用沙箱容器解压(拒绝:复杂度高) +2. 仅允许预定义技能(拒绝:限制用户自定义能力) diff --git a/Dockerfile b/Dockerfile index 976dfdb..f178064 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,7 +8,7 @@ WORKDIR /app ENV PYTHONPATH=/app ENV PYTHONUNBUFFERED=1 -# 安装系统依赖 +# 安装系统依赖(含 LibreOffice 和 sharp 所需的 libvips) RUN apt-get update && apt-get install -y \ curl \ wget \ @@ -16,6 +16,11 @@ RUN apt-get update && apt-get install -y \ ca-certificates \ libpq-dev \ chromium \ + libreoffice-writer-nogui \ + libreoffice-calc-nogui \ + libreoffice-impress-nogui \ + libvips-dev \ + fonts-noto-cjk \ && rm -rf /var/lib/apt/lists/* # 安装Node.js (支持npx命令) @@ -35,7 +40,7 @@ RUN pip install --no-cache-dir -r requirements.txt # 安装 Playwright 并下载 Chromium RUN pip install --no-cache-dir playwright && \ playwright install chromium -RUN npm install -g playwright && \ +RUN npm install -g playwright sharp && \ npx playwright install chromium # 复制应用代码 diff --git a/Dockerfile.modelscope b/Dockerfile.modelscope index f5fbd55..b1dd0f0 100644 --- a/Dockerfile.modelscope +++ b/Dockerfile.modelscope @@ -8,7 +8,7 @@ WORKDIR /app ENV PYTHONPATH=/app ENV PYTHONUNBUFFERED=1 -# 安装系统依赖 +# 安装系统依赖(含 LibreOffice 和 sharp 所需的 libvips) RUN sed -i 's|http://deb.debian.org|http://mirrors.aliyun.com|g' /etc/apt/sources.list.d/debian.sources && \ apt-get update && apt-get install -y \ curl \ @@ -17,6 +17,11 @@ RUN sed -i 's|http://deb.debian.org|http://mirrors.aliyun.com|g' /etc/apt/source ca-certificates \ libpq-dev \ chromium \ + libreoffice-writer-nogui \ + libreoffice-calc-nogui \ + libreoffice-impress-nogui \ + libvips-dev \ + fonts-noto-cjk \ && rm -rf /var/lib/apt/lists/* # 安装Node.js (支持npx命令) @@ -36,7 +41,7 @@ RUN pip install --no-cache-dir -i https://mirrors.aliyun.com/pypi/simple/ -r req # 安装 Playwright 并下载 Chromium RUN pip install --no-cache-dir -i https://mirrors.aliyun.com/pypi/simple/ playwright && \ playwright install chromium -RUN npm install -g playwright && \ +RUN npm install -g playwright sharp && \ npx playwright install chromium # 安装modelscope diff --git a/agent/prompt_loader.py b/agent/prompt_loader.py index d5bfe6b..6fe7142 100644 --- a/agent/prompt_loader.py +++ b/agent/prompt_loader.py @@ -138,33 +138,59 @@ async def load_system_prompt_async(config) -> str: -def replace_mcp_placeholders(mcp_settings: List[Dict], dataset_dir: str, bot_id: str, dataset_ids: List[str]) -> List[Dict]: +def replace_mcp_placeholders(mcp_settings: List[Dict], dataset_dir: str, bot_id: str, dataset_ids: List[str], shell_env: Optional[Dict[str, str]] = None) -> List[Dict]: """ 替换 MCP 配置中的占位符 + + 支持的占位符来源(优先级从高到低): + 1. 内置变量: {dataset_dir}, {bot_id}, {dataset_ids} + 2. shell_env 中的自定义环境变量 + 3. 系统环境变量 (os.environ) """ if not mcp_settings or not isinstance(mcp_settings, list): return mcp_settings dataset_id_str = ','.join(dataset_ids) if dataset_ids else '' + # 构建占位符映射:系统环境变量 < shell_env < 内置变量(优先级递增) + import re + placeholders = {} + placeholders.update(os.environ) + if shell_env: + placeholders.update(shell_env) + placeholders.update({ + 'dataset_dir': dataset_dir, + 'bot_id': bot_id, + 'dataset_ids': dataset_id_str, + }) + + def _safe_format(s: str) -> str: + """安全地替换字符串中的占位符,未匹配的占位符保持原样""" + try: + def _replacer(match): + key = match.group(1) + return placeholders.get(key, match.group(0)) + return re.sub(r'\{(\w+)\}', _replacer, s) + except Exception: + return s + def replace_placeholders_in_obj(obj): """递归替换对象中的占位符""" if isinstance(obj, dict): for key, value in obj.items(): if key == 'args' and isinstance(value, list): - # 特别处理 args 列表 - obj[key] = [item.format(dataset_dir=dataset_dir, bot_id=bot_id, dataset_ids=dataset_id_str) if isinstance(item, str) else item + obj[key] = [_safe_format(item) if isinstance(item, str) else item for item in value] elif isinstance(value, (dict, list)): obj[key] = replace_placeholders_in_obj(value) elif isinstance(value, str): - obj[key] = value.format(dataset_dir=dataset_dir, bot_id=bot_id, dataset_ids=dataset_id_str) + obj[key] = _safe_format(value) elif isinstance(obj, list): - return [replace_placeholders_in_obj(item) if isinstance(item, (dict, list)) else - item.format(dataset_dir=dataset_dir, bot_id=bot_id, dataset_ids=dataset_id_str) if isinstance(item, str) else item + return [replace_placeholders_in_obj(item) if isinstance(item, (dict, list)) else + _safe_format(item) if isinstance(item, str) else item for item in obj] return obj - + return replace_placeholders_in_obj(mcp_settings) async def load_mcp_settings_async(config) -> List[Dict]: @@ -269,7 +295,8 @@ async def load_mcp_settings_async(config) -> List[Dict]: # 替换 MCP 配置中的 {dataset_dir} 占位符 if dataset_dir is None: dataset_dir = "" - merged_settings = replace_mcp_placeholders(merged_settings, dataset_dir, bot_id, dataset_ids) + shell_env = getattr(config, 'shell_env', None) or {} + merged_settings = replace_mcp_placeholders(merged_settings, dataset_dir, bot_id, dataset_ids, shell_env) return merged_settings diff --git a/docker-compose-with-pgsql.yml b/docker-compose-with-pgsql.yml index e0332dd..cf998f0 100644 --- a/docker-compose-with-pgsql.yml +++ b/docker-compose-with-pgsql.yml @@ -31,7 +31,6 @@ services: # 应用配置 - BACKEND_HOST=http://api-dev.gbase.ai - MAX_CONTEXT_TOKENS=262144 - - DEFAULT_THINKING_ENABLE=true # PostgreSQL 配置 - CHECKPOINT_DB_URL=postgresql://postgres:E5ACJo6zJub4QS@postgres:5432/agent_db - R2_UPLOAD_CONFIG=/app/config/local-upload.yaml diff --git a/docker-compose.yml b/docker-compose.yml index d0886a6..16a757e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -12,7 +12,6 @@ services: # 应用配置 - BACKEND_HOST=http://api-dev.gbase.ai - MAX_CONTEXT_TOKENS=262144 - - DEFAULT_THINKING_ENABLE=true - R2_UPLOAD_CONFIG=/app/config/s3-upload-sparticle.yaml volumes: # 挂载项目数据目录 diff --git a/prompt/system_prompt.md b/prompt/system_prompt.md index 632af9e..32439de 100644 --- a/prompt/system_prompt.md +++ b/prompt/system_prompt.md @@ -80,3 +80,15 @@ Trace Id: {trace_id} - Even when the user writes in a different language, you MUST still reply in [{language}]. - Do NOT mix languages. Do NOT fall back to English or any other language under any circumstances. - Technical terms, code identifiers, file paths, and tool names may remain in their original form, but all surrounding text MUST be in [{language}]. + +**Citation Requirement (RAG Only)**: When answering questions based on `rag_retrieve` tool results, you MUST add XML citation tags for factual claims derived from the knowledge base. + +**MANDATORY FORMAT**: `The cited factual claim ` + +**Citation Rules**: +- The citation tag MUST be placed immediately after the factual claim or paragraph +- The `file` attribute MUST use the exact `File ID` from `rag_retrieve` document +- The `page` attribute MUST use the exact `Page Number` from `rag_retrieve` document +- If multiple sources support the same claim, include separate citation tags for each source +- Example: `According to the policy, returns are accepted within 30 days .` +- This requirement ONLY applies when using `rag_retrieve` results to answer questions. diff --git a/utils/api_models.py b/utils/api_models.py index 5985056..645fba6 100644 --- a/utils/api_models.py +++ b/utils/api_models.py @@ -5,7 +5,6 @@ API data models and response schemas. from typing import Dict, List, Optional, Any, AsyncGenerator from pydantic import BaseModel, Field, field_validator, ConfigDict -from utils.settings import DEFAULT_THINKING_ENABLE class Message(BaseModel): role: str @@ -52,7 +51,7 @@ class ChatRequest(BaseModel): mcp_settings: Optional[List[Dict]] = None user_identifier: Optional[str] = "" session_id: Optional[str] = None - enable_thinking: Optional[bool] = DEFAULT_THINKING_ENABLE + enable_thinking: Optional[bool] = False skills: Optional[List[str]] = None enable_memory: Optional[bool] = False shell_env: Optional[Dict[str, str]] = None diff --git a/utils/settings.py b/utils/settings.py index 61c38bc..ebb60d5 100644 --- a/utils/settings.py +++ b/utils/settings.py @@ -35,8 +35,6 @@ SENTENCE_TRANSFORMER_MODEL = os.getenv("SENTENCE_TRANSFORMER_MODEL", "TaylorAI/g TOOL_OUTPUT_MAX_LENGTH = int(SUMMARIZATION_MAX_TOKENS/4) TOOL_OUTPUT_TRUNCATION_STRATEGY = os.getenv("TOOL_OUTPUT_TRUNCATION_STRATEGY", "smart") -# THINKING ENABLE -DEFAULT_THINKING_ENABLE = os.getenv("DEFAULT_THINKING_ENABLE", "true") == "true" # WebDAV Authentication