From bb176f6f9d6ebbd48fee4a250d8ea4c1fc9c1f5a Mon Sep 17 00:00:00 2001 From: ThearchyHelios Date: Mon, 5 Oct 2026 08:47:18 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=9A=E6=97=B6=E4=BB=BB=E5=8A=A1=E5=B0=8F?= =?UTF-8?q?=E4=BF=AE=EF=BC=9A=E8=BE=93=E5=85=A5=E6=A8=A1=E6=9D=BF=E7=9A=84?= =?UTF-8?q?=E5=AD=97=E6=AE=B5=E5=90=8D=E5=8E=BB=E6=8E=89=E9=A6=96=E5=B0=BE?= =?UTF-8?q?=E7=A9=BA=E7=99=BD=E5=86=8D=E5=AD=98=EF=BC=8C=E5=90=8E=E5=8F=B0?= =?UTF-8?q?=E9=80=9A=E7=9F=A5=E4=BB=BB=E5=8A=A1=E7=9A=84=E5=BC=82=E5=B8=B8?= =?UTF-8?q?=E8=AE=B0=E6=97=A5=E5=BF=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 「 stat_date」原样交给运行,入口节点认不出它、必填项照样报缺:保存时去掉首尾空白,去掉后重名的拦下 - 通知扫描是另起的后台任务、没人 await:出错时异常留在任务里,只会在回收时冒出一句「从未取回的异常」。 包一层,出错记日志,下一拍照常 --- backend/app/scheduler/loop.py | 10 +++++++++- backend/app/scheduler/service.py | 12 +++++++++--- backend/tests/test_scheduler_inputs.py | 7 +++++++ 3 files changed, 25 insertions(+), 4 deletions(-) diff --git a/backend/app/scheduler/loop.py b/backend/app/scheduler/loop.py index 2eb7df6a..d3620f0a 100644 --- a/backend/app/scheduler/loop.py +++ b/backend/app/scheduler/loop.py @@ -116,6 +116,14 @@ async def tick(now: datetime | None = None) -> None: await webhook.notify_due(now) +async def _notify(webhook) -> None: # noqa: ANN001 - 传进来的是 app.scheduler.webhook 模块 + """后台跑一遍通知扫描。出错只记日志:这个任务没人 await,异常留在里面只会变成一句「从未取回的异常」。""" + try: + await webhook.notify_due() + except Exception: # noqa: BLE001 + logger.exception("定时任务的通知推送这一拍出错,下一拍照常进行") + + class _Runner: """调度循环的任务。照 sandbox/manager.py 回收闲置会话的写法:一个 asyncio 任务,出错记日志接着下一拍。""" @@ -142,7 +150,7 @@ class _Runner: try: await fire_due() if self.notify is None or self.notify.done(): - self.notify = asyncio.create_task(webhook.notify_due(), name="agentlab-scheduler-notify") + self.notify = asyncio.create_task(_notify(webhook), name="agentlab-scheduler-notify") except asyncio.CancelledError: raise except Exception: # noqa: BLE001 - 这一拍出错不该让循环停下 diff --git a/backend/app/scheduler/service.py b/backend/app/scheduler/service.py index 1255381f..76562115 100644 --- a/backend/app/scheduler/service.py +++ b/backend/app/scheduler/service.py @@ -107,13 +107,19 @@ def check_template(template: Any) -> dict[str, str]: return {} if not isinstance(template, dict): raise TemplateError("输入模板需要是「字段名:模板」的对象,例如 stat_date 对应 {{ schedule.yesterday }}") - for name, text in template.items(): - if not isinstance(name, str) or not name.strip(): + out: dict[str, str] = {} + for raw, text in template.items(): + # 字段名去掉首尾空白再存:「 stat_date」交给运行,入口节点认不出它,必填项照样报缺 + name = raw.strip() if isinstance(raw, str) else "" + if not name: raise TemplateError("输入模板里有空的字段名,请填写工作流的输入字段名") + if name in out: + raise TemplateError(f"输入模板里字段「{name}」写了两次,请只保留一个") if not isinstance(text, str): raise TemplateError(f"输入「{name}」的值需要是文本(可以带 {{{{ schedule.yesterday }}}} 这样的变量),当前为「{text}」") _check_text(name, text) - return dict(template) + out[name] = text + return out def _check_text(name: str, text: str) -> None: diff --git a/backend/tests/test_scheduler_inputs.py b/backend/tests/test_scheduler_inputs.py index 83f48b69..7e274aca 100644 --- a/backend/tests/test_scheduler_inputs.py +++ b/backend/tests/test_scheduler_inputs.py @@ -124,6 +124,13 @@ def test_bad_templates_explain_in_chinese(template, fragments): assert fragment in str(caught.value), str(caught.value) +def test_field_names_are_trimmed_and_unique(): + assert service.check_template({" stat_date ": "{{ schedule.yesterday }}"}) == {"stat_date": "{{ schedule.yesterday }}"} + with pytest.raises(TemplateError) as caught: + service.check_template({"stat_date": "a", "stat_date ": "b"}) + assert "stat_date" in str(caught.value) and "两次" in str(caught.value) + + def test_variable_catalog(): items = service.variables(datetime(2026, 10, 5, 1, 0, tzinfo=UTC), "Asia/Shanghai") names = [i["name"] for i in items]