diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md
index df0e3b9e..c7eb8db3 100644
--- a/ARCHITECTURE.md
+++ b/ARCHITECTURE.md
@@ -94,8 +94,8 @@ graph TB
T_Time["get_current_time
获取当前时间"]
T_GetPicture["get_picture
获取图片"]
T_GetUserInfo["get_user_info
获取用户信息"]
- T_ArxivPaper["arxiv_paper
arXiv 论文发送"]
- T_BilibiliVideo["bilibili_video
B站视频下载发送"]
+ T_ArxivPaper["arxiv_paper
arXiv 论文发送 / UID 获取"]
+ T_BilibiliVideo["bilibili_video
B站视频下载发送 / UID 获取"]
end
subgraph Toolsets["工具集 (skills/toolsets/, 11大类)"]
@@ -115,7 +115,7 @@ graph TB
subgraph IntelligentAgents["智能体 Agents (skills/agents/, 7个)"]
A_Info["info_agent
信息查询助手
(18个工具)
• weather_query
• *hot 热搜
• bilibili_*
• arxiv_search
• whois"]
A_Web["web_agent
网络搜索助手
(3个工具 + MCP)
• web_search
• crawl_webpage
• Playwright MCP"]
- A_File["file_analysis_agent
文件分析助手
(14个工具)
• extract_* (PDF/Word/Excel/PPT)
• analyze_code
• analyze_multimodal"]
+ A_File["file_analysis_agent
文件分析助手
• extract_* (PDF/Word/Excel/PPT)
• describe_pdf_page
• analyze_code
• analyze_multimodal"]
A_Naga["naga_code_analysis_agent
NagaAgent 代码分析
(7个工具)
• read_file / glob
• search_file_content"]
A_Self["undefined_self_code_agent
Undefined 自身代码查阅
(4个工具)
• read_file / list_directory
• glob / search_file_content"]
A_Entertainment["entertainment_agent
娱乐助手
(9个工具)
• ai_draw_one
• horoscope
• video_random_recommend"]
@@ -877,7 +877,7 @@ description: 从 PDF 文件中提取文本和表格,填写表单。当用户
|-------|---------|---------|---------|
| **info_agent** | 信息查询助手 | 18个 | 天气查询、热搜榜单、网络检测、B站信息查询、arXiv 搜索等 |
| **web_agent** | 网络搜索助手 | 3个 + MCP | 网页搜索、爬虫、Playwright MCP |
-| **file_analysis_agent** | 文件分析助手 | 14个 | PDF/Word/Excel/PPT解析、代码分析、多模态分析 |
+| **file_analysis_agent** | 文件分析助手 | 多个 | PDF/Word/Excel/PPT解析、指定 PDF 页视觉分析、代码分析、多模态分析、arXiv/Bilibili UID 获取分析 |
| **naga_code_analysis_agent** | NagaAgent 代码分析 | 7个 | 代码库浏览、文件搜索、目录遍历 |
| **undefined_self_code_agent** | Undefined 自身代码查阅 | 4个 | 受限读取源码、测试、文档、资源、脚本与 App |
| **entertainment_agent** | 娱乐助手 | 9个 | AI 绘图、星座运势、小说搜索、随机视频推荐等 |
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5ec6d285..c8b83ff7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,15 @@
+## v3.6.1 Agent 路由收敛、文件分析增强与发版脚本修复
+
+本版本围绕 Agent 职责边界和文件分析链路做小版本收敛:把 arXiv 论文分析从独立 Agent 合并到通用文件分析 Agent,让论文、PDF 页面视觉分析和视频附件获取都走统一的附件 UID 语义;同时修复版本迭代脚本重写 Tauri 配置格式导致 pre-commit 失败的问题,保证后续版本号同步、lock 文件刷新和自动提交流程更稳定。
+
+- 合并 arXiv 论文分析职责到文件分析链路。移除独立 `arxiv_analysis_agent`,`file_analysis_agent` 现在可通过 `arxiv_paper(output_mode=uid)` 获取论文 PDF 附件 UID,并结合 `extract_pdf` 与 `describe_pdf_page` 完成文本和指定页视觉分析。
+- 扩展媒体工具的附件输出能力。`arxiv_paper` 与 `bilibili_video` 新增 `output_mode=uid` 调用方式,可把下载或提取结果登记为当前会话附件,供后续 Agent 直接引用和复用。
+- 明确 Agent 职责边界。更新代码交付、文件分析、信息查询、娱乐、总结、Naga 代码分析、Undefined 自查与 Web Agent 的 intro / prompt,减少项目问题、通用搜索、文件理解和代码修改任务之间的误路由。
+- 补齐文件分析视觉工具与测试。新增 `describe_pdf_page` 工具及回归测试,覆盖 PDF 指定页截图/视觉描述链路;同步更新 arXiv、B 站发送器和工具注册测试。
+- 修复版本迭代脚本的 JSON 格式漂移。`scripts/bump_version.py` 更新 JSON manifest 时只替换顶层 `version` 字段,保留 Tauri 配置原有格式,避免 `--commit` 触发的 Biome 检查因 `targets` 数组被重排而失败;脚本文档与单元测试同步覆盖该行为。
+
+---
+
## v3.6.0 原生 Chat、WebChat 多会话与运行时管理增强
本版本把 Undefined 的“管理控制台内聊天”扩展为一套更完整的跨端聊天与运行时管理体系:一边新增面向桌面端和 Android 的原生 Chat 客户端,一边把 WebUI WebChat 升级为可长期使用的多会话工作台;底层则补齐 Runtime / Management API、任务续接、附件、命令、定时任务和发布构建能力。围绕这些入口,v3.6.0 也整理了 Agent 路由、认知记忆、附件标签和工程验证,让 WebUI、原生客户端和 QQ 侧共享更一致的运行时语义。
diff --git a/README.md b/README.md
index 92938aa0..24dc6ad4 100644
--- a/README.md
+++ b/README.md
@@ -60,8 +60,8 @@
- **MCP 协议支持**:支持通过 MCP (Model Context Protocol) 连接外部工具和数据源,扩展 AI 能力。
- **Agent 私有 MCP**:可为单个 agent 提供独立 MCP 配置,按调用即时加载并释放,工具仅对该 agent 可见。
- **Anthropic Skills**:支持 Anthropic Agent Skills(SKILL.md 格式),遵循 agentskills.io 开放标准,提供领域知识注入能力。
-- **Bilibili 视频提取**:自动检测消息中的 B 站视频链接/BV 号/小程序分享,下载 1080p 视频并通过 QQ 发送;同时提供 AI 工具调用入口。
-- **arXiv 论文提取与搜索**:自动检测消息中的 arXiv 链接/标识并发送论文信息与 PDF;同时提供 `arxiv_paper` 发送工具和 `arxiv_search` 检索工具。
+- **Bilibili 视频提取与分析**:自动检测消息中的 B 站视频链接/BV 号/小程序分享,下载视频并通过 QQ 合并转发;`bilibili_video` 也可只返回附件 UID,供 `file_analysis_agent` 做视频内容分析。
+- **arXiv 论文提取、搜索与分析**:自动检测消息中的 arXiv 链接/标识并发送论文信息与 PDF;`arxiv_paper` 也可只返回 PDF 附件 UID,供 `file_analysis_agent` 做文本提取或指定页视觉分析;`arxiv_search` 负责论文检索。
- **GitHub 仓库卡片**:自动检测 GitHub 仓库链接或 `owner/repo` 仓库 ID,获取 public 仓库信息并发送简洁图片卡片,展示头像、简介、stars、forks、issues、contributors 等概览。
- **自动处理管线**:Bilibili、arXiv、GitHub 等自动提取统一运行在 `skills/pipelines` 中,斜杠命令优先级更高;命令输入/输出会写入历史,非命令消息会并行检测和处理命中管线,结果通过统一发送层写入历史并登记附件 UID 后再进入 AI 回复。远程大附件超过 `[attachments].remote_download_max_size_mb` 时只登记 URL 引用,避免无界下载和缓存膨胀。
- **同 sender 短时消息合并**:默认开启。连续发的多条消息会合并到同一轮 AI 调用,AI 一次看到全部意图自行识别"独立请求/修正/打断";告别"画猫→改成狗"的重复触发与回复打架。主提示词按 batcher 的"当前输入批次"语义适配,关闭该功能可能导致连续补充/修正消息与提示词不匹配,需要单独适配。可选投机预发送让用户停顿时 LLM 提前开跑、新消息可在未发出回复前取消,进一步压低响应延迟。详见 [docs/message-batching.md](docs/message-batching.md)。
diff --git a/apps/undefined-chat/package-lock.json b/apps/undefined-chat/package-lock.json
index b167d07b..077c6509 100644
--- a/apps/undefined-chat/package-lock.json
+++ b/apps/undefined-chat/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "undefined-chat",
- "version": "3.6.0",
+ "version": "3.6.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "undefined-chat",
- "version": "3.6.0",
+ "version": "3.6.1",
"dependencies": {
"@tauri-apps/api": "^2.3.0",
"@tauri-apps/plugin-dialog": "^2.7.1",
diff --git a/apps/undefined-chat/package.json b/apps/undefined-chat/package.json
index b1176d1e..71aaf1ae 100644
--- a/apps/undefined-chat/package.json
+++ b/apps/undefined-chat/package.json
@@ -1,7 +1,7 @@
{
"name": "undefined-chat",
"private": true,
- "version": "3.6.0",
+ "version": "3.6.1",
"type": "module",
"scripts": {
"tauri": "tauri",
diff --git a/apps/undefined-chat/src-tauri/Cargo.lock b/apps/undefined-chat/src-tauri/Cargo.lock
index 4c74e676..57704049 100644
--- a/apps/undefined-chat/src-tauri/Cargo.lock
+++ b/apps/undefined-chat/src-tauri/Cargo.lock
@@ -5431,7 +5431,7 @@ dependencies = [
[[package]]
name = "undefined_chat"
-version = "3.6.0"
+version = "3.6.1"
dependencies = [
"futures-util",
"keyring",
diff --git a/apps/undefined-chat/src-tauri/Cargo.toml b/apps/undefined-chat/src-tauri/Cargo.toml
index 399f3d1d..e75855b0 100644
--- a/apps/undefined-chat/src-tauri/Cargo.toml
+++ b/apps/undefined-chat/src-tauri/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "undefined_chat"
-version = "3.6.0"
+version = "3.6.1"
description = "Undefined native chat client"
authors = ["Undefined contributors"]
license = "MIT"
diff --git a/apps/undefined-chat/src-tauri/tauri.conf.json b/apps/undefined-chat/src-tauri/tauri.conf.json
index 1b38bb1a..215e05cb 100644
--- a/apps/undefined-chat/src-tauri/tauri.conf.json
+++ b/apps/undefined-chat/src-tauri/tauri.conf.json
@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "Undefined Chat",
- "version": "3.6.0",
+ "version": "3.6.1",
"identifier": "com.undefined.chat",
"build": {
"beforeDevCommand": "npm run dev",
diff --git a/apps/undefined-console/package-lock.json b/apps/undefined-console/package-lock.json
index 8809814a..313382f2 100644
--- a/apps/undefined-console/package-lock.json
+++ b/apps/undefined-console/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "undefined-console",
- "version": "3.6.0",
+ "version": "3.6.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "undefined-console",
- "version": "3.6.0",
+ "version": "3.6.1",
"dependencies": {
"@tauri-apps/api": "^2.3.0",
"@tauri-apps/plugin-http": "^2.3.0"
diff --git a/apps/undefined-console/package.json b/apps/undefined-console/package.json
index ca28f7bc..5b227cd3 100644
--- a/apps/undefined-console/package.json
+++ b/apps/undefined-console/package.json
@@ -1,7 +1,7 @@
{
"name": "undefined-console",
"private": true,
- "version": "3.6.0",
+ "version": "3.6.1",
"type": "module",
"scripts": {
"tauri": "tauri",
diff --git a/apps/undefined-console/src-tauri/Cargo.lock b/apps/undefined-console/src-tauri/Cargo.lock
index 5a6975dc..96b29fcd 100644
--- a/apps/undefined-console/src-tauri/Cargo.lock
+++ b/apps/undefined-console/src-tauri/Cargo.lock
@@ -4063,7 +4063,7 @@ checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb"
[[package]]
name = "undefined_console"
-version = "3.6.0"
+version = "3.6.1"
dependencies = [
"serde",
"serde_json",
diff --git a/apps/undefined-console/src-tauri/Cargo.toml b/apps/undefined-console/src-tauri/Cargo.toml
index e0bfa215..14001baf 100644
--- a/apps/undefined-console/src-tauri/Cargo.toml
+++ b/apps/undefined-console/src-tauri/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "undefined_console"
-version = "3.6.0"
+version = "3.6.1"
description = "Undefined cross-platform management console"
authors = ["Undefined contributors"]
license = "MIT"
diff --git a/apps/undefined-console/src-tauri/tauri.conf.json b/apps/undefined-console/src-tauri/tauri.conf.json
index 2b255807..3a2874f1 100644
--- a/apps/undefined-console/src-tauri/tauri.conf.json
+++ b/apps/undefined-console/src-tauri/tauri.conf.json
@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "Undefined Console",
- "version": "3.6.0",
+ "version": "3.6.1",
"identifier": "com.undefined.console",
"build": {
"beforeDevCommand": "npm run dev",
diff --git a/docs/configuration.md b/docs/configuration.md
index 1eb85423..dd7f441c 100644
--- a/docs/configuration.md
+++ b/docs/configuration.md
@@ -784,6 +784,7 @@ Prompt caching 补充:
- 命中 `arxiv.org/abs/...`、`arxiv.org/pdf/...` 或 `arXiv:` 时直接触发。
- 裸新式编号仅在消息中同时出现 `arxiv` 关键词时触发,避免误判普通数字串。
- PDF 下载或上传失败时不会额外发送失败提示,只保留论文信息消息。
+- 自动提取仍默认发送论文信息与 PDF;若用户要求分析 arXiv 论文内容,`file_analysis_agent` 会通过 `arxiv_paper(output_mode=uid)` 获取 PDF 附件 UID 后再分析。
---
diff --git a/docs/pipelines.md b/docs/pipelines.md
index 77eba0ed..07e89eb1 100644
--- a/docs/pipelines.md
+++ b/docs/pipelines.md
@@ -11,7 +11,7 @@
3. 若消息命中斜杠命令,立即分发命令并结束本轮后续流程;命令输入和命令输出会写入历史,供后续 AI 轮次读取。
4. 未命中命令时,`PipelineRegistry` 并行调用所有已注册管线的 `detect(context)`。
5. 对所有命中的管线,并行调用对应的 `process(detection, context)`。
-6. 管线发送出的信息、图片、文件或视频摘要通过统一发送器写入历史;本地图片、文件和视频会自动登记为当前会话可见的统一附件 UID,历史正文同样使用 `` 作为可复用引用。
+6. 管线发送出的信息、图片、文件或视频摘要通过统一发送器写入历史;本地图片、文件和视频会自动登记为当前会话可见的统一附件 UID,历史正文同样使用 `` 作为可复用引用。需要“只获取 UID 不发送”的 arXiv/Bilibili 分析场景由 `file_analysis_agent` 调用共享主工具处理,不改变自动管线的发送行为。
7. 自动处理完成后,当前消息和管线输出一起进入 AI 自动回复/Agent 循环。
命中自动处理管线的消息会继续进入 AI 自动回复,让 AI 基于用户消息和刚写入的自动处理结果判断后续行为。
diff --git a/docs/usage.md b/docs/usage.md
index 969c854b..791bc947 100644
--- a/docs/usage.md
+++ b/docs/usage.md
@@ -70,12 +70,14 @@ Undefined 搭载了基于 ChromaDB 向量数据库的后台认知系统,无需
### `file_analysis_agent` — 文件分析助手
-支持对代码、PDF、Word、Excel 等多种格式文件进行解析与分析。用户只需将文件发送至对话中即可。
+支持对代码、PDF、Word、Excel 等多种格式文件进行解析与分析。用户可以直接发送文件,也可以提供附件 UID、URL、arXiv ID/URL 或 Bilibili BV/AV/URL。
-**子工具**:`analyze_pdf`、`analyze_docx`、`analyze_xlsx`、`analyze_code`、`read_file`
+**子工具**:`download_file`、`extract_pdf`、`describe_pdf_page`、`extract_docx`、`extract_xlsx`、`analyze_code`、`analyze_multimodal`
**示例:**
> *"请分析这份 PDF 文档,提取其中第三章的核心数据。"*
+> *"请看 arXiv:2501.01234 的第 3-5 页图表,解释实验结论。"*
+> *"分析这个 BV1xx411c7mD 视频里主要讲了什么。"*
> *"请检查这份 Python 代码,找出其中潜在的性能瓶颈。"*
---
@@ -283,8 +285,8 @@ Undefined 搭载了基于 ChromaDB 向量数据库的后台认知系统,无需
| `get_picture` | 获取指定类型的图片(二次元、壁纸、白丝、黑丝、JK、历史上的今天等 10 余种类别) |
| `qq_like` | 给指定 QQ 号的资料卡点赞(默认 10 次) |
| `python_interpreter` | 在隔离的 **Docker 容器**中执行 Python 代码,支持按需安装第三方库,可在执行后自动发送生成的文件(图片、CSV 等) |
-| `bilibili_video` | 下载并发送哔哩哔哩视频(支持 BV 号、链接) |
-| `arxiv_paper` | 下载并发送 arXiv 论文 PDF(支持 arXiv ID、链接) |
+| `bilibili_video` | 下载并发送哔哩哔哩视频;也支持返回视频附件 UID 供文件分析(支持 BV 号、链接) |
+| `arxiv_paper` | 下载并发送 arXiv 论文 PDF;也支持返回 PDF 附件 UID 供文件分析(支持 arXiv ID、链接) |
| `fetch_image_uid` | 将指定 URL 的图片下载并转换为系统内部 uid |
| `task_progress` | 向用户发送长任务的阶段性进度通知 |
| `changelog_query` | 查询系统内置版本更新日志 |
diff --git a/pyproject.toml b/pyproject.toml
index ef7d9e03..e36a5b7f 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "Undefined-bot"
-version = "3.6.0"
+version = "3.6.1"
description = "QQ bot platform with cognitive memory architecture and multi-agent Skills, via OneBot V11."
readme = "README.md"
authors = [
diff --git a/scripts/README.md b/scripts/README.md
index 3330daa7..65ff0c72 100644
--- a/scripts/README.md
+++ b/scripts/README.md
@@ -143,6 +143,7 @@ uv run python scripts/bump_version.py 3.6.0 --commit
- `apps/undefined-chat/src-tauri/Cargo.lock`
非 dry-run 时脚本还会执行 `uv sync`,并分别在 Console / Chat 下执行 `npm install --package-lock-only` 与 `cargo update --workspace`,保证 lock 文件和 manifest 不漂移。
+脚本更新 JSON manifest 时只替换顶层 `version` 字段,保留现有格式,避免 Tauri 配置与 Biome 格式化规则漂移。
### prepare_tauri_android.py — 生成后 Android 修补
diff --git a/scripts/bump_version.py b/scripts/bump_version.py
index de49ddcb..f4de994d 100644
--- a/scripts/bump_version.py
+++ b/scripts/bump_version.py
@@ -68,6 +68,30 @@ def _update_text_file(
return _write_if_changed(path, new_text, dry_run)
+def _update_json_string_field(
+ path: Path,
+ field: str,
+ value: str,
+ dry_run: bool,
+) -> bool:
+ text = path.read_text(encoding="utf-8")
+ data = cast(dict[str, Any], json.loads(text))
+ old_value = data.get(field)
+ if not isinstance(old_value, str):
+ raise ValueError(f"{path} 缺少字符串字段 {field}")
+ if old_value == value:
+ return False
+
+ field_pattern = re.compile(
+ rf'^(\s*"{re.escape(field)}"\s*:\s*)"[^"]*"',
+ re.MULTILINE,
+ )
+ new_text, count = field_pattern.subn(rf'\g<1>"{value}"', text, count=1)
+ if count == 0:
+ raise ValueError(f"{path} 未匹配到 {field} 字段")
+ return _write_if_changed(path, new_text, dry_run)
+
+
def _update_json_version(path: Path, version: str, dry_run: bool) -> bool:
data = cast(dict[str, Any], json.loads(path.read_text(encoding="utf-8")))
old_version = data.get("version")
@@ -159,7 +183,7 @@ def _update_native_app_versions(
updates: tuple[tuple[Path, bool], ...] = (
(
package_json,
- _update_json_version(package_json, version, dry_run),
+ _update_json_string_field(package_json, "version", version, dry_run),
),
(
package_lock,
@@ -171,7 +195,7 @@ def _update_native_app_versions(
),
(
tauri_conf,
- _update_json_version(tauri_conf, version, dry_run),
+ _update_json_string_field(tauri_conf, "version", version, dry_run),
),
(
cargo_lock,
diff --git a/src/Undefined/__init__.py b/src/Undefined/__init__.py
index 2187a8be..5ed45c18 100644
--- a/src/Undefined/__init__.py
+++ b/src/Undefined/__init__.py
@@ -24,7 +24,7 @@
from .skills.registry import BaseRegistry as BaseRegistry
from .skills.tools import ToolRegistry as ToolRegistry
-__version__ = "3.6.0"
+__version__ = "3.6.1"
# symbol -> (module_path, attribute_name);首次访问时才 importlib 加载
_LAZY_IMPORTS: dict[str, tuple[str, str]] = {
diff --git a/src/Undefined/arxiv/sender.py b/src/Undefined/arxiv/sender.py
index 499dc49d..5ee14c75 100644
--- a/src/Undefined/arxiv/sender.py
+++ b/src/Undefined/arxiv/sender.py
@@ -4,8 +4,9 @@
import asyncio
import logging
+from pathlib import Path
import time
-from typing import TYPE_CHECKING, Literal
+from typing import TYPE_CHECKING, Any, Literal
from Undefined.arxiv.client import get_paper_info
from Undefined.arxiv.downloader import cleanup_download_path, download_paper_pdf
@@ -144,6 +145,43 @@ async def _send_file_message(
await sender.send_private_file(target_id, file_path, file_name)
+def _build_uid_message(info: PaperInfo, uid: str, file_name: str) -> str:
+ title = info.title or f"arXiv:{info.paper_id}"
+ lines = [
+ f"已获取 arXiv 论文 PDF:{title}",
+ f"ID: {info.paper_id}",
+ f'PDF: ',
+ ]
+ if file_name:
+ lines.append(f"文件名: {file_name}")
+ if info.abs_url:
+ lines.append(info.abs_url)
+ return "\n".join(lines)
+
+
+async def _register_pdf_attachment(
+ *,
+ attachment_registry: Any,
+ scope_key: str,
+ pdf_path: Path,
+ info: PaperInfo,
+) -> str:
+ record = await attachment_registry.register_local_file(
+ scope_key,
+ pdf_path,
+ kind="file",
+ display_name=pdf_path.name,
+ source_kind="arxiv_paper",
+ source_ref=info.pdf_url or _build_pdf_url(info.paper_id),
+ segment_data={
+ "paper_id": info.paper_id,
+ "abs_url": info.abs_url,
+ "pdf_url": info.pdf_url or _build_pdf_url(info.paper_id),
+ },
+ )
+ return _build_uid_message(info, str(record.uid), pdf_path.name)
+
+
async def _send_arxiv_paper_once(
*,
paper_id: str,
@@ -205,6 +243,60 @@ async def _send_arxiv_paper_once(
await cleanup_download_path(task_dir)
+async def fetch_arxiv_paper_attachment(
+ *,
+ paper_id: str,
+ attachment_registry: Any,
+ scope_key: str,
+ max_file_size: int,
+ author_preview_limit: int,
+ summary_preview_chars: int,
+ context: dict[str, object] | None = None,
+) -> str:
+ """下载 arXiv PDF 并注册为附件 UID,不发送消息。"""
+ normalized = normalize_arxiv_id(paper_id)
+ if normalized is None:
+ return f"无法解析 arXiv 标识: {paper_id}"
+
+ if attachment_registry is None:
+ return "缺少必要的运行时组件(attachment_registry)"
+ if not str(scope_key or "").strip():
+ return "无法确定附件作用域,不能注册 arXiv PDF"
+
+ info: PaperInfo
+ metadata_ready = True
+ try:
+ info = await get_paper_info(normalized, context=context)
+ except Exception:
+ metadata_ready = False
+ info = _minimal_paper_info(normalized)
+ logger.exception("[arXiv] 获取论文元信息失败: paper=%s", normalized)
+
+ download_result, task_dir = await download_paper_pdf(
+ info,
+ max_file_size_mb=max_file_size,
+ context=context,
+ )
+ try:
+ if download_result.path is None:
+ info_message = _build_info_message(
+ info,
+ author_preview_limit=author_preview_limit,
+ summary_preview_chars=summary_preview_chars,
+ )
+ if metadata_ready:
+ return f"未能下载 PDF,已获取论文信息:\n{info_message}"
+ return f"未能下载 PDF,已获取论文最小信息:\n{info_message}"
+ return await _register_pdf_attachment(
+ attachment_registry=attachment_registry,
+ scope_key=scope_key,
+ pdf_path=download_result.path,
+ info=info,
+ )
+ finally:
+ await cleanup_download_path(task_dir)
+
+
async def send_arxiv_paper(
*,
paper_id: str,
diff --git a/src/Undefined/bilibili/sender.py b/src/Undefined/bilibili/sender.py
index 84e471f9..fb8d9190 100644
--- a/src/Undefined/bilibili/sender.py
+++ b/src/Undefined/bilibili/sender.py
@@ -16,6 +16,7 @@
)
from Undefined.bilibili.models import DanmakuItem
from Undefined.bilibili.parser import normalize_to_bvid
+from Undefined.utils.io import get_file_size
if TYPE_CHECKING:
from Undefined.bilibili.downloader import VideoInfo
@@ -142,6 +143,27 @@ def _build_video_history_message(
return "\n".join(lines)
+def _build_uid_message(
+ info: "VideoInfo",
+ *,
+ uid: str,
+ quality_name: str | None,
+ file_size_mb: float | None,
+ file_name: str,
+) -> str:
+ lines = [
+ f"已获取 Bilibili 视频:{info.title}",
+ f"BV: {info.bvid}",
+ f'视频: ',
+ ]
+ if quality_name and file_size_mb is not None:
+ lines.append(f"清晰度: {quality_name} | 大小: {file_size_mb:.1f}MB")
+ if file_name:
+ lines.append(f"文件名: {file_name}")
+ lines.append(info.url)
+ return "\n".join(lines)
+
+
def _build_danmaku_text(item: DanmakuItem) -> str:
return f"[{_format_progress(item.progress_ms)}] {item.content}"
@@ -289,7 +311,7 @@ async def send_bilibili_video(
)
info_prefix = f"({video_status})"
else:
- file_size_mb = video_path.stat().st_size / 1024 / 1024
+ file_size_mb = await get_file_size(video_path) / 1024 / 1024
max_size = max_file_size if max_file_size > 0 else float("inf")
if file_size_mb > max_size:
@@ -310,7 +332,7 @@ async def send_bilibili_video(
max_duration=max_duration,
)
if video_path is not None:
- file_size_mb = video_path.stat().st_size / 1024 / 1024
+ file_size_mb = await get_file_size(video_path) / 1024 / 1024
if video_path is not None and file_size_mb is not None:
if file_size_mb > max_size:
@@ -403,6 +425,107 @@ async def send_bilibili_video(
cleanup_file(video_path)
+async def fetch_bilibili_video_attachment(
+ video_id: str,
+ *,
+ attachment_registry: Any,
+ scope_key: str,
+ cookie: str = "",
+ prefer_quality: int = 80,
+ max_duration: int = 0,
+ max_file_size: int = 0,
+ oversize_strategy: str = "downgrade",
+ sessdata: str = "",
+) -> str:
+ """下载 Bilibili 视频并注册为附件 UID,不发送消息。"""
+ bvid = await normalize_to_bvid(video_id)
+ if not bvid:
+ return f"无法解析视频标识: {video_id}"
+ if attachment_registry is None:
+ return "缺少必要的运行时组件(attachment_registry)"
+ if not str(scope_key or "").strip():
+ return "无法确定附件作用域,不能注册 Bilibili 视频"
+
+ if not cookie and sessdata:
+ cookie = sessdata
+
+ video_path: Path | None = None
+ video_info: VideoInfo | None = None
+ actual_qn = 0
+ file_size_mb: float | None = None
+
+ try:
+ video_path, video_info, actual_qn = await download_video(
+ bvid=bvid,
+ cookie=cookie,
+ prefer_quality=prefer_quality,
+ max_duration=max_duration,
+ )
+ if video_path is None:
+ return f"视频时长 {_format_duration(video_info.duration)} 超过限制,未下载视频文件"
+
+ file_size_mb = await get_file_size(video_path) / 1024 / 1024
+ max_size = max_file_size if max_file_size > 0 else float("inf")
+
+ if file_size_mb > max_size:
+ if oversize_strategy == "downgrade" and actual_qn > 32:
+ cleanup_file(video_path)
+ video_path = None
+ lower_qn = _get_lower_quality(actual_qn)
+ logger.info(
+ "[Bilibili] 文件 %.1fMB 超限 %dMB,UID 模式降级到 qn=%d 重试",
+ file_size_mb,
+ max_file_size,
+ lower_qn,
+ )
+ video_path, video_info, actual_qn = await download_video(
+ bvid=bvid,
+ cookie=cookie,
+ prefer_quality=lower_qn,
+ max_duration=max_duration,
+ )
+ if video_path is None:
+ return "降级后仍未下载到视频文件"
+ file_size_mb = await get_file_size(video_path) / 1024 / 1024
+
+ if file_size_mb > max_size:
+ return f"视频文件 {file_size_mb:.1f}MB 超过限制,未注册附件"
+
+ quality_name = QUALITY_MAP.get(actual_qn, str(actual_qn)) if actual_qn else None
+ record = await attachment_registry.register_local_file(
+ scope_key,
+ video_path,
+ kind="file",
+ display_name=video_path.name,
+ source_kind="bilibili_video",
+ source_ref=video_info.url,
+ segment_data={
+ "bvid": video_info.bvid,
+ "url": video_info.url,
+ "title": video_info.title,
+ "quality": quality_name or "",
+ },
+ )
+ return _build_uid_message(
+ video_info,
+ uid=str(record.uid),
+ quality_name=quality_name,
+ file_size_mb=file_size_mb,
+ file_name=video_path.name,
+ )
+ except Exception as exc:
+ logger.exception("[Bilibili] UID 模式处理视频失败: %s", bvid)
+ if video_info is None:
+ try:
+ video_info = await get_video_info(bvid, cookie=cookie)
+ except Exception:
+ return f"视频处理失败:无法获取视频信息: {exc}"
+ return f"视频处理失败:{exc}"
+ finally:
+ if video_path is not None:
+ cleanup_file(video_path)
+
+
def _get_lower_quality(current_qn: int) -> int:
"""获取比当前清晰度低一级的 qn。"""
ordered = sorted(QUALITY_MAP.keys(), reverse=True)
diff --git a/src/Undefined/skills/agents/README.md b/src/Undefined/skills/agents/README.md
index f9acf715..a0941365 100644
--- a/src/Undefined/skills/agents/README.md
+++ b/src/Undefined/skills/agents/README.md
@@ -260,33 +260,51 @@ mv skills/tools/my_tool skills/agents/my_agent/tools/
## 现有 Agents
### web_agent(网络搜索助手)
-- **功能**:网页搜索和网页内容获取
-- **适用场景**:获取互联网最新信息、搜索新闻、爬取网页内容
-- **子工具**:`grok_search`, `web_search`, `crawl_webpage`
+- **功能**:联网搜索、网页阅读、来源核验和最新信息获取。
+- **适用场景**:新闻/公告/资料搜索、指定 URL 摘要、多来源对比、时效性问题核验。
+- **不适用**:天气、金价、热搜、Whois、B 站、arXiv 检索等结构化查询;用户附件或文件解析。
+- **子工具**:`grok_search`, `web_search`, `crawl_webpage`。
- **grok_search 参数**:优先使用 `search_request`,用自然语言完整叙述搜索要求,不要只传关键词。
### file_analysis_agent(文件分析助手)
-- **功能**:分析代码、PDF、Docx、Xlsx 等多种格式文件
-- **适用场景**:代码分析、文档解析、文件内容提取
-- **子工具**:`read_file`, `analyze_code`, `analyze_pdf`, `analyze_docx`, `analyze_xlsx`
+- **功能**:分析用户提供的附件、内部 UID、URL、legacy file_id、arXiv 论文标识或 Bilibili 视频标识,提取文件内容。
+- **适用场景**:PDF/Word/Excel/PPT/文本/代码/压缩包解析,图片、音频、视频等多模态内容识别,arXiv 论文 PDF 分析,Bilibili 视频内容分析。
+- **不适用**:没有文件来源的开放式搜索、需要联网查资料的问题、执行文件或安全鉴定。
+- **子工具**:`download_file`, `detect_file_type`, `read_text_file`, `extract_pdf`, `describe_pdf_page`, `extract_docx`, `extract_xlsx`, `extract_pptx`, `extract_archive`, `analyze_code`, `analyze_multimodal`, `cleanup_temp`;还可调用共享主工具 `arxiv_paper(output_mode=uid)` 与 `bilibili_video(output_mode=uid)` 获取待分析附件 UID。
### naga_code_analysis_agent(NagaAgent 代码分析助手)
-- **功能**:专门用于分析 NagaAgent 框架源码
-- **适用场景**:深入分析 NagaAgent 架构、模块实现、代码线索
-- **子工具**:`read_file`, `list_directory`, `glob`, `search_file_content`, `read_naga_intro`
+- **功能**:只读分析 NagaAgent 项目的结构、源码、配置、构建、部署和实现细节。
+- **适用场景**:追踪 NagaAgent 模块实现、目录结构、配置项、报错线索和项目内文档。
+- **不适用**:Undefined 自身源码、用户上传文件、代码编写修改、执行验证或外部联网搜索。
+- **子工具**:`read_file`, `list_directory`, `glob`, `search_file_content`, `read_naga_intro`。
### undefined_self_code_agent(Undefined 自身代码查阅助手)
-- **功能**:只读查阅 Undefined 当前仓库的源码、测试、文档、资源、脚本、配置示例和 App 实现
-- **适用场景**:解释 Undefined 自身实现、定位模块、核对配置示例、查看测试覆盖
-- **访问范围**:`src/`, `scripts/`, `tests/`, `res/`, `docs/`, `apps/`, `README.md`, `CHANGELOG.md`, `ARCHITECTURE.md`, `config.toml.example`
-- **子工具**:`read_file`, `list_directory`, `glob`, `search_file_content`
+- **功能**:只读查阅 Undefined 当前仓库的源码、测试、文档、资源、脚本、配置示例和 App 实现。
+- **适用场景**:解释 Undefined 自身实现、定位模块、核对配置示例、查看测试覆盖。
+- **访问范围**:`src/`, `scripts/`, `tests/`, `res/`, `docs/`, `apps/`, `README.md`, `CHANGELOG.md`, `ARCHITECTURE.md`, `config.toml.example`。
+- **不适用**:写代码、改文件、运行命令、验证打包、NagaAgent 子模块、用户上传文件解析。
+- **子工具**:`read_file`, `list_directory`, `glob`, `search_file_content`。
### info_agent(信息查询助手)
-- **功能**:查询天气、热搜、历史、WHOIS、B 站信息、arXiv 检索等
-- **适用场景**:天气查询、热点榜单、域名查询、B 站视频和 UP 主信息查询、论文搜索
-- **子工具**:`weather_query`, `*hot`, `whois`, `bilibili_search`, `bilibili_user_info`, `arxiv_search`
+- **功能**:调用结构化工具完成参数明确的信息查询。
+- **适用场景**:天气、金价、热搜、历史、Whois、网络诊断、测速、编码/哈希、B 站、QQ 等级、arXiv 检索。
+- **不适用**:开放式网页搜索、网页阅读、来源核验、文件解析或长篇研究。
+- **子工具**:`weather_query`, `gold_price`, `baiduhot`, `weibohot`, `douyinhot`, `history`, `whois`, `net_check`, `speed`, `tcping`, `base64`, `hash`, `bilibili_search`, `bilibili_user_info`, `qq_level_query`, `arxiv_search`。
### entertainment_agent(娱乐助手)
-- **功能**:运势、小说、创意内容与随机视频推荐等娱乐功能
-- **适用场景**:查看运势、获取休闲内容、随机刷视频
-- **子工具**:`horoscope`, `novel_search`, `ai_draw_one`, `video_random_recommend`
+- **功能**:轻松互动、趣味内容和休闲创作。
+- **适用场景**:AI 绘画、参考图生图、星座运势、趣味占卜、随机内容、Minecraft 皮肤/头像渲染、小说搜索、表情包或反应图需求。
+- **不适用**:严肃专业建议、事实核验、新闻资料搜索、文件内容解析。
+- **子工具**:`ai_draw_one`, `horoscope`, `minecraft_skin`, `renjian`, `wenchang_dijun`。
+
+### summary_agent(消息总结助手)
+- **功能**:按条数或时间范围拉取聊天记录并生成客观总结。
+- **适用场景**:总结最近 N 条消息、过去一段时间的讨论、指定主题的结论、待办、参与者贡献和链接资源。
+- **不适用**:实时监控、情绪评判、未来预测、脱离聊天记录的推测;`/summary` 与 `/sum` 斜杠命令由命令层直连 summary 模型。
+- **子工具**:`fetch_messages`。
+
+### code_delivery_agent(代码交付助手)
+- **功能**:把代码需求实现为可交付的文件或工程。
+- **适用场景**:单文件脚本/配置/文档交付,多文件工程创建、修改、调试、测试和打包,从空目录或 Git 仓库开始。
+- **不适用**:只读源码讲解、用户上传文件解析、单纯资料调研。
+- **子工具**:`init_docker`, `read`, `write`, `copy`, `delete`, `tree`, `glob`, `grep`, `diff`, `run_bash_command`, `todo`, `end`。
diff --git a/src/Undefined/skills/agents/arxiv_analysis_agent/__init__.py b/src/Undefined/skills/agents/arxiv_analysis_agent/__init__.py
deleted file mode 100644
index e69de29b..00000000
diff --git a/src/Undefined/skills/agents/arxiv_analysis_agent/callable.json b/src/Undefined/skills/agents/arxiv_analysis_agent/callable.json
deleted file mode 100644
index 855776f7..00000000
--- a/src/Undefined/skills/agents/arxiv_analysis_agent/callable.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
- "enabled": true,
- "allowed_callers": ["info_agent", "web_agent", "naga_code_analysis_agent"]
-}
diff --git a/src/Undefined/skills/agents/arxiv_analysis_agent/config.json b/src/Undefined/skills/agents/arxiv_analysis_agent/config.json
deleted file mode 100644
index a97b0a97..00000000
--- a/src/Undefined/skills/agents/arxiv_analysis_agent/config.json
+++ /dev/null
@@ -1,21 +0,0 @@
-{
- "type": "function",
- "function": {
- "name": "arxiv_analysis_agent",
- "description": "arXiv 论文深度分析助手,下载并解析 arXiv 论文 PDF 全文,进行结构化学术深度分析。支持按 arXiv ID 或 URL 获取论文。",
- "parameters": {
- "type": "object",
- "properties": {
- "paper_id": {
- "type": "string",
- "description": "arXiv 论文 ID(如 '2301.07041')、arXiv URL(如 'https://arxiv.org/abs/2301.07041')或 'arXiv:2301.07041' 格式"
- },
- "prompt": {
- "type": "string",
- "description": "可选的分析需求,例如:'重点分析方法论和实验设计'、'关注与 diffusion model 的对比'"
- }
- },
- "required": ["paper_id"]
- }
- }
-}
diff --git a/src/Undefined/skills/agents/arxiv_analysis_agent/handler.py b/src/Undefined/skills/agents/arxiv_analysis_agent/handler.py
deleted file mode 100644
index 024a9d7f..00000000
--- a/src/Undefined/skills/agents/arxiv_analysis_agent/handler.py
+++ /dev/null
@@ -1,51 +0,0 @@
-from __future__ import annotations
-
-import logging
-from pathlib import Path
-from typing import Any
-
-from Undefined.arxiv.parser import normalize_arxiv_id
-from Undefined.skills.agents.runner import (
- DEFAULT_AGENT_MAX_ITERATIONS,
- run_agent_with_tools,
-)
-
-logger = logging.getLogger(__name__)
-
-
-async def execute(args: dict[str, Any], context: dict[str, Any]) -> str:
- """执行 arxiv_analysis_agent。"""
-
- raw_paper_id = str(args.get("paper_id", "")).strip()
- user_prompt = str(args.get("prompt", "")).strip()
-
- if not raw_paper_id:
- return "请提供 arXiv 论文 ID 或 URL"
-
- paper_id = normalize_arxiv_id(raw_paper_id)
- if paper_id is None:
- return f"无法解析 arXiv 标识:{raw_paper_id}"
-
- context["arxiv_paper_id"] = paper_id
-
- context_messages = [
- {
- "role": "system",
- "content": f"当前任务:深度分析 arXiv 论文 {paper_id}",
- }
- ]
-
- user_content = user_prompt if user_prompt else f"请深度分析论文 arXiv:{paper_id}"
-
- return await run_agent_with_tools(
- agent_name="arxiv_analysis_agent",
- user_content=user_content,
- context_messages=context_messages,
- empty_user_content_message="请提供 arXiv 论文 ID 或 URL",
- default_prompt="你是一个学术论文深度分析助手。",
- context=context,
- agent_dir=Path(__file__).parent,
- logger=logger,
- max_iterations=DEFAULT_AGENT_MAX_ITERATIONS,
- tool_error_prefix="错误",
- )
diff --git a/src/Undefined/skills/agents/arxiv_analysis_agent/intro.md b/src/Undefined/skills/agents/arxiv_analysis_agent/intro.md
deleted file mode 100644
index 1b306e32..00000000
--- a/src/Undefined/skills/agents/arxiv_analysis_agent/intro.md
+++ /dev/null
@@ -1 +0,0 @@
-arXiv 论文深度分析助手:下载 arXiv 论文 PDF 全文并进行结构化学术深度分析,涵盖方法论、实验、创新点、局限性等维度。
diff --git a/src/Undefined/skills/agents/arxiv_analysis_agent/prompt.md b/src/Undefined/skills/agents/arxiv_analysis_agent/prompt.md
deleted file mode 100644
index 029ab1e8..00000000
--- a/src/Undefined/skills/agents/arxiv_analysis_agent/prompt.md
+++ /dev/null
@@ -1,28 +0,0 @@
-你是学术论文深度分析助手,专门对 arXiv 论文进行全面、结构化的学术分析。
-
-工作流程:
-1. 先调用 `fetch_paper` 获取论文元数据和摘要
-2. 调用 `read_paper_pages` 分批阅读论文全文(每次读取一定页数范围)
-3. 读完后产出结构化深度分析
-
-阅读策略:
-- 先通过 `fetch_paper` 了解总页数和摘要
-- 用 `read_paper_pages` 按区间阅读(如 1-5、6-10、11-15 等),每次读 5 页
-- 对于较长的论文(>20 页),可以选择性跳过附录/参考文献,集中精力分析正文
-- 短论文可以一次性读完
-
-分析输出结构:
-- **概要**:一句话总结论文核心贡献
-- **研究背景与动机**:论文要解决什么问题、为什么重要
-- **方法论**:核心技术方案、算法、模型架构的详细解析
-- **实验与结果**:实验设置、基准对比、主要结论
-- **创新点与贡献**:论文的主要新颖之处
-- **局限性与未来方向**:作者提到的或你分析出的不足和可改进之处
-- **总评**:论文的整体质量和影响力评估
-
-注意事项:
-- 保持学术严谨,用客观语言描述
-- 如果用户指定了分析侧重(prompt),要重点展开相关部分
-- 对公式和算法用自然语言解释,不要直接复制 LaTeX
-- PDF 提取可能有格式问题(乱码/缺失),遇到时说明情况继续分析
-- 如果论文是中文,用中文输出分析;否则用中文输出分析但保留关键术语英文原文
diff --git a/src/Undefined/skills/agents/arxiv_analysis_agent/tools/__init__.py b/src/Undefined/skills/agents/arxiv_analysis_agent/tools/__init__.py
deleted file mode 100644
index e69de29b..00000000
diff --git a/src/Undefined/skills/agents/arxiv_analysis_agent/tools/fetch_paper/config.json b/src/Undefined/skills/agents/arxiv_analysis_agent/tools/fetch_paper/config.json
deleted file mode 100644
index 5e69a691..00000000
--- a/src/Undefined/skills/agents/arxiv_analysis_agent/tools/fetch_paper/config.json
+++ /dev/null
@@ -1,17 +0,0 @@
-{
- "type": "function",
- "function": {
- "name": "fetch_paper",
- "description": "获取 arXiv 论文元数据并下载 PDF,返回论文基本信息(标题、作者、摘要、页数)。调用后可用 read_paper_pages 分页阅读正文。",
- "parameters": {
- "type": "object",
- "properties": {
- "paper_id": {
- "type": "string",
- "description": "arXiv 论文 ID,如 '2301.07041'"
- }
- },
- "required": ["paper_id"]
- }
- }
-}
diff --git a/src/Undefined/skills/agents/arxiv_analysis_agent/tools/fetch_paper/handler.py b/src/Undefined/skills/agents/arxiv_analysis_agent/tools/fetch_paper/handler.py
deleted file mode 100644
index d75cfcd1..00000000
--- a/src/Undefined/skills/agents/arxiv_analysis_agent/tools/fetch_paper/handler.py
+++ /dev/null
@@ -1,75 +0,0 @@
-"""获取 arXiv 论文元数据并下载 PDF 到本地缓存。"""
-
-from __future__ import annotations
-
-import logging
-from typing import Any
-
-import fitz
-
-from Undefined.arxiv.client import get_paper_info
-from Undefined.arxiv.downloader import download_paper_pdf
-from Undefined.arxiv.parser import normalize_arxiv_id
-
-logger = logging.getLogger(__name__)
-
-_MAX_FILE_SIZE_MB = 50
-
-
-async def execute(args: dict[str, Any], context: dict[str, Any]) -> str:
- raw_id = str(args.get("paper_id", "")).strip()
- if not raw_id:
- return "错误:请提供 arXiv 论文 ID"
-
- paper_id = normalize_arxiv_id(raw_id) or raw_id
-
- request_context = {"request_id": context.get("request_id", "-")}
-
- try:
- paper = await get_paper_info(paper_id, context=request_context)
- except Exception as exc:
- logger.exception("[arxiv_analysis] 获取论文元数据失败: %s", exc)
- return f"错误:获取论文 {paper_id} 元数据失败 — {exc}"
-
- if paper is None:
- return f"错误:未找到论文 arXiv:{paper_id}"
-
- lines: list[str] = [
- f"论文: {paper.title}",
- f"ID: {paper.paper_id}",
- f"作者: {'、'.join(paper.authors[:10])}{'(等 ' + str(len(paper.authors)) + ' 位)' if len(paper.authors) > 10 else ''}",
- f"分类: {paper.primary_category}",
- f"发布: {paper.published[:10]}",
- f"更新: {paper.updated[:10]}",
- f"链接: {paper.abs_url}",
- f"\n摘要:\n{paper.summary}",
- ]
-
- try:
- result, task_dir = await download_paper_pdf(
- paper, max_file_size_mb=_MAX_FILE_SIZE_MB, context=request_context
- )
- except Exception as exc:
- logger.exception("[arxiv_analysis] PDF 下载失败: %s", exc)
- lines.append(f"\nPDF 下载失败: {exc}(可基于摘要进行分析)")
- return "\n".join(lines)
-
- if result.path is None:
- lines.append(f"\nPDF 不可用(状态: {result.status}),请基于摘要进行分析")
- return "\n".join(lines)
-
- try:
- doc = fitz.open(str(result.path))
- try:
- page_count = len(doc)
- lines.append(f"\nPDF 已下载: {page_count} 页")
- context["_arxiv_pdf_path"] = str(result.path)
- context["_arxiv_pdf_pages"] = page_count
- context["_arxiv_task_dir"] = str(task_dir)
- finally:
- doc.close()
- except Exception as exc:
- logger.exception("[arxiv_analysis] PDF 打开失败: %s", exc)
- lines.append(f"\nPDF 无法打开: {exc}(可基于摘要进行分析)")
-
- return "\n".join(lines)
diff --git a/src/Undefined/skills/agents/arxiv_analysis_agent/tools/read_paper_pages/config.json b/src/Undefined/skills/agents/arxiv_analysis_agent/tools/read_paper_pages/config.json
deleted file mode 100644
index abc268e5..00000000
--- a/src/Undefined/skills/agents/arxiv_analysis_agent/tools/read_paper_pages/config.json
+++ /dev/null
@@ -1,21 +0,0 @@
-{
- "type": "function",
- "function": {
- "name": "read_paper_pages",
- "description": "读取已下载论文 PDF 的指定页范围文本。必须先调用 fetch_paper 下载论文。",
- "parameters": {
- "type": "object",
- "properties": {
- "start_page": {
- "type": "integer",
- "description": "起始页码(从 1 开始)"
- },
- "end_page": {
- "type": "integer",
- "description": "结束页码(包含该页)"
- }
- },
- "required": ["start_page", "end_page"]
- }
- }
-}
diff --git a/src/Undefined/skills/agents/arxiv_analysis_agent/tools/read_paper_pages/handler.py b/src/Undefined/skills/agents/arxiv_analysis_agent/tools/read_paper_pages/handler.py
deleted file mode 100644
index ed013d1d..00000000
--- a/src/Undefined/skills/agents/arxiv_analysis_agent/tools/read_paper_pages/handler.py
+++ /dev/null
@@ -1,68 +0,0 @@
-"""分页读取已下载的 arXiv 论文 PDF 文本。"""
-
-from __future__ import annotations
-
-import logging
-from typing import Any
-
-import fitz
-
-logger = logging.getLogger(__name__)
-
-_MAX_CHARS_PER_READ = 15000
-
-
-async def execute(args: dict[str, Any], context: dict[str, Any]) -> str:
- pdf_path = context.get("_arxiv_pdf_path")
- total_pages = context.get("_arxiv_pdf_pages")
-
- if not pdf_path:
- return "错误:请先调用 fetch_paper 下载论文"
-
- try:
- start_page = int(args.get("start_page", 1))
- end_page = int(args.get("end_page", start_page))
- except (TypeError, ValueError):
- return "错误:页码必须为整数"
-
- if start_page < 1:
- start_page = 1
- if total_pages and end_page > total_pages:
- end_page = total_pages
- if start_page > end_page:
- return f"错误:起始页 {start_page} 大于结束页 {end_page}"
-
- try:
- doc = fitz.open(pdf_path)
- except Exception as exc:
- logger.exception("[arxiv_analysis] PDF 打开失败: %s", exc)
- return f"错误:PDF 文件无法打开 — {exc}"
-
- try:
- actual_pages = len(doc)
- if start_page > actual_pages:
- return f"错误:论文共 {actual_pages} 页,请求的起始页 {start_page} 超出范围"
-
- end_page = min(end_page, actual_pages)
- text_parts: list[str] = []
- total_chars = 0
-
- for page_num in range(start_page - 1, end_page):
- page = doc.load_page(page_num)
- raw_text = page.get_text()
- page_text = str(raw_text) if raw_text else ""
-
- if total_chars + len(page_text) > _MAX_CHARS_PER_READ and text_parts:
- text_parts.append(
- f"\n[第 {page_num + 1} 页起文本已截断,请用更小的页范围重新读取]"
- )
- break
-
- text_parts.append(f"--- 第 {page_num + 1} 页 ---")
- text_parts.append(page_text if page_text.strip() else "(此页无可提取文本)")
- total_chars += len(page_text)
-
- header = f"论文内容(第 {start_page}-{end_page} 页,共 {actual_pages} 页)"
- return f"{header}\n\n" + "\n".join(text_parts)
- finally:
- doc.close()
diff --git a/src/Undefined/skills/agents/code_delivery_agent/intro.md b/src/Undefined/skills/agents/code_delivery_agent/intro.md
index 0e08a787..7a771e1d 100644
--- a/src/Undefined/skills/agents/code_delivery_agent/intro.md
+++ b/src/Undefined/skills/agents/code_delivery_agent/intro.md
@@ -1 +1,16 @@
-代码交付 Agent。支持单文件轻量交付和多文件工程交付两种模式:(1) 单文件模式:直接发送单个文本文件(脚本、配置、文档),无需 Docker;(2) 多文件模式:在隔离的 Docker 容器中编写代码、执行命令、运行验证,最终打包上传。支持从 Git 仓库克隆或空目录开始。
+# 代码交付助手
+
+用于把用户的代码需求实际做成可交付文件或工程。
+
+可处理:
+- 单个脚本、配置、文档等轻量文本文件交付
+- 多文件工程创建、修改、调试、测试和打包
+- 从空目录开始,或从 Git 仓库指定分支/tag/commit 开始
+- 在隔离 Docker 容器中安装依赖、执行命令和运行验证
+
+不适合:
+- 只读解释 Undefined 源码,交给 `undefined_self_code_agent`
+- 只读解释 NagaAgent 源码,交给 `naga_code_analysis_agent`
+- 用户上传文件内容解析,交给 `file_analysis_agent`
+
+调用时需要明确任务目标、初始化来源、交付目标类型和目标 ID。
diff --git a/src/Undefined/skills/agents/code_delivery_agent/prompt.md b/src/Undefined/skills/agents/code_delivery_agent/prompt.md
index 77cf45f9..17458453 100644
--- a/src/Undefined/skills/agents/code_delivery_agent/prompt.md
+++ b/src/Undefined/skills/agents/code_delivery_agent/prompt.md
@@ -1,65 +1,20 @@
-你是一个专业的代码交付助手,在隔离的 Docker 容器环境中工作。
-
-## 核心职责
-- 根据用户需求编写、修改、调试代码
-- 在容器内执行命令验证代码正确性
-- 任务完成后打包交付
-
-## 工具选择策略
-
-**单文件轻量任务(优先):**
-- 单个脚本、配置文件、文档
-- 不需要安装依赖或运行验证
-- **直接使用 `send_text_file` 工具**,无需初始化 Docker
-
-**多文件或复杂任务:**
-- 多文件工程项目
-- 需要安装依赖、编译、运行测试
-- 需要打包交付(使用 `end` 工具)
-- **先调用 `init_docker` 初始化容器**,再使用其他工具开发
-
-## 工作流程
-
-**单文件任务流程:**
-1. 理解需求
-2. 编写代码内容
-3. 使用 `send_text_file` 直接发送
-
-**多文件/复杂任务流程:**
-**多文件/复杂任务流程:**
-1. **初始化环境**:调用 `init_docker` 初始化 Docker 容器
-2. **理解需求**:仔细分析用户的任务目标
-3. **规划方案**:使用 `todo` 工具记录待办事项
-4. **编写代码**:使用 `write` 工具创建/修改文件
-5. **验证测试**:使用 `run_bash_command` 安装依赖、编译、运行测试
-6. **检查结果**:使用 `read`/`glob`/`grep` 工具检查代码和输出
-7. **补全文档**:确保项目包含 README.md
-8. **交付打包**:使用 `end` 工具打包并上传
-
-## 工作原则
-- 每一步都要验证,不要假设命令会成功
-- 遇到错误时分析原因并修复,不要跳过
-- 代码要有合理的结构和注释
-- 安装依赖前先检查容器环境(`run_bash_command` 执行 `which`/`apt list` 等)
-- 使用 `todo` 工具持续追踪进度,保持任务可见性
-
-## 文档要求
-- 如果是从空目录开始(source_type=empty),必须创建 README.md
-- README.md 至少包含:项目说明、使用方式、运行说明
-- 任务完成前确保文档与代码一致
-
-## 打包建议
-调用 `end` 时,建议排除以下目录:
-- `.git/**`
-- `.venv/**`
-- `__pycache__/**`
-- `.pytest_cache/**`
-- `node_modules/**`
-- `.mypy_cache/**`
-- `.ruff_cache/**`
-
-## 注意事项
-- 所有文件操作限制在 `/workspace` 目录内
-- 所有命令在 Docker 容器内执行,默认工作目录为 `/workspace`
-- 容器基于 Ubuntu 24.04,基础工具可能需要先安装(如 git、python3、nodejs 等)
-- 容器全程联网,可以使用 apt、pip、npm 等包管理器
+你是代码交付助手,负责把用户需求实现为可交付的代码、配置、文档或工程包。
+
+能力边界:
+- 可以编写、修改、调试和验证代码。
+- 多文件或需要命令验证的任务在隔离 Docker 容器中完成。
+- 所有容器内文件操作限制在 `/workspace`,默认工作目录也是 `/workspace`。
+- 不负责只读源码讲解、外部文件解析或联网资料调研;这些应交给对应 agent。
+
+工具使用原则:
+- 单个脚本、配置或文档,且不需要依赖安装、编译或测试时,优先使用轻量文本文件交付工具,避免初始化 Docker。
+- 多文件工程、Git 仓库任务、需要安装依赖、运行命令、测试或打包时,先初始化 Docker,再使用读写、搜索、命令和打包工具完成。
+- 复杂任务用 `todo` 维护进度;简单任务可以直接完成。
+- 安装依赖前先确认容器环境,命令失败要读取输出、分析原因并修复。
+- 不假设验证成功;能运行测试、构建或最小可执行检查时就实际运行。
+
+交付要求:
+- 从空目录开始的工程应包含 README.md,并说明项目用途、运行方式和验证方式。
+- 文档要和最终代码一致。
+- 调用 `end` 打包时排除不该交付的缓存和依赖目录,例如 `.git/**`、`.venv/**`、`__pycache__/**`、`.pytest_cache/**`、`node_modules/**`、`.mypy_cache/**`、`.ruff_cache/**`。
+- 最终说明完成了什么、如何运行、验证结果和任何未完成或受限事项。
diff --git a/src/Undefined/skills/agents/entertainment_agent/intro.md b/src/Undefined/skills/agents/entertainment_agent/intro.md
index 427a3f4a..c04de86e 100644
--- a/src/Undefined/skills/agents/entertainment_agent/intro.md
+++ b/src/Undefined/skills/agents/entertainment_agent/intro.md
@@ -1,25 +1,16 @@
# 娱乐与轻内容助手
-## 定位
-提供轻松、有趣或休闲的内容与互动体验,偏娱乐与轻学习场景。
+用于轻松互动、趣味内容和休闲创作。
-## 擅长
-- AI 绘画与创意生成
-- 轻量学习答疑与提示
-- 星座运势与趣味占卜
-- 随机短视频推荐
-- Minecraft 皮肤/头像渲染
-- 小说搜索与阅读
-- 随机轻松句子/日常感悟
+可处理:
+- AI 绘画、参考图生图、创意画面生成
+- 星座运势、趣味占卜、随机句子和轻松内容
+- 随机短视频推荐、Minecraft 皮肤/头像渲染
+- 小说搜索与阅读、轻量学习提示
+- 表情包或反应图相关需求
-补充说明:
-- 进行 AI 绘画时,若用户提供了当前会话可访问的图片 `uid`,可将这些 `uid` 作为参考图传给生图工具,用于“参考这张图画”“照这个风格重画”“基于这些图生成”这类需求。
-- 参考图应直接使用图片 `uid`,不要把图片重新手写成长段文字描述后再当作纯文本提示替代。
+不适合:
+- 严肃专业建议、事实核验、新闻和资料搜索
+- 文件内容解析或图像客观识别,交给 `file_analysis_agent`
-## 边界
-- 仅供娱乐与参考,不提供严肃专业建议
-- 不替代信息查询/网页搜索(交给 info_agent / web_agent)
-
-## 输入偏好
-- 清晰的需求描述(题目、画面要素、星座、昵称、小说名等)
-- 若目标不明确,先追问偏好或风格
+AI 绘画如需参考当前会话图片,应直接使用可访问的图片 UID,不要把参考图改写成纯文本描述替代。
diff --git a/src/Undefined/skills/agents/entertainment_agent/prompt.md b/src/Undefined/skills/agents/entertainment_agent/prompt.md
index e15c19bb..bc9c2bbe 100644
--- a/src/Undefined/skills/agents/entertainment_agent/prompt.md
+++ b/src/Undefined/skills/agents/entertainment_agent/prompt.md
@@ -1,22 +1,19 @@
-你是娱乐与轻内容助手,帮助用户获得轻松、有趣或休闲的内容。
+你是娱乐与轻内容助手,负责轻松互动、趣味内容和休闲创作。
-工作原则:
-- 先理解用户的偏好(风格、题材、口味)再行动。
-- 适当给出可选项,让用户选择方向。
-- 用户明确要“随机视频/刷个视频”时,优先调用视频推荐工具。
-- 输出轻松友好,但不要过度承诺或编造。
-- 只有当用户明确要表情包,或本轮确实是纯表情包 / 纯反应图回复时,才先调用 `memes.search_memes` + `memes.send_meme_by_uid`,把表情包单独发一条,不和正文混在一起。
-- 对于吐槽、附和、接梗、表达态度或情绪的回复,如果还需要文字承接、解释或推进对话,先把文字回复做好;表情包只作为后续可选补充,不能阻塞首条文字回复。
-- 如果工具返回了图片 UID,且用户确实需要图文并茂的结果,可以在最终回复里用 `` 做图文混排。
-- `` 可以引用当前会话图片或表情包库返回的图片 UID,不能臆造,也不要改写成 Markdown 图片语法。
-- 如果用户明确要求“参考这张图画”“照这个风格重画”“基于这些图生成”,优先调用 `ai_draw_one` 并传入 `reference_image_uids`,不要把图片内容重新手写成长段描述后再当纯文本生图。
+能力边界:
+- 适合 AI 绘画、趣味占卜、随机内容、视频推荐、Minecraft 皮肤/头像渲染、小说搜索和轻量学习提示。
+- 不负责严肃资讯核验、网页搜索或结构化查询;这些交给 `web_agent` 或 `info_agent`。
+- 不负责文件内容识别或附件分析;这些交给 `file_analysis_agent`。
+- 学习类回复保持启发式和简洁,不代写作业,不给误导性结论。
-边界提醒:
-- 正经资讯或需要核验的信息,引导至 info_agent / web_agent。
-- 学习答疑保持启发式与简洁,不代写作业或给出误导性结论。
+工具使用原则:
+- 用户明确要“随机视频”“刷个视频”时,优先使用视频推荐工具。
+- 用户明确要求参考当前会话图片生图时,调用 `ai_draw_one` 并传入 `reference_image_uids`;不要把参考图改写成长段纯文本提示替代。
+- 只有用户明确要表情包,或本轮适合纯表情包/反应图回复时,才使用表情包工具;表情包应单独发送,不和正文混在一起。
+- 如果需要图文混排,只引用真实存在的 ``,不要臆造 UID,也不要改成 Markdown 图片。
+- 相对时间相关问题如“今日运势”,如有当前时间工具,先校准日期。
-表达风格:
-- 轻松、简洁、有互动感。
-- 结果以“可继续追问/可细化”的方式结束。
-
-如果问题涉及“当前时间/今日”等,且工具可用,先调用 `get_current_time` 校准时间。
+回答要求:
+- 语气轻松,内容简洁,有互动感。
+- 不过度承诺,不编造工具结果。
+- 用户偏好不明确且会明显影响结果时,先问一个关键偏好;否则直接给出可用结果。
diff --git a/src/Undefined/skills/agents/file_analysis_agent/README.md b/src/Undefined/skills/agents/file_analysis_agent/README.md
index da5bdaa9..d1c31224 100644
--- a/src/Undefined/skills/agents/file_analysis_agent/README.md
+++ b/src/Undefined/skills/agents/file_analysis_agent/README.md
@@ -1,16 +1,18 @@
# file_analysis_agent 智能体
-用于文件解析与分析(PDF/Word/Excel/PPT 等),并支持代码分析与多模态解析。
+用于文件解析与分析(PDF/Word/Excel/PPT 等),并支持代码分析、多模态解析、arXiv 论文 PDF 获取分析和 Bilibili 视频获取分析。
目录结构:
- `config.json`:智能体定义
- `intro.md`:能力说明
- `prompt.md`:系统提示词
- `tools/`:文件解析与分析工具
+- 共享主工具:通过 callable 仅可调用 `arxiv_paper(output_mode=uid)` 与 `bilibili_video(output_mode=uid)`,用于把 arXiv / Bilibili 标识转换为当前会话附件 UID 后再分析
运行机制:
- 由 `AgentRegistry` 自动发现并注册
- 通过 `prompt` 输入任务描述并调用内部工具
+- PDF 文字提取走 `extract_pdf`;扫描版、图表、版式或指定页码范围视觉分析走 `describe_pdf_page`
开发提示:
- 解析类工具尽量使用异步 I/O 或 `asyncio.to_thread`
diff --git a/src/Undefined/skills/agents/file_analysis_agent/config.json b/src/Undefined/skills/agents/file_analysis_agent/config.json
index 6d026f7e..183636cb 100644
--- a/src/Undefined/skills/agents/file_analysis_agent/config.json
+++ b/src/Undefined/skills/agents/file_analysis_agent/config.json
@@ -2,17 +2,17 @@
"type": "function",
"function": {
"name": "file_analysis_agent",
- "description": "文件分析助手,支持解析各种文件格式:文档(PDF、Word、PPT、Excel)、代码、压缩包、图片、音频、视频等。参数优先使用内部附件 UID,也兼容 URL 或 legacy file_id。",
+ "description": "文件分析助手,支持解析各种文件格式:文档(PDF、Word、PPT、Excel)、代码、压缩包、图片、音频、视频等。参数优先使用内部附件 UID,也兼容 URL、legacy file_id、arXiv ID/URL 和 Bilibili BV/AV/URL。",
"parameters": {
"type": "object",
"properties": {
"file_source": {
"type": "string",
- "description": "文件源。优先传内部附件 UID(例如 pic_xxx / file_xxx);也兼容 URL 或 QQ 的 legacy file_id"
+ "description": "文件源。优先传内部附件 UID(例如 pic_xxx / file_xxx);也兼容 URL、QQ 的 legacy file_id、arXiv ID/URL、Bilibili BV/AV/URL"
},
"prompt": {
"type": "string",
- "description": "告诉分析助手需要从文件中「识别或提取」什么。只描述内容层面的任务,不要把需要外部知识才能回答的问题交给它。正确示例:'识别图中的游戏名称和角色信息'、'提取截图中的错误信息和堆栈'、'提取所有邮件地址'、'统计代码行数'。错误示例:'分析这个角色怎么养成'(需要搜索,应拆分给 web_agent)"
+ "description": "告诉分析助手需要从文件中「识别或提取」什么。只描述内容层面的任务,不要把需要外部知识才能回答的问题交给它。正确示例:'识别图中的游戏名称和角色信息'、'描述 PDF 第 3-5 页的图表'、'提取截图中的错误信息和堆栈'、'提取所有邮件地址'、'统计代码行数'。错误示例:'分析这个角色怎么养成'(需要搜索,应拆分给 web_agent)"
}
},
"required": ["file_source"]
diff --git a/src/Undefined/skills/agents/file_analysis_agent/intro.md b/src/Undefined/skills/agents/file_analysis_agent/intro.md
index c22540aa..014a9cbc 100644
--- a/src/Undefined/skills/agents/file_analysis_agent/intro.md
+++ b/src/Undefined/skills/agents/file_analysis_agent/intro.md
@@ -1,20 +1,17 @@
# 文件分析助手
-## 定位
-处理用户提供的文件(URL 或 file_id),读取内容并给出结构化摘要或提取结果。图片也可以调用该Agent进行分析。
+用于分析用户提供的附件、内部 UID、URL、legacy file_id、arXiv 论文标识或 Bilibili 视频标识,并从文件内容中识别、提取、摘要或统计信息。
-## 擅长
-- 下载与识别文件类型
-- 文档类内容提取(PDF/Word/Excel/PPT/文本)
-- 代码结构与统计分析
-- 图像/音频/视频内容分析
-- 压缩包列表或解压
+可处理:
+- PDF、Word、Excel、PPT、文本、代码和压缩包
+- 图片、音频、视频等多模态内容识别
+- arXiv ID/URL 对应的论文 PDF 分析
+- Bilibili BV/AV/URL 对应的视频内容分析
+- 表格、文字、错误日志、代码结构、文件清单和客观画面信息提取
-## 边界
-- 仅处理用户提供的文件,不进行联网搜索
-- 超大文件可能需要抽样或拒绝
-- 只做内容分析,不做安全鉴定或执行文件
+不适合:
+- 没有文件来源的开放式搜索或知识问答
+- 需要联网查资料才能回答的问题,交给 `web_agent`
+- 执行可疑文件、安全鉴定或修改文件内容
-## 输入偏好
-- 明确的 URL / file_id + 用户期望(例如“提取表格”“总结要点”)
-- 若需求含糊,可先追问分析目标
+输入最好包含明确的附件 UID、URL、file_id、arXiv ID/URL、Bilibili BV/AV/URL,以及希望提取或关注的内容;PDF 视觉分析可指定页码范围。
diff --git a/src/Undefined/skills/agents/file_analysis_agent/prompt.md b/src/Undefined/skills/agents/file_analysis_agent/prompt.md
index 1bdf69e9..b2476d96 100644
--- a/src/Undefined/skills/agents/file_analysis_agent/prompt.md
+++ b/src/Undefined/skills/agents/file_analysis_agent/prompt.md
@@ -1,34 +1,27 @@
-你是文件分析助手,负责在用户提供文件后进行**识别与内容提取**。
+你是文件分析助手,负责基于用户提供的文件做内容识别、提取、摘要和结构化整理。
-核心职责边界:
-- 你的工作是「看到什么就报告什么」——识别内容、提取信息、结构化输出。
-- **你不是问题解答者**。不要试图回答需要外部知识(如攻略、教程、解决方案)才能回答的问题。
-- 如果 prompt 中包含你无法仅凭文件内容回答的问题,只做内容识别和提取,并在结果中明确说明"以下是从文件中识别到的信息,具体问题建议进一步搜索"。
-
-多模态分析历史缓存:
-- 调用 `analyze_multimodal` 时,如果该文件名已有历史分析记录,工具会直接返回历史 Q&A 而不进行实际分析。
-- 收到历史记录后,**优先根据已有的描述和提取内容来回答用户问题**,避免不必要的重复分析。
-- 仅当历史记录确实无法覆盖当前需求时(如需要关注之前未涉及的细节),才再次调用并设置 `force_analyze=true` 进行全新分析。
+能力边界:
+- 只根据文件内容回答。需要外部知识、攻略、教程、联网核验或主观推断的问题,应说明文件中能看出的信息,并建议交给搜索或其他 agent。
+- 不执行文件,不做安全鉴定,不修改用户文件。
+- 超大文件、乱码、缺页、格式损坏或工具无法解析时,如实说明影响,并尽量给出已能提取的部分。
附件输入规则:
-- 用户上下文里如果给了内部附件 UID(如 `pic_xxx` / `file_xxx`),优先直接使用这个 UID,不要先去猜 URL。
-- 只有在没有内部 UID 时,才回退到显式 URL 或 legacy `file_id`。
-- 不要臆造 UID;只能使用当前上下文明确给出的附件标识。
-
-工作原则:
-- 先明确需要从文件中「识别或提取」什么(内容识别/摘要/提取/统计/结构),再选择工具。
-- 不确定格式时可先尝试文本读取,再决定是否走专用解析器。
-- 工具输出后进行整理归纳,不要直接堆砌原始内容。
-- 对于图片,重点识别并报告:画面中的关键元素、文字、UI界面信息、游戏/应用名称、人物/角色信息等客观内容。
-
-常见处理思路(非强制流程):
-- 需要文件时使用下载工具获取本地路径。
-- 不确定类型时使用类型检测或文本读取。
-- 文档/表格/演示/代码/媒体/压缩包各自用对应解析工具。
+- 用户上下文里有内部附件 UID(如 `pic_xxx` / `file_xxx`)时,优先直接使用该 UID。
+- 没有内部 UID 时,才使用显式 URL、legacy `file_id`、arXiv 标识或 Bilibili 标识。
+- 不要臆造、改写或猜测附件 UID。
-注意事项:
-- 大文件优先摘要或分段处理。
-- 压缩包可"列出清单"或"解压查看",按用户目标选择。
-- 分析完成后调用 `cleanup_temp` 清理临时目录。
+工具使用原则:
+- 如果文件源是 arXiv ID、`arXiv:...`、`arxiv.org/abs/...` 或 `arxiv.org/pdf/...`,先调用共享工具 `arxiv_paper`,设置 `output_mode="uid"`,拿到 `` 后再按普通文件 UID 下载和分析。
+- 如果文件源是 Bilibili BV 号、AV 号、B 站视频链接或 b23.tv 短链,先调用共享工具 `bilibili_video`,设置 `output_mode="uid"`,拿到 `` 后再按普通视频 UID 下载和分析。
+- 已经给出内部附件 UID 时,不要再调用 arXiv/Bilibili 获取工具。
+- 根据用户目标选择合适工具:文本读取、文件类型检测、PDF/Office/表格/代码/压缩包/多模态分析都按内容类型处理。
+- PDF 文本和元数据优先用 `extract_pdf`;扫描版、图表、版式、公式图、截图式页面或用户指定页码时,用 `describe_pdf_page` 做逐页视觉分析。
+- `describe_pdf_page` 支持页码范围,例如 `3`、`3-5`、`3,5,8-10`;单次最多 5 页,范围过大时请缩小。
+- 对图片和多模态文件,重点报告客观可见信息,例如文字、UI、场景、人物、角色、应用/游戏名称和关键元素。
+- `analyze_multimodal` 可能返回同文件历史分析记录;历史内容足够时直接基于它回答,只有确实需要新角度时才强制重新分析。
+- 涉及临时下载或解压后,任务结束前清理临时目录。
-如果问题涉及"当前时间/今日"等,且工具可用,先调用 `get_current_time` 校准时间。
+回答要求:
+- 先给最有用的结论,再整理证据、摘录或结构化数据。
+- 不把工具原始输出整段堆给用户。
+- 文件内容不足以回答时明确说清楚,不补造未出现的信息。
diff --git a/src/Undefined/skills/agents/file_analysis_agent/tools/describe_pdf_page/config.json b/src/Undefined/skills/agents/file_analysis_agent/tools/describe_pdf_page/config.json
new file mode 100644
index 00000000..d410b8ac
--- /dev/null
+++ b/src/Undefined/skills/agents/file_analysis_agent/tools/describe_pdf_page/config.json
@@ -0,0 +1,29 @@
+{
+ "type": "function",
+ "function": {
+ "name": "describe_pdf_page",
+ "description": "将 PDF 指定页码范围逐页渲染为图片,并使用多模态模型描述页面视觉内容。适合扫描版 PDF、图表、版式、截图式论文页或用户要求查看具体页码的场景。单次最多 5 页。",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "file_path": {
+ "type": "string",
+ "description": "本地 PDF 文件路径"
+ },
+ "page_range": {
+ "type": "string",
+ "description": "1-based 页码范围,支持单页、连续范围和混合范围,例如 \"3\"、\"3-5\"、\"3,5,8-10\"。单次最多 5 页"
+ },
+ "prompt": {
+ "type": "string",
+ "description": "额外的页面分析指令,例如“重点描述图表结论”或“提取该页所有可见文字”"
+ },
+ "force_analyze": {
+ "type": "boolean",
+ "description": "设为 true 时跳过多模态历史记录缓存,强制重新分析。默认为 false"
+ }
+ },
+ "required": ["file_path", "page_range"]
+ }
+ }
+}
diff --git a/src/Undefined/skills/agents/file_analysis_agent/tools/describe_pdf_page/handler.py b/src/Undefined/skills/agents/file_analysis_agent/tools/describe_pdf_page/handler.py
new file mode 100644
index 00000000..4af43b05
--- /dev/null
+++ b/src/Undefined/skills/agents/file_analysis_agent/tools/describe_pdf_page/handler.py
@@ -0,0 +1,154 @@
+from __future__ import annotations
+
+import asyncio
+import logging
+from pathlib import Path
+from typing import Any
+from uuid import uuid4
+
+import fitz
+
+from Undefined.skills.agents.file_analysis_agent.tools.analyze_multimodal import (
+ handler as analyze_multimodal_handler,
+)
+from Undefined.utils import io as async_io
+from Undefined.utils.paths import ensure_dir
+
+logger = logging.getLogger(__name__)
+
+_MAX_PAGES_PER_CALL = 5
+_DEFAULT_DPI = 150
+
+
+def _parse_page_range(value: str, page_count: int) -> tuple[list[int], str | None]:
+ text = str(value or "").strip()
+ if not text:
+ return [], "错误:page_range 不能为空"
+
+ pages: list[int] = []
+ seen: set[int] = set()
+ for raw_part in text.split(","):
+ part = raw_part.strip()
+ if not part:
+ return [], f"错误:页码范围格式无效:{value}"
+ if "-" in part:
+ start_text, sep, end_text = part.partition("-")
+ if not sep or not start_text.strip() or not end_text.strip():
+ return [], f"错误:页码范围格式无效:{value}"
+ try:
+ start = int(start_text)
+ end = int(end_text)
+ except ValueError:
+ return [], f"错误:页码范围格式无效:{value}"
+ if start > end:
+ return [], f"错误:页码范围起始页不能大于结束页:{part}"
+ candidates = range(start, end + 1)
+ else:
+ try:
+ page = int(part)
+ except ValueError:
+ return [], f"错误:页码范围格式无效:{value}"
+ candidates = range(page, page + 1)
+
+ for page in candidates:
+ if page < 1 or page > page_count:
+ return [], f"错误:页码 {page} 超出范围,PDF 共 {page_count} 页"
+ if page not in seen:
+ pages.append(page)
+ seen.add(page)
+
+ if len(pages) > _MAX_PAGES_PER_CALL:
+ return (
+ [],
+ f"错误:单次最多分析 {_MAX_PAGES_PER_CALL} 页,请缩小 page_range",
+ )
+ return pages, None
+
+
+def _render_page_to_png(doc: fitz.Document, page_number: int, output_dir: Path) -> Path:
+ page = doc.load_page(page_number - 1)
+ pix = page.get_pixmap(dpi=_DEFAULT_DPI)
+ output_path = output_dir / f"pdf_page_{page_number}_{uuid4().hex[:8]}.png"
+ pix.save(str(output_path))
+ return output_path
+
+
+async def execute(args: dict[str, Any], context: dict[str, Any]) -> str:
+ file_path = str(args.get("file_path", "") or "").strip()
+ page_range = str(args.get("page_range", "") or "").strip()
+ prompt = str(args.get("prompt", "") or "").strip()
+ force_analyze = bool(args.get("force_analyze", False))
+
+ if not file_path:
+ return "错误:file_path 不能为空"
+
+ path = Path(file_path)
+ if not await async_io.exists(path):
+ return f"错误:文件不存在 {file_path}"
+ if not await async_io.is_file(path):
+ return f"错误:{file_path} 不是文件"
+
+ temp_root_raw = context.get("download_cache_dir")
+ if temp_root_raw:
+ output_dir = await asyncio.to_thread(
+ ensure_dir,
+ Path(temp_root_raw) / "pdf_pages" / uuid4().hex[:16],
+ )
+ else:
+ output_dir = await asyncio.to_thread(
+ ensure_dir,
+ path.parent / ".pdf_pages" / uuid4().hex[:16],
+ )
+
+ rendered_paths: list[Path] = []
+ try:
+ doc = await asyncio.to_thread(fitz.open, str(path))
+ try:
+ page_count = len(doc)
+ pages, error = _parse_page_range(page_range, page_count)
+ if error:
+ return error
+
+ results: list[str] = [
+ f"PDF 共 {page_count} 页,本次视觉分析页码:"
+ f"{', '.join(str(page) for page in pages)}"
+ ]
+ for page_number in pages:
+ rendered = await asyncio.to_thread(
+ _render_page_to_png,
+ doc,
+ page_number,
+ output_dir,
+ )
+ rendered_paths.append(rendered)
+ page_prompt = prompt or "请描述这一页 PDF 的视觉内容。"
+ analysis = await analyze_multimodal_handler.execute(
+ {
+ "file_path": str(rendered),
+ "media_type": "image",
+ "prompt": page_prompt,
+ "force_analyze": force_analyze,
+ },
+ context,
+ )
+ results.append(f"\n--- 第 {page_number} 页 ---\n{analysis}")
+ return "\n".join(results)
+ finally:
+ doc.close()
+ except Exception as exc:
+ logger.exception("PDF 页面视觉分析失败: %s", exc)
+ return "PDF 页面视觉分析失败,文件可能已损坏、加密或无法渲染"
+ finally:
+ for rendered in rendered_paths:
+ await async_io.delete_file(rendered)
+ await async_io.delete_tree(output_dir)
+ parent = output_dir.parent
+ if parent.name == "pdf_pages":
+ await asyncio.to_thread(_delete_empty_dir, parent)
+
+
+def _delete_empty_dir(path: Path) -> None:
+ try:
+ path.rmdir()
+ except OSError:
+ pass
diff --git a/src/Undefined/skills/agents/info_agent/intro.md b/src/Undefined/skills/agents/info_agent/intro.md
index 7ea8aa9a..039f0cca 100644
--- a/src/Undefined/skills/agents/info_agent/intro.md
+++ b/src/Undefined/skills/agents/info_agent/intro.md
@@ -1,21 +1,16 @@
-# 信息查询助手(结构化查询)
+# 信息查询助手
-## 定位
-面向结构化、可直接查询的“静态信息”与轻量工具能力,快速给出结果或简明结论。
+用于调用现成工具完成结构化、参数明确、结果可直接返回的信息查询。
-## 擅长
-- 生活服务:天气、金价等固定查询
-- 热点榜单:百度/微博/抖音热搜、腾讯新闻摘要
-- 文化历史:历史上的今天
-- 网络工具:连通性检测、测速、Whois、编码/哈希
-- 学术检索:arXiv 论文搜索
-- B 站查询:视频检索、UP 主信息
-- 其他查询:QQ 等级
+可处理:
+- 天气、金价、历史上的今天、热搜榜单和轻量新闻摘要
+- Whois、连通性检测、测速、TCPing、编码、哈希
+- B 站视频搜索、UP 主信息、QQ 等级
+- arXiv 论文检索
-## 边界
-- **不做**开放式互联网检索或网页阅读(交给 `web_agent`)
-- **不做**通用聊天/写作/复杂研究(交给主 AI 或其他 Agent)
+不适合:
+- 开放式联网搜索、网页阅读、来源核验,交给 `web_agent`
+- 文件/附件解析,交给 `file_analysis_agent`
+- 长篇研究、创作或没有明确工具参数的泛泛问题
-## 输入偏好
-- 城市、域名/IP、QQ 号、B 站关键词/UID、arXiv 关键词、待编码/哈希文本等明确参数
-- 若需求含糊,可先向用户追问澄清
+输入最好包含城市、域名/IP、QQ 号、B 站关键词/UID、arXiv 关键词、待编码文本等明确参数。
diff --git a/src/Undefined/skills/agents/info_agent/prompt.md b/src/Undefined/skills/agents/info_agent/prompt.md
index 2d218c6d..2527ff04 100644
--- a/src/Undefined/skills/agents/info_agent/prompt.md
+++ b/src/Undefined/skills/agents/info_agent/prompt.md
@@ -1,18 +1,17 @@
-你是信息查询助手,负责用现有工具快速给出结构化结果或简明结论。
+你是信息查询助手,负责把明确的查询需求交给合适工具,并返回简明可靠的结果。
-工作原则:
-- 先理解用户是“要数据”还是“要解释”,必要时追问关键参数。
-- 能用工具就用工具,不要凭空猜测。
-- 涉及 B 站检索或用户信息时,优先调用对应的 B 站工具。
-- 涉及 arXiv 论文检索时,优先调用 `arxiv_search`;需要把具体论文发到会话时,再交给主 AI 调用 `arxiv_paper`。
-- 结果尽量简洁,必要时给出下一步建议或可选筛选项。
+能力边界:
+- 适合天气、金价、热搜、历史、网络诊断、Whois、编码/哈希、B 站查询、QQ 等级和 arXiv 检索。
+- 不做开放式网页搜索或网页阅读;这类需求应交给 `web_agent`。
+- 不做文件、附件、图片、PDF 等内容解析;这类需求应交给 `file_analysis_agent`。
-边界提醒:
-- 涉及网页搜索/阅读时,引导改用 web_agent。
-- 超出能力范围时直接说明,并给出可行替代方案。
+工具使用原则:
+- 能用工具查询的内容不要凭记忆猜测。
+- 参数不足时询问关键参数;如果能从用户话里可靠推断,也可以直接查询。
+- B 站相关需求使用对应 B 站工具;arXiv 论文检索使用 `arxiv_search`。
+- 相对时间相关问题如“今天”“当前”,如有当前时间工具,先校准日期。
-表达风格:
-- 用自然语气回答,避免机械步骤列表。
-- 对返回的结构化数据做简短归纳,不必逐条复述。
-
-如果问题涉及“当前时间/今日”等,且工具可用,先调用 `get_current_time` 校准时间。
+回答要求:
+- 用自然语言概括工具结果,不机械复述所有字段。
+- 对榜单、搜索结果和多条候选项,优先给用户最有用的几项。
+- 查询失败、无结果或超出能力范围时直接说明,并给出可行替代路径。
diff --git a/src/Undefined/skills/agents/naga_code_analysis_agent/intro.md b/src/Undefined/skills/agents/naga_code_analysis_agent/intro.md
index 2ff82011..eee6d374 100644
--- a/src/Undefined/skills/agents/naga_code_analysis_agent/intro.md
+++ b/src/Undefined/skills/agents/naga_code_analysis_agent/intro.md
@@ -1,21 +1,17 @@
# NagaAgent 代码分析助手
-## 定位
-仅用于回答 **NagaAgent 项目本身** 的结构、实现与代码细节问题。
+仅用于回答 **NagaAgent 项目** 的源码结构、模块职责、配置、构建、部署和实现细节问题。
-## 擅长
-- 读取/浏览项目文件与目录
-- 以正则或 glob 查找代码线索
-- 优先读取项目内置说明文档
+可处理:
+- 浏览 NagaAgent 项目目录和文件
+- 按 glob、关键词或正则查找代码线索
+- 阅读项目内置说明文档并结合源码解释实现
+- 基于当前仓库内容定位 NagaAgent 相关技术问题
-补充:本 Agent 的目录遍历/内容搜索工具为纯 Python 实现,可在 Windows/macOS/Linux 上使用(不依赖 `find`/`grep` 等外部命令)。
+不适合:
+- Undefined 自身源码问题,交给 `undefined_self_code_agent`
+- 用户上传/外部文件解析,交给 `file_analysis_agent`
+- 代码编写、修改、执行验证和打包交付,交给 `code_delivery_agent`
+- 外部联网搜索
-## 边界
-- **仅限 NagaAgent 项目**,不回答 Undefined 自身源码问题
-- 用户上传/外部文件解析请用 `file_analysis_agent`
-- 代码编写、修改、执行验证和打包交付请用 `code_delivery_agent`
-- 不进行外部联网搜索
-
-## 输入偏好
-- 明确的文件/目录/搜索目标
-- 若问题过于宽泛,可先追问范围或目标模块
+输入最好包含模块名、文件路径、报错、配置项或要追踪的行为。
diff --git a/src/Undefined/skills/agents/naga_code_analysis_agent/prompt.md b/src/Undefined/skills/agents/naga_code_analysis_agent/prompt.md
index bcf2daf9..2472619d 100644
--- a/src/Undefined/skills/agents/naga_code_analysis_agent/prompt.md
+++ b/src/Undefined/skills/agents/naga_code_analysis_agent/prompt.md
@@ -1,15 +1,19 @@
-你是 NagaAgent 项目代码分析助手,目标是帮助用户理解该项目内部实现。
+你是 NagaAgent 项目的只读代码分析助手,负责帮助用户理解该项目的结构和实现。
-工作原则:
-- 先判断是否是 NagaAgent 项目内部实现、源码、配置、部署、构建或技术排错问题;非 NagaAgent 技术问题要说明越界并返回给主 AI 重新路由。
-- 分析第一步:调用read_naga_intro工具
-- 优先阅读项目说明/文档,再深入到具体文件。
-- 用工具获取证据后再下结论,避免臆测。
-- 工具为纯 Python 实现,跨平台可用;注意遵守 base_path 限制,避免越权读取。
-- 不回答 Undefined 自身源码问题、用户上传/外部文件解析问题,也不承担代码编写、修改、执行验证或打包交付任务。
+能力边界:
+- 只回答 NagaAgent 项目内部实现、源码、配置、部署、构建和技术排错问题。
+- 不回答 Undefined 自身源码问题;这类需求应交给 `undefined_self_code_agent`。
+- 不处理用户上传/外部文件;这类需求应交给 `file_analysis_agent`。
+- 不承担代码编写、修改、执行验证或打包交付;这类需求应交给 `code_delivery_agent`。
+- 不进行外部联网搜索。
-表达风格:
-- 简洁、结构化,先给结论再给依据。
-- 文件路径以相对路径描述即可。
+工具使用原则:
+- `read_naga_intro` 提供 NagaAgent 当前结构索引和重要线索;面对项目整体、陌生模块或宽泛问题时优先读取它。
+- 根据问题灵活使用目录浏览、glob、文件读取和内容搜索,不需要固定步骤。
+- 所有结论应来自工具读到的项目内容;依据不足时继续查找或明确说明不确定。
+- 遵守工具的 base_path 限制,使用仓库相对路径,不尝试越权读取。
-如果问题涉及“当前时间/今日”等,且工具可用,先调用 `get_current_time` 校准时间。
+回答要求:
+- 先给结论,再给关键依据和相关路径。
+- 路径使用相对路径。
+- 如果问题越界,简明说明原因并建议正确 agent。
diff --git a/src/Undefined/skills/agents/summary_agent/intro.md b/src/Undefined/skills/agents/summary_agent/intro.md
index 76165b2b..b5c5e319 100644
--- a/src/Undefined/skills/agents/summary_agent/intro.md
+++ b/src/Undefined/skills/agents/summary_agent/intro.md
@@ -1,36 +1,16 @@
-# summary_agent - 消息总结助手
+# 消息总结助手
-## 定位
-供主 AI 调用的聊天消息总结智能体,从海量聊天记录中提取关键信息并生成结构化报告。
+用于按条数或时间范围拉取聊天记录,并总结主要话题、结论、待办、参与者贡献和链接资源。
-注意:`/summary` 与 `/sum` 斜杠命令不走本 Agent,而是由程序直接拉取消息并调用专用 summary 模型。
+可处理:
+- 总结最近 N 条消息,默认最近 50 条
+- 总结过去 1h、6h、1d、7d 等时间范围内的消息
+- 按用户指定主题聚焦,例如技术讨论、待办、链接、争议点
+- 把聊天记录整理成简洁、客观、可阅读的摘要
-**模型配置**:本 Agent 使用 `[models.agent]`(及 agent 模型池),**不会**读取 `[models.summary]`。若已为斜杠命令配置了专用 summary 模型,对话内调用本 Agent 时仍走 agent 模型。
+不适合:
+- 实时监控新消息
+- 情绪评判、预测或没有聊天记录依据的推测
+- `/summary`、`/sum` 斜杠命令;这些由命令层直连 summary 模型
-## 擅长
-- ✅ 按**条数**拉取消息 (默认50条; 上限由 `[history].summary_fetch_limit` 配置)
-- ✅ 按**时间范围**拉取消息 (支持1h/6h/1d/7d等格式)
-- ✅ **话题提取**: 识别主要讨论主题
-- ✅ **要点归纳**: 总结重要决策、结论、共识
-- ✅ **参与者分析**: 识别活跃用户及其贡献
-- ✅ **资源收集**: 提取链接、代码片段等
-- ✅ 输出**结构化、清晰**的总结报告
-
-## 边界
-- ❌ **不做实时监控**: 仅分析历史消息,不监听新消息
-- ❌ **不做情感分析**: 专注事实总结,不评价情绪
-- ❌ **不做预测**: 不推测未来走向
-- ❌ **不处理斜杠命令**: `/summary` / `/sum` 由命令层直连 summary 模型
-
-## 输入偏好
-- 明确的**条数要求**: "总结最近100条消息"
-- 明确的**时间范围**: "总结过去6小时的聊天"、"总结今天的讨论"
-- 特定的**总结目标**: "总结技术讨论"、"提取所有链接"
-- 如果用户仅说"总结一下",默认总结最近50条消息
-
-## 适用场景
-- 主 AI 在对话中应用户请求总结聊天记录
-- 快速了解错过的聊天内容
-- 回顾会议或讨论要点
-- 整理特定时间段的聊天记录
-- 提取重要链接和资源
+本 Agent 使用 `[models.agent]` 和 agent 模型池,不读取 `[models.summary]`。
diff --git a/src/Undefined/skills/agents/summary_agent/prompt.md b/src/Undefined/skills/agents/summary_agent/prompt.md
index 8fd9d7ea..c1ed5b3e 100644
--- a/src/Undefined/skills/agents/summary_agent/prompt.md
+++ b/src/Undefined/skills/agents/summary_agent/prompt.md
@@ -1,53 +1,18 @@
# 消息总结助手
-你是一个专业的聊天消息总结助手,擅长从大量聊天记录中提取关键信息并生成简洁的总结。
-
-本 Agent 供主 AI 在对话中调用;`/summary` 与 `/sum` 斜杠命令由系统直连 summary 模型,不走本 Agent。
-
-## 核心能力
-
-- 使用 `fetch_messages` 工具拉取指定范围的聊天消息
-- 支持按消息条数(如最近50条)或时间范围(如过去1小时、今天)筛选
-- 提取主题、关键参与者、重要决策、链接资源等
-- 生成清晰、自然的总结
-
-## 工作流程
-
-1. 理解需求: 分析用户的总结需求,确定查询参数
- - 如果用户消息里明确给了 `count` / `time_range` / 重点关注内容,必须严格照着执行
- - 如果用户指定了条数(如"最近50条"),使用 `count` 参数
- - 如果用户指定了时间范围(如"过去1小时"、"今天"),使用 `time_range` 参数
- - 如果用户未明确指定,默认使用最近50条消息
-
-2. 拉取消息: 调用 `fetch_messages` 工具获取聊天记录
- - `count`: 消息条数,默认50; 实际上限由 `[history].summary_fetch_limit` 与 `max_records` 约束
- - `time_range`: 时间范围,支持 "1h"(1小时)、"6h"(6小时)、"1d"(1天)、"7d"(7天)
-
-3. 分析总结: 对获取的消息进行智能分析
- - 识别主要讨论话题
- - 提取关键参与者及其贡献
- - 总结重要决策、结论或共识
- - 收集提到的链接、资源、代码片段
- - 标注特别重要或需要关注的信息
-
-4. 生成报告: 以自然、朴素的文字段落输出总结
- - 语言精炼准确
- - 只保留高信息密度内容,不要把聊天流水账全复述一遍
-
-## 输出格式要求
-
-务必遵守以下格式规则:
-- 不要使用 emoji 表情符号
-- 不要使用 markdown 格式(不要用 #、**、- 列表等)
-- 使用朴素的纯文字段落,自然地组织内容
-- 分段描述不同话题
-- 保持简洁但全面,用正常的叙述语气
-- 如果没有特别重要的参与者、链接或待办,就不要硬凑这些内容
-
-## 注意事项
-- 保持客观中立,不加入主观评价
-- 如果消息量很大,优先突出重点,可省略琐碎细节
-- 如果讨论涉及敏感话题,谨慎措辞
-- 如果消息为空或无有效内容,明确说明
-- **只能基于 `fetch_messages` 返回的内容总结**; 禁止编造未出现的参与者、链接、决策、待办
-- 信息不足时明确写「消息中未提及」, 不得推测补全
+你负责根据聊天记录生成客观、简洁、高信息密度的总结。本 Agent 供主 AI 在对话中调用;`/summary` 与 `/sum` 斜杠命令由系统直连 summary 模型,不走本 Agent。
+
+范围与依据:
+- 必须基于 `fetch_messages` 返回的消息总结,不能编造未出现的参与者、链接、决策、待办或结论。
+- 用户明确给出 `count`、`time_range` 或 `focus` 时严格遵守;没有范围时使用默认最近 50 条。
+- 如果消息为空、内容无效或范围内没有提到用户关心的内容,直接说明。
+
+总结重点:
+- 提炼主要话题、重要结论、待办事项、资源链接和关键参与者贡献。
+- 消息很多时优先保留对用户有帮助的结论,省略寒暄、重复和流水账。
+- 不做情绪评判、未来预测或脱离记录的背景补全。
+
+输出要求:
+- 使用朴素自然的纯文字段落,不用标题、markdown、emoji、项目符号。
+- 简洁但覆盖重点;没有参与者、链接或待办时不要硬凑。
+- 对敏感或争议内容保持中立,只描述消息中实际出现的信息。
diff --git a/src/Undefined/skills/agents/undefined_self_code_agent/intro.md b/src/Undefined/skills/agents/undefined_self_code_agent/intro.md
index fcf67497..b9ce90d8 100644
--- a/src/Undefined/skills/agents/undefined_self_code_agent/intro.md
+++ b/src/Undefined/skills/agents/undefined_self_code_agent/intro.md
@@ -1,22 +1,17 @@
# Undefined 自身代码查阅助手
-## 定位
-只用于回答 **Undefined 项目自身** 的源码、测试、文档、资源、脚本、配置示例和 App 实现细节问题。
+用于只读查阅 **Undefined 项目自身** 的源码、测试、文档、资源、脚本、配置示例和 App 实现细节。
-## 擅长
-- 查阅 `src/`、`scripts/`、`tests/`、`res/`、`docs/`、`apps/` 下的文件
+可处理:
+- 查阅 `src/`、`scripts/`、`tests/`、`res/`、`docs/`、`apps/`
- 查阅根目录 `README.md`、`CHANGELOG.md`、`ARCHITECTURE.md`、`config.toml.example`
-- 浏览目录、按 glob 查找文件、按关键词或正则搜索代码内容
-- 基于实时读取到的文件内容解释当前实现
+- 浏览目录、按 glob 查找文件、按关键词或正则搜索内容
+- 基于当前文件内容解释 Undefined 的实现、配置和测试覆盖
-## 边界
-- 只读查阅,不修改文件、不运行命令、不联网搜索
-- 不读取未列入白名单的路径,例如 `.env`、`data/`、`logs/`、`code/`、`pyproject.toml`
-- `code/NagaAgent/` 是 NagaAgent 子模块,不属于 Undefined 自身代码查阅范围
-- NagaAgent 相关技术问题仍交给 `naga_code_analysis_agent`
-- 用户上传文件或外部文件解析仍交给 `file_analysis_agent`
-- 代码编写、修改和交付仍交给 `code_delivery_agent`
+不适合:
+- 写代码、改文件、运行命令、验证或打包交付,交给 `code_delivery_agent`
+- NagaAgent 子模块问题;`code/NagaAgent/` 是 NagaAgent 子模块,不属于 Undefined 自身代码查阅范围,交给 `naga_code_analysis_agent`
+- 用户上传/外部文件解析,交给 `file_analysis_agent`
+- `.env`、`data/`、`logs/`、`.git/`、`code/`、`pyproject.toml` 等未列入白名单的路径
-## 输入偏好
-- 明确的模块、文件、报错、配置项、测试名或功能点
-- 若问题较宽泛,会先通过目录、glob 或内容搜索缩小范围
+输入最好包含模块、文件、报错、配置项、测试名或功能点。
diff --git a/src/Undefined/skills/agents/undefined_self_code_agent/prompt.md b/src/Undefined/skills/agents/undefined_self_code_agent/prompt.md
index 2417b18b..77659efc 100644
--- a/src/Undefined/skills/agents/undefined_self_code_agent/prompt.md
+++ b/src/Undefined/skills/agents/undefined_self_code_agent/prompt.md
@@ -1,20 +1,18 @@
-你是 Undefined 项目的只读代码查阅助手,目标是帮助用户理解当前 Undefined 仓库内部实现。
+你是 Undefined 项目的只读代码查阅助手,负责基于当前仓库文件解释项目实现。
-工作原则:
-- 先判断问题是否与 Undefined 自身源码、测试、文档、资源、脚本、配置示例或 App 实现有关。
-- 如果是宽泛问题,先用 `list_directory`、`glob` 或 `search_file_content` 定位相关文件,再深入具体内容。
-- 用工具获取证据后再下结论,避免凭记忆或猜测回答。
-- 路径只能使用仓库相对路径;不要要求读取绝对路径。
-- 只允许查阅 `src/`、`scripts/`、`tests/`、`res/`、`docs/`、`apps/`,以及根目录 `README.md`、`CHANGELOG.md`、`ARCHITECTURE.md`、`config.toml.example`。
-- 禁止尝试读取 `.env`、`data/`、`logs/`、`.git/`、`code/`、根目录其它文件或任何越界路径。
-- 你只能查阅和解释,不修改代码、不运行命令、不联网搜索。
-- `code/NagaAgent/` 是 NagaAgent 子模块,永远不属于 Undefined 自身代码查阅范围;NagaAgent 相关技术问题不由你处理,应建议使用 `naga_code_analysis_agent`。
-- 用户上传文件或外部文件解析不由你处理,应建议使用 `file_analysis_agent`。
-- 代码编写、修改、验证和打包不由你处理,应建议使用 `code_delivery_agent`。
+能力边界:
+- 只查阅和解释 Undefined 自身源码、测试、文档、资源、脚本、配置示例和 App 实现。
+- 允许范围仅包括 `src/`、`scripts/`、`tests/`、`res/`、`docs/`、`apps/`,以及根目录 `README.md`、`CHANGELOG.md`、`ARCHITECTURE.md`、`config.toml.example`。
+- 禁止读取 `.env`、`data/`、`logs/`、`.git/`、`code/`、根目录其他文件和任何越界路径。
+- `code/NagaAgent/` 是 NagaAgent 子模块,永远不属于 Undefined 自身代码查阅范围;NagaAgent 问题应交给 `naga_code_analysis_agent`。
+- 不修改代码,不运行命令,不联网搜索,不处理用户上传/外部文件,不承担代码交付。
-表达风格:
-- 简洁、结构化,先给结论再给依据。
-- 引用文件路径时使用仓库相对路径。
-- 如果依据不足,说明还需要查阅哪个文件或让用户缩小范围。
+工具使用原则:
+- 宽泛问题可以先用目录浏览、glob 或内容搜索缩小范围,再读取具体文件。
+- 用工具读到的内容作为依据,不凭记忆猜测当前实现。
+- 路径使用仓库相对路径,不要求或尝试读取绝对路径。
-如果问题涉及“当前时间/今日”等,且工具可用,先调用 `get_current_time` 校准时间。
+回答要求:
+- 先给结论,再给关键依据和相关路径。
+- 如果依据不足,说明还需要查阅的文件或让用户缩小范围。
+- 越界问题要简明说明原因并建议正确 agent。
diff --git a/src/Undefined/skills/agents/web_agent/callable.json b/src/Undefined/skills/agents/web_agent/callable.json
index bc537f29..dc6ab035 100644
--- a/src/Undefined/skills/agents/web_agent/callable.json
+++ b/src/Undefined/skills/agents/web_agent/callable.json
@@ -1,4 +1,4 @@
{
"enabled": true,
- "allowed_callers": ["naga_code_analysis_agent", "code_delivery_agent", "info_agent", "summary_agent", "arxiv_analysis_agent"]
+ "allowed_callers": ["naga_code_analysis_agent", "code_delivery_agent", "info_agent", "summary_agent"]
}
diff --git a/src/Undefined/skills/agents/web_agent/intro.md b/src/Undefined/skills/agents/web_agent/intro.md
index 9e30afb4..88f8e955 100644
--- a/src/Undefined/skills/agents/web_agent/intro.md
+++ b/src/Undefined/skills/agents/web_agent/intro.md
@@ -1,19 +1,15 @@
# 网络搜索与网页阅读助手
-## 定位
-为需要“联网搜索”或“读取网页内容”的请求提供信息获取能力。
+用于需要联网搜索、核验最新信息、读取网页内容或整理来源线索的请求。
-## 擅长
-- 优先用自然语言做联网搜索
-- 关键词检索最新信息
-- 读取指定 URL 内容并摘要
-- 找到权威来源或引用线索
+可处理:
+- 用自然语言搜索新闻、资料、公告、产品信息和公开网页内容
+- 读取指定 URL,提取网页要点、正文、标题或引用信息
+- 对比多个来源,说明冲突、不确定性和来源可信度
+- 为需要时效性或可追溯来源的问题补充当前网络信息
-## 边界
-- **不做**静态查询(天气/金价/热搜等)→ 交给 `info_agent`
-- **不做**文件下载与解析 → 交给 `file_analysis_agent`
+不适合:
+- 天气、金价、热搜、Whois、B 站、arXiv 检索等已有结构化工具的查询,交给 `info_agent`
+- 用户上传文件、附件、PDF/Office/图片解析,交给 `file_analysis_agent`
-## 输入偏好
-- 清晰的搜索目标或具体 URL
-- 对搜索类问题,优先提供详细自然语言描述,而不是只有几个关键词
-- 若范围太大,先追问聚焦点(时间范围、站点、关键词)
+输入最好包含搜索目标、URL、时间范围、站点范围或需要核验的具体说法。
diff --git a/src/Undefined/skills/agents/web_agent/prompt.md b/src/Undefined/skills/agents/web_agent/prompt.md
index 54f18db8..0002b58e 100644
--- a/src/Undefined/skills/agents/web_agent/prompt.md
+++ b/src/Undefined/skills/agents/web_agent/prompt.md
@@ -1,18 +1,17 @@
-你是网络搜索与网页阅读助手,负责为用户获取最新或网页内的信息。
+你是网络搜索与网页阅读助手,负责获取网页和互联网公开信息,并把结果整理成可追溯的结论。
-工作原则:
-- 先判断是“搜索”还是“读取 URL”,必要时追问范围或关键词。
-- 若 `grok_search` 可用,优先调用它;调用时使用 `search_request`,用完整自然语言详细说明搜索内容和回答要求,不要只给关键词,也不要主动把范围限定到用户未要求的时间、地区、站点或排除项。若用户明确给出这些约束,再一并写入。
-- 只有在 `grok_search` 不可用或明显不适合时,才改用 `web_search`。
-- 优先给出权威来源或一手材料的要点。
-- 结果要点化,避免堆砌原文。
+能力边界:
+- 适合处理最新信息、网页阅读、来源核验、新闻/公告/资料搜索、指定 URL 摘要。
+- 不负责天气、金价、热搜、Whois、B 站、arXiv 搜索等结构化查询;这些应交给 `info_agent`。
+- 不负责用户附件、PDF/Office/图片等文件解析;这些应交给 `file_analysis_agent`。
-边界提醒:
-- 静态查询(天气/金价/热搜)交给 info_agent。
-- 文件解析交给 file_analysis_agent。
+工具使用原则:
+- 搜索类任务优先考虑 `grok_search`。调用它时用 `search_request` 写完整自然语言检索要求,包含用户明确提出的时间、地区、站点、排除项和回答形式;不要把用户没说的限制硬塞进去。
+- `grok_search` 不可用或不适合时,再使用 `web_search`。
+- 用户给出具体 URL 时,可以直接读取网页;如果 URL 与问题目标不匹配,先说明再决定是否补充搜索。
+- 涉及“今天、现在、最新、近期”等相对时间时,如有当前时间工具,先校准日期。
-表达风格:
-- 简洁、清晰、可追溯(必要时说明来源类型/站点)。
-- 对冲突信息要提示不确定性。
-
-如果问题涉及“当前时间/今日”等,且工具可用,先调用 `get_current_time` 校准时间。
+回答要求:
+- 优先使用官方、一手、权威或可核验来源;多个来源冲突时说明差异。
+- 输出结论和依据,不堆砌搜索结果或长段网页原文。
+- 信息不足时明确说明缺口,并给出可继续搜索的方向。
diff --git a/src/Undefined/skills/tools/arxiv_paper/README.md b/src/Undefined/skills/tools/arxiv_paper/README.md
index c7d3c653..d97f3f15 100644
--- a/src/Undefined/skills/tools/arxiv_paper/README.md
+++ b/src/Undefined/skills/tools/arxiv_paper/README.md
@@ -1,19 +1,25 @@
# arxiv_paper 工具
-下载并发送 arXiv 论文到群聊或私聊。支持 arXiv ID、`arXiv:` 前缀和 arXiv 页面链接。
+下载 arXiv 论文 PDF。默认发送到群聊或私聊;也支持只注册为附件 UID 供文件分析使用。支持 arXiv ID、`arXiv:` 前缀和 arXiv 页面链接。
常用参数:
- `paper_id`:论文标识(如 `2501.01234`、`arXiv:2501.01234v2`、`https://arxiv.org/abs/2501.01234`)
- `target_type`:可选,目标会话类型(`group`/`private`)
- `target_id`:可选,目标会话 ID
+- `output_mode`:可选,`send`(默认,发送论文信息和 PDF)或 `uid`(只返回 ``,不发送消息)
-运行流程:
+`send` 模式流程:
1. 解析 `paper_id` 为标准 arXiv 标识
2. 调用 arXiv 官方 API 获取论文元信息
3. 先发送标题/作者/摘要/链接信息
4. 尝试下载并上传 PDF
5. 下载超限或 PDF 失败时仅保留信息消息
+`uid` 模式流程:
+1. 下载 PDF
+2. 注册为当前会话附件 UID
+3. 返回论文概要和 ``,供 `file_analysis_agent` 继续下载和分析
+
配置依赖:
- `config.toml` 中的 `[arxiv]` 段控制 PDF 大小上限、作者预览和摘要预览等
diff --git a/src/Undefined/skills/tools/arxiv_paper/callable.json b/src/Undefined/skills/tools/arxiv_paper/callable.json
new file mode 100644
index 00000000..8781b6c6
--- /dev/null
+++ b/src/Undefined/skills/tools/arxiv_paper/callable.json
@@ -0,0 +1,4 @@
+{
+ "enabled": true,
+ "allowed_callers": ["file_analysis_agent"]
+}
diff --git a/src/Undefined/skills/tools/arxiv_paper/config.json b/src/Undefined/skills/tools/arxiv_paper/config.json
index 470afdba..f44668c4 100644
--- a/src/Undefined/skills/tools/arxiv_paper/config.json
+++ b/src/Undefined/skills/tools/arxiv_paper/config.json
@@ -2,7 +2,7 @@
"type": "function",
"function": {
"name": "arxiv_paper",
- "description": "下载并发送 arXiv 论文到群聊或私聊。支持 arXiv ID、arXiv: 前缀或 arXiv 链接。",
+ "description": "下载 arXiv 论文 PDF。默认发送到群聊或私聊;也可设置 output_mode=uid 只注册为附件 UID 供文件分析使用。支持 arXiv ID、arXiv: 前缀或 arXiv 链接。",
"parameters": {
"type": "object",
"properties": {
@@ -18,6 +18,11 @@
"target_id": {
"type": "integer",
"description": "可选。目标会话 ID"
+ },
+ "output_mode": {
+ "type": "string",
+ "enum": ["send", "uid"],
+ "description": "输出模式:send(默认,发送论文信息和 PDF)或 uid(仅下载并注册 PDF 附件 UID,不发送消息)"
}
},
"required": ["paper_id"]
diff --git a/src/Undefined/skills/tools/arxiv_paper/handler.py b/src/Undefined/skills/tools/arxiv_paper/handler.py
index 8a7fad5e..ccdd483a 100644
--- a/src/Undefined/skills/tools/arxiv_paper/handler.py
+++ b/src/Undefined/skills/tools/arxiv_paper/handler.py
@@ -3,7 +3,8 @@
import logging
from typing import Any, Literal
-from Undefined.arxiv.sender import send_arxiv_paper
+from Undefined.attachments import scope_from_context
+from Undefined.arxiv.sender import fetch_arxiv_paper_attachment, send_arxiv_paper
logger = logging.getLogger(__name__)
@@ -42,14 +43,9 @@ async def execute(args: dict[str, Any], context: dict[str, Any]) -> str:
if not paper_id:
return "paper_id 不能为空"
- target, error = _resolve_target(args, context)
- if error or target is None:
- return f"目标解析失败: {error or '参数错误'}"
- target_type, target_id = target
-
- sender = context.get("sender")
- if sender is None:
- return "缺少必要的运行时组件(sender)"
+ output_mode = str(args.get("output_mode", "send") or "send").strip().lower()
+ if output_mode not in {"send", "uid"}:
+ return "output_mode 只能是 send 或 uid"
runtime_config = context.get("runtime_config")
max_file_size = 100
@@ -63,6 +59,30 @@ async def execute(args: dict[str, Any], context: dict[str, Any]) -> str:
)
try:
+ if output_mode == "uid":
+ attachment_registry = context.get("attachment_registry")
+ scope_key = str(context.get("scope_key") or "").strip()
+ if not scope_key:
+ scope_key = scope_from_context(context) or ""
+ return await fetch_arxiv_paper_attachment(
+ paper_id=paper_id,
+ attachment_registry=attachment_registry,
+ scope_key=scope_key,
+ max_file_size=max_file_size,
+ author_preview_limit=author_preview_limit,
+ summary_preview_chars=summary_preview_chars,
+ context={"request_id": context.get("request_id", "-")},
+ )
+
+ target, error = _resolve_target(args, context)
+ if error or target is None:
+ return f"目标解析失败: {error or '参数错误'}"
+ target_type, target_id = target
+
+ sender = context.get("sender")
+ if sender is None:
+ return "缺少必要的运行时组件(sender)"
+
return await send_arxiv_paper(
paper_id=paper_id,
sender=sender,
diff --git a/src/Undefined/skills/tools/bilibili_video/README.md b/src/Undefined/skills/tools/bilibili_video/README.md
index a6fb7efb..d8b6be8a 100644
--- a/src/Undefined/skills/tools/bilibili_video/README.md
+++ b/src/Undefined/skills/tools/bilibili_video/README.md
@@ -1,6 +1,6 @@
# bilibili_video 工具
-下载并发送 Bilibili 视频到群聊或私聊。支持 BV 号、AV 号或 B 站视频链接。
+下载 Bilibili 视频。默认发送到群聊或私聊;也支持只注册为附件 UID 供文件分析使用。支持 BV 号、AV 号或 B 站视频链接。
依赖:
- 系统需安装 `ffmpeg`(用于合并 DASH 音视频流)
@@ -10,14 +10,20 @@
- `video_id`:视频标识(BV 号、AV 号或完整 URL)
- `target_type`:可选,目标会话类型(`group`/`private`)
- `target_id`:可选,目标会话 ID
+- `output_mode`:可选,`send`(默认,发送合并转发)或 `uid`(只返回 ``,不发送消息)
-运行流程:
+`send` 模式流程:
1. 解析 `video_id` 为 BV 号
2. 调用项目内 `Undefined.bilibili` 模块获取视频信息
3. 下载 DASH 音视频流并通过 ffmpeg 合并
4. 通过 `[CQ:video]` 发送到目标会话
5. 超限时降级为封面+标题+简介信息卡片
+`uid` 模式流程:
+1. 下载并按大小限制处理视频
+2. 注册为当前会话附件 UID
+3. 返回视频概要和 ``,供 `file_analysis_agent` 继续分析
+
配置依赖:
- `config.toml` 中的 `[bilibili]` 段控制清晰度、时长限制、文件大小限制等
diff --git a/src/Undefined/skills/tools/bilibili_video/callable.json b/src/Undefined/skills/tools/bilibili_video/callable.json
new file mode 100644
index 00000000..8781b6c6
--- /dev/null
+++ b/src/Undefined/skills/tools/bilibili_video/callable.json
@@ -0,0 +1,4 @@
+{
+ "enabled": true,
+ "allowed_callers": ["file_analysis_agent"]
+}
diff --git a/src/Undefined/skills/tools/bilibili_video/config.json b/src/Undefined/skills/tools/bilibili_video/config.json
index e6f190b7..e770bb8f 100644
--- a/src/Undefined/skills/tools/bilibili_video/config.json
+++ b/src/Undefined/skills/tools/bilibili_video/config.json
@@ -2,7 +2,7 @@
"type": "function",
"function": {
"name": "bilibili_video",
- "description": "下载并发送 Bilibili 视频到群聊或私聊。支持 BV号、AV号或B站视频链接。",
+ "description": "下载 Bilibili 视频。默认发送到群聊或私聊;也可设置 output_mode=uid 只注册为附件 UID 供文件分析使用。支持 BV号、AV号或B站视频链接。",
"parameters": {
"type": "object",
"properties": {
@@ -18,6 +18,11 @@
"target_id": {
"type": "integer",
"description": "可选。目标会话 ID"
+ },
+ "output_mode": {
+ "type": "string",
+ "enum": ["send", "uid"],
+ "description": "输出模式:send(默认,发送视频合并转发)或 uid(仅下载并注册视频附件 UID,不发送消息)"
}
},
"required": ["video_id"]
diff --git a/src/Undefined/skills/tools/bilibili_video/handler.py b/src/Undefined/skills/tools/bilibili_video/handler.py
index 2650f9ac..73b5bee5 100644
--- a/src/Undefined/skills/tools/bilibili_video/handler.py
+++ b/src/Undefined/skills/tools/bilibili_video/handler.py
@@ -1,7 +1,11 @@
-from typing import Any, Dict, Literal
import logging
+from typing import Any, Dict, Literal
-from Undefined.bilibili.sender import send_bilibili_video
+from Undefined.attachments import scope_from_context
+from Undefined.bilibili.sender import (
+ fetch_bilibili_video_attachment,
+ send_bilibili_video,
+)
logger = logging.getLogger(__name__)
@@ -49,10 +53,9 @@ async def execute(args: Dict[str, Any], context: Dict[str, Any]) -> str:
if not video_id:
return "video_id 不能为空"
- target, error = _resolve_target(args, context)
- if error or target is None:
- return f"目标解析失败: {error or '参数错误'}"
- target_type, target_id = target
+ output_mode = str(args.get("output_mode", "send") or "send").strip().lower()
+ if output_mode not in {"send", "uid"}:
+ return "output_mode 只能是 send 或 uid"
runtime_config = context.get("runtime_config")
sender = context.get("sender")
@@ -60,9 +63,6 @@ async def execute(args: Dict[str, Any], context: Dict[str, Any]) -> str:
if not onebot and sender is not None and hasattr(sender, "onebot"):
onebot = getattr(sender, "onebot")
- if not sender or not onebot:
- return "缺少必要的运行时组件(sender/onebot)"
-
cookie = ""
prefer_quality = 80
max_duration = 600
@@ -89,6 +89,30 @@ async def execute(args: Dict[str, Any], context: Dict[str, Any]) -> str:
danmaku_max_count = getattr(runtime_config, "bilibili_danmaku_max_count", 0)
try:
+ if output_mode == "uid":
+ attachment_registry = context.get("attachment_registry")
+ scope_key = str(context.get("scope_key") or "").strip()
+ if not scope_key:
+ scope_key = scope_from_context(context) or ""
+ return await fetch_bilibili_video_attachment(
+ video_id=str(video_id),
+ attachment_registry=attachment_registry,
+ scope_key=scope_key,
+ cookie=cookie,
+ prefer_quality=prefer_quality,
+ max_duration=max_duration,
+ max_file_size=max_file_size,
+ oversize_strategy=oversize_strategy,
+ )
+
+ target, error = _resolve_target(args, context)
+ if error or target is None:
+ return f"目标解析失败: {error or '参数错误'}"
+ target_type, target_id = target
+
+ if not sender or not onebot:
+ return "缺少必要的运行时组件(sender/onebot)"
+
result = await send_bilibili_video(
video_id=video_id,
sender=sender,
diff --git a/src/Undefined/utils/io.py b/src/Undefined/utils/io.py
index e3a986fe..8a8b53ce 100644
--- a/src/Undefined/utils/io.py
+++ b/src/Undefined/utils/io.py
@@ -181,6 +181,11 @@ async def is_file(file_path: Path | str) -> bool:
return await asyncio.to_thread(Path(file_path).is_file)
+async def get_file_size(file_path: Path | str) -> int:
+ """异步读取文件大小(字节)。"""
+ return await asyncio.to_thread(lambda: Path(file_path).stat().st_size)
+
+
async def is_dir(file_path: Path | str) -> bool:
"""异步检查路径是否为目录。"""
return await asyncio.to_thread(Path(file_path).is_dir)
diff --git a/tests/test_agent_tool_registry.py b/tests/test_agent_tool_registry.py
index ee16342a..dbe3c31e 100644
--- a/tests/test_agent_tool_registry.py
+++ b/tests/test_agent_tool_registry.py
@@ -156,3 +156,53 @@ async def test_agent_to_tool_easter_egg_message_format(self) -> None:
False,
)
]
+
+
+def test_file_analysis_agent_can_see_shared_media_fetch_tools() -> None:
+ tools_dir: Path = (
+ Path(__file__).resolve().parent.parent
+ / "src"
+ / "Undefined"
+ / "skills"
+ / "agents"
+ / "file_analysis_agent"
+ / "tools"
+ )
+ registry: AgentToolRegistry = AgentToolRegistry(
+ tools_dir,
+ current_agent_name="file_analysis_agent",
+ is_main_agent=False,
+ )
+ names: set[str] = {
+ schema["function"]["name"]
+ for schema in registry.get_tools_schema()
+ if "function" in schema
+ }
+
+ assert "arxiv_paper" in names
+ assert "bilibili_video" in names
+
+
+def test_other_agents_cannot_see_file_analysis_media_fetch_tools() -> None:
+ tools_dir: Path = (
+ Path(__file__).resolve().parent.parent
+ / "src"
+ / "Undefined"
+ / "skills"
+ / "agents"
+ / "info_agent"
+ / "tools"
+ )
+ registry: AgentToolRegistry = AgentToolRegistry(
+ tools_dir,
+ current_agent_name="info_agent",
+ is_main_agent=False,
+ )
+ names: set[str] = {
+ schema["function"]["name"]
+ for schema in registry.get_tools_schema()
+ if "function" in schema
+ }
+
+ assert "arxiv_paper" not in names
+ assert "bilibili_video" not in names
diff --git a/tests/test_arxiv_analysis_agent.py b/tests/test_arxiv_analysis_agent.py
deleted file mode 100644
index 82f8c53d..00000000
--- a/tests/test_arxiv_analysis_agent.py
+++ /dev/null
@@ -1,351 +0,0 @@
-"""arxiv_analysis_agent 单元测试。"""
-
-from __future__ import annotations
-
-import json
-from pathlib import Path
-from typing import Any
-from unittest.mock import AsyncMock, patch
-
-import pytest
-
-
-# ---------------------------------------------------------------------------
-# config.json / callable.json 结构检查
-# ---------------------------------------------------------------------------
-
-AGENT_DIR = (
- Path(__file__).resolve().parent.parent
- / "src"
- / "Undefined"
- / "skills"
- / "agents"
- / "arxiv_analysis_agent"
-)
-
-
-def _load_json(name: str) -> dict[str, Any]:
- result: dict[str, Any] = json.loads((AGENT_DIR / name).read_text(encoding="utf-8"))
- return result
-
-
-class TestAgentConfig:
- def test_config_json_schema(self) -> None:
- cfg = _load_json("config.json")
- assert cfg["type"] == "function"
- func = cfg["function"]
- assert func["name"] == "arxiv_analysis_agent"
- assert "paper_id" in func["parameters"]["properties"]
- assert "paper_id" in func["parameters"]["required"]
-
- def test_callable_json(self) -> None:
- cfg = _load_json("callable.json")
- assert cfg["enabled"] is True
- assert isinstance(cfg["allowed_callers"], list)
- assert len(cfg["allowed_callers"]) > 0
-
- def test_tools_exist(self) -> None:
- tools_dir = AGENT_DIR / "tools"
- assert (tools_dir / "fetch_paper" / "config.json").exists()
- assert (tools_dir / "fetch_paper" / "handler.py").exists()
- assert (tools_dir / "read_paper_pages" / "config.json").exists()
- assert (tools_dir / "read_paper_pages" / "handler.py").exists()
-
- def test_prompt_md_exists(self) -> None:
- assert (AGENT_DIR / "prompt.md").exists()
- content = (AGENT_DIR / "prompt.md").read_text(encoding="utf-8")
- assert len(content) > 100
-
-
-# ---------------------------------------------------------------------------
-# handler.py
-# ---------------------------------------------------------------------------
-
-
-class TestHandler:
- @pytest.mark.asyncio
- async def test_empty_paper_id(self) -> None:
- from Undefined.skills.agents.arxiv_analysis_agent.handler import execute
-
- result = await execute({"paper_id": ""}, {})
- assert "请提供" in result
-
- @pytest.mark.asyncio
- async def test_invalid_paper_id(self) -> None:
- from Undefined.skills.agents.arxiv_analysis_agent.handler import execute
-
- result = await execute({"paper_id": "not-a-valid-id"}, {})
- assert "无法解析" in result
-
- @pytest.mark.asyncio
- async def test_valid_paper_id_calls_runner(self) -> None:
- from Undefined.skills.agents.arxiv_analysis_agent.handler import execute
-
- mock_result = "分析结果"
- with patch(
- "Undefined.skills.agents.arxiv_analysis_agent.handler.run_agent_with_tools",
- new_callable=AsyncMock,
- return_value=mock_result,
- ) as mock_run:
- result = await execute(
- {"paper_id": "2301.07041", "prompt": "分析方法论"},
- {"request_id": "test-123"},
- )
- assert result == mock_result
- mock_run.assert_awaited_once()
- call_kwargs = mock_run.call_args[1]
- assert call_kwargs["agent_name"] == "arxiv_analysis_agent"
- assert "分析方法论" in call_kwargs["user_content"]
-
- @pytest.mark.asyncio
- async def test_url_input_normalized(self) -> None:
- from Undefined.skills.agents.arxiv_analysis_agent.handler import execute
-
- with patch(
- "Undefined.skills.agents.arxiv_analysis_agent.handler.run_agent_with_tools",
- new_callable=AsyncMock,
- return_value="ok",
- ):
- ctx: dict[str, Any] = {}
- await execute(
- {"paper_id": "https://arxiv.org/abs/2301.07041"},
- ctx,
- )
- assert ctx["arxiv_paper_id"] == "2301.07041"
-
-
-# ---------------------------------------------------------------------------
-# fetch_paper tool
-# ---------------------------------------------------------------------------
-
-
-def _make_paper_info(
- paper_id: str = "2301.07041",
-) -> Any:
- from Undefined.arxiv.models import PaperInfo
-
- return PaperInfo(
- paper_id=paper_id,
- title="Test Paper Title",
- authors=("Author A", "Author B"),
- summary="This is a test abstract.",
- published="2023-01-17T00:00:00Z",
- updated="2023-01-18T00:00:00Z",
- primary_category="cs.AI",
- abs_url=f"https://arxiv.org/abs/{paper_id}",
- pdf_url=f"https://arxiv.org/pdf/{paper_id}.pdf",
- )
-
-
-class TestFetchPaper:
- @pytest.mark.asyncio
- async def test_empty_paper_id(self) -> None:
- from Undefined.skills.agents.arxiv_analysis_agent.tools.fetch_paper.handler import (
- execute,
- )
-
- result = await execute({"paper_id": ""}, {})
- assert "错误" in result
-
- @pytest.mark.asyncio
- async def test_metadata_only_on_download_failure(self) -> None:
- from Undefined.skills.agents.arxiv_analysis_agent.tools.fetch_paper.handler import (
- execute,
- )
-
- paper = _make_paper_info()
- with (
- patch(
- "Undefined.skills.agents.arxiv_analysis_agent.tools.fetch_paper.handler.get_paper_info",
- new_callable=AsyncMock,
- return_value=paper,
- ),
- patch(
- "Undefined.skills.agents.arxiv_analysis_agent.tools.fetch_paper.handler.download_paper_pdf",
- new_callable=AsyncMock,
- side_effect=RuntimeError("network error"),
- ),
- ):
- result = await execute({"paper_id": "2301.07041"}, {})
- assert "Test Paper Title" in result
- assert "Author A" in result
- assert "PDF 下载失败" in result
-
- @pytest.mark.asyncio
- async def test_paper_not_found(self) -> None:
- from Undefined.skills.agents.arxiv_analysis_agent.tools.fetch_paper.handler import (
- execute,
- )
-
- with patch(
- "Undefined.skills.agents.arxiv_analysis_agent.tools.fetch_paper.handler.get_paper_info",
- new_callable=AsyncMock,
- return_value=None,
- ):
- result = await execute({"paper_id": "9999.99999"}, {})
- assert "未找到" in result
-
- @pytest.mark.asyncio
- async def test_successful_fetch_with_pdf(self, tmp_path: Path) -> None:
- from Undefined.skills.agents.arxiv_analysis_agent.tools.fetch_paper.handler import (
- execute,
- )
-
- paper = _make_paper_info()
- pdf_path = tmp_path / "test.pdf"
-
- import fitz
-
- doc = fitz.open()
- page = doc.new_page()
- page.insert_text((72, 72), "Hello world")
- doc.save(str(pdf_path))
- doc.close()
-
- from Undefined.arxiv.downloader import PaperDownloadResult
-
- download_result = PaperDownloadResult(
- path=pdf_path, size_bytes=1024, status="downloaded"
- )
-
- with (
- patch(
- "Undefined.skills.agents.arxiv_analysis_agent.tools.fetch_paper.handler.get_paper_info",
- new_callable=AsyncMock,
- return_value=paper,
- ),
- patch(
- "Undefined.skills.agents.arxiv_analysis_agent.tools.fetch_paper.handler.download_paper_pdf",
- new_callable=AsyncMock,
- return_value=(download_result, tmp_path),
- ),
- ):
- ctx: dict[str, Any] = {}
- result = await execute({"paper_id": "2301.07041"}, ctx)
- assert "Test Paper Title" in result
- assert "1 页" in result
- assert ctx["_arxiv_pdf_path"] == str(pdf_path)
- assert ctx["_arxiv_pdf_pages"] == 1
-
-
-# ---------------------------------------------------------------------------
-# read_paper_pages tool
-# ---------------------------------------------------------------------------
-
-
-class TestReadPaperPages:
- @pytest.mark.asyncio
- async def test_no_pdf_downloaded(self) -> None:
- from Undefined.skills.agents.arxiv_analysis_agent.tools.read_paper_pages.handler import (
- execute,
- )
-
- result = await execute({"start_page": 1, "end_page": 1}, {})
- assert "先调用 fetch_paper" in result
-
- @pytest.mark.asyncio
- async def test_read_single_page(self, tmp_path: Path) -> None:
- from Undefined.skills.agents.arxiv_analysis_agent.tools.read_paper_pages.handler import (
- execute,
- )
-
- import fitz
-
- pdf_path = tmp_path / "paper.pdf"
- doc = fitz.open()
- page = doc.new_page()
- page.insert_text((72, 72), "Page 1 content")
- page2 = doc.new_page()
- page2.insert_text((72, 72), "Page 2 content")
- doc.save(str(pdf_path))
- doc.close()
-
- ctx: dict[str, Any] = {
- "_arxiv_pdf_path": str(pdf_path),
- "_arxiv_pdf_pages": 2,
- }
- result = await execute({"start_page": 1, "end_page": 1}, ctx)
- assert "第 1 页" in result
- assert "Page 1 content" in result
- assert "Page 2 content" not in result
-
- @pytest.mark.asyncio
- async def test_read_page_range(self, tmp_path: Path) -> None:
- from Undefined.skills.agents.arxiv_analysis_agent.tools.read_paper_pages.handler import (
- execute,
- )
-
- import fitz
-
- pdf_path = tmp_path / "paper.pdf"
- doc = fitz.open()
- for i in range(3):
- page = doc.new_page()
- page.insert_text((72, 72), f"Content of page {i + 1}")
- doc.save(str(pdf_path))
- doc.close()
-
- ctx: dict[str, Any] = {
- "_arxiv_pdf_path": str(pdf_path),
- "_arxiv_pdf_pages": 3,
- }
- result = await execute({"start_page": 1, "end_page": 3}, ctx)
- assert "第 1-3 页" in result or "第 1 页" in result
- assert "Content of page 1" in result
- assert "Content of page 3" in result
-
- @pytest.mark.asyncio
- async def test_out_of_range_page(self, tmp_path: Path) -> None:
- from Undefined.skills.agents.arxiv_analysis_agent.tools.read_paper_pages.handler import (
- execute,
- )
-
- import fitz
-
- pdf_path = tmp_path / "paper.pdf"
- doc = fitz.open()
- doc.new_page()
- doc.save(str(pdf_path))
- doc.close()
-
- ctx: dict[str, Any] = {
- "_arxiv_pdf_path": str(pdf_path),
- "_arxiv_pdf_pages": 1,
- }
- result = await execute({"start_page": 5, "end_page": 10}, ctx)
- assert "错误" in result
-
- @pytest.mark.asyncio
- async def test_invalid_page_numbers(self) -> None:
- from Undefined.skills.agents.arxiv_analysis_agent.tools.read_paper_pages.handler import (
- execute,
- )
-
- ctx: dict[str, Any] = {
- "_arxiv_pdf_path": "/some/path.pdf",
- "_arxiv_pdf_pages": 10,
- }
- result = await execute({"start_page": "abc", "end_page": "def"}, ctx)
- assert "整数" in result
-
-
-# ---------------------------------------------------------------------------
-# web_agent callable.json 更新检查
-# ---------------------------------------------------------------------------
-
-
-class TestWebAgentCallable:
- def test_web_agent_allows_new_callers(self) -> None:
- web_agent_callable = (
- Path(__file__).resolve().parent.parent
- / "src"
- / "Undefined"
- / "skills"
- / "agents"
- / "web_agent"
- / "callable.json"
- )
- cfg = json.loads(web_agent_callable.read_text(encoding="utf-8"))
- callers = cfg["allowed_callers"]
- assert "summary_agent" in callers
- assert "arxiv_analysis_agent" in callers
diff --git a/tests/test_arxiv_tools.py b/tests/test_arxiv_tools.py
index fbcfb04f..1f21bd82 100644
--- a/tests/test_arxiv_tools.py
+++ b/tests/test_arxiv_tools.py
@@ -1,15 +1,21 @@
from __future__ import annotations
+from pathlib import Path
from types import SimpleNamespace
+from unittest.mock import AsyncMock
import pytest
+import Undefined.arxiv.sender as arxiv_sender
+from Undefined.arxiv.downloader import PaperDownloadResult
from Undefined.arxiv.client import SearchResponse
from Undefined.arxiv.models import PaperInfo
+from Undefined.attachments import AttachmentRegistry
from Undefined.skills.agents.info_agent.tools.arxiv_search import (
handler as arxiv_search,
)
from Undefined.skills.tools.arxiv_paper import handler as arxiv_paper
+from Undefined.utils import io
@pytest.mark.asyncio
@@ -46,6 +52,79 @@ async def _fake_send_arxiv_paper(**kwargs: object) -> str:
assert captured["summary_preview_chars"] == 2048
+@pytest.mark.asyncio
+async def test_arxiv_paper_tool_uid_mode_registers_pdf(
+ monkeypatch: pytest.MonkeyPatch,
+ tmp_path: Path,
+) -> None:
+ pdf_path = tmp_path / "paper.pdf"
+ await io.write_bytes(pdf_path, b"%PDF-1.4")
+ registry = AttachmentRegistry(
+ registry_path=tmp_path / "attachment_registry.json",
+ cache_dir=tmp_path / "attachments",
+ )
+
+ async def _fake_get_paper_info(
+ _paper_id: str,
+ *,
+ context: dict[str, object] | None = None,
+ ) -> PaperInfo:
+ _ = context
+ return PaperInfo(
+ paper_id="2501.01234",
+ title="UID Paper",
+ authors=("Alice",),
+ summary="summary",
+ published="2025-01-02T00:00:00Z",
+ updated="",
+ primary_category="cs.AI",
+ abs_url="https://arxiv.org/abs/2501.01234",
+ pdf_url="https://arxiv.org/pdf/2501.01234.pdf",
+ )
+
+ async def _fake_download_paper_pdf(
+ _info: PaperInfo,
+ *,
+ max_file_size_mb: int,
+ context: dict[str, object] | None = None,
+ ) -> tuple[PaperDownloadResult, Path]:
+ _ = max_file_size_mb, context
+ return (
+ PaperDownloadResult(
+ pdf_path, await io.get_file_size(pdf_path), "downloaded"
+ ),
+ tmp_path,
+ )
+
+ cleanup_mock = AsyncMock()
+ monkeypatch.setattr(arxiv_sender, "get_paper_info", _fake_get_paper_info)
+ monkeypatch.setattr(arxiv_sender, "download_paper_pdf", _fake_download_paper_pdf)
+ monkeypatch.setattr(arxiv_sender, "cleanup_download_path", cleanup_mock)
+
+ result = await arxiv_paper.execute(
+ {"paper_id": "2501.01234", "output_mode": "uid"},
+ {
+ "request_type": "private",
+ "user_id": 12345,
+ "attachment_registry": registry,
+ "runtime_config": SimpleNamespace(
+ arxiv_max_file_size=42,
+ arxiv_author_preview_limit=7,
+ arxiv_summary_preview_chars=2048,
+ ),
+ },
+ )
+
+ assert "UID Paper" in result
+ assert ' None:
+ video_path = tmp_path / "video.mp4"
+ video_path.write_bytes(b"video bytes")
+ registry = AttachmentRegistry(
+ registry_path=tmp_path / "attachment_registry.json",
+ cache_dir=tmp_path / "attachments",
+ )
+
+ monkeypatch.setattr(
+ bilibili_sender,
+ "normalize_to_bvid",
+ AsyncMock(return_value="BV1xx411c7mD"),
+ )
+ monkeypatch.setattr(
+ bilibili_sender,
+ "download_video",
+ AsyncMock(return_value=(video_path, _video_info(), 80)),
+ )
+ cleanup_mock = MagicMock()
+ monkeypatch.setattr(bilibili_sender, "cleanup_file", cleanup_mock)
+
+ result = await bilibili_sender.fetch_bilibili_video_attachment(
+ "BV1xx411c7mD",
+ attachment_registry=registry,
+ scope_key="group:123456",
+ max_file_size=100,
+ )
+
+ assert "测试视频" in result
+ assert ' None:
encoding="utf-8",
)
(tauri_root / "tauri.conf.json").write_text(
- json.dumps({"productName": app_dir, "version": version}, indent="\t")
- + "\n",
+ (
+ "{\n"
+ f'\t"productName": "{app_dir}",\n'
+ f'\t"version": "{version}",\n'
+ '\t"bundle": {\n'
+ '\t\t"targets": ["appimage", "deb", "dmg", "msi", "nsis"]\n'
+ "\t}\n"
+ "}\n"
+ ),
encoding="utf-8",
)
(tauri_root / "Cargo.lock").write_text(
@@ -166,6 +173,9 @@ def test_bump_project_versions_updates_console_and_chat_manifests_and_locks(
encoding="utf-8"
)
assert _json_version(tauri_root / "tauri.conf.json") == "2.0.0"
+ assert '\t\t"targets": ["appimage", "deb", "dmg", "msi", "nsis"]' in (
+ tauri_root / "tauri.conf.json"
+ ).read_text(encoding="utf-8")
assert (
_cargo_lock_root_version(tauri_root / "Cargo.lock", cargo_package)
== "2.0.0"
diff --git a/tests/test_file_analysis_pdf_page.py b/tests/test_file_analysis_pdf_page.py
new file mode 100644
index 00000000..264a5d87
--- /dev/null
+++ b/tests/test_file_analysis_pdf_page.py
@@ -0,0 +1,85 @@
+from __future__ import annotations
+
+from pathlib import Path
+from typing import Any, cast
+
+import fitz
+import pytest
+
+from Undefined.skills.agents.file_analysis_agent.tools.describe_pdf_page import (
+ handler as describe_pdf_page,
+)
+
+
+def _make_pdf(path: Path, page_count: int = 3) -> None:
+ doc = fitz.open()
+ for index in range(page_count):
+ page = doc.new_page()
+ page.insert_text((72, 72), f"Page {index + 1}")
+ doc.save(str(path))
+ doc.close()
+
+
+@pytest.mark.asyncio
+async def test_describe_pdf_page_supports_mixed_page_range(
+ tmp_path: Path,
+ monkeypatch: pytest.MonkeyPatch,
+) -> None:
+ pdf_path = tmp_path / "demo.pdf"
+ _make_pdf(pdf_path, page_count=5)
+ calls: list[dict[str, Any]] = []
+
+ async def _fake_analyze(args: dict[str, Any], context: dict[str, Any]) -> str:
+ _ = context
+ calls.append(args)
+ return f"分析 {Path(str(args['file_path'])).name}"
+
+ monkeypatch.setattr(
+ cast(Any, describe_pdf_page).analyze_multimodal_handler,
+ "execute",
+ _fake_analyze,
+ )
+
+ result = await describe_pdf_page.execute(
+ {
+ "file_path": str(pdf_path),
+ "page_range": "1,3-4",
+ "prompt": "描述图表",
+ },
+ {"download_cache_dir": tmp_path / "downloads", "ai_client": object()},
+ )
+
+ assert "本次视觉分析页码:1, 3, 4" in result
+ assert "--- 第 1 页 ---" in result
+ assert "--- 第 3 页 ---" in result
+ assert "--- 第 4 页 ---" in result
+ assert len(calls) == 3
+ assert {call["media_type"] for call in calls} == {"image"}
+ assert {call["prompt"] for call in calls} == {"描述图表"}
+ assert not list((tmp_path / "downloads").glob("**/*.png"))
+
+
+@pytest.mark.asyncio
+async def test_describe_pdf_page_rejects_too_many_pages(tmp_path: Path) -> None:
+ pdf_path = tmp_path / "demo.pdf"
+ _make_pdf(pdf_path, page_count=8)
+
+ result = await describe_pdf_page.execute(
+ {"file_path": str(pdf_path), "page_range": "1-6"},
+ {"download_cache_dir": tmp_path / "downloads", "ai_client": object()},
+ )
+
+ assert "单次最多分析 5 页" in result
+
+
+@pytest.mark.asyncio
+async def test_describe_pdf_page_rejects_out_of_range_page(tmp_path: Path) -> None:
+ pdf_path = tmp_path / "demo.pdf"
+ _make_pdf(pdf_path, page_count=2)
+
+ result = await describe_pdf_page.execute(
+ {"file_path": str(pdf_path), "page_range": "3"},
+ {"download_cache_dir": tmp_path / "downloads", "ai_client": object()},
+ )
+
+ assert "超出范围" in result
diff --git a/tests/test_naga_code_analysis_agent.py b/tests/test_naga_code_analysis_agent.py
index 4ad59ca2..2aa7eafe 100644
--- a/tests/test_naga_code_analysis_agent.py
+++ b/tests/test_naga_code_analysis_agent.py
@@ -24,13 +24,14 @@ def test_prompt_and_intro_define_naga_only_scope() -> None:
prompt = (AGENT_DIR / "prompt.md").read_text("utf-8")
intro = (AGENT_DIR / "intro.md").read_text("utf-8")
- assert "分析第一步:调用read_naga_intro工具" in prompt
- assert "非 NagaAgent 技术问题要说明越界并返回给主 AI 重新路由" in prompt
+ assert "`read_naga_intro` 提供 NagaAgent 当前结构索引" in prompt
+ assert "如果问题越界,简明说明原因并建议正确 agent" in prompt
assert "不回答 Undefined 自身源码问题" in prompt
- assert "不承担代码编写、修改、执行验证或打包交付任务" in prompt
- assert "**仅限 NagaAgent 项目**,不回答 Undefined 自身源码问题" in intro
- assert "用户上传/外部文件解析请用 `file_analysis_agent`" in intro
- assert "代码编写、修改、执行验证和打包交付请用 `code_delivery_agent`" in intro
+ assert "不承担代码编写、修改、执行验证或打包交付" in prompt
+ assert "仅用于回答 **NagaAgent 项目**" in intro
+ assert "Undefined 自身源码问题,交给 `undefined_self_code_agent`" in intro
+ assert "用户上传/外部文件解析,交给 `file_analysis_agent`" in intro
+ assert "代码编写、修改、执行验证和打包交付,交给 `code_delivery_agent`" in intro
def test_config_description_defines_naga_only_scope() -> None:
diff --git a/uv.lock b/uv.lock
index 9d1af02f..e42e2c6c 100644
--- a/uv.lock
+++ b/uv.lock
@@ -4626,7 +4626,7 @@ wheels = [
[[package]]
name = "undefined-bot"
-version = "3.6.0"
+version = "3.6.1"
source = { editable = "." }
dependencies = [
{ name = "aiofiles" },