Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,18 @@
## v3.16.0 Bilibili 图文(opus)提取

本版本为 B 站内容自动提取补齐图文(opus / 动态)类型,命中图文链接后发送一次合并转发:第一条是图文元数据、第二条起是正文,文字与图片按原顺序混排,正文超长自动分段;正文里的图文与视频卡片各自成为嵌套转发节点;新增的 `bilibili_opus` 工具还支持按字数区间或关键词读取正文。

- 新增 Bilibili 图文自动提取管线。命中 `bilibili.com/opus/<id>`、`m.bilibili.com/opus/<id>`、`t.bilibili.com/<id>`、`b23.tv` 短链,以及 QQ 小程序与 news 分享卡片中的跳转链接后自动发送合并转发;与视频管线互不干扰,同一条消息同时含 BV 号与图文链接时两条管线各自发送。
- 转发结构与视频一致:第一条节点是图文信息(封面、标题、UP主、时间、阅读 / 点赞 / 评论 / 转发、原文链接),第二条起是正文。正文按单节点 4000 字切分(节点名形如 `正文 1/3`),不截断、不丢内容,图片作为消息段保留在原文位置并可与文本共处同一节点;正文为空时以占位文案说明。
- 正文里的卡片各自展开为独立节点:图文卡片再拉取一次递归展开,视频卡片真实下载视频文件并复用清晰度、时长、体积与超限策略限制且附带弹幕,链接、商品、直播、投票等其它卡片渲染为单个链接卡片节点。展开受 `opus_nested_depth`(默认 5 层)与 `opus_nested_max_cards`(默认 8 张)约束,超出边界降级为一行「标题 — 链接」;嵌套失败只降级该节点,不影响整条转发。
- 图文详情兼容两个接口与两种响应结构:优先 `x/polymer/web-dynamic/v1/opus/detail`,风控(`-352`)、旧版专栏或动态不可见时回退 `x/polymer/web-dynamic/v1/detail`(带 WBI 签名)。`modules` 为列表或字典(含 `major.opus.summary` 摘要)都能解析,段落覆盖文本、图片、分割线、块引用、列表、代码与链接卡片,富文本节点覆盖 @、表情、公式、话题与 BV / CV / 网页链接;指向自身的图文卡片不再被当作嵌套卡片,避免自引用递归。
- 新增 `bilibili_opus` 工具,`output_mode` 与 `bilibili_video` 对齐并额外提供 text 模式:`send` 发送合并转发,`uid` 把正文图片逐张登记为 `<attachment uid="pic_xxx"/>`(默认最多 9 张,单张失败不中断并逐条报告原因),`info` 只返回元信息,`text` 按字数读取正文。该工具已暴露给 `file_analysis_agent`,图文图片可直接做多模态分析。
- `output_mode=text` 支持起始字、结束字与关键词:默认返回前 1000 字(`limit` 上限 20000),`keyword` 返回命中区间(最多 5 处)与前后各 60 字上下文并自动合并重叠片段。返回头给出正文总字数与本次范围,未读完时提示下一段用 `start=N` 继续;关键词未命中、正文为空、`start` 超出范围都有明确文案,非法参数返回可读错误而不是静默截断。
- 新增 `[bilibili].opus_enabled`(默认开启,仍需 `auto_extract_enabled`)、`opus_max_items`(单条消息最多处理几篇图文)、`opus_nested_depth`、`opus_nested_max_cards`;群聊与私聊白名单复用视频侧的 `auto_extract_group_ids` / `auto_extract_private_ids`,旧 `config.toml` 不做改动也可直接运行。
- 图文转发与视频转发同样写入历史:实时 AI 上下文只看到 `forward_...` UID,需要内容时按层调用 `messages.get_forward_msg` 读取,不会因为一张长图文把上下文撑爆。视频与图文的数字缩写、时长、时间戳与 4000 字切分逻辑共用同一份实现,视频原有的提取行为不变。

---

## v3.15.0 按功能拆分 Embedding 配置与稳定性修复

本版本将嵌入模型配置按功能拆分,知识库、认知记忆与表情包可以共用默认模型或各自覆写连接、维度与指令;同时修复侧写并发合并丢失更新、Agent 技能无法加载、热更新静默失效、SIGTERM 停机缺失等多处影响长期运行的问题。
Expand Down
4 changes: 2 additions & 2 deletions apps/undefined-chat/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion apps/undefined-chat/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "undefined-chat",
"private": true,
"version": "3.15.0",
"version": "3.16.0",
"type": "module",
"scripts": {
"tauri": "tauri",
Expand Down
2 changes: 1 addition & 1 deletion apps/undefined-chat/src-tauri/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion apps/undefined-chat/src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "undefined_chat"
version = "3.15.0"
version = "3.16.0"
description = "Undefined native chat client"
authors = ["Undefined contributors"]
license = "MIT"
Expand Down
2 changes: 1 addition & 1 deletion apps/undefined-chat/src-tauri/tauri.conf.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "Undefined Chat",
"version": "3.15.0",
"version": "3.16.0",
"identifier": "com.undefined.chat",
"build": {
"beforeDevCommand": "npm run dev",
Expand Down
22 changes: 17 additions & 5 deletions apps/undefined-chat/src/test-setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,27 @@ import "@testing-library/jest-dom/vitest";
import { beforeEach, vi } from "vitest";
import { LOCALE_STORAGE_KEY } from "./i18n";

// vitest 4 把 jsdom 的 window 复制到测试全局时会丢掉 Storage 的方法:
// 复制出来的 window.localStorage 没有 setItem/getItem(原型退化成 Object.prototype),
// 而 globalThis.jsdom.window.localStorage 仍是完整的 Storage 实例。
// 这里把真身挂回 window,保证 App 代码与测试用例读写的是同一个可用的 localStorage。
// 参考 vitest populateGlobal 对 Storage/accessor 属性的处理限制。
const jsdomWindow = (globalThis as unknown as { jsdom?: { window?: Window } })
.jsdom?.window;
const jsdomStorage = jsdomWindow?.localStorage as Storage | undefined;
if (jsdomStorage && typeof jsdomStorage.setItem === "function") {
Object.defineProperty(window, "localStorage", {
writable: true,
configurable: true,
value: jsdomStorage,
});
}

// 全局锁定 zh-CN:jsdom 默认 navigator.language=en-US 会让 LanguageProvider 回退到 en,
// 破坏现有中文文案断言。每个测试前显式写入 localStorage,确保默认中文;
// 需要测试英文的用例可在测试内自行覆盖(setLocale 或写入 localStorage)。
beforeEach(() => {
try {
window.localStorage.setItem(LOCALE_STORAGE_KEY, "zh-CN");
} catch {
// localStorage 不可用时忽略
}
window.localStorage.setItem(LOCALE_STORAGE_KEY, "zh-CN");
});

// Mock Tauri API
Expand Down
4 changes: 2 additions & 2 deletions apps/undefined-console/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion apps/undefined-console/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "undefined-console",
"private": true,
"version": "3.15.0",
"version": "3.16.0",
"type": "module",
"scripts": {
"tauri": "tauri",
Expand Down
2 changes: 1 addition & 1 deletion apps/undefined-console/src-tauri/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion apps/undefined-console/src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "undefined_console"
version = "3.15.0"
version = "3.16.0"
description = "Undefined cross-platform management console"
authors = ["Undefined contributors"]
license = "MIT"
Expand Down
2 changes: 1 addition & 1 deletion apps/undefined-console/src-tauri/tauri.conf.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "Undefined Console",
"version": "3.15.0",
"version": "3.16.0",
"identifier": "com.undefined.console",
"build": {
"beforeDevCommand": "npm run dev",
Expand Down
12 changes: 12 additions & 0 deletions config.toml.example
Original file line number Diff line number Diff line change
Expand Up @@ -1473,6 +1473,18 @@ auto_extract_group_ids = []
# zh: 自动提取功能的私聊白名单(空=跟随全局 access.allowed_private_ids)。
# en: Private chat allowlist for auto-extraction (empty = follow global access.allowed_private_ids).
auto_extract_private_ids = []
# zh: 是否在自动提取中处理 B 站图文(opus / 动态)链接。仍需 auto_extract_enabled 打开。
# en: Whether Bilibili opus/dynamic links are handled during auto-extraction. auto_extract_enabled must also be on.
opus_enabled = true
# zh: 单条消息最多自动处理几篇图文。<=0 回退 3,>10 截断到 10。
# en: Max number of opus posts to auto-process from one message. <=0 falls back to 3, >10 is capped to 10.
opus_max_items = 3
# zh: 图文正文里的图文/视频卡片最多再展开几层(每层都是嵌套合并转发)。0=不展开,只给链接;上限 5。
# en: Max nesting depth for opus/video cards inside a post body (each level is a nested forward). 0=link only; capped at 5.
opus_nested_depth = 5
# zh: 单篇图文最多展开多少张嵌套卡片,超出部分降级为一行文本+链接。上限 20。
# en: Max nested cards expanded per post; extras degrade to a single text line with the link. Capped at 20.
opus_nested_max_cards = 8

# zh: 抖音视频自动提取配置。
# en: Douyin video auto-extraction settings.
Expand Down
11 changes: 11 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -1016,13 +1016,24 @@ api_key = "replace-with-your-key"
| `danmaku_max_count` | `0` | 最多提取多少条弹幕,`0` 不限 | `<0` 回退 `0` |
| `auto_extract_group_ids` | `[]` | 功能级群白名单 | 空时跟随全局 access |
| `auto_extract_private_ids` | `[]` | 功能级私聊白名单 | 空时跟随全局 access |
| `opus_enabled` | `true` | 是否在自动提取中处理 B 站图文(opus / 动态)链接 | 仍需 `auto_extract_enabled` 打开 |
| `opus_max_items` | `3` | 单条消息最多自动处理几篇图文 | `<=0` 回退 `3`,`>10` 截断到 `10` |
| `opus_nested_depth` | `5` | 正文里图文/视频卡片最多再展开几层 | `<0` 回退 `0`,`>5` 截断到 `5` |
| `opus_nested_max_cards` | `8` | 单篇图文最多展开多少张嵌套卡片 | `<0` 回退 `0`,`>20` 截断到 `20` |

自动提取行为:
- 命中 B 站链接、BV 号或 AV 号后,自动提取会发送一次外层合并转发,固定包含三个节点:视频信息、视频文件或视频状态、弹幕列表。
- 弹幕通过 Bilibili protobuf 接口分段拉取;项目内置了解码逻辑,无需安装 `protoc` 或额外生成 protobuf 代码。
- 弹幕列表节点会按每 100 条弹幕生成一个内层合并转发;每条弹幕对应内层合并转发中的一个节点,便于在客户端逐条查看。
- 视频文件下载、清晰度、时长和体积限制仍由本节配置控制;自动提取的转发消息会通过统一发送层写入历史。后续实时 AI 上下文遇到合并转发时只看到 `forward_...` UID,需要内容时按层调用 `messages.get_forward_msg` 读取。

图文(opus)自动提取行为:
- 命中 `bilibili.com/opus/<id>`、`t.bilibili.com/<id>`、`b23.tv` 短链或 QQ 小程序分享卡片后发送一次外层合并转发:**第一条节点是图文元数据**(封面、标题、UP主、时间、阅读/点赞/评论/转发、原文链接),**第二条起是正文内容节点**(文本与图片按原始顺序混排)。
- 正文按单节点 4000 字切分,超出部分追加新的内容节点(节点名形如 `正文 1/3`),不做截断、不丢内容;图片始终保留在原文位置。
- 正文里的图文卡片与视频卡片各自成为**独立的嵌套合并转发节点**(`嵌套图文: ...` / `嵌套视频: ...`),按 `opus_nested_depth` 与 `opus_nested_max_cards` 递归展开;超出边界的卡片降级为一行 `标题 — 链接` 文本节点,不再发请求。其余卡片类型(链接、商品、直播、投票等)渲染为单个 `链接卡片` 节点。
- 嵌套视频会真实下载视频文件,复用 `prefer_quality` / `max_duration` / `max_file_size` / `oversize_strategy` 限制;超限或下载失败时只发视频信息节点,不影响整条转发的发送。
- 图文详情优先请求 `x/polymer/web-dynamic/v1/opus/detail`,失败(含风控、旧版专栏)时自动回退 `x/polymer/web-dynamic/v1/detail`(带 WBI 签名);两者返回的 `modules` 结构不同,解析层都会兼容。

---

### 4.21.1 `[douyin]` 自动提取
Expand Down
4 changes: 2 additions & 2 deletions docs/development.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ src/Undefined/
│ └── multimodal/# 多模态检测、解析与分析
├── attachments/ # 附件注册、渲染、作用域隔离
├── arxiv/ # arXiv 论文解析、元信息获取、PDF 下载与发送
├── bilibili/ # B站视频流解析、分段下载与异步发送
├── bilibili/ # B站视频流解析、分段下载、异步发送;图文(opus)解析与合并转发
├── cognitive/ # 认知记忆系统(service/ 门面 + historian/ 史官后台)
├── config/ # 配置系统(parsers/ 域解析 + load_sections/ 分段加载 + loader shim)
├── handlers/ # OneBot 消息分流(message_flow / poke / repeat / auto_extract)
Expand All @@ -29,7 +29,7 @@ src/Undefined/
│ │ └── cognitive/ # 认知记忆主动暴露工具 (search_events, get_profile 等)
│ ├── agents/ # 智能体 (含 runner/ 通用循环子包)
│ ├── commands/ # 中心化斜杠指令系统 (实现如 /help, /stats, /admin 等平台功能)
│ ├── pipelines/ # 自动提取管线 (bilibili / arxiv / github 等)
│ ├── pipelines/ # 自动提取管线 (bilibili / bilibili_opus / arxiv / github 等)
│ └── anthropic_skills/# Anthropic 协议集成的外部 Skills (兼容 SKILL.md 格式)
├── api/ # Management API + Runtime API
│ ├── routes/ # 路由子模块 (chat, tools, naga/, system, memes, memory, cognitive, health)
Expand Down
23 changes: 22 additions & 1 deletion docs/pipelines.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# 自动处理管线开发指南

自动处理管线位于 `src/Undefined/skills/pipelines/`,用于在普通消息进入 AI 自动回复前执行自动提取,例如 Bilibili 视频、抖音视频、arXiv 论文和 GitHub 仓库卡片。斜杠命令优先级高于自动处理管线,命中命令后不会继续触发自动提取或 AI 回复。
自动处理管线位于 `src/Undefined/skills/pipelines/`,用于在普通消息进入 AI 自动回复前执行自动提取,例如 Bilibili 视频、Bilibili 图文(opus)、抖音视频、arXiv 论文和 GitHub 仓库卡片。斜杠命令优先级高于自动处理管线,命中命令后不会继续触发自动提取或 AI 回复。

`MessageHandler` 启动时会通过异步初始化在线程中加载管线配置和 handler 模块,避免目录扫描、`config.json` 读取和模块导入阻塞事件循环;注册 OneBot 消息回调前会等待首次加载完成,后续热重载也在线程中执行。

Expand Down Expand Up @@ -30,6 +30,24 @@ Douyin 自动提取管线命中 `v.douyin.com/...`、`douyin.com/video/<id>` 或

下载链路读取抖音 SSR share 页中的 `window._ROUTER_DATA`,从 `video.play_addr` 提取 token,再按 `[douyin].prefer_ratios` 探测 `aweme/v1/play/`。探测使用 2 字节 Range GET,并优先按 `Content-Range` 中的总长度对重复文件去重,缺失时回退 `Content-Length`。

## 内置 Bilibili 图文(opus)管线

Bilibili 图文管线命中 `bilibili.com/opus/<id>`、`t.bilibili.com/<id>`、`b23.tv` 短链或 QQ 小程序分享卡片后,发送一次外层合并转发,节点顺序固定:

1. `图文信息`:封面 + 标题 / UP主 / 时间 / 阅读点赞评论转发 / 原文链接;
2. `正文 …`:正文文本与图片按原始顺序混排,按单节点 4000 字切分(`正文 1/3` 这类节点名),不截断、不丢内容;
3. 嵌套节点:正文里的图文卡片与视频卡片各自成为独立嵌套合并转发(`嵌套图文: …` / `嵌套视频: …`),其它卡片类型渲染为单个 `链接卡片` 节点。

嵌套展开受 `[bilibili].opus_nested_depth`(默认 5 层)与 `opus_nested_max_cards`(默认 8 张)约束,超出边界时卡片降级为一行 `标题 — 链接` 文本节点。嵌套视频会真实下载视频文件并复用视频侧的清晰度/时长/体积限制,超限或失败时只发信息节点。

数据来源与解析:

- 优先 `https://api.bilibili.com/x/polymer/web-dynamic/v1/opus/detail`(`modules` 为列表,按 `module_type` 分组);
- 失败时回退 `https://api.bilibili.com/x/polymer/web-dynamic/v1/detail`(`modules` 为字典,正文在 `module_dynamic.desc.text`,图片在 `module_dynamic.major.draw`);
- `OpusInfo` 的 `blocks` 由 `module_content.paragraphs[]` 按 `para_type` 转换而来:文本 1、图片 2、分割线 3、块引用 4、列表 5、链接卡片 6、代码 7。

管线只在 `auto_extract_enabled` 与 `opus_enabled` 同时为真、且会话命中白名单时生效;与视频管线相互独立,同一条消息同时包含 BV 号与图文链接时两条管线各自发送。

## 目录结构

```text
Expand All @@ -41,6 +59,9 @@ src/Undefined/skills/pipelines/
├── bilibili/
│ ├── config.json
│ └── handler.py
├── bilibili_opus/
│ ├── config.json
│ └── handler.py
├── douyin/
│ ├── config.json
│ └── handler.py
Expand Down
1 change: 1 addition & 0 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,7 @@ QQ/NapCat 在 `sendMsg` 阶段返回超时并不等于消息未送达:服务
| `qq_like` | 给指定 QQ 号的资料卡点赞(默认 10 次) |
| `python_interpreter` | 在隔离的 **Docker 容器**中执行 Python 代码,支持按需安装第三方库,可在执行后自动发送生成的文件(图片、CSV 等) |
| `bilibili_video` | 发送哔哩哔哩视频、返回视频附件 UID,或只获取视频信息(支持 BV 号、AV 号、链接) |
| `bilibili_opus` | 发送哔哩哔哩图文(opus)合并转发、返回正文图片附件 UID、获取图文信息,或按字数区间 / 关键词读取正文文字(支持动态 ID、图文链接、b23.tv 短链) |
| `douyin_video` | 发送抖音视频、返回视频附件 UID,或只获取视频信息(支持短链、长链、aweme_id) |
| `arxiv_paper` | 发送 arXiv 论文 PDF、返回 PDF 附件 UID,或只获取论文信息(支持 arXiv ID、链接) |
| `fetch_image_uid` | 将指定 URL 的图片下载并转换为系统内部 uid |
Expand Down
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "Undefined-bot"
version = "3.15.0"
version = "3.16.0"
description = "QQ bot platform with cognitive memory architecture and multi-agent Skills, via OneBot V11."
readme = "README.md"
authors = [
Expand Down Expand Up @@ -103,7 +103,7 @@ exclude = [
python_version = "3.12"
strict = true
ignore_missing_imports = true
exclude = ["^code/", "^apps/.*/src-tauri/target/"]
exclude = ["^code/", "^apps/.*/src-tauri/target/", "^data/cache/uv/"]

[tool.ruff]
exclude = ["code"]
Expand Down
2 changes: 1 addition & 1 deletion src/Undefined/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
from .skills.registry import BaseRegistry as BaseRegistry
from .skills.tools import ToolRegistry as ToolRegistry

__version__: str = "3.15.0"
__version__: str = "3.16.0"

# symbol -> (module_path, attribute_name);首次访问时才 importlib 加载
_LAZY_IMPORTS: dict[str, tuple[str, str]] = {
Expand Down
8 changes: 7 additions & 1 deletion src/Undefined/bilibili/__init__.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
"""Bilibili 视频提取模块

提供 B 站视频标识符解析、视频下载和发送功能。
提供 B 站视频标识符解析、视频下载和发送功能,以及图文(opus)解析。
"""

from Undefined.bilibili.opus_parser import (
extract_opus_from_json_message,
extract_opus_ids_with_shortlinks,
)
from Undefined.bilibili.parser import (
extract_bilibili_ids,
extract_from_json_message,
Expand All @@ -12,5 +16,7 @@
__all__ = [
"extract_bilibili_ids",
"extract_from_json_message",
"extract_opus_from_json_message",
"extract_opus_ids_with_shortlinks",
"normalize_to_bvid",
]
Loading
Loading