mirror of
https://github.com/ThearchyHelios/AgentLab.git
synced 2026-11-15 01:31:59 +00:00
W3-d 收尾:关登录时不发汇总和到期提醒、分享页不下发入口表单、邮件标题去换行;测试互不干扰
- notify/digest:关登录时「派给我的」认不出是谁、访问密钥也不生效,待办汇总和密钥到期提醒一概不发,也不认领当天 - resolve_page 的分享旁路不下发入口的表单(分享链接里不能发起) - 邮件标题里的换行合成空格:应用名、节点名是人起的,邮件头里有换行就是头注入 - 邮件、汇总的测试前后各清一遍渠道、投递记录和 notify 一组,对外访问地址的缓存跟着清;汇总测试只删自己建的待办。 全量并行跑时不再被同一进程里别的测试留下的行带偏
This commit is contained in:
@@ -701,7 +701,8 @@ async def resolve_page(session: AsyncSession, app: Any, *, page_id: str, princip
|
||||
# 会有(Source.message),不进 problems——进了,页面顶上挂一块「无法显示」,使用者以为应用坏了
|
||||
for source in ctx.sources.values():
|
||||
await _load(session, source)
|
||||
for entry in spec.get("entries") or []:
|
||||
# 分享链接里不能发起:入口的表单不下发
|
||||
for entry in (spec.get("entries") or []) if share is None else []:
|
||||
form = resolve_form(spec, entry.get("form"))
|
||||
if form is not None and entry.get("id"):
|
||||
ctx.forms[str(entry["id"])] = form
|
||||
|
||||
@@ -8,13 +8,15 @@ notify_key_expiring),推送放在后台(service.spawn):接收端挂住
|
||||
一条(没有待办的人不发);
|
||||
- 按日期在 settings 表里认领一行(键 STATE_PREFIX + "digest:<日期>",主键冲突就是已经认领过):同一天只发一次,重启了也不
|
||||
重发。认领之后推送失败不补发——通知本来就是至多一次,投递记录里照实记失败。停机错过的那一天不补;
|
||||
- 认领行只留最近 KEEP_DAYS 天。
|
||||
- 认领行只留最近 KEEP_DAYS 天;
|
||||
- 关登录时不发:「派给我的」要认得出是谁(关登录时人人等同管理员,待办箱里是全部待办),个人渠道本来也只挂在认证过的账号上。
|
||||
|
||||
访问密钥快到期(key.expiring):
|
||||
- 到期前 7 天、1 天各推一次(THRESHOLDS):个人访问密钥推给拥有者的个人渠道,服务账号的推给订阅了的全局渠道;
|
||||
- 去重靠审计:推之前先记一条 access_key.expiring(data.days 是哪一档),记过的不再推(进程里另记一份,省得每拍查审计);
|
||||
- 不推的:建的时候离到期已经不到这一档(自己设了短有效期,知道它快到期);轮换下来、24 小时后到期的旧密钥(新的那把已经
|
||||
在用);撤销了的;拥有者停用了的;个人密钥开关关着时的个人密钥(本来就不认了)。
|
||||
在用);撤销了的;拥有者停用了的;个人密钥开关关着时的个人密钥(本来就不认了);关登录时的一概不推(访问密钥只在开了登录时
|
||||
生效)。
|
||||
|
||||
这两种状态(认领行、自动建过邮件渠道的人)存在 settings 表里,键都以 STATE_PREFIX 开头:它们是通知自己记的账,不是用户设置,
|
||||
GET /api/settings 不列、PUT 不收(api/settings.py)。
|
||||
@@ -85,12 +87,13 @@ async def _claim(day: str, now: datetime) -> bool:
|
||||
async def digest_due(now: datetime | None = None) -> list[str]:
|
||||
"""到点了就认领今天、给订阅了汇总的人各发一条(后台推)。交回这一拍要发给谁(用户 id),没到点、已经发过是空的。"""
|
||||
from app.api.settings import NOTIFY_GROUP
|
||||
from app.auth import current_mode
|
||||
from app.notify import service
|
||||
|
||||
now = now or utcnow()
|
||||
local = now.astimezone()
|
||||
day = local.date().isoformat()
|
||||
if day in _claimed:
|
||||
if day in _claimed or current_mode() == "off":
|
||||
return []
|
||||
async with SessionLocal() as session:
|
||||
row = await session.get(Setting, NOTIFY_GROUP)
|
||||
@@ -123,13 +126,15 @@ async def _digest_one(user_id: str) -> None:
|
||||
|
||||
async def keys_due(now: datetime | None = None) -> list[str]:
|
||||
"""访问密钥快到期了的,按档推一次(见模块说明)。交回这一拍推了哪几把(密钥 id)。"""
|
||||
from app.auth import personal_keys_enabled
|
||||
from app.auth import current_mode, personal_keys_enabled
|
||||
from app.auth.audit import record
|
||||
from app.auth.keys import SERVICE_KIND
|
||||
from app.db.models import AuditEvent
|
||||
from app.db.models_identity import AccessKey
|
||||
from app.notify import service
|
||||
|
||||
if current_mode() == "off":
|
||||
return []
|
||||
now = now or utcnow()
|
||||
horizon = now + timedelta(days=max(THRESHOLDS))
|
||||
async with SessionLocal() as session:
|
||||
|
||||
@@ -253,7 +253,8 @@ _FOOTER = "这封邮件由 AgentLab 自动发出,请勿直接回复。不想
|
||||
def build(cfg: MailConfig, to: list[str], msg: Message, *, unsubscribe: str | None) -> EmailMessage:
|
||||
"""一封邮件:纯文本一份、HTML 一份(全部转义)。unsubscribe 是「设置 → 通知」的完整地址,没有就不带退订头。"""
|
||||
em = EmailMessage()
|
||||
em["Subject"] = f"【AgentLab】{msg.title}"[:200]
|
||||
# 标题里的换行(应用名、节点名是人起的)合成空格:邮件头里有换行就是头注入,标准库也会直接拒收
|
||||
em["Subject"] = f"【AgentLab】{' '.join(msg.title.split())}"[:200]
|
||||
em["From"] = formataddr((cfg.from_name or DEFAULTS["from_name"], cfg.from_addr or ""))
|
||||
em["To"] = ", ".join(to)
|
||||
em["Date"] = formatdate(localtime=True)
|
||||
|
||||
@@ -51,21 +51,38 @@ async def login_on(received):
|
||||
"""开着账号密码登录(个人渠道挂在认证过的账号上)。依赖 received:收尾时等推送推完,假的接收端还在。"""
|
||||
await auth_env.reset()
|
||||
digest.forget()
|
||||
await auth_env.turn_on_local()
|
||||
async with SessionLocal() as session:
|
||||
# 别的测试可能留下了 notify 一组、渠道和投递记录:整组换成这里要的,渠道和记录先清掉
|
||||
await session.execute(delete(Setting).where(Setting.key == "notify"))
|
||||
await session.execute(delete(NotifyDelivery))
|
||||
await session.execute(delete(NotifyChannel))
|
||||
session.add(Setting(key="notify", value={"public_base_url": "https://agentlab.example.com",
|
||||
"digest_at": "09:00"}))
|
||||
await session.commit()
|
||||
yield
|
||||
await auth_env.turn_on_local()
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
await _cleanup()
|
||||
|
||||
|
||||
async def _cleanup() -> None:
|
||||
await service.drain()
|
||||
async with SessionLocal() as session:
|
||||
for model in (NotifyDelivery, NotifyChannel, TaskAssignment, Task):
|
||||
mine = select(Run.id).where(Run.workflow_name == "汇总测试")
|
||||
tasks = select(Task.id).where(Task.run_id.in_(mine))
|
||||
await session.execute(delete(TaskAssignment).where(TaskAssignment.task_id.in_(tasks)))
|
||||
await session.execute(delete(Task).where(Task.run_id.in_(mine)))
|
||||
for model in (NotifyDelivery, NotifyChannel):
|
||||
await session.execute(delete(model))
|
||||
await session.execute(delete(Run).where(Run.workflow_name == "汇总测试"))
|
||||
await session.execute(delete(App).where(App.name == "报销审批"))
|
||||
await session.execute(delete(Setting).where(Setting.key.like(digest.STATE_PREFIX + "%")))
|
||||
await session.execute(delete(Setting).where(Setting.key == "notify"))
|
||||
await session.commit()
|
||||
from app.core import public_url
|
||||
|
||||
public_url.remember(None)
|
||||
digest.forget()
|
||||
await auth_env.reset()
|
||||
|
||||
@@ -151,6 +168,23 @@ async def test_digest_follows_the_configured_time_and_skips_people_with_nothing_
|
||||
assert received == [], "没有待办不发「0 条」"
|
||||
|
||||
|
||||
async def test_nothing_is_sent_when_login_is_off(received):
|
||||
"""关登录时「派给我的」认不出是谁(人人等同管理员),访问密钥也不生效:汇总、到期提醒都不发,也不认领今天。"""
|
||||
ana = await auth_env.make_user("ana", "user")
|
||||
await personal(ana, ["task.digest"])
|
||||
await tasks_for("ana", 2)
|
||||
svc = await auth_env.make_service("svc-off")
|
||||
key, _ = await auth_env.make_key(svc, days=3)
|
||||
await _age(key, 30)
|
||||
from app.auth import remember_settings
|
||||
|
||||
remember_settings({})
|
||||
assert await digest.digest_due(local(9, 0)) == []
|
||||
assert await digest.keys_due(utcnow()) == []
|
||||
async with SessionLocal() as session:
|
||||
assert await session.get(Setting, digest.DIGEST_CLAIM + local(9, 0).astimezone().date().isoformat()) is None
|
||||
|
||||
|
||||
async def test_claim_rows_are_not_listed_as_settings(received):
|
||||
await digest.digest_due(local(9, 0))
|
||||
async with auth_env.client() as c:
|
||||
|
||||
@@ -77,14 +77,23 @@ def smtp(monkeypatch):
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
async def clean():
|
||||
"""前后各清一遍:同一个进程里别的测试留下的渠道、投递记录不算进来。"""
|
||||
await _clean()
|
||||
yield
|
||||
await service.drain()
|
||||
await _clean()
|
||||
|
||||
|
||||
async def _clean() -> None:
|
||||
async with SessionLocal() as session:
|
||||
await session.execute(delete(NotifyDelivery))
|
||||
await session.execute(delete(NotifyChannel))
|
||||
await session.execute(delete(Setting).where(Setting.key.in_([mail.GROUP, "notify", service.MAIL_AUTO_KEY])))
|
||||
await session.execute(delete(User).where(User.username.like("mail-%")))
|
||||
await session.commit()
|
||||
from app.core import public_url
|
||||
|
||||
public_url.remember(None) # 发信时按库里的对外访问地址对齐了缓存:删了那一行,缓存也清掉
|
||||
|
||||
|
||||
async def configure(**over) -> None:
|
||||
@@ -100,6 +109,9 @@ async def configure(**over) -> None:
|
||||
notify = await session.get(Setting, "notify")
|
||||
if notify is None:
|
||||
session.add(Setting(key="notify", value={"public_base_url": "https://agentlab.example.com"}))
|
||||
else:
|
||||
# 别的测试留下的 notify 一组:对外访问地址换成这里要的(退订头按它拼)
|
||||
notify.value = {**(notify.value or {}), "public_base_url": "https://agentlab.example.com"}
|
||||
await session.commit()
|
||||
|
||||
|
||||
@@ -241,6 +253,11 @@ async def test_partially_refused_recipients_are_said_plainly(smtp, monkeypatch):
|
||||
assert out.ok is True and out.error == "已交给邮件服务器,其中 1 个收件地址被拒收"
|
||||
|
||||
|
||||
def test_line_breaks_in_titles_never_reach_the_headers():
|
||||
em = mail.build(_cfg(), ["ana@example.com"], Message(title="新的待办:审批\r\nBcc: x@example.com"), unsubscribe=None)
|
||||
assert em["Subject"] == "【AgentLab】新的待办:审批 Bcc: x@example.com" and em["Bcc"] is None
|
||||
|
||||
|
||||
def test_ssl_security_uses_no_starttls(smtp):
|
||||
cfg = mail.config_of(mail.stored_value({"enabled": True, "host": "smtp.example.com", "port": 465,
|
||||
"security": "ssl", "from_addr": "agentlab@example.com"}))
|
||||
|
||||
Reference in New Issue
Block a user