mirror of
https://github.com/ThearchyHelios/AgentLab.git
synced 2026-11-15 20:01:57 +00:00
a2a19df9df
- 表:schedules(Webhook 地址和签名密钥加密存放、next_run_at 存 UTC、version 乐观锁)、schedule_fires((任务,
fire_at, kind) 唯一)、webhook_deliveries;新表由 create_all 建,不用迁移
- 输入模板:{{ schedule.yesterday }} 这类变量按计划触发时间在任务时区算(不按「现在」:补发晚了、零点前后立即
运行,「昨天」都是那次触发的昨天),上一次计划触发时间按 cron 往前找,同一次触发永远渲染出同一份输入;未知
变量、未知过滤器、{{ }} 不成对都拦下,声明了入口字段的工作流还对照字段名和必填项
- 调度循环:每 15 秒一拍,条件 UPDATE 认领到点的任务并推进到下一次,两拍并发只有一拍认领得到,触发记录的唯一
约束是第二道保险;上一次运行没结束(含停在审批、服务重启挂起的)记跳过;错过的只看最近一次,在宽限内补一次、
超出记跳过,积压不补;发起走 launch_run,拒绝的原话记成未能发起,别的异常记通用说明、详情进日志;stop 等手上
这一拍收尾,开关关着时 start 不起循环
- Webhook:通用 JSON(可选 HMAC 签名头)、飞书文本、企业微信 markdown、钉钉 markdown,按各平台算法签名;运行
结束、失败、确有待审批时按订阅推一次,结束的等封存好再推,没订阅、没配 Webhook 的也记下已处理,之后配上不会
补推旧的;10 秒超时、失败重试 3 次(4xx 不重试),每次推送记一条记录;通知里没有报告数字和原始报错,日志和
错误里只有主机名(httpx 自己的请求日志也换成主机名)
- 设置 notify.public_base_url:通知里的运行链接 = 它 + /runs/{id},只收 http(s) 地址,末尾的 / 去掉
78 lines
3.3 KiB
Python
78 lines
3.3 KiB
Python
"""通知设置(api/settings.py 的 notify 一组):定时任务的 Webhook 里拼运行链接用的对外访问地址。
|
|
|
|
- 默认没填(null),通知里就不附运行链接
|
|
- 只收 http(s) 开头的完整地址;末尾的 / 去掉、首尾空白去掉,空串当没填;带 ? 或 # 的拒收
|
|
- 写错整个请求回 422、detail 是中文,一项都不落库
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
from httpx import ASGITransport, AsyncClient
|
|
from sqlalchemy import delete
|
|
|
|
from app.api.settings import NOTIFY_GROUP, notify_settings, notify_values
|
|
from app.db.base import SessionLocal
|
|
from app.db.models import Setting
|
|
from app.main import app
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
async def clean():
|
|
async with SessionLocal() as session:
|
|
await session.execute(delete(Setting).where(Setting.key.in_([NOTIFY_GROUP, "run"])))
|
|
await session.commit()
|
|
yield
|
|
|
|
|
|
def client() -> AsyncClient:
|
|
return AsyncClient(transport=ASGITransport(app=app), base_url="http://test")
|
|
|
|
|
|
async def test_default_is_no_link():
|
|
async with client() as c:
|
|
body = (await c.get("/api/settings")).json()
|
|
assert body["notify"] == {"public_base_url": None}
|
|
async with SessionLocal() as session:
|
|
assert await notify_settings(session) == {"public_base_url": None}
|
|
|
|
|
|
@pytest.mark.parametrize("given, stored", [
|
|
("https://agentlab.example.com/", "https://agentlab.example.com"),
|
|
(" http://10.20.30.40:8080 ", "http://10.20.30.40:8080"),
|
|
("https://intra.example.com/agentlab/", "https://intra.example.com/agentlab"),
|
|
("", None),
|
|
(None, None),
|
|
])
|
|
async def test_saved_and_normalized(given, stored):
|
|
async with client() as c:
|
|
put = await c.put("/api/settings", json={"values": {NOTIFY_GROUP: {"public_base_url": given}}})
|
|
assert put.status_code == 200, put.text
|
|
assert put.json()["notify"] == {"public_base_url": stored}
|
|
assert (await c.get("/api/settings")).json()["notify"] == {"public_base_url": stored}
|
|
async with SessionLocal() as session:
|
|
assert await notify_settings(session) == {"public_base_url": stored}
|
|
|
|
|
|
@pytest.mark.parametrize("value, fragment", [
|
|
({"public_base_url": "agentlab.example.com"}, "http:// 或 https://"),
|
|
({"public_base_url": "ftp://files.example.com"}, "http:// 或 https://"),
|
|
({"public_base_url": "https://"}, "http:// 或 https://"),
|
|
({"public_base_url": 8080}, "http:// 或 https://"),
|
|
({"public_base_url": "https://agentlab.example.com/?from=bot"}, "? 或 #"),
|
|
({"public_url": "https://agentlab.example.com"}, "不支持「public_url」"),
|
|
("https://agentlab.example.com", "格式有误"),
|
|
])
|
|
async def test_invalid_values_are_refused_in_chinese(value, fragment):
|
|
async with client() as c:
|
|
put = await c.put("/api/settings", json={"values": {NOTIFY_GROUP: value, "run": {"stream_tokens": False}}})
|
|
assert put.status_code == 422
|
|
assert fragment in put.json()["detail"]
|
|
assert "对外访问地址" in put.json()["detail"] or "通知" in put.json()["detail"]
|
|
# 先查后写:同一请求里别的组也没有落库
|
|
assert (await c.get("/api/settings")).json()["run"]["stream_tokens"] is True
|
|
|
|
|
|
def test_broken_stored_value_reads_as_unset():
|
|
assert notify_values({"public_base_url": "不是地址"}) == {"public_base_url": None}
|
|
assert notify_values("坏掉的") == {"public_base_url": None}
|