nonebot-plugin-mail 0.7.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.
@@ -0,0 +1,30 @@
1
+ # python3
2
+ # -*- coding: utf-8 -*-
3
+ # @Version : 0.7.0
4
+ # 规划备注:插件入口:PluginMetadata、注册所有 handlers
5
+
6
+ from nonebot.plugin import PluginMetadata
7
+ from .config import MailConfig
8
+
9
+ __plugin_meta__ = PluginMetadata(
10
+ name="nonebot-plugin-mail",
11
+ description="QQ 邮件登记机器人插件,支持 Notion 邮件登记、查询、签收和群聊信封图片智能识别。",
12
+ usage=(
13
+ "/mail contacts 查看联系人表\n"
14
+ "/mail records 查看邮件记录\n"
15
+ "寄信 进入人工寄件登记流程\n"
16
+ "查询 查询最近 7 天邮件\n"
17
+ "签收 查询并签收未签收邮件\n"
18
+ "识别信件 使用群聊图片智能识别并登记"
19
+ ),
20
+ type="application",
21
+ homepage="https://github.com/WhyPilotXia/nonebot_plugin_mail",
22
+ config=MailConfig,
23
+ supported_adapters={"~onebot.v11"},
24
+ )
25
+
26
+ from .handlers import mail as mail
27
+ from .handlers import query as query
28
+ from .handlers import receive as receive
29
+ from .handlers import recognize as recognize
30
+ from .handlers import send as send
@@ -0,0 +1,29 @@
1
+ # python3
2
+ # -*- coding: utf-8 -*-
3
+ # @Version : 0.7.0
4
+ # 规划备注:配置:Notion、AI_BASE_URL、AI_API_KEY、AI_MODEL、数据库 ID
5
+
6
+ from pydantic import BaseModel
7
+ from nonebot import get_driver
8
+
9
+
10
+ class MailConfig(BaseModel):
11
+ notion_token: str = ""
12
+ notion_version: str = "2025-09-03"
13
+ ras_data_source_id: str = "31e70d82-c716-80ba-b4d2-000b1892f62c"
14
+ ras_database_id: str = "31e70d82-c716-80d3-9f2d-e73dcc4033b3"
15
+ contact_data_source_id: str = "31e70d82-c716-8034-b23d-000ba20878af"
16
+ ai_base_url: str = "https://api.openai.com/v1"
17
+ ai_api_key: str = ""
18
+ ai_model: str = "gpt-4o"
19
+ mail_image_max_count: int = 12
20
+
21
+
22
+ def get_config() -> MailConfig:
23
+ raw_config = get_driver().config
24
+ if hasattr(MailConfig, "model_validate"): # 兼容pydantic v1/2
25
+ return MailConfig.model_validate(raw_config)
26
+ return MailConfig.parse_obj(raw_config)
27
+
28
+
29
+ config = get_config()
@@ -0,0 +1,18 @@
1
+ # python3
2
+ # -*- coding: utf-8 -*-
3
+ # @Version : 0.7.0
4
+ # 规划备注:邮件类型、字段名、默认值、正则、澳门规则常量
5
+
6
+ from pathlib import Path
7
+
8
+ PLUGIN_DIR = Path(__file__).resolve().parent
9
+ DATA_DIR = PLUGIN_DIR / "data" / "mail_imgs"
10
+ DATA_DIR.mkdir(parents=True, exist_ok=True)
11
+
12
+ SPECIAL_CAKE_ID = "31e70d82-c716-81ef-9ecb-ec45fbaabaf2"
13
+ SPECIAL_YUN_ID = "31e70d82-c716-8180-9fa9-e6328d4db9c0"
14
+ SPECIAL_HK_ID = "31e70d82-c716-8172-8088-c4cc856f8422"
15
+ SPECIAL_BIRDGREEN_ID = "31e70d82-c716-81a8-b2c2-ca848376185e"
16
+ SPECIAL_YING_ID = "31f70d82-c716-81ea-9fe9-cff8aee2d0c2"
17
+ SPECIAL_DANDAN_ID = "31e70d82-c716-815e-9cce-c216a363a9df"
18
+ SPECIAL_SCHOOL_ID = "31e70d82-c716-8148-95fb-f8e38f1d9292"
@@ -0,0 +1,4 @@
1
+ # python3
2
+ # -*- coding: utf-8 -*-
3
+ # @Version : 0.7.0
4
+ # 规划备注:叶恩杰项目 FALLBACK_CONTACTS 转 Python
@@ -0,0 +1,48 @@
1
+ # python3
2
+ # -*- coding: utf-8 -*-
3
+ # @Version : 0.7.0
4
+ # 规划备注:叶恩杰项目 FALLBACK_CONTACTS 转 Python
5
+
6
+ import hashlib
7
+ from typing import Any
8
+
9
+
10
+ def contact(
11
+ name: str,
12
+ phone: str,
13
+ postcode1: str,
14
+ address1: str,
15
+ postcode2: str,
16
+ address2: str,
17
+ qq: str,
18
+ extra: dict[str, Any] | None = None,
19
+ ) -> dict[str, Any]:
20
+ extra = extra or {}
21
+ local_id = hashlib.sha1(f"{name}|{qq}".encode("utf-8")).hexdigest()[:12]
22
+ return {
23
+ "id": extra.get("id") or f"local-{local_id}",
24
+ "name": name,
25
+ "phone": phone,
26
+ "email": "",
27
+ "postcode1": postcode1,
28
+ "address1": address1,
29
+ "postcode2": postcode2,
30
+ "address2": address2,
31
+ "qq": qq,
32
+ "url": "",
33
+ "source": "fallback",
34
+ "aliases": extra.get("aliases", []),
35
+ "macauRecipient": bool(extra.get("macauRecipient", False)),
36
+ }
37
+
38
+ try:
39
+ from .fallback_contacts_privacy import FALLBACK_CONTACTS as FALLBACK_CONTACTS
40
+ except ImportError: # 此为示例,按此格式增加你的通讯录
41
+ FALLBACK_CONTACTS = [
42
+ contact("王xx(工亚xxx)", "153xxx", "730xxx", "甘肃省兰州市xxxx校区", "", "", "192xxxx"),
43
+ contact("陳xx(诺x)", "172xxx", "43xxx", "湖北省武xxx", "",
44
+ "澳门地址:AI 识别到澳门或 Chan Kuok Cheng 时匹配此联系人", "90xxx", {
45
+ "aliases": ["陈国政", "陳國政", "诺斯", "Chan Kuok Cheng", "Chan Kwok Cheng"],
46
+ "macauRecipient": True, # 硬编码示例
47
+ }),
48
+ ]
@@ -0,0 +1,25 @@
1
+ from .fallback_contacts import contact
2
+
3
+ FALLBACK_CONTACTS = [
4
+ contact("四月八", "13370163498", "102249", "北京市昌平区府学路27号", "415700", "湖南省常德市桃源县浔阳街道办事处", "2300790043"),
5
+ contact("陆昱岐(心非黍离)", "13307320807", "117000", "辽宁省本溪市明山区春明小区33-15号楼一单元6层18号", "", "", "3429068514"),
6
+ contact("李昊(Hao Lee)", "18251860728", "211300", "江苏省南京市高淳区漆桥街道双富嘉园62幢201", "210000", "江苏省南京市浦口区浦珠南路30号南京工业大学南苑11栋", "2830063477"),
7
+ contact("戴高乐(蛋糕)", "17355107252", "230061", "安徽省合肥市庐阳区永红路15号12栋301室", "", "", "2092494182"),
8
+ contact("钟浩轩(help660)", "18470483739", "341299", "江西省赣州市上犹县东山镇上西村路66号江西省上犹中学", "", "", "2817162250,2107399661"),
9
+ contact("刘天榆(Bluerain)", "18943761450;18520143197", "342400", "江西省赣州市兴国县和睦路5号云顶公馆2号楼1单元2902", "", "", "2662751570"),
10
+ contact("陈垚烨(英)", "17759087767", "355101", "福建省宁德市霞浦县松港街道兴建弄43-1号", "", "", "1292465559"),
11
+ contact("叶恩杰(不爱吃苹果的小苹果)", "13372636761", "400025", "重庆市江北区观音桥街道鸿恩二路138号保利山庄22-1-301", "", "", "3658503541,1874826835"),
12
+ contact("孙思源(最笨的蛋蛋)", "15342267168", "430048", "湖北省武汉市东西湖区环湖中路36号武汉轻工大学金银湖校区", "", "", "8630023"),
13
+ contact("陳國政(诺斯)", "17205931062", "430079", "湖北省武汉市洪山区珞喻路129号武汉大学信息学部国软C4", "", "澳门地址:AI 识别到澳门或 Chan Kuok Cheng 时匹配此联系人", "907347520", {
14
+ "aliases": ["陈国政", "陳國政", "诺斯", "Chan Kuok Cheng", "Chan Kwok Cheng"],
15
+ "macauRecipient": True,
16
+ }),
17
+ contact("苏业森(杉 Caam)", "18218680555", "510540", "广东省广州市白云区太和镇兴太三路638号广州理工学院", "", "", "1226824536"),
18
+ contact("罗曦(flashier)", "15207556674", "518060", "广东省深圳市南山区南海大道3688号深圳大学粤海校区红豆斋", "", "", "2915773430"),
19
+ contact("李铭修(Ad)", "18307760360", "533000", "广西壮族自治区百色市右江区百高路9号百色高级中学2407", "533000", "广西壮族自治区百色市右江区东洲大道碧桂园翡翠郡12-1-2302", "3423118775,3839761587"),
20
+ contact("柳源昊(鸟绿)", "18363800296", "610054", "四川省成都市成华区建设北路二段4号电子科技大学", "264025", "山东省烟台市芝罘区红旗中路186号 45-361", "2176700635,2431394341"),
21
+ contact("张淞云(想要上岸)", "15982263486", "611731", "(暂时勿寄此地址)四川省成都市高新西区西源大道2006号电子科技大学", "610017", "四川省成都市青羊区东通顺街2号", "1920143820"),
22
+ contact("李浩诚( )", "15850787478", "611731", "四川省成都市高新西区西源大道2006号电子科技大学清水河校区", "210000", "江苏省南京市秦淮区朝天宫街道石鼓路小区小桂子(李浩诚先生收)", "2743218818"),
23
+ contact("孙彦恒(秋)", "18533718711", "710038", "陕西省西安市灞桥区狄寨街道水安路28号西安思源学院", "065000", "河北省廊坊市广阳区北凤道 阿尔卡迪亚小区华景园8号楼二单元201室", "2703065363"),
24
+ contact("王子涵(工亚一生黑)", "15349271968", "730050", "甘肃省兰州市七里河区彭家坪36号兰州理工大学彭家坪校区", "", "", "1925879836"),
25
+ ]
@@ -0,0 +1,4 @@
1
+ # python3
2
+ # -*- coding: utf-8 -*-
3
+ # @Version : 0.7.0
4
+ # 规划备注:群聊命令处理器包
@@ -0,0 +1,69 @@
1
+ # python3
2
+ # -*- coding: utf-8 -*-
3
+ # @Version : 0.7.0
4
+ # 规划备注:/mail contacts、/mail records、/mail @某人
5
+
6
+ from nonebot import on_command
7
+ from nonebot.adapters.onebot.v11 import Bot, GroupMessageEvent, Message, MessageSegment
8
+ from nonebot.params import CommandArg
9
+
10
+ from .utils import At
11
+ from ..services.contacts import get_contacts, get_key_by_qq, qqmap
12
+ from ..services.render import contacts_to_image, latest_mail_records_to_image
13
+
14
+ matcher = on_command("mail", priority=5, block=True)
15
+
16
+
17
+ @matcher.handle()
18
+ async def _(bot: Bot, event: GroupMessageEvent, arg: Message = CommandArg()):
19
+ contacts = await get_contacts()
20
+ qqmap(contacts)
21
+ cmd = arg.extract_plain_text().strip().lower()
22
+ at = At(event.json())
23
+
24
+ if cmd in ("contacts", "联系人", "contact"):
25
+ img_path = await contacts_to_image()
26
+ await matcher.finish(MessageSegment.image(f"file:///{img_path}"))
27
+
28
+ elif cmd in ("records", "record", "邮件", "mail"):
29
+ img_path = await latest_mail_records_to_image(15)
30
+ await matcher.finish(MessageSegment.image(f"file:///{img_path}"))
31
+
32
+ elif at:
33
+ result_blocks = []
34
+ for idx, qq in enumerate(at, 1):
35
+ target_qq = str(qq)
36
+ target_uuid = get_key_by_qq(target_qq)
37
+ if not target_uuid:
38
+ result_blocks.append(f"--- 第 {idx} 位 ---\n" f"QQ:{target_qq}\n" f"没有找到这个联系人的信息哦")
39
+ continue
40
+ target_contact = None
41
+ for c in contacts:
42
+ if c["id"] == target_uuid:
43
+ target_contact = c
44
+ break
45
+ if not target_contact:
46
+ result_blocks.append(f"--- 第 {idx} 位 ---\n" f"QQ:{target_qq}\n" f"没有找到这个联系人的详细资料哦")
47
+ continue
48
+ lines = [
49
+ f"--- 第 {idx} 位 ---",
50
+ f"姓名:{target_contact.get('姓名', '') or '无'}",
51
+ f"电话:{target_contact.get('电话', '') or '无'}",
52
+ f"地址1:{target_contact.get('地址1', '') or '无'}",
53
+ f"邮编1:{target_contact.get('邮编1', '') or '无'}",
54
+ ]
55
+ if target_contact.get("地址2"):
56
+ lines.append(f"地址2:{target_contact.get('地址2', '')}")
57
+ if target_contact.get("邮编2"):
58
+ lines.append(f"邮编2:{target_contact.get('邮编2', '')}")
59
+ result_blocks.append("\n".join(lines))
60
+ await matcher.finish("\n\n".join(result_blocks))
61
+
62
+ else:
63
+ await matcher.finish(
64
+ "用法:\n"
65
+ "/mail contacts 查看联系人表(全部)\n"
66
+ "/mail records 查看邮件记录(最新15条)\n"
67
+ "/mail @某人 查看该联系人的信息\n"
68
+ "/mail @甲 @乙 按顺序查看多位联系人的信息"
69
+ )
@@ -0,0 +1,48 @@
1
+ # python3
2
+ # -*- coding: utf-8 -*-
3
+ # @Version : 0.7.0
4
+ # 规划备注:“查询/查件”
5
+
6
+ import random
7
+
8
+ from nonebot import on_command
9
+ from nonebot.adapters.onebot.v11 import Bot, GroupMessageEvent
10
+ from nonebot.typing import T_State
11
+
12
+ from ..constants import SPECIAL_CAKE_ID, SPECIAL_YUN_ID
13
+ from ..services.contacts import get_contacts, get_key_by_qq, get_name_by_uuid, qq_map, qqmap
14
+ from ..services.notion import query_recent_mails_by_addressee, simplify_mail_results
15
+
16
+ query = on_command("查询", priority=5, block=True, aliases={"查件"})
17
+
18
+
19
+ @query.handle()
20
+ async def _(state: T_State, bot: Bot, event: GroupMessageEvent):
21
+ qq_str = event.get_user_id()
22
+ nickname = event.sender.nickname
23
+ await query.send(f"你好呀{nickname},让我帮你查询一下最近有没有人给你寄件呢")
24
+ contacts = await get_contacts()
25
+ qqmap(contacts)
26
+ query_addressee = get_key_by_qq(event.get_user_id())
27
+ query_result = query_recent_mails_by_addressee(addressee_id=query_addressee, days=7, limit=10, rec=False)
28
+ mails = simplify_mail_results(query_result)
29
+ if not mails:
30
+ if qq_str in qq_map.get(SPECIAL_CAKE_ID, []):
31
+ query_message = random.choice(["可恶的蛋糕,遭报应了吧,最近7天内没人给你寄信","这倒霉的蛋糕,是不是你平时诅咒别人太多了?这7天可没人给你写信啊", "真是个讨厌的蛋糕,看来你平常没少咒人,最近一周都没人联系你","这破蛋糕,怕不是你老爱诅咒别人吧,这七天一个给你寄信的都没有","这个可恨的蛋糕,大概是你咒人太多的报应吧,最近七天没人给你寄信"])
32
+ elif qq_str in qq_map.get(SPECIAL_YUN_ID, []):
33
+ query_message = random.choice(["云云,最近7天内没人给你寄信,可惜"])
34
+ else:
35
+ query_message = f"太遗憾了{nickname},7天内没有人给你寄信啊"
36
+ else:
37
+ query_message = f"""最近 {7} 天内,{f"查询到{len(mails)}条" if len(mails) <= 10 else "寄给你的信真是太多了,你真是个人气王,我只帮你查最近10条哦"}:"""
38
+ if qq_str in qq_map.get(SPECIAL_CAKE_ID, []):
39
+ query_message = "坏蛋糕," + query_message
40
+ elif qq_str in qq_map.get(SPECIAL_YUN_ID, []):
41
+ query_message = "本✌," + query_message
42
+ for i, mail in enumerate(mails, 1):
43
+ lines = [f"--- 第 {i} 条 ---", f"寄出日期: {mail['寄出日期']}", f"类别: {mail['备注']}"]
44
+ if mail["邮件编号"]:
45
+ lines.append(f"邮件编号: {mail['邮件编号']}")
46
+ lines.append(f"寄件人: {await get_name_by_uuid(mail['寄件人_uuid'], contacts)}")
47
+ query_message += "\n" + "\n".join(lines) + "\n"
48
+ await query.finish(query_message)
@@ -0,0 +1,69 @@
1
+ # python3
2
+ # -*- coding: utf-8 -*-
3
+ # @Version : 0.7.0
4
+ # 规划备注:“签收/收件”
5
+
6
+ import random
7
+ import re
8
+
9
+ from nonebot import on_command
10
+ from nonebot.adapters.onebot.v11 import Bot, GroupMessageEvent, MessageEvent
11
+ from nonebot.log import logger
12
+ from nonebot.params import ArgStr
13
+ from nonebot.typing import T_State
14
+
15
+ from .utils import MsgText
16
+ from ..constants import SPECIAL_CAKE_ID, SPECIAL_YUN_ID
17
+ from ..services.contacts import get_contacts, get_key_by_qq, get_name_by_uuid, qq_map, qqmap
18
+ from ..services.notion import mark_signed_from_input, query_recent_mails_by_addressee, simplify_mail_results
19
+
20
+ receive = on_command("签收", priority=5, block=True, aliases={"收件"})
21
+ label_to_page_id = {}
22
+
23
+
24
+ @receive.handle()
25
+ async def _(state: T_State, bot: Bot, event: GroupMessageEvent):
26
+ contacts = await get_contacts()
27
+ qq_str = event.get_user_id()
28
+ nickname = event.sender.nickname
29
+ await receive.send(f"你好呀{nickname},让我帮你查询一下你有没有在途的邮件呢")
30
+ qqmap(contacts)
31
+ query_addressee = get_key_by_qq(event.get_user_id())
32
+ query_result = query_recent_mails_by_addressee(addressee_id=query_addressee, days=7, limit=10, rec=True)
33
+ mails = simplify_mail_results(query_result)
34
+ if not mails:
35
+ if qq_str in qq_map.get(SPECIAL_CAKE_ID, []):
36
+ query_message = random.choice(["可恶的蛋糕,你没有等待签收的邮件,是不是因为诅咒的人太多了没人写给你?"])
37
+ elif qq_str in qq_map.get(SPECIAL_YUN_ID, []):
38
+ query_message = random.choice(["云云,你没有等待签收的邮件,要不去gayhub找找同好?"])
39
+ else:
40
+ query_message = f"{nickname},你目前没有未签收的邮件!快让别人多寄寄给你吧"
41
+ await receive.finish(query_message)
42
+ else:
43
+ query_message = f"""{f"查询到{len(mails)}条,请回复编号以签收" if len(mails) <= 10 else "寄给你的信真是太多了,我只能显示最近10条哦,先签收这一轮的吧!"}:"""
44
+ for i, mail in enumerate(mails):
45
+ label = chr(65 + i)
46
+ label_to_page_id[label] = mail["page_id"]
47
+ lines = [f"--- 第 {label} 条 ---", f"寄出日期: {mail['寄出日期']}", f"类别: {mail['备注']}"]
48
+ if mail["邮件编号"]:
49
+ lines.append(f"邮件编号: {mail['邮件编号']}")
50
+ lines.append(f"寄件人: {await get_name_by_uuid(mail['寄件人_uuid'], contacts)}")
51
+ query_message += "\n" + "\n".join(lines) + "\n"
52
+ await receive.send(query_message)
53
+
54
+
55
+ @receive.got("a1")
56
+ async def _(state: T_State, bot: Bot, event: MessageEvent, lst: str = ArgStr("a1")):
57
+ msgtext = MsgText(event.json())
58
+ s = msgtext.upper()
59
+ result = []
60
+ for ch in re.findall(r"[A-J]", s):
61
+ if ch not in result:
62
+ result.append(ch)
63
+ if not result:
64
+ await receive.finish("输入无效!请稍后重试!")
65
+ else:
66
+ parse_letters = result
67
+ updated = mark_signed_from_input(parse_letters, label_to_page_id)
68
+ logger.info("已更新如下页面:" + str(updated))
69
+ await receive.finish(f"已签收第 {','.join(parse_letters)} 条")
@@ -0,0 +1,134 @@
1
+ # python3
2
+ # -*- coding: utf-8 -*-
3
+ # @Version : 0.7.0
4
+ # 规划备注:新增:群内发送信件图片触发智能识别、人工确认、提交 Notion
5
+
6
+ import re
7
+
8
+ import httpx
9
+ from nonebot import on_command
10
+ from nonebot.adapters.onebot.v11 import Bot, Event, GroupMessageEvent
11
+ from nonebot.params import ArgStr
12
+ from nonebot.typing import T_State
13
+ from nonebot.log import logger
14
+
15
+ from .utils import At, MsgText
16
+ from ..config import config
17
+ from ..services.contacts import get_contacts, get_key_by_qq, get_name_by_uuid, qqmap
18
+ from ..services.images import collect_message_images
19
+ from ..services.notion import mail_record
20
+ from ..services.recognizer import AiRecognizeError, recognize_images
21
+ from ..services.render import render_recognition_text
22
+ from ..services.rules import normalize_date, normalize_mail_type, normalize_tracking
23
+
24
+ recognize = on_command("识别信件", priority=5, block=True, aliases={"智能寄信", "信件识别", "识别邮件"})
25
+
26
+
27
+ @recognize.handle()
28
+ async def _(state: T_State, bot: Bot, event: GroupMessageEvent):
29
+ try:
30
+ await bot.call_api('set_msg_emoji_like', group_id=event.group_id, message_id=event.message_id, emoji_id='294',
31
+ set=True)
32
+ except Exception as e:
33
+ logger.opt(exception=e).warning("贴表情失败")
34
+ contacts = await get_contacts()
35
+ qqmap(contacts)
36
+ state["contacts"] = contacts
37
+ await recognize.send("请发送至少一张信封图片,我会识别收件人、寄件人、邮戳日期和邮件类型。")
38
+
39
+
40
+ @recognize.got("images")
41
+ async def _(state: T_State, bot: Bot, event: Event):
42
+ contacts = state["contacts"]
43
+ images = await collect_message_images(bot, event)
44
+ if not images:
45
+ await recognize.reject("请发送至少一张信封图片")
46
+ await recognize.send("收到图片,正在识别信封信息,请稍等。")
47
+ try:
48
+ result = await recognize_images(images, contacts)
49
+ except AiRecognizeError as e:
50
+ await recognize.finish(str(e))
51
+ except Exception as e:
52
+ await recognize.finish(f"识别失败:{e}")
53
+ records = result["records"]
54
+ state["records"] = records
55
+ preview = await render_recognition_text(records, contacts)
56
+ await recognize.send(preview)
57
+
58
+
59
+ @recognize.got("confirm")
60
+ async def _(state: T_State, bot: Bot, event: Event, text: str = ArgStr("confirm")):
61
+ contacts = state["contacts"]
62
+ records = state["records"]
63
+ msgtext = MsgText(event.json()) or text
64
+ if msgtext.strip() not in {"确认", "提交", "ok", "OK", "好的"}:
65
+ apply_corrections(records, contacts, event, msgtext)
66
+ preview = await render_recognition_text(records, contacts)
67
+ await recognize.reject(preview)
68
+
69
+ contact_ids = {c["id"] for c in contacts}
70
+ for index, record in enumerate(records, 1):
71
+ if record.get("senderId") not in contact_ids:
72
+ await recognize.reject(f"第 {index} 条寄件人未匹配,请手动选择")
73
+ if record.get("recipientId") not in contact_ids:
74
+ await recognize.reject(f"第 {index} 条收件人未匹配,请手动选择")
75
+ if str(record.get("senderId", "")).startswith("local-") or str(record.get("recipientId", "")).startswith("local-"):
76
+ await recognize.reject(f"第 {index} 条联系人只来自本地参考表,缺少 Notion 页面 id。请确认 NOTION_TOKEN 可读取联系人表后重试")
77
+
78
+ results = []
79
+ try:
80
+ for record in records:
81
+ created = mail_record(
82
+ DATABASE_ID=config.ras_database_id,
83
+ SENDER_ID=record["senderId"],
84
+ ADDRESSEE_ID=record["recipientId"],
85
+ SEND_DATE=normalize_date(record.get("sendDate") or "") or record.get("sendDate"),
86
+ TRACKING_NO=normalize_tracking(record.get("trackingNo") or ""),
87
+ TYPE=normalize_mail_type(record.get("mailType") or "平信"),
88
+ title="由AI识图提交",
89
+ )
90
+ results.append(created)
91
+ except httpx.ConnectError as e:
92
+ await recognize.finish(f"Notion 请求异常,请重试: {e}")
93
+ return
94
+ except Exception as e:
95
+ await recognize.finish(f"提交失败:{e}")
96
+ return
97
+
98
+ lines = ["登记成功!"]
99
+ for index, res in enumerate(results, 1):
100
+ lines.append(f"第 {index} 条:{res['url']}")
101
+ await recognize.finish("\n".join(lines))
102
+
103
+
104
+ def apply_corrections(records: list[dict], contacts: list[dict], event: Event, text: str):
105
+ if not records:
106
+ return
107
+ index = 0
108
+ match_index = re.search(r"第\s*(\d+)", text)
109
+ if match_index:
110
+ index = max(0, min(len(records) - 1, int(match_index.group(1)) - 1))
111
+ record = records[index]
112
+ at = At(event.json())
113
+ if "寄件人" in text and at:
114
+ uuid = get_key_by_qq(str(at[0]))
115
+ if uuid:
116
+ record["senderId"] = uuid
117
+ if "收件人" in text and at:
118
+ uuid = get_key_by_qq(str(at[-1]))
119
+ if uuid:
120
+ record["recipientId"] = uuid
121
+ date_match = re.search(r"(\d{4}-\d{1,2}-\d{1,2})", text)
122
+ if date_match:
123
+ date = normalize_date(date_match.group(1))
124
+ if date:
125
+ record["sendDate"] = date
126
+ type_match = re.search(r"类型\s*[:: ]\s*([^\s]+)", text)
127
+ if not type_match:
128
+ type_match = re.search(r"类别\s*[:: ]\s*([^\s]+)", text)
129
+ if type_match:
130
+ record["mailType"] = normalize_mail_type(type_match.group(1))
131
+ tracking_match = re.search(r"编号\s*[:: ]\s*([A-Za-z0-9\-]+)", text)
132
+ if tracking_match:
133
+ record["trackingNo"] = normalize_tracking(tracking_match.group(1))
134
+ record["errors"] = []