From ba885ca20183746494163ee6483f2479d654cb51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=91=A3=E5=AE=8F=E5=87=AF?= Date: Wed, 24 Jun 2026 11:47:16 +0800 Subject: [PATCH] =?UTF-8?q?fix(=E5=AE=9A=E6=97=B6=E4=BB=BB=E5=8A=A1):=20re?= =?UTF-8?q?quire=5Fhtml=20=E9=80=89=E5=86=85=E7=BD=AE=E6=A8=A1=E6=9D=BF?= =?UTF-8?q?=E6=97=B6=E7=94=A8=E7=9C=9F=E5=AE=9E=E6=A8=A1=E6=9D=BF=E5=87=BA?= =?UTF-8?q?=E9=A1=B5=EF=BC=8C=E4=BF=9D=E7=95=99=E5=85=A8=E9=83=A8=E5=AD=90?= =?UTF-8?q?=E9=A1=B5=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 模板是 JS 驱动多页 SPA,子页面靠脚本从 DATA 渲染;之前用 style_reference 抽 CSS 摘要 让模型自由渲染,结果只剩主页、子页面全丢 - 改为:_run_html_page_attempt 见 tpl: 参考即提前分支到 _run_html_template_reference, 用真实模板文件出页——可转换模板=检索数据→转换→fill_template;只展示模板=原样输出; 转换失败兜底默认数据,保证页面与子页面始终可用 - 抽出 _convert_to_template_data / _finalize_template_html 复用,删除已废弃的 style_reference - 测试覆盖:真模板保留 const DATA/script/id=tabs;只展示原样;转换失败回退 Co-Authored-By: Claude Opus 4.8 (1M context) --- offline-backend-20260512/backend/CLAUDE.md | 2 +- .../deerflow/runtime/scheduler/service.py | 361 ++++++++++++------ .../runtime/scheduler/template_fill.py | 31 +- 3 files changed, 242 insertions(+), 152 deletions(-) diff --git a/offline-backend-20260512/backend/CLAUDE.md b/offline-backend-20260512/backend/CLAUDE.md index 2bb1d3eb..58a776fd 100644 --- a/offline-backend-20260512/backend/CLAUDE.md +++ b/offline-backend-20260512/backend/CLAUDE.md @@ -300,7 +300,7 @@ Two on-demand/automatic refresh paths layer on top: **Unified output switches (`require_html` + `require_markdown`).** The task form no longer splits "???? / HTML ??" into separate kinds. `execution_context` now carries two **independent** booleans that may both be on: `require_html` (???? HTML) and `require_markdown` (???? Markdown). `_task_requires_html()` returns true for `require_html` **or** legacy `task_kind == "html_page"` (so old tasks need no data migration), and `_run_task_attempt()` dispatches to `_run_html_page_attempt` when it is true, else the regular agent attempt. When **both** switches are on, `_gather_html_data()` returns `(text, files)` ? the gather agent run is additionally told to save a `.md` file ? and `_run_html_page_attempt` validates it (`MarkdownRequirementError` on failure) and surfaces it alongside the generated page, so the run delivers both artifacts. The single main `prompt` doubles as the HTML page requirement (`html_config.page_prompt`); there is no separate page-prompt field. Tests: `tests/test_scheduled_task_require_html.py` + the markdown-coexistence cases in `tests/test_html_page_tasks.py`. -**Template-fill mode (JSON → 固定 HTML 模板填充).** A third execution mode, distinct from the free-form `require_html` render pipeline: convert a user-supplied JSON into a **fixed** HTML template's data structure and fill it. Triggered when `execution_context.template_config.template_id` is set (chosen in the task form after selecting the built-in **「模板数据转换器」** agent `template-json-builder`). `_task_template_id()` reads it and `_run_task_attempt()` dispatches to `_run_template_fill_attempt` **before** the `require_html` check. Pipeline: take the source (the task `prompt`, pasted or uploaded .json) — which may be **pure JSON, plain text, or text-with-embedded-JSON** (`template_fill.extract_embedded_json` pre-extracts any embedded `{…}`/`[…]` with a string-aware balanced scan and surfaces it as a labeled block while keeping the full original text; plain text → no block, the model reads it semantically) → load the selected agent's `SOUL.md`+model and run a **direct** `_invoke_conversion_model` call (system=SOUL, human=conversion prompt carrying the template's compact **schema skeleton** + the recognized-JSON block + source) → `extract_json_object` → `template_fill.validate_data` (top-level keys + container types) → **on failure feed the structure errors + `field_coverage_gaps` back and retry up to `_MAX_TEMPLATE_FILL_RETRIES`=2** → `template_fill.fill_template` replaces the template's `const DATA={…}` line (and refreshes `MANIFEST.系统.更新时间`) → archive `index.html` + persist a `scheduled_html_pages` row → return `format:"html"` (reusing the existing HTML preview at `/runs/{id}/html`). Any terminal failure raises `TemplateFillError` → run marked failed (no retry, message shown). **Pure engine** `deerflow/runtime/scheduler/template_fill.py` (harness, no `app.*`): the template registry (`TEMPLATES`: id=`no1` name=`no.1` 台湾滨海防卫大屏, and id=`no2` name=`no.2` 台湾飞行部队平台 with **`convertible=False`** — a 「只展示」 template that renders its existing HTML **as-is**, no model/validation/fill, until conversion is wired in later; `_run_template_fill_attempt` short-circuits to `read_template_text` for it, and `list_templates`/`is_convertible` expose the flag so the form hides the JSON input), `list_templates`/`reference_data`/`schema_skeleton_json`/`validate_data`/`field_coverage_gaps`/`fill_template` — **the template's own embedded `DATA` is the schema source of truth** (parsed at load, 1-sample-per-array skeleton fed to the LLM, top-level structure as the validation basis); template HTML files live under `runtime/scheduler/templates//`, add a `TemplateSpec` to register a no.2. The agent is seeded like the AI-writing ones: `app/gateway/routers/_template_builder_seed.py` + `_template_builder_seed_assets/template-json-builder/{config.yaml,SOUL.md}`, called from the lifespan before `_sync_legacy_agents` (which upserts it into the `agents` table as built-in/published so it shows in `/api/agents` → the task form's 执行智能体 dropdown). Template list API: `GET /api/scheduled-tasks/html-templates` → `{templates:[{id,name,description,convertible}]}` (open). **Debug-render API** `POST /api/scheduled-tasks/template-fill/render` `{template_id, text?|data?}` → `{ok, display_only?, errors, gaps, html}` (open): no model call — `extract_embedded_json(text)` (or `data`) → `validate_data` → `fill_template`, returning the filled HTML **plus** the validation errors/field-gaps (fills even when invalid so you can see the effect). Powers the chat-page debug button below; non-convertible templates return their raw HTML. **Templates as `require_html` style references**: the 参考收藏页面 list in the task form also offers the templates (value `tpl:`, e.g. `tpl:no1`); when one is picked, `_run_html_page_attempt` calls `template_fill.style_reference(id)` (extracts the `", re.DOTALL | re.IGNORECASE) -_SCRIPT_BLOCK_RE = re.compile(r"]*>.*?", re.DOTALL | re.IGNORECASE) -_BODY_RE = re.compile(r"]*>(.*?)", re.DOTALL | re.IGNORECASE) - def reference_template_id(reference: str | None) -> str | None: """``tpl:no1`` → ``no1``;非模板参考(普通收藏 id / 空)返回 None。""" if isinstance(reference, str) and reference.startswith(REFERENCE_PREFIX): return reference[len(REFERENCE_PREFIX) :] or None return None - - -def style_reference(template_id: str, *, max_style: int = 24000, max_layout: int = 6000) -> dict[str, str]: - """抽取模板的紧凑「风格/版式参考」(``