Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
ec04412
fix(agent): remove invoke timeout caps
69gg Mar 31, 2026
1c3061f
fix(image-gen): support base64 responses and preserve size
69gg Mar 31, 2026
49ea667
fix(skills): register dynamic modules before exec
69gg Mar 31, 2026
b8477b9
fix(image-gen): expose upstream errors and lock model
69gg Apr 1, 2026
03ce712
fix(image-gen): default responses to base64
69gg Apr 1, 2026
a4f402d
feat(multimodal): add attachment uid registry and pic embeds
69gg Apr 1, 2026
dad2970
feat(image-gen): add agent-based prompt moderation
69gg Apr 1, 2026
7cf6bef
feat(multimodal): support forwarded attachment uids
69gg Apr 1, 2026
9102ee2
feat(image-gen): support reference image edits
69gg Apr 1, 2026
73f3801
fix(cognitive): refresh profile display names on rename
69gg Apr 1, 2026
91e77bc
feat(multimodal): adapt image tools to attachment uid mechanism
69gg Apr 2, 2026
0e118ab
fix(multimodal): harden attachment rendering and image edit upload
69gg Apr 2, 2026
7356778
fix(ai): avoid blocking image edit file io
69gg Apr 2, 2026
ee03214
style(tests): format ai draw one handler test
69gg Apr 2, 2026
6506b53
chore(version): bump version to 3.2.8
69gg Apr 2, 2026
0ff699b
fix(runtime): harden profile sync and attachment loading
69gg Apr 2, 2026
8bb5ea3
style(tests): format runtime review regression tests
69gg Apr 2, 2026
fb56232
fix(api): preserve webui attachment scope
69gg Apr 3, 2026
88343f7
perf(prompt): move stable rules ahead of dynamic context
69gg Apr 3, 2026
651eda2
fix(runtime): tighten timeout and attachment registry handling
69gg Apr 3, 2026
fa77d8d
fix(attachments): remove constructor disk load
69gg Apr 3, 2026
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
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,22 @@
## v3.2.8 多模态附件与参考图生图

围绕多模态消息链路进行了较大增强,引入统一的附件 UID 注册与 `<pic uid="..."/>` 图文混排机制,打通群聊、私聊、WebUI 和合并转发中的图片/文件上下文。同步为生图工具补齐参考图生图、独立图片编辑模型配置与提示词审核能力,并完善 Runtime API、认知侧写和动态技能加载的稳定性。

- 新增统一附件注册系统,群聊、私聊、WebUI 会话和合并转发中的图片/文件会登记为内部附件 UID,并写入历史记录与提示词上下文。
- 系统提示词与发送链路支持 `<pic uid="..."/>` 图文混排,AI 可直接在回复中内嵌当前会话内的图片 UID。
- 新增 `fetch_image_uid` 工具,支持将远程图片 URL 拉取并注册为当前会话可复用的图片 UID。
- `get_picture`、`render_html`、`render_markdown`、`render_latex` 默认改为 `embed` 返回可嵌入图片 UID,同时保留 `send` 直接发送模式。
- `file_analysis_agent` 及相关文件/图片链路已适配附件 UID,优先使用内部 `pic_*` / `file_*` 标识,也继续兼容 URL 与 legacy `file_id`。
- `ai_draw_one` 支持基于 `reference_image_uids` 的参考图生图,新增 `[models.image_edit]` 配置节,并接入 OpenAI 兼容的 `/v1/images/edits` 接口。
- 新增基于 Agent 模型的生图提示词审核能力,并补充独立审核提示模板。
- 优化图片生成请求体验,支持 base64 返回、保留显式尺寸、锁定模型参数,并更清晰地暴露上游错误。
- 修复附件渲染、图片编辑上传与阻塞式文件读取问题,增强多模态链路稳定性。
- Runtime API 与 WebUI 现在会注册聊天附件、渲染图片 UID,并支持本地 `file://` 图片预览;同时取消 `_agent` 工具调用的固定超时上限。
- 认知服务会在用户昵称或群名变化时自动刷新 profile 展示名与向量索引,减少侧写名称陈旧问题。
- 修复动态技能模块加载与超时包装问题,并补充相关测试覆盖;同步整理 README,移除顶部头图。

---

## v3.2.7 arXiv 工具集与运行时变更感知

新增 arXiv 论文搜索与提取工具集,以及运行时 CHANGELOG 查询能力。重构了生图工具支持 OpenAI 兼容接口,引入 grok_search 联网搜索工具,并让 AI 在系统提示词中感知自身模型配置信息。同步修复了多项稳定性问题与 CI 效率优化。
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.2.7",
"version": "3.2.8",
"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.2.7"
version = "3.2.8"
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.2.7",
"version": "3.2.8",
"identifier": "com.undefined.console",
"build": {
"beforeDevCommand": "npm run dev",
Expand Down
17 changes: 17 additions & 0 deletions config.toml.example
Original file line number Diff line number Diff line change
Expand Up @@ -527,6 +527,23 @@ model_name = ""
# en: Extra request-body params (optional).
[models.image_gen.request_params]

# zh: 参考图生图模型配置(用于 ai_draw_one 传入 reference_image_uids 时调用 OpenAI 兼容的图片编辑接口)。
# en: Reference-image generation model config (used when ai_draw_one receives reference_image_uids and calls the OpenAI-compatible image editing API).
[models.image_edit]
# zh: OpenAI-compatible 基址 URL,例如 https://api.openai.com/v1(最终请求路径为 /v1/images/edits)。
# en: OpenAI-compatible base URL, e.g. https://api.openai.com/v1 (final request path is /v1/images/edits).
api_url = ""
# zh: API Key。
# en: API key.
api_key = ""
# zh: 模型名称,空则回退到 [models.image_gen] 的 model_name。
# en: Model name, empty falls back to [models.image_gen].model_name.
model_name = ""

# zh: 额外请求体参数(可选)。
# en: Extra request-body params (optional).
[models.image_edit.request_params]

# zh: 本地知识库配置。
# en: Local knowledge base settings.
[knowledge]
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "Undefined-bot"
version = "3.2.7"
version = "3.2.8"
description = "QQ bot platform with cognitive memory architecture and multi-agent Skills, via OneBot V11."
readme = "README.md"
authors = [
Expand Down
16 changes: 16 additions & 0 deletions res/prompts/image_gen_moderation.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
你是图片生成审核助手,只根据“待生成图片的提示词”判断是否允许生成。

拦截范围包括但不限于:
- 露骨色情、性剥削、未成年人性化
- 血腥暴力、肢解、虐杀等重口内容
- 明显违法、极端、恐怖主义相关内容
- 明显侵犯隐私、仇恨或其他高风险内容

审核原则:
- 只审核风险,不提供改写建议,不解释政策,不输出多余内容
- 安全、普通、模糊但不明显违规的内容默认放行
- 如果只是普通人物、风景、二次元、服饰、轻微动作描写,不要误杀

输出格式必须严格遵守:
- 允许:`ALLOW`
- 拒绝:`BLOCK: <不超过20字的简短中文原因>`
9 changes: 9 additions & 0 deletions res/prompts/undefined.xml
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,15 @@

**关键点:每次消息处理都必须以 end 结束,这是维持对话流的核心机制。**
</workflow>

<mixed_media_output priority="P0">
**图文混排规则:**
- 如果上下文或工具结果给了图片 UID(例如 `pic_ab12cd34`),你可以在 `send_message.message` 里直接插入 `<pic uid="pic_ab12cd34"/>`
- `<pic uid="..."/>` 是唯一允许的内嵌图片语法;不要改成 Markdown 图片、HTML `<img>`、代码块或自然语言描述
- 可以图文混排,例如:`我给你介绍一下`\n`<pic uid="pic_xxx"/>`\n`如图所示`
- 只能引用当前会话里明确给出的图片 UID,禁止臆造 UID
- 只有 `pic_*` 这类图片 UID 能放进 `<pic>`;普通文件 UID 不能放进去
</mixed_media_output>
</message_sending_mechanism>

<!-- 规则4: 对话结束机制 -->
Expand Down
9 changes: 9 additions & 0 deletions res/prompts/undefined_nagaagent.xml
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,15 @@

**关键点:每次消息处理都必须以 end 结束,这是维持对话流的核心机制。**
</workflow>

<mixed_media_output priority="P0">
**图文混排规则:**
- 如果上下文或工具结果给了图片 UID(例如 `pic_ab12cd34`),你可以在 `send_message.message` 里直接插入 `<pic uid="pic_ab12cd34"/>`
- `<pic uid="..."/>` 是唯一允许的内嵌图片语法;不要改成 Markdown 图片、HTML `<img>`、代码块或自然语言描述
- 可以图文混排,例如:`我给你介绍一下`\n`<pic uid="pic_xxx"/>`\n`如图所示`
- 只能引用当前会话里明确给出的图片 UID,禁止臆造 UID
- 只有 `pic_*` 这类图片 UID 能放进 `<pic>`;普通文件 UID 不能放进去
</mixed_media_output>
</message_sending_mechanism>

<!-- 规则4: 对话结束机制 -->
Expand Down
2 changes: 1 addition & 1 deletion src/Undefined/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
"""Undefined - A high-performance, highly scalable QQ group and private chat robot based on a self-developed architecture."""

__version__ = "3.2.7"
__version__ = "3.2.8"
13 changes: 13 additions & 0 deletions src/Undefined/ai/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

import httpx

from Undefined.attachments import AttachmentRegistry
from Undefined.ai.llm import ModelRequester
from Undefined.ai.model_selector import ModelSelector
from Undefined.ai.multimodal import MultimodalAnalyzer
Expand Down Expand Up @@ -136,6 +137,7 @@ def __init__(
self._token_counter = TokenCounter()
self._knowledge_manager: Any = None
self._cognitive_service: Any = cognitive_service
self.attachment_registry = AttachmentRegistry(http_client=self._http_client)

# 私聊发送回调
self._send_private_message_callback: Optional[SendPrivateMessageCallback] = None
Expand Down Expand Up @@ -325,6 +327,13 @@ async def close(self) -> None:
if hasattr(self, "anthropic_skill_registry"):
await self.anthropic_skill_registry.stop_hot_reload()

attachment_registry = getattr(self, "attachment_registry", None)
if attachment_registry is not None and hasattr(attachment_registry, "flush"):
try:
await attachment_registry.flush()
except Exception as exc:
logger.warning("[清理] 刷新附件注册表失败: %s", exc)

# 3) 最后关闭共享 HTTP client
if hasattr(self, "_http_client"):
logger.info("[清理] 正在关闭 AIClient HTTP 客户端...")
Expand Down Expand Up @@ -973,6 +982,10 @@ async def ask(
tool_context.setdefault("onebot_client", onebot_client)
tool_context.setdefault("scheduler", scheduler)
tool_context.setdefault("send_image_callback", self._send_image_callback)
tool_context.setdefault(
"attachment_registry",
getattr(self, "attachment_registry", None),
)
tool_context.setdefault("memory_storage", self.memory_storage)
tool_context.setdefault("knowledge_manager", self._knowledge_manager)
tool_context.setdefault("cognitive_service", self._cognitive_service)
Expand Down
31 changes: 19 additions & 12 deletions src/Undefined/ai/prompts.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

import aiofiles

from Undefined.attachments import attachment_refs_to_xml
from Undefined.context import RequestContext
from Undefined.end_summary_storage import (
EndSummaryStorage,
Expand All @@ -26,7 +27,7 @@
logger = logging.getLogger(__name__)

_CURRENT_MESSAGE_RE = re.compile(
r"<message\b(?P<attrs>[^>]*)>\s*<content>(?P<content>.*?)</content>\s*</message>",
r"<message\b(?P<attrs>[^>]*)>.*?<content>(?P<content>.*?)</content>.*?</message>",
re.DOTALL | re.IGNORECASE,
)
_XML_ATTR_RE = re.compile(r'(?P<key>[a-zA-Z_][a-zA-Z0-9_-]*)="(?P<value>[^"]*)"')
Expand Down Expand Up @@ -326,6 +327,15 @@ async def build_messages(
len(self._anthropic_skill_registry.get_all_skills()),
)

each_rules = await self._load_each_rules()
if each_rules:
messages.append(
{
"role": "system",
"content": f"【强制规则 - 必须在进行任何操作前仔细阅读并严格遵守】\n{each_rules}",
}
)

if self._memory_storage:
memories = self._memory_storage.get_all()
if memories:
Expand Down Expand Up @@ -519,15 +529,6 @@ async def build_messages(
}
)

each_rules = await self._load_each_rules()
if each_rules:
messages.append(
{
"role": "system",
"content": f"【强制规则 - 必须在进行任何操作前仔细阅读并严格遵守】\n{each_rules}",
}
)

messages.append({"role": "user", "content": f"【当前消息】\n{question}"})
logger.debug(
"[Prompt] messages_ready=%s question_len=%s",
Expand Down Expand Up @@ -648,6 +649,7 @@ async def _inject_recent_messages(
chat_name = msg.get("chat_name", "未知群聊")
timestamp = msg.get("timestamp", "")
text = msg.get("message", "")
attachments = msg.get("attachments", [])
role = msg.get("role", "member")
title = msg.get("title", "")
message_id = msg.get("message_id")
Expand All @@ -664,6 +666,11 @@ async def _inject_recent_messages(
msg_id_attr = ""
if message_id is not None:
msg_id_attr = f' message_id="{escape_xml_attr(str(message_id))}"'
attachment_xml = (
f"\n{attachment_refs_to_xml(attachments)}"
if isinstance(attachments, list) and attachments
else ""
)

if msg_type_val == "group":
location = (
Expand All @@ -673,14 +680,14 @@ async def _inject_recent_messages(
xml_msg = (
f'<message{msg_id_attr} sender="{safe_sender}" sender_id="{safe_sender_id}" group_id="{safe_chat_id}" '
f'group_name="{safe_chat_name}" location="{safe_location}" role="{safe_role}" title="{safe_title}" '
f'time="{safe_time}">\n<content>{safe_text}</content>\n</message>'
f'time="{safe_time}">\n<content>{safe_text}</content>{attachment_xml}\n</message>'
)
else:
location = "私聊"
safe_location = escape_xml_attr(location)
xml_msg = (
f'<message{msg_id_attr} sender="{safe_sender}" sender_id="{safe_sender_id}" location="{safe_location}" '
f'time="{safe_time}">\n<content>{safe_text}</content>\n</message>'
f'time="{safe_time}">\n<content>{safe_text}</content>{attachment_xml}\n</message>'
)
context_lines.append(xml_msg)

Expand Down
Loading
Loading