nonebot-plugin-smart-message-storage 1.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (24) hide show
  1. nonebot_plugin_smart_message_storage/__init__.py +40 -0
  2. nonebot_plugin_smart_message_storage/config.py +30 -0
  3. nonebot_plugin_smart_message_storage/constants.py +17 -0
  4. nonebot_plugin_smart_message_storage/db.py +15 -0
  5. nonebot_plugin_smart_message_storage/handlers/__init__.py +7 -0
  6. nonebot_plugin_smart_message_storage/handlers/notices.py +110 -0
  7. nonebot_plugin_smart_message_storage/handlers/recognize.py +139 -0
  8. nonebot_plugin_smart_message_storage/handlers/search.py +111 -0
  9. nonebot_plugin_smart_message_storage/handlers/store.py +62 -0
  10. nonebot_plugin_smart_message_storage/models.py +22 -0
  11. nonebot_plugin_smart_message_storage/prompt.py +44 -0
  12. nonebot_plugin_smart_message_storage/services/__init__.py +2 -0
  13. nonebot_plugin_smart_message_storage/services/contacts.py +31 -0
  14. nonebot_plugin_smart_message_storage/services/context.py +138 -0
  15. nonebot_plugin_smart_message_storage/services/image_tasks.py +46 -0
  16. nonebot_plugin_smart_message_storage/services/images.py +69 -0
  17. nonebot_plugin_smart_message_storage/services/message_utils.py +45 -0
  18. nonebot_plugin_smart_message_storage/services/pending.py +321 -0
  19. nonebot_plugin_smart_message_storage/vision.py +129 -0
  20. nonebot_plugin_smart_message_storage-1.1.0.dist-info/METADATA +990 -0
  21. nonebot_plugin_smart_message_storage-1.1.0.dist-info/RECORD +24 -0
  22. nonebot_plugin_smart_message_storage-1.1.0.dist-info/WHEEL +4 -0
  23. nonebot_plugin_smart_message_storage-1.1.0.dist-info/entry_points.txt +2 -0
  24. nonebot_plugin_smart_message_storage-1.1.0.dist-info/licenses/LICENSE +674 -0
@@ -0,0 +1,40 @@
1
+ # python3
2
+ # -*- coding: utf-8 -*-
3
+
4
+ from nonebot import get_driver, require
5
+ from nonebot.log import logger
6
+ from nonebot.plugin import PluginMetadata
7
+
8
+ require("nonebot_plugin_localstore")
9
+
10
+ from .config import MessageStorageConfig
11
+ from .db import init_db
12
+ from .services.pending import start_stale_flush_loop
13
+
14
+ __plugin_meta__ = PluginMetadata(
15
+ name="智能消息存储",
16
+ description="支持群聊/私聊消息归档、检索和 AI 图片理解总结的 NoneBot2 插件。",
17
+ usage=(
18
+ "/查消息 关键词\n"
19
+ "/查消息 群号 关键词\n"
20
+ "/识别\n"
21
+ "/立即识别\n"
22
+ "/立即识别 全部"
23
+ ),
24
+ type="application",
25
+ config=MessageStorageConfig,
26
+ supported_adapters={"~onebot.v11"},
27
+ )
28
+
29
+ init_db()
30
+
31
+ driver = get_driver()
32
+
33
+
34
+ @driver.on_startup
35
+ async def _startup() -> None:
36
+ start_stale_flush_loop()
37
+ logger.debug("Smart message storage plugin started.")
38
+
39
+
40
+ from . import handlers as handlers
@@ -0,0 +1,30 @@
1
+ # python3
2
+ # -*- coding: utf-8 -*-
3
+
4
+ import os
5
+
6
+ from nonebot import get_plugin_config
7
+ from pydantic import BaseModel
8
+
9
+
10
+ class MessageStorageConfig(BaseModel):
11
+ ai_base_url: str = "https://api.exesim.com/v1"
12
+ ai_api_key: str = ""
13
+ ai_model: str = "gemini-3.5-flash"
14
+ image_batch_size: int = 5
15
+ image_flush_seconds: int = 30 * 60
16
+ image_context_before_chars: int = 100
17
+ image_context_after_chars: int = 100
18
+ db_url: str = "sqlite:///qq_messages.db"
19
+
20
+
21
+ config = get_plugin_config(MessageStorageConfig)
22
+
23
+ config.ai_base_url = os.getenv("AI_BASE_URL", config.ai_base_url)
24
+ config.ai_api_key = os.getenv("AI_API_KEY", config.ai_api_key)
25
+ config.ai_model = os.getenv("AI_MODEL", config.ai_model)
26
+ config.db_url = os.getenv("DB_URL", config.db_url)
27
+ config.image_batch_size = int(os.getenv("IMAGE_BATCH_SIZE", config.image_batch_size))
28
+ config.image_flush_seconds = int(os.getenv("IMAGE_FLUSH_SECONDS", config.image_flush_seconds))
29
+ config.image_context_before_chars = int(os.getenv("IMAGE_CONTEXT_BEFORE_CHARS", config.image_context_before_chars))
30
+ config.image_context_after_chars = int(os.getenv("IMAGE_CONTEXT_AFTER_CHARS", config.image_context_after_chars))
@@ -0,0 +1,17 @@
1
+ # python3
2
+ # -*- coding: utf-8 -*-
3
+
4
+ from nonebot.log import logger
5
+ from nonebot_plugin_localstore import get_data_dir
6
+
7
+ from .config import config
8
+
9
+ DATA_DIR = get_data_dir("nonebot_plugin_smart_message_storage")
10
+ IMAGE_CACHE_DIR = DATA_DIR / "image_cache"
11
+ PENDING_FILE = DATA_DIR / "pending_images.json"
12
+
13
+ IMAGE_CACHE_DIR.mkdir(parents=True, exist_ok=True)
14
+
15
+ logger.info(f"Smart message storage data dir: {DATA_DIR}")
16
+ if not config.ai_api_key:
17
+ logger.info("Smart message storage AI image recognition is disabled: ai_api_key is not configured.")
@@ -0,0 +1,15 @@
1
+ # python3
2
+ # -*- coding: utf-8 -*-
3
+
4
+ from sqlalchemy import create_engine
5
+ from sqlalchemy.orm import sessionmaker
6
+
7
+ from .config import config
8
+ from .models import Base
9
+
10
+ engine = create_engine(config.db_url, echo=False, future=True)
11
+ SessionLocal = sessionmaker(bind=engine, autoflush=False, autocommit=False)
12
+
13
+
14
+ def init_db() -> None:
15
+ Base.metadata.create_all(bind=engine)
@@ -0,0 +1,7 @@
1
+ # python3
2
+ # -*- coding: utf-8 -*-
3
+
4
+ from . import notices as notices
5
+ from . import recognize as recognize
6
+ from . import search as search
7
+ from . import store as store
@@ -0,0 +1,110 @@
1
+ # python3
2
+ # -*- coding: utf-8 -*-
3
+
4
+ from datetime import datetime
5
+
6
+ from nonebot import on_notice
7
+ from nonebot.adapters.onebot.v11 import (
8
+ Bot,
9
+ FriendRecallNoticeEvent,
10
+ GroupDecreaseNoticeEvent,
11
+ GroupIncreaseNoticeEvent,
12
+ GroupRecallNoticeEvent,
13
+ PokeNotifyEvent,
14
+ )
15
+ from nonebot.log import logger
16
+
17
+ from ..db import SessionLocal
18
+ from ..models import GroupMessage
19
+ from ..services.contacts import get_display_name
20
+ from ..services.message_utils import conversation_group_id
21
+
22
+ notice_logger = on_notice(priority=100)
23
+
24
+
25
+ def build_poke_raw_message(event: PokeNotifyEvent, target_name: str) -> str:
26
+ action = "戳了戳"
27
+ suffix = ""
28
+ raw_info = event.dict().get("raw_info") or []
29
+ texts = [
30
+ item["txt"]
31
+ for item in raw_info
32
+ if isinstance(item, dict) and item.get("type") == "nor" and item.get("txt")
33
+ ]
34
+
35
+ if texts:
36
+ action = texts[0]
37
+ if len(texts) >= 2:
38
+ suffix = "".join(texts[1:])
39
+ if not texts:
40
+ action = getattr(event, "action", "戳了戳")
41
+ suffix = getattr(event, "suffix", "")
42
+
43
+ return f"[戳一戳]{action}{event.target_id}({target_name}){suffix}"
44
+
45
+
46
+ def build_notice_raw_message(event) -> str:
47
+ if isinstance(event, GroupDecreaseNoticeEvent):
48
+ if event.operator_id == event.user_id:
49
+ return f"[退群]用户{event.user_id}自己退群"
50
+ return f"[被踢]用户{event.user_id}被{event.operator_id}移出本群"
51
+
52
+ if isinstance(event, GroupIncreaseNoticeEvent):
53
+ return f"[加群]用户{event.user_id}由{event.operator_id}同意加入本群"
54
+
55
+ if isinstance(event, GroupRecallNoticeEvent):
56
+ return f"[撤回]消息{event.message_id}被用户{event.operator_id}撤回"
57
+
58
+ if isinstance(event, FriendRecallNoticeEvent):
59
+ return f"[撤回]消息{event.message_id}被用户{event.user_id}撤回"
60
+
61
+ return ""
62
+
63
+
64
+ @notice_logger.handle()
65
+ async def log_notice_event(bot: Bot, event):
66
+ if not isinstance(
67
+ event,
68
+ (
69
+ PokeNotifyEvent,
70
+ GroupDecreaseNoticeEvent,
71
+ GroupIncreaseNoticeEvent,
72
+ GroupRecallNoticeEvent,
73
+ FriendRecallNoticeEvent,
74
+ ),
75
+ ):
76
+ return
77
+
78
+ group_id = conversation_group_id(event)
79
+ session = SessionLocal()
80
+ try:
81
+ if isinstance(event, PokeNotifyEvent):
82
+ target_name = await get_display_name(bot, event.target_id, group_id)
83
+ raw_message = build_poke_raw_message(event, target_name)
84
+ else:
85
+ raw_message = build_notice_raw_message(event)
86
+
87
+ if not raw_message:
88
+ return
89
+
90
+ sender_nickname = await get_display_name(bot, event.user_id, group_id)
91
+ sender_card = sender_nickname if group_id == -1 else ""
92
+ msg = GroupMessage(
93
+ time=datetime.now(),
94
+ self_id=event.self_id,
95
+ user_id=event.user_id,
96
+ group_id=group_id,
97
+ raw_message=raw_message,
98
+ sender_nickname=sender_nickname,
99
+ sender_card=sender_card,
100
+ message_id=-1,
101
+ reply_id=-1,
102
+ )
103
+ session.add(msg)
104
+ session.commit()
105
+ logger.debug(f"[DB] 已记录 notice 到消息表: group_id={group_id}, raw_message={raw_message}")
106
+ except Exception as e:
107
+ logger.error(f"[DB] 保存 notice 到消息表失败: {e}")
108
+ session.rollback()
109
+ finally:
110
+ session.close()
@@ -0,0 +1,139 @@
1
+ # python3
2
+ # -*- coding: utf-8 -*-
3
+
4
+ from nonebot import get_driver, on_command
5
+ from nonebot.adapters.onebot.v11 import Bot, GroupMessageEvent, Message, MessageEvent, PrivateMessageEvent
6
+ from nonebot.log import logger
7
+ from nonebot.params import CommandArg
8
+
9
+ from ..db import SessionLocal
10
+ from ..models import GroupMessage
11
+ from ..services.image_tasks import collect_pending_images
12
+ from ..services.message_utils import IMAGE_CQ_RE, conversation_group_id
13
+ from ..services.pending import flush_pending
14
+
15
+
16
+ def _reply_command_rule(event: MessageEvent) -> bool:
17
+ return isinstance(event, MessageEvent) and _has_reply(event)
18
+
19
+
20
+ recognize_now = on_command("立即识别", priority=5, block=True)
21
+ recognize_reply = on_command("识别", priority=5, block=True, rule=_reply_command_rule)
22
+
23
+
24
+ @recognize_now.handle()
25
+ async def handle_recognize_now(event: MessageEvent, args: Message = CommandArg()):
26
+ text = str(args).strip()
27
+ group_id = conversation_group_id(event)
28
+
29
+ if text == "全部":
30
+ superusers = {str(user) for user in get_driver().config.superusers}
31
+ if str(event.user_id) not in superusers:
32
+ await recognize_now.finish("只有 SUPERUSERS 可以执行全局识别。")
33
+ count = await flush_pending(reason="command_all", all_conversations=True)
34
+ await recognize_now.finish(f"已提交全局待识别图片 {count} 张。")
35
+
36
+ if not isinstance(event, (GroupMessageEvent, PrivateMessageEvent)):
37
+ await recognize_now.finish("当前会话不支持立即识别。")
38
+
39
+ count = await flush_pending(reason="command", group_id=group_id, user_id=event.user_id)
40
+ await recognize_now.finish(f"已提交当前会话待识别图片 {count} 张。")
41
+
42
+
43
+ @recognize_reply.handle()
44
+ async def handle_recognize_reply(bot: Bot, event: MessageEvent):
45
+ reply_message_id = _get_reply_message_id(event)
46
+ if reply_message_id is None:
47
+ return
48
+
49
+ msg = _get_stored_message(reply_message_id)
50
+ if not msg:
51
+ await recognize_reply.finish("没有在数据库中找到回复的消息。")
52
+
53
+ raw_message = msg.raw_message or ""
54
+ if not IMAGE_CQ_RE.search(raw_message):
55
+ if _has_recognized_image(raw_message):
56
+ await _like_command(bot, event, "320")
57
+ await recognize_reply.finish(raw_message)
58
+ await recognize_reply.finish("回复的消息似乎不包含图片哦")
59
+
60
+ await _like_command(bot, event, "314")
61
+ reply_message = await _get_reply_message(bot, event, reply_message_id, raw_message)
62
+ if reply_message:
63
+ await collect_pending_images(bot, reply_message_id, reply_message, msg)
64
+
65
+ await flush_pending(reason="reply_command", group_id=msg.group_id, user_id=msg.user_id)
66
+ refreshed = _get_stored_message(reply_message_id)
67
+ await recognize_reply.finish((refreshed.raw_message if refreshed else raw_message) or raw_message)
68
+
69
+
70
+ def _has_reply(event: MessageEvent) -> bool:
71
+ if event.reply:
72
+ return True
73
+ return any(seg.type == "reply" for seg in event.message)
74
+
75
+
76
+ def _get_reply_message_id(event: MessageEvent) -> int | None:
77
+ if event.reply:
78
+ return event.reply.message_id
79
+ for seg in event.message:
80
+ if seg.type != "reply":
81
+ continue
82
+ try:
83
+ return int(seg.data["id"])
84
+ except Exception:
85
+ logger.warning(f"读取回复消息 ID 失败: {seg.data}")
86
+ return None
87
+ return None
88
+
89
+
90
+ async def _get_reply_message(bot: Bot, event: MessageEvent, message_id: int, fallback_raw_message: str) -> Message | None:
91
+ if event.reply and event.reply.message:
92
+ return event.reply.message
93
+
94
+ for seg in event.message:
95
+ if seg.type == "reply" and str(seg.data.get("id", "")) == str(message_id):
96
+ try:
97
+ msg = await bot.get_msg(message_id=message_id)
98
+ return Message(msg["message"])
99
+ except Exception as e:
100
+ logger.opt(exception=e).warning(f"通过 get_msg 获取回复消息失败: message_id={message_id}")
101
+ break
102
+
103
+ try:
104
+ return Message(fallback_raw_message)
105
+ except Exception as e:
106
+ logger.opt(exception=e).warning(f"从数据库 raw_message 解析回复消息失败: message_id={message_id}")
107
+ return None
108
+
109
+
110
+ def _get_stored_message(message_id: int) -> GroupMessage | None:
111
+ session = SessionLocal()
112
+ try:
113
+ return (
114
+ session.query(GroupMessage)
115
+ .filter(GroupMessage.message_id == message_id)
116
+ .order_by(GroupMessage.id.desc())
117
+ .first()
118
+ )
119
+ finally:
120
+ session.close()
121
+
122
+
123
+ def _has_recognized_image(raw_message: str) -> bool:
124
+ return "[image:{" in raw_message
125
+
126
+
127
+ async def _like_command(bot: Bot, event: MessageEvent, emoji_id: str) -> None:
128
+ if not isinstance(event, GroupMessageEvent):
129
+ return
130
+ try:
131
+ await bot.call_api(
132
+ "set_msg_emoji_like",
133
+ group_id=event.group_id,
134
+ message_id=event.message_id,
135
+ emoji_id=emoji_id,
136
+ set=True,
137
+ )
138
+ except Exception as e:
139
+ logger.opt(exception=e).warning("贴表情失败")
@@ -0,0 +1,111 @@
1
+ # python3
2
+ # -*- coding: utf-8 -*-
3
+
4
+ import base64
5
+ import io
6
+ import re
7
+ import textwrap
8
+
9
+ from nonebot import on_command
10
+ from nonebot.adapters.onebot.v11 import Bot, GroupMessageEvent, Message, MessageEvent, MessageSegment, PrivateMessageEvent
11
+ from nonebot.exception import FinishedException
12
+ from nonebot.log import logger
13
+ from nonebot.params import CommandArg
14
+ from PIL import Image, ImageDraw, ImageFont
15
+ from sqlalchemy import Integer, cast, desc
16
+
17
+ from ..db import SessionLocal
18
+ from ..models import GroupMessage
19
+ from ..services.message_utils import conversation_group_id
20
+
21
+ search_message = on_command("查消息", priority=5)
22
+
23
+
24
+ def text_to_base64_image(lines: list[str]) -> str:
25
+ font = ImageFont.truetype("simkai.ttf", size=20)
26
+ wrapped_lines = []
27
+ for line in lines:
28
+ wrapped = textwrap.wrap(line, width=80)
29
+ if not wrapped:
30
+ wrapped = [""]
31
+ wrapped_lines.extend(wrapped)
32
+
33
+ line_height = 30
34
+ img_width = 1200
35
+ img_height = max(100, len(wrapped_lines) * line_height + 40)
36
+ img = Image.new("RGB", (img_width, img_height), "white")
37
+ draw = ImageDraw.Draw(img)
38
+ y = 20
39
+ for line in wrapped_lines:
40
+ draw.text((20, y), line, fill="black", font=font)
41
+ y += line_height
42
+
43
+ buffer = io.BytesIO()
44
+ img.save(buffer, format="PNG")
45
+ return "base64://" + base64.b64encode(buffer.getvalue()).decode()
46
+
47
+
48
+ @search_message.handle()
49
+ async def handle_search(bot: Bot, event: MessageEvent, args: Message = CommandArg()):
50
+ if not isinstance(event, (GroupMessageEvent, PrivateMessageEvent)):
51
+ return
52
+
53
+ args_text = str(args).strip()
54
+ if not args_text:
55
+ await search_message.finish("用法:/查消息 关键词\n或\n/查消息 群号 关键词")
56
+
57
+ if isinstance(event, GroupMessageEvent):
58
+ try:
59
+ await bot.call_api(
60
+ "set_msg_emoji_like",
61
+ group_id=event.group_id,
62
+ message_id=event.message_id,
63
+ emoji_id="269",
64
+ set=True,
65
+ )
66
+ except Exception as e:
67
+ logger.warning(f"贴表情失败: {e}")
68
+
69
+ target_group_id = conversation_group_id(event)
70
+ target_user_id = event.user_id
71
+ keyword = args_text
72
+
73
+ m = re.match(r"^(\d{8,11})\s+(.+)$", args_text)
74
+ if m:
75
+ target_group_id = int(m.group(1))
76
+ target_user_id = None
77
+ keyword = m.group(2).strip()
78
+
79
+ if not keyword:
80
+ await search_message.finish("请输入关键词")
81
+
82
+ session = SessionLocal()
83
+ try:
84
+ query = session.query(GroupMessage).filter(
85
+ GroupMessage.group_id == cast(target_group_id, Integer),
86
+ GroupMessage.raw_message.like(f"%{keyword}%"),
87
+ )
88
+ if target_group_id == -1 and target_user_id is not None:
89
+ query = query.filter(GroupMessage.user_id == target_user_id)
90
+
91
+ results = query.order_by(desc(GroupMessage.time)).limit(100).all()
92
+ if not results:
93
+ await search_message.finish("七七翻遍了消息也没找到这个关键词哇/(ㄒoㄒ)/~~")
94
+
95
+ lines = []
96
+ for msg in reversed(results):
97
+ name = msg.sender_card or msg.sender_nickname
98
+ lines.append(
99
+ f"[{msg.time.strftime('%Y-%m-%d %H:%M:%S')}] "
100
+ f"{name}({msg.user_id}): "
101
+ f"{msg.raw_message}"
102
+ )
103
+
104
+ await search_message.finish(MessageSegment.image(text_to_base64_image(lines)))
105
+ except FinishedException:
106
+ pass
107
+ except Exception as e:
108
+ logger.error(f"查消息失败: {e}")
109
+ await search_message.finish("查询失败")
110
+ finally:
111
+ session.close()
@@ -0,0 +1,62 @@
1
+ # python3
2
+ # -*- coding: utf-8 -*-
3
+
4
+ from datetime import datetime
5
+
6
+ from nonebot import on_message
7
+ from nonebot.adapters.onebot.v11 import Bot, GroupMessageEvent, MessageEvent, PrivateMessageEvent
8
+ from nonebot.log import logger
9
+
10
+ from ..config import config
11
+ from ..db import SessionLocal
12
+ from ..models import GroupMessage
13
+ from ..services.contacts import get_display_name
14
+ from ..services.image_tasks import collect_pending_images
15
+ from ..services.message_utils import conversation_group_id, raw_message_text
16
+ from ..services.pending import maybe_flush_batch_pending
17
+
18
+ message_logger = on_message(priority=100)
19
+
20
+
21
+ @message_logger.handle()
22
+ async def log_message(bot: Bot, event: MessageEvent):
23
+ if not isinstance(event, (GroupMessageEvent, PrivateMessageEvent)):
24
+ return
25
+
26
+ group_id = conversation_group_id(event)
27
+ session = SessionLocal()
28
+ msg = None
29
+ try:
30
+ reply_id = None
31
+ if event.reply:
32
+ reply_id = event.reply.message_id
33
+
34
+ sender_nickname = getattr(event.sender, "nickname", "") or await get_display_name(bot, event.user_id, group_id)
35
+ sender_card = getattr(event.sender, "card", "") or ""
36
+ raw_message = raw_message_text(event)
37
+
38
+ msg = GroupMessage(
39
+ time=datetime.now(),
40
+ self_id=event.self_id,
41
+ user_id=event.user_id,
42
+ group_id=group_id,
43
+ raw_message=raw_message,
44
+ sender_nickname=sender_nickname,
45
+ sender_card=sender_card,
46
+ message_id=event.message_id,
47
+ reply_id=reply_id,
48
+ )
49
+ session.add(msg)
50
+ session.commit()
51
+ session.refresh(msg)
52
+ logger.debug(f"[DB] 已记录消息: group_id={group_id}, user_id={event.user_id}, message_id={event.message_id}")
53
+ except Exception as e:
54
+ logger.error(f"[DB] 保存消息失败: {e}")
55
+ session.rollback()
56
+ return
57
+ finally:
58
+ session.close()
59
+
60
+ if msg and config.ai_api_key:
61
+ await collect_pending_images(bot, event.message_id, event.message, msg)
62
+ await maybe_flush_batch_pending()
@@ -0,0 +1,22 @@
1
+ # python3
2
+ # -*- coding: utf-8 -*-
3
+
4
+ from sqlalchemy import Column, DateTime, Integer, String, Text
5
+ from sqlalchemy.orm import declarative_base
6
+
7
+ Base = declarative_base()
8
+
9
+
10
+ class GroupMessage(Base):
11
+ __tablename__ = "group_messages"
12
+
13
+ id = Column(Integer, primary_key=True, autoincrement=True)
14
+ time = Column(DateTime)
15
+ self_id = Column(Integer)
16
+ user_id = Column(Integer)
17
+ group_id = Column(Integer)
18
+ raw_message = Column(Text)
19
+ sender_nickname = Column(String(255))
20
+ sender_card = Column(String(255))
21
+ message_id = Column(Integer)
22
+ reply_id = Column(Integer, nullable=True)
@@ -0,0 +1,44 @@
1
+ # python3
2
+ # -*- coding: utf-8 -*-
3
+
4
+ import json
5
+
6
+
7
+ def build_vision_prompt(items: list[dict], timeline: list[dict]) -> str:
8
+ return f"""你是聊天图片总结助手。你会收到一组聊天图片,以及按真实聊天顺序排列的聊天时间线。只输出 JSON,不要输出 Markdown 或解释文字。
9
+
10
+ 目标:
11
+ - 尽可能反映图片里的文字、人物/物体、场景、截图中的关键信息。
12
+ - 如果图片是聊天截图、通知、网页、表格、账单、游戏截图或软件界面,提炼核心结论,并结合上下文推断这张图在聊天里的作用。
13
+ - 如果图片是表情包、梗图、二创图或网络流行图,尽量说明它通常表达的情绪、梗义或用法;若知道是梗图但无法确定梗义,把不确定说明写入 tip。
14
+ - 如果图片没有文字,也要描述能确定的画面内容、主体、动作、风格和可能用途。
15
+ - 如果看不清、被遮挡、分辨率太低或信息不足,不要猜测;能确定的写入 summary,警告和不确定性写入 tip。
16
+ - summary 用自然中文,可以多句,尽量具体;tip 只放不确定性、风险、看不清、疑似梗但不理解等提醒,没有则为空字符串。
17
+
18
+ 示例风格:
19
+ summary: "写于2021年3月30日,是作者与“周家葆”在一起的第9天。文字表达了虽有摩擦但被偏爱的幸福,以及对未来共同完成愿望的期许。右侧写有寄语:“人类自私又善变 而我 永远屈服于温柔和真诚”。卡片上盖有两处“换酒”字样的三角形印章,暗示撰写地点可能是一家名为“换酒”的店。"
20
+ tip: ""
21
+
22
+ 聊天时间线 JSON:
23
+ {json.dumps(timeline, ensure_ascii=False, indent=2)}
24
+
25
+ 图片任务 JSON:
26
+ {json.dumps(items, ensure_ascii=False, indent=2)}
27
+
28
+ 返回格式必须是:
29
+ {{
30
+ "images": [
31
+ {{
32
+ "imageIndex": 0,
33
+ "summary": "",
34
+ "tip": ""
35
+ }}
36
+ ]
37
+ }}
38
+
39
+ 严格要求:
40
+ - images 数量必须等于图片数量,imageIndex 从 0 开始对应上传顺序。
41
+ - 聊天时间线中的 {{"type":"image","index":0}} 对应 imageIndex=0 的输入图片。
42
+ - 聊天时间线已按真实聊天顺序排列;请结合图片前后的文字理解图片用途。
43
+ - summary 和 tip 都必须是字符串。
44
+ - 不要输出 JSON 以外的任何文字。"""
@@ -0,0 +1,2 @@
1
+ # python3
2
+ # -*- coding: utf-8 -*-
@@ -0,0 +1,31 @@
1
+ # python3
2
+ # -*- coding: utf-8 -*-
3
+
4
+ from typing import Optional
5
+
6
+ from nonebot.adapters.onebot.v11 import Bot
7
+ from nonebot.log import logger
8
+
9
+
10
+ async def get_display_name(bot: Bot, user_id: int, group_id: Optional[int] = None) -> str:
11
+ if group_id is not None and group_id != -1:
12
+ try:
13
+ member = await bot.get_group_member_info(group_id=group_id, user_id=user_id, no_cache=False)
14
+ return member.get("card") or member.get("nickname") or str(user_id)
15
+ except Exception as e:
16
+ logger.warning(f"[DB] 获取群成员信息失败: group={group_id}, user={user_id}, error={e}")
17
+
18
+ try:
19
+ friends = await bot.get_friend_list()
20
+ for friend in friends:
21
+ if int(friend.get("user_id", 0)) == int(user_id):
22
+ return friend.get("remark") or friend.get("nickname") or str(user_id)
23
+ except Exception as e:
24
+ logger.warning(f"[DB] 获取好友列表失败: user={user_id}, error={e}")
25
+
26
+ try:
27
+ stranger = await bot.get_stranger_info(user_id=user_id, no_cache=False)
28
+ return stranger.get("nickname") or str(user_id)
29
+ except Exception as e:
30
+ logger.warning(f"[DB] 获取陌生人信息失败: user={user_id}, error={e}")
31
+ return str(user_id)