Talegorithm 3 дней назад
Родитель
Сommit
9399762148

+ 63 - 0
QUICK_MIGRATION.md

@@ -0,0 +1,63 @@
+# 快速迁移参考
+
+## 一键迁移命令
+
+```bash
+# 1. 连接服务器
+ssh agent_server
+cd ~/main_agent
+
+# 2. 备份数据库
+cp knowhub.db knowhub.db.backup.$(date +%Y%m%d_%H%M%S)
+
+# 3. 拉取迁移脚本
+git fetch origin
+git checkout origin/main -- migrate_to_resource.py
+
+# 4. 执行迁移
+python migrate_to_resource.py
+
+# 5. 拉取新代码
+git pull origin main
+
+# 6. 重启服务
+# 根据你的服务管理方式选择:
+sudo systemctl restart knowhub
+# 或
+pkill -f "uvicorn knowhub.server" && uvicorn knowhub.server:app --host 0.0.0.0 --port 8000 &
+```
+
+## 迁移内容
+
+1. **resources表**:contents → resources + 新字段
+2. **knowledge表**:添加resource_ids字段
+
+## 验证
+
+```bash
+# 检查表
+sqlite3 knowhub.db "SELECT name FROM sqlite_master WHERE type='table';"
+
+# 检查字段
+sqlite3 knowhub.db "PRAGMA table_info(resources);" | grep -E "secure_body|content_type|metadata|updated_at"
+sqlite3 knowhub.db "PRAGMA table_info(knowledge);" | grep resource_ids
+
+# 测试API
+curl http://localhost:8000/api/resource
+```
+
+## 回滚
+
+```bash
+# 停止服务
+pkill -f "uvicorn knowhub.server"
+
+# 恢复备份
+cp knowhub.db.backup.YYYYMMDD_HHMMSS knowhub.db
+
+# 回退代码
+git reset --hard HEAD~1
+
+# 重启服务
+uvicorn knowhub.server:app --host 0.0.0.0 --port 8000 &
+```

+ 62 - 0
RENAME_CONTENT_TO_RESOURCE.md

@@ -0,0 +1,62 @@
+# Content → Resource 重命名总结
+
+## 重命名原因
+
+避免与Knowledge表的`content`字段混淆,提高命名清晰度。
+
+## 已完成的修改
+
+### 1. 数据库
+- ✅ 表名:`contents` → `resources`
+
+### 2. 代码 (knowhub/server.py)
+- ✅ 模型:`ContentIn` → `ResourceIn`
+- ✅ 模型:`ContentOut` → `ResourceOut`
+- ✅ 模型:`ContentPatchIn` → `ResourcePatchIn`
+- ✅ 模型:`ContentNode` → `ResourceNode`
+- ✅ 函数:`submit_content` → `submit_resource`
+- ✅ 函数:`get_content` → `get_resource`
+- ✅ 函数:`patch_content` → `patch_resource`
+- ✅ 函数:`list_contents` → `list_resources`
+- ✅ 函数:`encrypt_content` → `encrypt_resource`
+- ✅ 函数:`decrypt_content` → `decrypt_resource`
+- ✅ 参数:`content_id` → `resource_id`
+- ✅ API路径:`/api/content` → `/api/resource`
+
+### 3. 文档
+- ✅ `knowhub/docs/content-storage.md` → `resource-storage.md`
+- ✅ `knowhub/docs/content-storage-examples.md` → `resource-storage-examples.md`
+- ✅ 更新 `knowhub/docs/knowledge-management.md`
+- ✅ 更新 `knowhub/docs/decisions.md`
+
+### 4. 测试和工具
+- ✅ 测试脚本:`test_resource_storage.py`
+- ✅ 实现总结:`RESOURCE_STORAGE_IMPLEMENTATION.md`
+
+## API端点变更
+
+| 旧端点 | 新端点 |
+|--------|--------|
+| POST /api/content | POST /api/resource |
+| GET /api/content/{content_id} | GET /api/resource/{resource_id} |
+| PATCH /api/content/{content_id} | PATCH /api/resource/{resource_id} |
+| GET /api/content | GET /api/resource |
+
+## 验证
+
+```bash
+# 1. 导入测试
+python -c "from knowhub.server import app; print('✅ OK')"
+
+# 2. 启动服务
+uvicorn knowhub.server:app --reload
+
+# 3. 运行测试
+python test_resource_storage.py
+```
+
+## 注意事项
+
+- 数据库表已重命名,旧的API端点不再可用
+- 如果有外部系统调用旧API,需要更新
+- 文档中所有引用已更新

+ 95 - 0
RESOURCE_STORAGE_IMPLEMENTATION.md

@@ -0,0 +1,95 @@
+# Resource存储系统实现总结
+
+## 实现内容
+
+### 1. 数据库扩展
+- 扩展`contents`表,添加以下字段:
+  - `secure_body`: 敏感内容(加密存储)
+  - `content_type`: 内容类型(text|code|credential|cookie)
+  - `metadata`: JSON元数据
+  - `updated_at`: 更新时间
+
+### 2. 加密机制
+- 使用AES-256-GCM加密算法
+- 密钥从环境变量`ORG_KEYS`读取
+- 格式:`encrypted:AES256-GCM:{base64_data}`
+- 实现函数:
+  - `encrypt_resource()`: 加密
+  - `decrypt_resource()`: 解密
+  - `get_org_key()`: 获取组织密钥
+
+### 3. API端点
+- `POST /api/resource`: 提交资源(自动加密secure_body)
+- `GET /api/resource/{id}`: 获取资源(支持X-Org-Key头解密)
+- `PATCH /api/resource/{id}`: 更新资源
+- `GET /api/resource`: 列出资源(支持content_type过滤)
+
+### 4. 数据模型
+- `ResourceIn`: 提交请求模型
+- `ResourcePatchIn`: 更新请求模型
+- `ResourceOut`: 响应模型
+
+### 5. 文档
+- `knowhub/docs/resource-storage.md`: 设计文档
+- `knowhub/docs/resource-storage-examples.md`: 使用示例
+- `knowhub/docs/decisions.md`: 决策记录(新增第0条)
+- `knowhub/docs/knowledge-management.md`: 更新架构说明
+
+### 6. 工具
+- `migrate_resources.py`: 数据库迁移脚本
+- `test_resource_storage.py`: 测试脚本
+
+## 使用方法
+
+### 1. 配置密钥
+
+```bash
+# 生成密钥
+python -c "import os, base64; print(base64.b64encode(os.urandom(32)).decode())"
+
+# 添加到.env
+echo "ORG_KEYS=test:你的密钥base64" >> .env
+```
+
+### 2. 迁移数据库
+
+```bash
+python migrate_resources.py
+```
+
+### 3. 启动服务
+
+```bash
+uvicorn knowhub.server:app --reload
+```
+
+### 4. 测试功能
+
+```bash
+python test_resource_storage.py
+```
+
+## 设计特点
+
+1. **分离公开/敏感内容**:body明文可搜索,secure_body加密保护
+2. **灵活的密钥管理**:支持多组织密钥,通过resource_id前缀区分
+3. **访问控制**:需要提供正确的X-Org-Key才能解密
+4. **元数据支持**:记录获取时间、过期时间、语言等信息
+5. **层级结构**:通过ID路径实现树形组织(如tools/selenium/login)
+
+## 安全考虑
+
+- ✅ 敏感内容加密存储
+- ✅ 密钥不入库,存储在环境变量
+- ✅ 访问需要验证密钥
+- ✅ 记录提交者和时间戳
+- ⚠️ 密钥在HTTP头中传输(建议使用HTTPS)
+- ⚠️ 数据库文件泄露仍有风险(建议文件系统加密)
+
+## 后续改进
+
+1. 支持密钥轮换
+2. 添加访问日志
+3. 支持密钥过期时间
+4. 前端管理界面
+5. 批量导入/导出功能

+ 214 - 0
SERVER_MIGRATION_GUIDE.md

@@ -0,0 +1,214 @@
+# 服务器迁移指南
+
+## ⚠️ 重要:不要直接拉取代码!
+
+由于我们进行了以下数据库变更,必须先执行迁移,否则会导致数据丢失或功能异常:
+
+1. **resources表**:contents → resources(表重命名)
+2. **resources表**:添加新字段(secure_body, content_type, metadata, updated_at)
+3. **knowledge表**:添加resource_ids字段(用于关联资源)
+
+## 迁移步骤
+
+### 1. 连接到服务器
+
+```bash
+ssh agent_server
+cd ~/main_agent
+```
+
+### 2. 备份数据库(重要!)
+
+```bash
+# 备份数据库文件
+cp knowhub.db knowhub.db.backup.$(date +%Y%m%d_%H%M%S)
+
+# 验证备份
+ls -lh knowhub.db*
+```
+
+### 3. 拉取迁移脚本(不拉取全部代码)
+
+```bash
+# 只拉取迁移脚本
+git fetch origin
+git checkout origin/main -- migrate_to_resource.py
+
+# 或者手动创建脚本(如果git checkout不工作)
+# 将migrate_to_resource.py的内容复制到服务器
+```
+
+### 4. 执行迁移
+
+```bash
+# 确保在包含knowhub.db的目录中
+python migrate_to_resource.py
+```
+
+**预期输出**:
+```
+============================================================
+KnowHub 数据库迁移
+变更内容:
+  1. contents表 → resources表
+  2. resources表添加新字段
+  3. knowledge表添加resource_ids字段
+============================================================
+数据库路径: /home/user/main_agent/knowhub.db
+
+当前表: {'contents', 'knowledge', 'experiences'}
+
+============================================================
+步骤1: 迁移 resources 表
+============================================================
+contents表中有 X 条记录
+现有字段: {...}
+
+添加 N 个新字段...
+  ALTER TABLE contents ADD COLUMN secure_body TEXT DEFAULT ''
+  ALTER TABLE contents ADD COLUMN content_type TEXT DEFAULT 'text'
+  ALTER TABLE contents ADD COLUMN metadata TEXT DEFAULT '{}'
+  ALTER TABLE contents ADD COLUMN updated_at TEXT DEFAULT ''
+✅ 字段添加完成
+
+重命名表: contents → resources
+✅ 表重命名完成
+resources表中有 X 条记录
+✅ 数据完整,X 条记录全部保留
+
+============================================================
+步骤2: 更新 knowledge 表
+============================================================
+现有字段: {...}
+
+添加 resource_ids 字段...
+✅ resource_ids字段添加完成
+
+============================================================
+迁移总结
+============================================================
+迁移后的表: {'resources', 'knowledge', 'experiences'}
+✅ resources表: X 条记录
+✅ knowledge表: Y 条记录, resource_ids字段: 存在
+
+✅ 迁移完成!现在可以拉取新代码并重启服务
+```
+
+### 5. 验证迁移
+
+```bash
+# 检查表是否存在
+sqlite3 knowhub.db "SELECT name FROM sqlite_master WHERE type='table';"
+
+# 检查resources表数据
+sqlite3 knowhub.db "SELECT COUNT(*) FROM resources;"
+
+# 检查knowledge表是否有resource_ids字段
+sqlite3 knowhub.db "PRAGMA table_info(knowledge);" | grep resource_ids
+```
+
+### 6. 拉取新代码
+
+```bash
+git pull origin main
+```
+
+### 7. 重启服务
+
+```bash
+# 根据你的服务管理方式
+# 如果使用systemd
+sudo systemctl restart knowhub
+
+# 如果使用screen/tmux
+# 先停止旧进程,然后启动新进程
+pkill -f "uvicorn knowhub.server"
+uvicorn knowhub.server:app --host 0.0.0.0 --port 8000 &
+
+# 或者使用你的启动脚本
+./start_knowhub.sh
+```
+
+### 8. 验证服务
+
+```bash
+# 测试API
+curl http://localhost:8000/api/resource
+
+# 检查日志
+tail -f logs/knowhub.log  # 根据实际日志路径
+```
+
+## 如果迁移失败
+
+### 恢复备份
+
+```bash
+# 停止服务
+pkill -f "uvicorn knowhub.server"
+
+# 恢复备份
+cp knowhub.db.backup.YYYYMMDD_HHMMSS knowhub.db
+
+# 回退代码
+git reset --hard HEAD~1
+
+# 重启服务
+uvicorn knowhub.server:app --host 0.0.0.0 --port 8000 &
+```
+
+### 检查问题
+
+```bash
+# 查看迁移脚本输出
+# 查看数据库状态
+sqlite3 knowhub.db ".tables"
+sqlite3 knowhub.db ".schema contents"
+sqlite3 knowhub.db ".schema resources"
+```
+
+## 数据库变更详情
+
+### 1. resources表(原contents表)
+
+**变更**:
+- 表名:`contents` → `resources`
+- 新增字段:
+  - `secure_body TEXT DEFAULT ''` - 敏感内容(加密存储)
+  - `content_type TEXT DEFAULT 'text'` - 内容类型(text|code|credential|cookie)
+  - `metadata TEXT DEFAULT '{}'` - JSON元数据
+  - `updated_at TEXT DEFAULT ''` - 更新时间
+
+**影响**:
+- 旧的`/api/content`端点不再可用
+- 需要使用新的`/api/resource`端点
+
+### 2. knowledge表
+
+**变更**:
+- 新增字段:`resource_ids TEXT DEFAULT '[]'` - 关联的资源ID列表(JSON数组)
+
+**用途**:
+- 知识条目可以引用多个资源
+- 示例:`["code/selenium/login", "credentials/website_a"]`
+- 通过`GET /api/resource/{id}`获取关联的资源详情
+
+## 注意事项
+
+1. **必须先备份**:迁移前务必备份数据库
+2. **按顺序执行**:不要跳过步骤
+3. **验证完整性**:迁移后检查数据条数是否一致
+4. **测试API**:重启后测试新的/api/resource端点
+
+## API端点变更
+
+迁移后,API端点已更改:
+
+| 旧端点 | 新端点 | 状态 |
+|--------|--------|------|
+| POST /api/content | POST /api/resource | ✅ 已更新 |
+| GET /api/content/{id} | GET /api/resource/{id} | ✅ 已更新 |
+| PATCH /api/content/{id} | PATCH /api/resource/{id} | ✅ 已更新 |
+| GET /api/content | GET /api/resource | ✅ 已更新 |
+
+如果有外部系统调用旧API,需要同步更新。

+ 39 - 2
agent/core/prompts/knowledge.py

@@ -18,6 +18,7 @@ REFLECT_PROMPT = """请回顾以上执行过程,将值得沉淀的经验直接
 2. 弯路:哪些尝试是不必要的,有没有更直接的方法
 3. 好的决策:哪些判断和选择是正确的,值得记住
 4. 工具使用:哪些工具用法是高效的,哪些可以改进
+5. **资源发现**:是否发现了有价值的资源需要保存(见下方说明)
 
 **每条经验调用一次 `knowledge_save`,参数说明**:
 - `task`: 这条经验适用的场景,格式:「在[什么情境]下,[要完成什么]」
@@ -25,9 +26,28 @@ REFLECT_PROMPT = """请回顾以上执行过程,将值得沉淀的经验直接
 - `types`: 选 `["strategy"]`;如果涉及工具用法也可加 `"tool"`
 - `tags`: 用 `intent`(任务意图)和 `state`(环境状态/相关工具名)标注,便于检索
 - `score`: 1-5,根据这条经验的价值评估
+- `resource_ids`: 如果关联了资源,填写资源 ID 列表(可选)
+
+**资源提取指南**:
+如果执行过程中涉及以下内容,应先用 `resource_save` 保存资源,再用 `knowledge_save` 提交相关的经验/知识:
+
+1. **复杂代码工具**(逻辑复杂、超过 100 行):
+   - 调用 `resource_save(resource_id="code/{category}/{name}", title="...", body="代码内容", content_type="code", metadata={"language": "python"})`
+   - 然后在 `knowledge_save` 中通过 `resource_ids=["code/{category}/{name}"]` 关联
+
+2. **账号密码凭证**:
+   - 调用 `resource_save(resource_id="credentials/{website}", title="...", body="使用说明", secure_body="账号:xxx\\n密码:xxx", content_type="credential", metadata={"acquired_at": "2026-03-06T10:00:00Z"})`
+   - 然后在 `knowledge_save` 中通过 `resource_ids=["credentials/{website}"]` 关联
+
+3. **Cookie 和登录态**:
+   - 调用 `resource_save(resource_id="cookies/{website}", title="...", body="获取方法", secure_body="cookie内容", content_type="cookie", metadata={"acquired_at": "...", "expires_at": "..."})`
+   - 然后在 `knowledge_save` 中通过 `resource_ids=["cookies/{website}"]` 关联
+
+4. **多资源引用**:
+   - 一个知识可以关联多个资源,如:`resource_ids=["code/selenium/login", "credentials/website_a"]`
 
 **注意**:
-- 只保存最有价值的 3-8 条,宁少勿滥
+- 只保存最有价值的经验,宁少勿滥;一次就成功或比较简单的经验就不要记录了,记录反复尝试或被用户指导后才成功的经验。
 - 不需要输出任何文字,直接调用工具即可
 - 如果没有值得保存的经验,不调用任何工具
 """
@@ -42,6 +62,7 @@ COMPLETION_REFLECT_PROMPT = """请对整个任务进行复盘,将值得沉淀
 2. 关键决策点:哪些决策显著影响了最终结果
 3. 可复用的模式:哪些做法在类似任务中可以直接复用
 4. 踩过的坑:哪些问题本可提前规避
+5. **资源沉淀**:任务中产生或发现的有价值资源(见下方说明)
 
 **每条经验调用一次 `knowledge_save`,参数说明**:
 - `task`: 这条经验适用的场景,格式:「在[什么情境]下,[要完成什么]」
@@ -49,9 +70,25 @@ COMPLETION_REFLECT_PROMPT = """请对整个任务进行复盘,将值得沉淀
 - `types`: 选 `["strategy"]`;如果涉及工具用法也可加 `"tool"`
 - `tags`: 用 `intent`(任务意图)和 `state`(环境状态/相关工具名)标注,便于检索
 - `score`: 1-5,根据这条经验的价值评估
+- `resource_ids`: 如果关联了资源,填写资源 ID 列表(可选)
+
+**资源提取指南**:
+如果任务中涉及以下内容,应先用 `resource_save` 保存资源,再用 `knowledge_save` 关联:
+
+1. **复杂代码工具**(逻辑复杂、超过 20 行、可复用):
+   - 调用 `resource_save(resource_id="code/{category}/{name}", title="...", body="代码内容", content_type="code", metadata={"language": "python"})`
+   - 然后在 `knowledge_save` 中通过 `resource_id` 关联
+
+2. **账号密码凭证**:
+   - 调用 `resource_save(resource_id="credentials/{website}", title="...", body="使用说明", secure_body="账号:xxx\\n密码:xxx", content_type="credential", metadata={"acquired_at": "2026-03-06T10:00:00Z"})`
+   - 然后在 `knowledge_save` 中通过 `secure_resource_id` 关联
+
+3. **Cookie 和登录态**:
+   - 调用 `resource_save(resource_id="cookies/{website}", title="...", body="获取方法", secure_body="cookie内容", content_type="cookie", metadata={"acquired_at": "...", "expires_at": "..."})`
+   - 然后在 `knowledge_save` 中通过 `secure_resource_id` 关联
 
 **注意**:
-- 只保存最有价值的 2-5 条,宁少勿滥
+- 只保存最有价值的经验,宁少勿滥;一次就成功或比较简单的经验就不要记录了,记录反复尝试或被用户指导后才成功的经验。
 - 不需要输出任何文字,直接调用工具即可
 - 如果没有值得保存的经验,不调用任何工具
 """

+ 117 - 0
agent/tools/builtin/knowledge.py

@@ -103,6 +103,7 @@ async def knowledge_save(
     tags: Optional[Dict[str, str]] = None,
     scopes: Optional[List[str]] = None,
     owner: Optional[str] = None,
+    resource_ids: Optional[List[str]] = None,
     source_name: str = "",
     source_category: str = "exp",
     urls: List[str] = None,
@@ -122,6 +123,7 @@ async def knowledge_save(
         tags: 业务标签(JSON 对象)
         scopes: 可见范围(默认 ["org:cybertogether"])
         owner: 所有者(默认 agent:{agent_id})
+        resource_ids: 关联的资源 ID 列表(可选)
         source_name: 来源名称
         source_category: 来源类别(paper/exp/skill/book)
         urls: 参考来源链接列表
@@ -149,6 +151,7 @@ async def knowledge_save(
             "scopes": scopes,
             "owner": owner,
             "content": content,
+            "resource_ids": resource_ids or [],
             "source": {
                 "name": source_name,
                 "category": source_category,
@@ -412,3 +415,117 @@ async def knowledge_slim(
             error=str(e)
         )
 
+
+# ==================== Resource 资源管理工具 ====================
+
+@tool(hidden_params=["context"])
+async def resource_save(
+    resource_id: str,
+    title: str,
+    body: str,
+    content_type: str = "text",
+    secure_body: str = "",
+    metadata: Optional[Dict[str, Any]] = None,
+    submitted_by: str = "",
+    context: Optional[ToolContext] = None,
+) -> ToolResult:
+    """
+    保存资源(代码片段、凭证、Cookie 等)
+
+    Args:
+        resource_id: 资源 ID(层级路径,如 "code/selenium/login" 或 "credentials/website_a")
+        title: 资源标题
+        body: 公开内容(明文存储,可搜索)
+        content_type: 内容类型(text/code/credential/cookie)
+        secure_body: 敏感内容(加密存储,需要组织密钥访问)
+        metadata: 元数据(如 {"language": "python", "acquired_at": "2026-03-06T10:00:00Z"})
+        submitted_by: 提交者
+        context: 工具上下文
+
+    Returns:
+        保存结果
+    """
+    try:
+        payload = {
+            "id": resource_id,
+            "title": title,
+            "body": body,
+            "secure_body": secure_body,
+            "content_type": content_type,
+            "metadata": metadata or {},
+            "submitted_by": submitted_by,
+        }
+
+        async with httpx.AsyncClient(timeout=30.0) as client:
+            response = await client.post(f"{KNOWHUB_API}/api/resource", json=payload)
+            response.raise_for_status()
+            data = response.json()
+
+        return ToolResult(
+            title="✅ 资源已保存",
+            output=f"资源 ID: {resource_id}\n类型: {content_type}\n标题: {title}",
+            long_term_memory=f"保存资源: {resource_id} ({content_type})",
+            metadata={"resource_id": resource_id}
+        )
+
+    except Exception as e:
+        logger.error(f"保存资源失败: {e}")
+        return ToolResult(
+            title="❌ 保存失败",
+            output=f"错误: {str(e)}",
+            error=str(e)
+        )
+
+
+@tool(hidden_params=["context"])
+async def resource_get(
+    resource_id: str,
+    org_key: Optional[str] = None,
+    context: Optional[ToolContext] = None,
+) -> ToolResult:
+    """
+    获取资源内容
+
+    Args:
+        resource_id: 资源 ID(层级路径)
+        org_key: 组织密钥(用于解密敏感内容,可选)
+        context: 工具上下文
+
+    Returns:
+        资源内容
+    """
+    try:
+        headers = {}
+        if org_key:
+            headers["X-Org-Key"] = org_key
+
+        async with httpx.AsyncClient(timeout=30.0) as client:
+            response = await client.get(
+                f"{KNOWHUB_API}/api/resource/{resource_id}",
+                headers=headers
+            )
+            response.raise_for_status()
+            data = response.json()
+
+        output = f"资源 ID: {data['id']}\n"
+        output += f"标题: {data['title']}\n"
+        output += f"类型: {data['content_type']}\n"
+        output += f"\n公开内容:\n{data['body']}\n"
+
+        if data.get('secure_body'):
+            output += f"\n敏感内容:\n{data['secure_body']}\n"
+
+        return ToolResult(
+            title=f"📦 {data['title']}",
+            output=output,
+            metadata=data
+        )
+
+    except Exception as e:
+        logger.error(f"获取资源失败: {e}")
+        return ToolResult(
+            title="❌ 获取失败",
+            output=f"错误: {str(e)}",
+            error=str(e)
+        )
+

+ 39 - 1
knowhub/docs/decisions.md

@@ -4,6 +4,44 @@
 
 ---
 
+## 0. Resource存储系统:分离公开/敏感内容
+
+**日期**:2026-03-06
+
+**背景**:用户需要存储有价值的工具资源,包括:
+- 复杂代码片段
+- 特定网站的账号密码
+- 登录状态的Cookie(含获取时间)
+
+**问题**:
+- 原有resources表只有body字段,明文存储不安全
+- 如果整体加密,会失去搜索能力
+- 缺少元数据(获取时间、过期时间、内容类型)
+
+**决策**:扩展resources表,分离公开/敏感内容
+- `body`: 公开内容(明文,可搜索)
+- `secure_body`: 敏感内容(AES-256-GCM加密)
+- `content_type`: 内容类型(text|code|credential|cookie)
+- `metadata`: JSON元数据(language, acquired_at, expires_at)
+
+**加密机制**:
+- 使用AES-256-GCM加密
+- 密钥从环境变量ORG_KEYS读取(格式:org1:key1_base64,org2:key2_base64)
+- resource_id前缀决定使用哪个组织密钥(如test/tools/...使用test的密钥)
+- 访问时需提供X-Org-Key头,验证通过才解密
+
+**实现位置**:`knowhub/server.py`
+- `encrypt_resource()`: 加密函数
+- `decrypt_resource()`: 解密函数
+- `POST /api/resource`: 提交资源
+- `GET /api/resource/{id}`: 获取资源(支持X-Org-Key头)
+- `PATCH /api/resource/{id}`: 更新资源
+- `GET /api/resource`: 列出资源
+
+**文档**:`knowhub/docs/resource-storage.md`
+
+---
+
 ## 1. 定位:经验层而非工具目录
 
 **背景**:调研发现现有生态(详见 `docs/knowledge/`)已充分覆盖工具发现:
@@ -43,7 +81,7 @@
 
 Server 只做检索 + GROUP BY,不做智能处理。Agent 拿到 JSON 后自行判断。体现"端侧算力"原则。
 
-**搜索范围**:仅命中 experience。Content 是 experience 的深入层,通过 content_id 关联获取,不参与搜索。如果后续发现 content 中有大量有价值信息未被 experience 覆盖,再扩展搜索范围。
+**搜索范围**:仅命中 experience。Content 是 experience 的深入层,通过 resource_id 关联获取,不参与搜索。如果后续发现 content 中有大量有价值信息未被 experience 覆盖,再扩展搜索范围。
 
 ---
 

+ 123 - 2
knowhub/docs/knowledge-management.md

@@ -10,14 +10,18 @@
 
 ## 架构
 
-KnowHub Server 是统一的知识管理服务,Agent 通过 API 调用来保存和检索知识。
+KnowHub Server 是统一的知识管理服务,包含两个子系统:
+
+1. **Knowledge 系统**:结构化知识管理(任务场景 + 经验内容)
+2. **Resource 系统**:原始资源存储(代码、凭证、Cookie等)→ 详见 `knowhub/docs/resource-storage.md`
 
 ```
 Agent                           KnowHub Server
 ├── knowledge_search 工具   →   GET /api/knowledge/search
 ├── knowledge_save 工具     →   POST /api/knowledge
 ├── knowledge_update 工具   →   PUT /api/knowledge/{id}
-└── goal focus 自动注入     →   GET /api/knowledge/search
+├── goal focus 自动注入     →   GET /api/knowledge/search
+└── resource 资源引用        →   GET /api/resource/{id}
 ```
 
 ---
@@ -39,6 +43,7 @@ Agent                           KnowHub Server
   "scopes": ["org:cybertogether"],
   "owner": "agent:research_agent",
   "content": "核心知识内容",
+  "resource_ids": ["code/selenium/login", "credentials/website_a"],
   "source": {
     "name": "资源名称",
     "category": "exp",
@@ -77,6 +82,7 @@ Agent                           KnowHub Server
 - **scopes**: 可见范围数组,默认 `["user:owner"]`
 - **owner**: 所有者,默认取用户的git email
 - **content**: 核心知识内容
+- **resource_ids**: 关联的资源 ID 列表(可选),引用 Resource 系统中的资源。每个资源可能包含公开内容(body)和敏感内容(secure_body)
 - **source**: 来源信息(嵌套对象)
   - **name**: 资源名称
   - **category**: 来源类别(paper/exp/skill/book)
@@ -208,6 +214,121 @@ async def knowledge_slim(
 
 ---
 
+## Resource 引用机制
+
+Knowledge 可以通过 `resource_id` 和 `secure_resource_id` 引用 Resource 系统中的原始资源。
+
+### 使用场景
+
+**场景 1:复杂代码工具**
+
+当工具实现逻辑复杂(如 Selenium 绕过检测、复杂的 API 调用封装)时:
+- 将代码片段保存为 Resource(`content_type=code`)
+- Knowledge 记录使用场景和经验,通过 `resource_ids` 引用代码
+
+```json
+{
+  "task": "使用 Selenium 登录某网站并绕过反爬检测",
+  "content": "使用 undetected_chromedriver 可以绕过大部分检测。关键点:1) 设置合适的 user-agent 2) 随机延迟 3) 避免频繁请求",
+  "types": ["tool", "strategy"],
+  "resource_ids": ["code/selenium/undetected_login"]
+}
+```
+
+对应的 Resource:
+```json
+{
+  "id": "code/selenium/undetected_login",
+  "title": "Selenium 绕过检测登录代码",
+  "body": "import undetected_chromedriver as uc\n\ndriver = uc.Chrome()...",
+  "content_type": "code",
+  "metadata": {"language": "python"}
+}
+```
+
+**场景 2:账号密码凭证**
+
+当发现或配置了有用的工具账号时:
+- 将敏感信息(账号、密码)保存为 Resource(`content_type=credential`,存入 `secure_body`)
+- Knowledge 记录使用方法和注意事项,通过 `resource_ids` 引用凭证
+
+```json
+{
+  "task": "登录某网站进行数据采集",
+  "content": "使用测试账号登录,注意:1) 每天限额 100 次 2) 需要在工作时间访问 3) 登录后 session 有效期 2 小时",
+  "types": ["tool", "usecase"],
+  "resource_ids": ["credentials/website_a"]
+}
+```
+
+对应的 Resource:
+```json
+{
+  "id": "credentials/website_a",
+  "title": "某网站测试账号",
+  "body": "使用方法:直接登录即可,每天限额 100 次",
+  "secure_body": "账号:user@example.com\n密码:SecurePass123",
+  "content_type": "credential",
+  "metadata": {"acquired_at": "2026-03-06T10:00:00Z"}
+}
+```
+
+**场景 3:Cookie 和登录态**
+
+当获取了有效的 Cookie 时:
+- 将 Cookie 保存为 Resource(`content_type=cookie`,存入 `secure_body`,设置 `expires_at`)
+- Knowledge 记录获取方法和使用场景,通过 `resource_ids` 引用
+
+```json
+{
+  "task": "使用已登录的 Cookie 访问某网站 API",
+  "content": "Cookie 获取方法:手动登录后从浏览器导出。使用时直接设置到请求头。注意检查过期时间。",
+  "types": ["tool"],
+  "resource_ids": ["cookies/website_a"]
+}
+```
+
+对应的 Resource:
+```json
+{
+  "id": "cookies/website_a",
+  "title": "某网站 Cookie",
+  "body": "适用于:已登录状态的 API 调用",
+  "secure_body": "session_id=abc123; auth_token=xyz789",
+  "content_type": "cookie",
+  "metadata": {
+    "acquired_at": "2026-03-06T10:00:00Z",
+    "expires_at": "2026-03-07T10:00:00Z"
+  }
+}
+```
+
+**场景 4:多资源引用**
+
+一个知识可以同时引用多个资源(如代码 + 凭证):
+
+```json
+{
+  "task": "使用 Selenium 登录某网站",
+  "content": "使用 undetected_chromedriver 绕过检测,配合测试账号登录",
+  "types": ["tool", "usecase"],
+  "resource_ids": ["code/selenium/undetected_login", "credentials/website_a"]
+}
+```
+
+### 提交流程
+
+1. **识别需要提取的 Resource**:在知识反思时,识别复杂代码、凭证、Cookie 等
+2. **调用 `resource_save` 工具**:提交 Resource 到 KnowHub
+3. **调用 `knowledge_save` 工具**:保存 Knowledge 并关联 Resource ID
+
+实现位置:
+- Resource 提交工具:`agent/tools/builtin/knowledge.py:resource_save`
+- Knowledge 保存工具:`agent/tools/builtin/knowledge.py:knowledge_save`
+- 知识反思 prompt:`agent/core/prompts/knowledge.py:KNOWLEDGE_EXTRACTION_PROMPT`
+
+---
+
 ## 知识注入机制
 
 知识注入在 goal focus 时自动触发。

+ 137 - 0
knowhub/docs/resource-storage-examples.md

@@ -0,0 +1,137 @@
+# Resource存储系统使用示例
+
+## 环境配置
+
+在`.env`文件中配置组织密钥:
+
+```bash
+# 生成密钥(Python)
+python -c "import os, base64; print(base64.b64encode(os.urandom(32)).decode())"
+
+# 配置到.env
+ORG_KEYS=test:生成的密钥base64,prod:另一个密钥base64
+```
+
+## 使用示例
+
+### 1. 存储代码片段
+
+```bash
+curl -X POST http://localhost:8000/api/resource \
+  -H "Content-Type: application/json" \
+  -d '{
+    "id": "test/code/selenium",
+    "title": "Selenium绕过检测",
+    "body": "import undetected_chromedriver as uc\ndriver = uc.Chrome()",
+    "content_type": "code",
+    "metadata": {"language": "python"},
+    "submitted_by": "user@example.com"
+  }'
+```
+
+### 2. 存储账号密码(加密)
+
+```bash
+curl -X POST http://localhost:8000/api/resource \
+  -H "Content-Type: application/json" \
+  -d '{
+    "id": "test/credentials/website",
+    "title": "某网站登录凭证",
+    "body": "使用方法:直接登录即可",
+    "secure_body": "账号:user@example.com\n密码:SecurePass123",
+    "content_type": "credential",
+    "metadata": {"acquired_at": "2026-03-06T10:00:00Z"},
+    "submitted_by": "user@example.com"
+  }'
+```
+
+### 3. 存储Cookie(加密)
+
+```bash
+curl -X POST http://localhost:8000/api/resource \
+  -H "Content-Type: application/json" \
+  -d '{
+    "id": "test/cookies/website",
+    "title": "某网站Cookie",
+    "body": "适用于:已登录状态的API调用",
+    "secure_body": "session_id=abc123; auth_token=xyz789",
+    "content_type": "cookie",
+    "metadata": {
+      "acquired_at": "2026-03-06T10:00:00Z",
+      "expires_at": "2026-03-07T10:00:00Z"
+    },
+    "submitted_by": "user@example.com"
+  }'
+```
+
+### 4. 获取资源(无密钥)
+
+```bash
+# 公开内容正常返回,敏感内容显示[ENCRYPTED]
+curl http://localhost:8000/api/resource/test/credentials/website
+```
+
+### 5. 获取资源(有密钥)
+
+```bash
+# 敏感内容解密返回
+curl http://localhost:8000/api/resource/test/credentials/website \
+  -H "X-Org-Key: 你的密钥base64"
+```
+
+### 6. 更新资源
+
+```bash
+curl -X PATCH http://localhost:8000/api/resource/test/credentials/website \
+  -H "Content-Type: application/json" \
+  -d '{
+    "title": "更新后的标题",
+    "metadata": {"acquired_at": "2026-03-06T11:00:00Z"}
+  }'
+```
+
+### 7. 列出所有资源
+
+```bash
+curl http://localhost:8000/api/resource
+```
+
+### 8. 按类型过滤
+
+```bash
+curl "http://localhost:8000/api/resource?content_type=credential"
+```
+
+## Knowledge引用Content
+
+在Knowledge中引用Content资源:
+
+```bash
+curl -X POST http://localhost:8000/api/knowledge \
+  -H "Content-Type: application/json" \
+  -d '{
+    "task": "登录某网站",
+    "content": "使用Selenium + undetected_chromedriver绕过检测,详见content资源",
+    "types": ["tool"],
+    "tags": {
+      "content_ref": "test/code/selenium",
+      "credential_ref": "test/credentials/website"
+    },
+    "owner": "agent:test",
+    "source": {
+      "submitted_by": "user@example.com"
+    }
+  }'
+```
+
+## 测试脚本
+
+运行测试脚本验证功能:
+
+```bash
+# 启动服务器
+uvicorn knowhub.server:app --reload
+
+# 运行测试(另一个终端)
+python test_content_storage.py
+```

+ 289 - 0
knowhub/docs/resource-storage.md

@@ -0,0 +1,289 @@
+# Resource 存储系统
+
+## 概述
+
+Resource 存储系统用于管理原始资源(代码片段、凭证、Cookie等),与 Knowledge 系统互补:
+- **Knowledge**:结构化知识条目(任务场景 + 经验内容)
+- **Content**:原始资源存储(代码、凭证、配置等)
+
+Knowledge 可以通过 `resource_id` 引用 Content 资源。
+
+---
+
+## 数据结构
+
+### 数据库表
+
+实现位置:`knowhub/server.py:init_db`
+
+```sql
+CREATE TABLE resources (
+    id            TEXT PRIMARY KEY,        -- 层级ID,如 "tools/selenium/login"
+    title         TEXT DEFAULT '',
+    body          TEXT NOT NULL,           -- 公开内容
+    secure_body   TEXT DEFAULT '',         -- 敏感内容(加密存储)
+    content_type  TEXT DEFAULT 'text',     -- text|code|credential|cookie
+    metadata      TEXT DEFAULT '{}',       -- JSON: {language, acquired_at, expires_at}
+    sort_order    INTEGER DEFAULT 0,
+    submitted_by  TEXT DEFAULT '',
+    created_at    TEXT NOT NULL,
+    updated_at    TEXT DEFAULT ''
+)
+```
+
+### 字段说明
+
+- **id**: 层级标识符,支持路径格式(如 `tools/selenium/login`)
+- **title**: 资源标题
+- **body**: 公开内容(明文存储,可搜索)
+- **secure_body**: 敏感内容(加密存储,需要组织密钥访问)
+- **content_type**: 内容类型
+  - `text`: 普通文本
+  - `code`: 代码片段
+  - `credential`: 账号密码
+  - `cookie`: Cookie数据
+- **metadata**: 元数据(JSON对象)
+  - `language`: 代码语言(当 content_type=code)
+  - `acquired_at`: 获取时间(ISO 8601格式)
+  - `expires_at`: 过期时间(用于cookie)
+- **sort_order**: 排序顺序(同级内容排序)
+- **submitted_by**: 提交者标识
+- **created_at**: 创建时间
+- **updated_at**: 更新时间
+
+---
+
+## 加密机制
+
+### 组织密钥
+
+敏感内容使用 AES-256-GCM 加密,密钥从环境变量读取:
+
+```bash
+# .env
+ORG_KEYS=org1:key1_base64,org2:key2_base64
+```
+
+每个 content 的 `id` 前缀决定使用哪个组织密钥(如 `org1/tools/...` 使用 `org1` 的密钥)。
+
+### 加密存储格式
+
+```
+encrypted:AES256-GCM:{base64_encoded_data}
+```
+
+实现位置:`knowhub/server.py:encrypt_resource`, `decrypt_resource`
+
+### 访问控制
+
+读取包含 `secure_body` 的 content 时:
+- 需要提供 `X-Org-Key` HTTP 头
+- 验证通过后解密返回
+- 验证失败返回 `[ENCRYPTED]` 占位符
+
+---
+
+## API 端点
+
+### `POST /api/content`
+
+提交新资源。
+
+**请求体**:
+
+```json
+{
+  "id": "tools/selenium/login",
+  "title": "Selenium登录代码",
+  "body": "使用undetected_chromedriver绕过检测",
+  "secure_body": "账号:xxx 密码:xxx",
+  "content_type": "credential",
+  "metadata": {
+    "acquired_at": "2026-03-06T10:00:00Z"
+  },
+  "submitted_by": "user@example.com"
+}
+```
+
+实现位置:`knowhub/server.py:submit_resource`
+
+### `GET /api/content/{resource_id}`
+
+获取资源(支持层级路径)。
+
+**请求头**(可选):
+```
+X-Org-Key: your_org_key_here
+```
+
+**响应**:
+
+```json
+{
+  "id": "tools/selenium/login",
+  "title": "Selenium登录代码",
+  "body": "使用undetected_chromedriver绕过检测",
+  "secure_body": "账号:xxx 密码:xxx",  // 有密钥时解密
+  "content_type": "credential",
+  "metadata": {
+    "acquired_at": "2026-03-06T10:00:00Z"
+  },
+  "toc": {"id": "tools", "title": "工具集"},
+  "children": [
+    {"id": "tools/selenium/login", "title": "登录代码"}
+  ]
+}
+```
+
+实现位置:`knowhub/server.py:get_resource`
+
+### `PATCH /api/content/{resource_id}`
+
+更新资源字段。
+
+**请求体**(所有字段可选):
+
+```json
+{
+  "title": "新标题",
+  "body": "新的公开内容",
+  "secure_body": "新的敏感内容",
+  "metadata": {
+    "expires_at": "2026-03-07T10:00:00Z"
+  }
+}
+```
+
+实现位置:`knowhub/server.py:patch_resource`
+
+### `GET /api/content`
+
+列出所有资源(支持过滤)。
+
+**参数**:
+- `content_type`: 按类型过滤(可选)
+- `limit`: 返回数量(默认100)
+
+实现位置:`knowhub/server.py:list_resources`
+
+---
+
+## 使用场景
+
+### 场景1:存储复杂代码
+
+```json
+{
+  "id": "code/selenium/undetected",
+  "title": "Selenium绕过检测",
+  "body": "import undetected_chromedriver as uc\n\ndriver = uc.Chrome()\n...",
+  "content_type": "code",
+  "metadata": {
+    "language": "python"
+  }
+}
+```
+
+### 场景2:存储账号密码
+
+```json
+{
+  "id": "credentials/某网站",
+  "title": "某网站登录凭证",
+  "body": "使用方法:直接登录即可",
+  "secure_body": "账号:user@example.com\n密码:SecurePass123",
+  "content_type": "credential",
+  "metadata": {
+    "acquired_at": "2026-03-06T10:00:00Z"
+  }
+}
+```
+
+### 场景3:存储Cookie
+
+```json
+{
+  "id": "cookies/某网站",
+  "title": "某网站Cookie",
+  "body": "适用于:已登录状态的API调用",
+  "secure_body": "session_id=abc123; auth_token=xyz789",
+  "content_type": "cookie",
+  "metadata": {
+    "acquired_at": "2026-03-06T10:00:00Z",
+    "expires_at": "2026-03-07T10:00:00Z"
+  }
+}
+```
+
+### 场景4:Knowledge引用Content
+
+```json
+{
+  "task": "登录某网站",
+  "content": "使用Selenium + undetected_chromedriver绕过检测",
+  "resource_id": "code/selenium/undetected",
+  "secure_resource_id": "credentials/某网站"
+}
+```
+
+---
+
+## 层级结构
+
+Content 通过 ID 路径实现树形组织:
+
+```
+tools/
+├── selenium/
+│   ├── login
+│   └── scraping
+└── api/
+    └── requests
+
+credentials/
+├── 网站A
+└── 网站B
+
+cookies/
+└── 网站A
+```
+
+获取 `tools/selenium/login` 时自动计算:
+- **TOC**: 根节点 `tools`
+- **Children**: 子节点列表
+- **Prev/Next**: 同级节点导航
+
+---
+
+## 安全考虑
+
+1. **加密存储**:敏感内容使用 AES-256-GCM 加密
+2. **访问控制**:需要组织密钥才能解密
+3. **密钥管理**:密钥存储在环境变量,不入库
+4. **审计日志**:记录 `submitted_by` 和时间戳
+5. **过期提醒**:Cookie 可设置 `expires_at`
+
+---
+
+## 与 Knowledge 的关系
+
+| 维度 | Knowledge | Content |
+|------|-----------|---------|
+| 用途 | 结构化知识(经验、策略) | 原始资源(代码、凭证) |
+| 存储 | task + content 字段 | body + secure_body 字段 |
+| 搜索 | 语义搜索 + 质量排序 | 层级浏览 |
+| 引用 | 可引用 resource_id | 被 knowledge 引用 |
+| 加密 | 不加密(或整体加密) | 分离公开/敏感内容 |
+
+---
+
+## 实现位置
+
+| 组件 | 文件路径 |
+|------|---------|
+| 数据库表 | `knowhub/server.py:init_db` |
+| 加密/解密 | `knowhub/server.py:encrypt_resource`, `decrypt_resource` |
+| POST /api/content | `knowhub/server.py:submit_resource` |
+| GET /api/content/{id} | `knowhub/server.py:get_resource` |
+| PATCH /api/content/{id} | `knowhub/server.py:patch_resource` |
+| GET /api/content | `knowhub/server.py:list_resources` |

+ 56 - 0
knowhub/migrate_contents.py

@@ -0,0 +1,56 @@
+#!/usr/bin/env python3
+"""
+数据库迁移脚本:为contents表添加新字段
+"""
+
+import sqlite3
+from pathlib import Path
+
+DB_PATH = Path(__file__).parent / "knowhub.db"
+
+def migrate():
+    print(f"数据库路径: {DB_PATH}")
+
+    if not DB_PATH.exists():
+        print("数据库不存在,无需迁移")
+        return
+
+    conn = sqlite3.connect(str(DB_PATH))
+    cursor = conn.cursor()
+
+    # 检查是否已有新字段
+    cursor.execute("PRAGMA table_info(contents)")
+    columns = {row[1] for row in cursor.fetchall()}
+    print(f"现有字段: {columns}")
+
+    migrations = []
+
+    if "secure_body" not in columns:
+        migrations.append("ALTER TABLE contents ADD COLUMN secure_body TEXT DEFAULT ''")
+
+    if "content_type" not in columns:
+        migrations.append("ALTER TABLE contents ADD COLUMN content_type TEXT DEFAULT 'text'")
+
+    if "metadata" not in columns:
+        migrations.append("ALTER TABLE contents ADD COLUMN metadata TEXT DEFAULT '{}'")
+
+    if "updated_at" not in columns:
+        migrations.append("ALTER TABLE contents ADD COLUMN updated_at TEXT DEFAULT ''")
+
+    if not migrations:
+        print("✅ 数据库已是最新版本,无需迁移")
+        conn.close()
+        return
+
+    print(f"执行 {len(migrations)} 个迁移...")
+    for sql in migrations:
+        print(f"  {sql}")
+        cursor.execute(sql)
+
+    conn.commit()
+    conn.close()
+    print("✅ 迁移完成")
+
+
+if __name__ == "__main__":
+    migrate()

+ 262 - 41
knowhub/server.py

@@ -10,12 +10,14 @@ import re
 import json
 import sqlite3
 import asyncio
+import base64
 from contextlib import asynccontextmanager
 from datetime import datetime, timezone
 from typing import Optional
 from pathlib import Path
+from cryptography.hazmat.primitives.ciphers.aead import AESGCM
 
-from fastapi import FastAPI, HTTPException, Query
+from fastapi import FastAPI, HTTPException, Query, Header
 from fastapi.responses import HTMLResponse
 from pydantic import BaseModel, Field
 
@@ -33,6 +35,15 @@ BRAND_NAME    = os.getenv("BRAND_NAME", "KnowHub")
 BRAND_API_ENV = os.getenv("BRAND_API_ENV", "KNOWHUB_API")
 BRAND_DB      = os.getenv("BRAND_DB", "knowhub.db")
 
+# 组织密钥配置(格式:org1:key1_base64,org2:key2_base64)
+ORG_KEYS_RAW = os.getenv("ORG_KEYS", "")
+ORG_KEYS = {}
+if ORG_KEYS_RAW:
+    for pair in ORG_KEYS_RAW.split(","):
+        if ":" in pair:
+            org, key_b64 = pair.split(":", 1)
+            ORG_KEYS[org.strip()] = key_b64.strip()
+
 DB_PATH = Path(__file__).parent / BRAND_DB
 
 # --- 数据库 ---
@@ -44,6 +55,77 @@ def get_db() -> sqlite3.Connection:
     return conn
 
 
+# --- 加密/解密 ---
+
+def get_org_key(resource_id: str) -> Optional[bytes]:
+    """从content_id提取组织前缀,返回对应密钥"""
+    if "/" in resource_id:
+        org = resource_id.split("/")[0]
+        if org in ORG_KEYS:
+            return base64.b64decode(ORG_KEYS[org])
+    return None
+
+
+def encrypt_content(resource_id: str, plaintext: str) -> str:
+    """加密内容,返回格式:encrypted:AES256-GCM:{base64_data}"""
+    if not plaintext:
+        return ""
+
+    key = get_org_key(resource_id)
+    if not key:
+        # 没有配置密钥,明文存储(不推荐)
+        return plaintext
+
+    aesgcm = AESGCM(key)
+    nonce = os.urandom(12)  # 96-bit nonce
+    ciphertext = aesgcm.encrypt(nonce, plaintext.encode("utf-8"), None)
+
+    # 组合 nonce + ciphertext
+    encrypted_data = nonce + ciphertext
+    encoded = base64.b64encode(encrypted_data).decode("ascii")
+
+    return f"encrypted:AES256-GCM:{encoded}"
+
+
+def decrypt_content(resource_id: str, encrypted_text: str, provided_key: Optional[str] = None) -> str:
+    """解密内容,如果没有提供密钥或密钥错误,返回[ENCRYPTED]"""
+    if not encrypted_text:
+        return ""
+
+    if not encrypted_text.startswith("encrypted:AES256-GCM:"):
+        # 未加密的内容,直接返回
+        return encrypted_text
+
+    # 提取加密数据
+    encoded = encrypted_text.split(":", 2)[2]
+    encrypted_data = base64.b64decode(encoded)
+
+    nonce = encrypted_data[:12]
+    ciphertext = encrypted_data[12:]
+
+    # 获取密钥
+    key = None
+    if provided_key:
+        # 使用提供的密钥
+        try:
+            key = base64.b64decode(provided_key)
+        except Exception:
+            return "[ENCRYPTED]"
+    else:
+        # 从配置中获取
+        key = get_org_key(resource_id)
+
+    if not key:
+        return "[ENCRYPTED]"
+
+    try:
+        aesgcm = AESGCM(key)
+        plaintext = aesgcm.decrypt(nonce, ciphertext, None)
+        return plaintext.decode("utf-8")
+    except Exception:
+        return "[ENCRYPTED]"
+
+
 def init_db():
     conn = get_db()
     conn.execute("""
@@ -64,13 +146,17 @@ def init_db():
     conn.execute("CREATE INDEX IF NOT EXISTS idx_name ON experiences(name)")
 
     conn.execute("""
-        CREATE TABLE IF NOT EXISTS contents (
+        CREATE TABLE IF NOT EXISTS resources (
             id            TEXT PRIMARY KEY,
             title         TEXT DEFAULT '',
             body          TEXT NOT NULL,
+            secure_body   TEXT DEFAULT '',
+            content_type  TEXT DEFAULT 'text',
+            metadata      TEXT DEFAULT '{}',
             sort_order    INTEGER DEFAULT 0,
             submitted_by  TEXT DEFAULT '',
-            created_at    TEXT NOT NULL
+            created_at    TEXT NOT NULL,
+            updated_at    TEXT DEFAULT ''
         )
     """)
 
@@ -84,6 +170,7 @@ def init_db():
             scopes        TEXT DEFAULT '["org:cybertogether"]',  -- JSON array
             owner         TEXT DEFAULT '',
             content       TEXT NOT NULL,
+            resource_ids  TEXT DEFAULT '[]',          -- JSON array: ["code/selenium/login", "credentials/website"]
             source        TEXT DEFAULT '{}',          -- JSON object: {name, category, urls, agent_id, submitted_by, timestamp}
             eval          TEXT DEFAULT '{}',          -- JSON object: {score, helpful, harmful, confidence, histories}
             created_at    TEXT NOT NULL,
@@ -101,14 +188,26 @@ def init_db():
 
 # --- Models ---
 
-class ContentIn(BaseModel):
+class ResourceIn(BaseModel):
     id: str
     title: str = ""
     body: str
+    secure_body: str = ""
+    content_type: str = "text"  # text|code|credential|cookie
+    metadata: dict = {}
     sort_order: int = 0
     submitted_by: str = ""
 
 
+class ResourcePatchIn(BaseModel):
+    """PATCH /api/resource/{id} 请求体"""
+    title: Optional[str] = None
+    body: Optional[str] = None
+    secure_body: Optional[str] = None
+    content_type: Optional[str] = None
+    metadata: Optional[dict] = None
+
+
 # Knowledge Models
 class KnowledgeIn(BaseModel):
     task: str
@@ -118,6 +217,7 @@ class KnowledgeIn(BaseModel):
     scopes: list[str] = ["org:cybertogether"]
     owner: str = ""
     message_id: str = ""
+    resource_ids: list[str] = []
     source: dict = {}  # {name, category, urls, agent_id, submitted_by, timestamp}
     eval: dict = {}    # {score, helpful, harmful, confidence}
 
@@ -131,6 +231,7 @@ class KnowledgeOut(BaseModel):
     scopes: list[str]
     owner: str
     content: str
+    resource_ids: list[str]
     source: dict
     eval: dict
     created_at: str
@@ -171,19 +272,22 @@ class KnowledgeSearchResponse(BaseModel):
     count: int
 
 
-class ContentNode(BaseModel):
+class ResourceNode(BaseModel):
     id: str
     title: str
 
 
-class ContentOut(BaseModel):
+class ResourceOut(BaseModel):
     id: str
     title: str
     body: str
-    toc: Optional[ContentNode] = None
-    children: list[ContentNode]
-    prev: Optional[ContentNode] = None
-    next: Optional[ContentNode] = None
+    secure_body: str = ""
+    content_type: str = "text"
+    metadata: dict = {}
+    toc: Optional[ResourceNode] = None
+    children: list[ResourceNode]
+    prev: Optional[ResourceNode] = None
+    next: Optional[ResourceNode] = None
 
 
 # --- App ---
@@ -199,75 +303,99 @@ app = FastAPI(title=BRAND_NAME, lifespan=lifespan)
 
 # --- Knowledge API ---
 
-@app.post("/api/content", status_code=201)
-def submit_content(content: ContentIn):
+@app.post("/api/resource", status_code=201)
+def submit_resource(resource: ResourceIn):
     conn = get_db()
     try:
         now = datetime.now(timezone.utc).isoformat()
+
+        # 加密敏感内容
+        encrypted_secure_body = encrypt_content(resource.id, resource.secure_body)
+
         conn.execute(
-            "INSERT OR REPLACE INTO contents"
-            "(id, title, body, sort_order, submitted_by, created_at)"
-            " VALUES (?, ?, ?, ?, ?, ?)",
-            (content.id, content.title, content.body, content.sort_order, content.submitted_by, now),
+            "INSERT OR REPLACE INTO resources"
+            "(id, title, body, secure_body, content_type, metadata, sort_order, submitted_by, created_at, updated_at)"
+            " VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
+            (
+                resource.id,
+                resource.title,
+                resource.body,
+                encrypted_secure_body,
+                resource.content_type,
+                json.dumps(resource.metadata),
+                resource.sort_order,
+                resource.submitted_by,
+                now,
+                now,
+            ),
         )
         conn.commit()
-        return {"status": "ok"}
+        return {"status": "ok", "id": resource.id}
     finally:
         conn.close()
 
 
-@app.get("/api/content/{content_id:path}", response_model=ContentOut)
-def get_content(content_id: str):
+@app.get("/api/resource/{resource_id:path}", response_model=ResourceOut)
+def get_resource(resource_id: str, x_org_key: Optional[str] = Header(None)):
     conn = get_db()
     try:
         row = conn.execute(
-            "SELECT id, title, body, sort_order FROM contents WHERE id = ?",
-            (content_id,),
+            "SELECT id, title, body, secure_body, content_type, metadata, sort_order FROM resources WHERE id = ?",
+            (resource_id,),
         ).fetchone()
         if not row:
-            raise HTTPException(status_code=404, detail=f"Content not found: {content_id}")
+            raise HTTPException(status_code=404, detail=f"Resource not found: {resource_id}")
+
+        # 解密敏感内容
+        secure_body = decrypt_content(resource_id, row["secure_body"] or "", x_org_key)
+
+        # 解析metadata
+        metadata = json.loads(row["metadata"] or "{}")
 
         # 计算导航上下文
-        root_id = content_id.split("/")[0] if "/" in content_id else content_id
+        root_id = resource_id.split("/")[0] if "/" in resource_id else resource_id
 
         # TOC (根节点)
         toc = None
-        if "/" in content_id:
+        if "/" in resource_id:
             toc_row = conn.execute(
-                "SELECT id, title FROM contents WHERE id = ?",
+                "SELECT id, title FROM resources WHERE id = ?",
                 (root_id,),
             ).fetchone()
             if toc_row:
-                toc = ContentNode(id=toc_row["id"], title=toc_row["title"])
+                toc = ResourceNode(id=toc_row["id"], title=toc_row["title"])
 
         # Children (子节点)
         children = []
         children_rows = conn.execute(
-            "SELECT id, title FROM contents WHERE id LIKE ? AND id != ? ORDER BY sort_order",
-            (f"{content_id}/%", content_id),
+            "SELECT id, title FROM resources WHERE id LIKE ? AND id != ? ORDER BY sort_order",
+            (f"{resource_id}/%", resource_id),
         ).fetchall()
-        children = [ContentNode(id=r["id"], title=r["title"]) for r in children_rows]
+        children = [ResourceNode(id=r["id"], title=r["title"]) for r in children_rows]
 
         # Prev/Next (同级节点)
         prev_node = None
         next_node = None
-        if "/" in content_id:
+        if "/" in resource_id:
             siblings = conn.execute(
-                "SELECT id, title, sort_order FROM contents WHERE id LIKE ? AND id NOT LIKE ? ORDER BY sort_order",
+                "SELECT id, title, sort_order FROM resources WHERE id LIKE ? AND id NOT LIKE ? ORDER BY sort_order",
                 (f"{root_id}/%", f"{root_id}/%/%"),
             ).fetchall()
             for i, sib in enumerate(siblings):
-                if sib["id"] == content_id:
+                if sib["id"] == resource_id:
                     if i > 0:
-                        prev_node = ContentNode(id=siblings[i-1]["id"], title=siblings[i-1]["title"])
+                        prev_node = ResourceNode(id=siblings[i-1]["id"], title=siblings[i-1]["title"])
                     if i < len(siblings) - 1:
-                        next_node = ContentNode(id=siblings[i+1]["id"], title=siblings[i+1]["title"])
+                        next_node = ResourceNode(id=siblings[i+1]["id"], title=siblings[i+1]["title"])
                     break
 
-        return ContentOut(
+        return ResourceOut(
             id=row["id"],
             title=row["title"],
             body=row["body"],
+            secure_body=secure_body,
+            content_type=row["content_type"],
+            metadata=metadata,
             toc=toc,
             children=children,
             prev=prev_node,
@@ -277,6 +405,97 @@ def get_content(content_id: str):
         conn.close()
 
 
+@app.patch("/api/resource/{resource_id:path}")
+def patch_resource(resource_id: str, patch: ResourcePatchIn):
+    """更新resource字段"""
+    conn = get_db()
+    try:
+        # 检查是否存在
+        row = conn.execute("SELECT id FROM resources WHERE id = ?", (resource_id,)).fetchone()
+        if not row:
+            raise HTTPException(status_code=404, detail=f"Resource not found: {resource_id}")
+
+        # 构建更新语句
+        updates = []
+        params = []
+
+        if patch.title is not None:
+            updates.append("title = ?")
+            params.append(patch.title)
+
+        if patch.body is not None:
+            updates.append("body = ?")
+            params.append(patch.body)
+
+        if patch.secure_body is not None:
+            encrypted = encrypt_content(resource_id, patch.secure_body)
+            updates.append("secure_body = ?")
+            params.append(encrypted)
+
+        if patch.content_type is not None:
+            updates.append("content_type = ?")
+            params.append(patch.content_type)
+
+        if patch.metadata is not None:
+            updates.append("metadata = ?")
+            params.append(json.dumps(patch.metadata))
+
+        if not updates:
+            return {"status": "ok", "message": "No fields to update"}
+
+        # 添加updated_at
+        updates.append("updated_at = ?")
+        params.append(datetime.now(timezone.utc).isoformat())
+
+        # 执行更新
+        params.append(resource_id)
+        sql = f"UPDATE resources SET {', '.join(updates)} WHERE id = ?"
+        conn.execute(sql, params)
+        conn.commit()
+
+        return {"status": "ok", "id": resource_id}
+    finally:
+        conn.close()
+
+
+@app.get("/api/resource")
+def list_resources(
+    content_type: Optional[str] = Query(None),
+    limit: int = Query(100, ge=1, le=1000)
+):
+    """列出所有resource"""
+    conn = get_db()
+    try:
+        sql = "SELECT id, title, content_type, metadata, created_at FROM resources"
+        params = []
+
+        if content_type:
+            sql += " WHERE content_type = ?"
+            params.append(content_type)
+
+        sql += " ORDER BY id LIMIT ?"
+        params.append(limit)
+
+        rows = conn.execute(sql, params).fetchall()
+
+        results = []
+        for row in rows:
+            results.append({
+                "id": row["id"],
+                "title": row["title"],
+                "content_type": row["content_type"],
+                "metadata": json.loads(row["metadata"] or "{}"),
+                "created_at": row["created_at"],
+            })
+
+        return {"results": results, "count": len(results)}
+    finally:
+        conn.close()
+
+
+# --- Knowledge API ---
+
+
 # ===== Knowledge API =====
 
 # 两阶段检索逻辑
@@ -313,7 +532,7 @@ async def _route_knowledge_by_llm(query_text: str, metadata_list: list[dict], k:
 
         response = await openrouter_llm_call(
             messages=[{"role": "user", "content": prompt}],
-            model="google/gemini-2.0-flash-001"
+            model="google/gemini-2.5-flash-lite"
         )
 
         content = response.get("content", "").strip()
@@ -521,8 +740,8 @@ def save_knowledge(knowledge: KnowledgeIn):
         conn.execute(
             """INSERT INTO knowledge
             (id, message_id, types, task, tags, scopes, owner, content,
-             source, eval, created_at, updated_at)
-            VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
+             resource_ids, source, eval, created_at, updated_at)
+            VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
             (
                 knowledge_id,
                 knowledge.message_id,
@@ -532,6 +751,7 @@ def save_knowledge(knowledge: KnowledgeIn):
                 json.dumps(knowledge.scopes),
                 owner,
                 knowledge.content,
+                json.dumps(knowledge.resource_ids),
                 json.dumps(source),
                 json.dumps(eval_data),
                 now,
@@ -655,6 +875,7 @@ def get_knowledge(knowledge_id: str):
             "scopes": json.loads(row["scopes"]),
             "owner": row["owner"],
             "content": row["content"],
+            "resource_ids": json.loads(row["resource_ids"]),
             "source": json.loads(row["source"]),
             "eval": json.loads(row["eval"]),
             "created_at": row["created_at"],
@@ -683,7 +904,7 @@ async def _evolve_knowledge_with_llm(old_content: str, feedback: str) -> str:
     try:
         response = await openrouter_llm_call(
             messages=[{"role": "user", "content": prompt}],
-            model="google/gemini-2.0-flash-001"
+            model="google/gemini-2.5-flash-lite"
         )
         evolved = response.get("content", "").strip()
         if len(evolved) < 5:
@@ -854,7 +1075,7 @@ async def batch_update_knowledge(batch: KnowledgeBatchUpdateIn):
 
 
 @app.post("/api/knowledge/slim")
-async def slim_knowledge(model: str = "google/gemini-2.0-flash-001"):
+async def slim_knowledge(model: str = "google/gemini-2.5-flash-lite"):
     """知识库瘦身:合并语义相似知识"""
     conn = get_db()
     try:
@@ -1084,7 +1305,7 @@ async def extract_knowledge_from_messages(extract_req: MessageExtractIn):
 
         response = await openrouter_llm_call(
             messages=[{"role": "user", "content": prompt}],
-            model="google/gemini-2.0-flash-001"
+            model="google/gemini-2.5-flash-lite"
         )
 
         content = response.get("content", "").strip()

+ 204 - 0
migrate_to_resource.py

@@ -0,0 +1,204 @@
+#!/usr/bin/env python3
+"""
+安全迁移脚本:contents → resources + knowledge.resource_ids
+在服务器上执行此脚本,然后再拉取新代码
+
+变更内容:
+1. contents表 → resources表
+2. 为resources表添加新字段(secure_body, content_type, metadata, updated_at)
+3. 为knowledge表添加resource_ids字段
+"""
+
+import sqlite3
+import sys
+from pathlib import Path
+
+def migrate_resources_table(conn, cursor):
+    """迁移resources表(原contents表)"""
+    print("\n" + "="*60)
+    print("步骤1: 迁移 resources 表")
+    print("="*60)
+
+    # 检查contents表是否存在
+    cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='contents'")
+    if not cursor.fetchone():
+        cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='resources'")
+        if cursor.fetchone():
+            print("✅ resources表已存在,跳过此步骤")
+            return True
+        else:
+            print("⚠️  既没有contents表也没有resources表,将在首次启动时创建")
+            return False
+
+    # 检查contents表中的数据
+    cursor.execute("SELECT COUNT(*) FROM contents")
+    count = cursor.fetchone()[0]
+    print(f"contents表中有 {count} 条记录")
+
+    # 检查是否已有新字段
+    cursor.execute("PRAGMA table_info(contents)")
+    columns = {row[1] for row in cursor.fetchall()}
+    print(f"现有字段: {columns}")
+
+    # 添加新字段(如果不存在)
+    migrations = []
+    if "secure_body" not in columns:
+        migrations.append("ALTER TABLE contents ADD COLUMN secure_body TEXT DEFAULT ''")
+    if "content_type" not in columns:
+        migrations.append("ALTER TABLE contents ADD COLUMN content_type TEXT DEFAULT 'text'")
+    if "metadata" not in columns:
+        migrations.append("ALTER TABLE contents ADD COLUMN metadata TEXT DEFAULT '{}'")
+    if "updated_at" not in columns:
+        migrations.append("ALTER TABLE contents ADD COLUMN updated_at TEXT DEFAULT ''")
+
+    if migrations:
+        print(f"\n添加 {len(migrations)} 个新字段...")
+        for sql in migrations:
+            print(f"  {sql}")
+            cursor.execute(sql)
+        conn.commit()
+        print("✅ 字段添加完成")
+
+    # 重命名表
+    print("\n重命名表: contents → resources")
+    try:
+        cursor.execute("ALTER TABLE contents RENAME TO resources")
+        conn.commit()
+        print("✅ 表重命名完成")
+    except Exception as e:
+        print(f"❌ 重命名失败: {e}")
+        return False
+
+    # 验证
+    cursor.execute("SELECT COUNT(*) FROM resources")
+    new_count = cursor.fetchone()[0]
+    print(f"resources表中有 {new_count} 条记录")
+
+    if new_count == count:
+        print(f"✅ 数据完整,{count} 条记录全部保留")
+        return True
+    else:
+        print(f"⚠️  数据不一致: 原 {count} 条 → 现 {new_count} 条")
+        return False
+
+
+def migrate_knowledge_table(conn, cursor):
+    """为knowledge表添加resource_ids字段"""
+    print("\n" + "="*60)
+    print("步骤2: 更新 knowledge 表")
+    print("="*60)
+
+    # 检查knowledge表是否存在
+    cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='knowledge'")
+    if not cursor.fetchone():
+        print("⚠️  knowledge表不存在,跳过此步骤")
+        return True
+
+    # 检查是否已有resource_ids字段
+    cursor.execute("PRAGMA table_info(knowledge)")
+    columns = {row[1] for row in cursor.fetchall()}
+    print(f"现有字段: {columns}")
+
+    if "resource_ids" in columns:
+        print("✅ resource_ids字段已存在,跳过此步骤")
+        return True
+
+    # 添加resource_ids字段
+    print("\n添加 resource_ids 字段...")
+    try:
+        cursor.execute("ALTER TABLE knowledge ADD COLUMN resource_ids TEXT DEFAULT '[]'")
+        conn.commit()
+        print("✅ resource_ids字段添加完成")
+        return True
+    except Exception as e:
+        print(f"❌ 添加字段失败: {e}")
+        return False
+
+
+def migrate():
+    """主迁移函数"""
+def migrate():
+    """主迁移函数"""
+    # 查找数据库文件
+    db_path = Path("knowhub.db")
+    if not db_path.exists():
+        print("❌ 找不到 knowhub.db")
+        print("请在包含数据库的目录中运行此脚本")
+        sys.exit(1)
+
+    print(f"数据库路径: {db_path.absolute()}")
+
+    conn = sqlite3.connect(str(db_path))
+    cursor = conn.cursor()
+
+    # 显示当前表状态
+    cursor.execute("SELECT name FROM sqlite_master WHERE type='table'")
+    tables = {row[0] for row in cursor.fetchall()}
+    print(f"\n当前表: {tables}")
+
+    # 执行迁移
+    success = True
+
+    # 步骤1: 迁移resources表
+    if not migrate_resources_table(conn, cursor):
+        success = False
+
+    # 步骤2: 更新knowledge表
+    if not migrate_knowledge_table(conn, cursor):
+        success = False
+
+    # 最终验证
+    print("\n" + "="*60)
+    print("迁移总结")
+    print("="*60)
+
+    cursor.execute("SELECT name FROM sqlite_master WHERE type='table'")
+    tables = {row[0] for row in cursor.fetchall()}
+    print(f"迁移后的表: {tables}")
+
+    # 检查resources表
+    if "resources" in tables:
+        cursor.execute("SELECT COUNT(*) FROM resources")
+        count = cursor.fetchone()[0]
+        print(f"✅ resources表: {count} 条记录")
+    else:
+        print("⚠️  resources表不存在")
+
+    # 检查knowledge表
+    if "knowledge" in tables:
+        cursor.execute("SELECT COUNT(*) FROM knowledge")
+        count = cursor.fetchone()[0]
+        cursor.execute("PRAGMA table_info(knowledge)")
+        columns = {row[1] for row in cursor.fetchall()}
+        has_resource_ids = "resource_ids" in columns
+        print(f"✅ knowledge表: {count} 条记录, resource_ids字段: {'存在' if has_resource_ids else '不存在'}")
+    else:
+        print("⚠️  knowledge表不存在")
+
+    conn.close()
+
+    if success:
+        print("\n✅ 迁移完成!现在可以拉取新代码并重启服务")
+    else:
+        print("\n⚠️  迁移过程中有警告,请检查上述输出")
+
+    return success
+
+
+if __name__ == "__main__":
+    print("=" * 60)
+    print("KnowHub 数据库迁移")
+    print("变更内容:")
+    print("  1. contents表 → resources表")
+    print("  2. resources表添加新字段")
+    print("  3. knowledge表添加resource_ids字段")
+    print("=" * 60)
+
+    try:
+        success = migrate()
+        sys.exit(0 if success else 1)
+    except Exception as e:
+        print(f"\n❌ 迁移失败: {e}")
+        import traceback
+        traceback.print_exc()
+        sys.exit(1)

+ 35 - 0
rename_to_resource.py

@@ -0,0 +1,35 @@
+#!/usr/bin/env python3
+"""
+重命名:contents → resources
+"""
+
+import sqlite3
+from pathlib import Path
+
+DB_PATH = Path(__file__).parent / "knowhub.db"
+
+def rename_table():
+    if not DB_PATH.exists():
+        print("数据库不存在")
+        return
+
+    conn = sqlite3.connect(str(DB_PATH))
+    cursor = conn.cursor()
+
+    # 检查表是否存在
+    cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='contents'")
+    if not cursor.fetchone():
+        print("contents表不存在,可能已经重命名")
+        conn.close()
+        return
+
+    print("重命名表:contents → resources")
+    cursor.execute("ALTER TABLE contents RENAME TO resources")
+
+    conn.commit()
+    conn.close()
+    print("✅ 重命名完成")
+
+
+if __name__ == "__main__":
+    rename_table()

+ 143 - 0
test_resource_storage.py

@@ -0,0 +1,143 @@
+#!/usr/bin/env python3
+"""测试Resource存储系统"""
+
+import requests
+import base64
+import os
+
+BASE_URL = "http://localhost:8000"
+
+# 生成测试密钥
+test_key = base64.b64encode(os.urandom(32)).decode('ascii')
+print(f"测试密钥: {test_key}")
+print(f"请在.env中设置: ORG_KEYS=test:{test_key}")
+print()
+
+def test_submit_resource():
+    """测试提交resource"""
+    print("=== 测试1: 提交普通代码 ===")
+    response = requests.post(f"{BASE_URL}/api/resource", json={
+        "id": "test/code/example",
+        "title": "示例代码",
+        "body": "print('Hello World')",
+        "content_type": "code",
+        "metadata": {"language": "python"},
+        "submitted_by": "test@example.com"
+    })
+    print(f"状态码: {response.status_code}")
+    print(f"响应: {response.json()}")
+    print()
+
+    print("=== 测试2: 提交敏感凭证 ===")
+    response = requests.post(f"{BASE_URL}/api/resource", json={
+        "id": "test/credentials/website",
+        "title": "网站登录凭证",
+        "body": "使用方法:直接登录",
+        "secure_body": "账号:user@example.com\n密码:SecurePass123",
+        "content_type": "credential",
+        "metadata": {"acquired_at": "2026-03-06T10:00:00Z"},
+        "submitted_by": "test@example.com"
+    })
+    print(f"状态码: {response.status_code}")
+    print(f"响应: {response.json()}")
+    print()
+
+    print("=== 测试3: 提交Cookie ===")
+    response = requests.post(f"{BASE_URL}/api/resource", json={
+        "id": "test/cookies/website",
+        "title": "网站Cookie",
+        "body": "适用于:已登录状态",
+        "secure_body": "session_id=abc123; auth_token=xyz789",
+        "content_type": "cookie",
+        "metadata": {
+            "acquired_at": "2026-03-06T10:00:00Z",
+            "expires_at": "2026-03-07T10:00:00Z"
+        },
+        "submitted_by": "test@example.com"
+    })
+    print(f"状态码: {response.status_code}")
+    print(f"响应: {response.json()}")
+    print()
+
+
+def test_get_resource():
+    """测试获取resource"""
+    print("=== 测试4: 获取普通代码(无需密钥)===")
+    response = requests.get(f"{BASE_URL}/api/resource/test/code/example")
+    print(f"状态码: {response.status_code}")
+    data = response.json()
+    print(f"标题: {data['title']}")
+    print(f"内容: {data['body']}")
+    print(f"敏感内容: {data['secure_body']}")
+    print()
+
+    print("=== 测试5: 获取凭证(无密钥)===")
+    response = requests.get(f"{BASE_URL}/api/resource/test/credentials/website")
+    print(f"状态码: {response.status_code}")
+    data = response.json()
+    print(f"标题: {data['title']}")
+    print(f"公开内容: {data['body']}")
+    print(f"敏感内容: {data['secure_body']}")  # 应该是 [ENCRYPTED]
+    print()
+
+    print("=== 测试6: 获取凭证(有密钥)===")
+    response = requests.get(
+        f"{BASE_URL}/api/resource/test/credentials/website",
+        headers={"X-Org-Key": test_key}
+    )
+    print(f"状态码: {response.status_code}")
+    data = response.json()
+    print(f"标题: {data['title']}")
+    print(f"公开内容: {data['body']}")
+    print(f"敏感内容: {data['secure_body']}")  # 应该解密
+    print()
+
+
+def test_patch_resource():
+    """测试更新resource"""
+    print("=== 测试7: 更新resource ===")
+    response = requests.patch(
+        f"{BASE_URL}/api/resource/test/credentials/website",
+        json={
+            "title": "更新后的标题",
+            "metadata": {"acquired_at": "2026-03-06T11:00:00Z"}
+        }
+    )
+    print(f"状态码: {response.status_code}")
+    print(f"响应: {response.json()}")
+    print()
+
+
+def test_list_resources():
+    """测试列出resource"""
+    print("=== 测试8: 列出所有resource ===")
+    response = requests.get(f"{BASE_URL}/api/resource")
+    print(f"状态码: {response.status_code}")
+    data = response.json()
+    print(f"总数: {data['count']}")
+    for item in data['results']:
+        print(f"  - {item['id']}: {item['title']} ({item['content_type']})")
+    print()
+
+    print("=== 测试9: 按类型过滤 ===")
+    response = requests.get(f"{BASE_URL}/api/resource?content_type=credential")
+    print(f"状态码: {response.status_code}")
+    data = response.json()
+    print(f"凭证数量: {data['count']}")
+    for item in data['results']:
+        print(f"  - {item['id']}: {item['title']}")
+    print()
+
+
+if __name__ == "__main__":
+    print("请确保服务器正在运行: uvicorn knowhub.server:app --reload")
+    print()
+
+    try:
+        test_submit_resource()
+        test_get_resource()
+        test_patch_resource()
+        test_list_resources()
+        print("✅ 所有测试完成")
+    except Exception as e:
+        print(f"❌ 测试失败: {e}")