fix(tools,ppt): 工具失败聚集查前3——修 glob 绝对路径崩溃 + PPT 页脚漂移治本(bump 0.58.45)
- glob(聚集#2):绝对 pattern(/workspace/**/*.py)先拆「静态前缀目录+相对 glob」再匹配,
不再抛 NotImplementedError: Non-relative patterns unsupported;补首字符 / 判定兼容 Windows 宿主;
删掉原 if"**"/else 两分支完全相同的死重构。/sandbox/tools 是 Dockerfile COPY,需重建沙箱镜像生效
- ppt(聚集#1,A+C):15/16 exit-1 是手写坐标偏离 layout_grid 锁定值 2-16px 的近失,footer_y 最大头。
A:spec_lock 加 ## footer 锁定片段(x/y 用 margin_x/footer_y 预填、{PAGE} 占位),executor 逐字粘贴只换页码;
C:_ALIGN_TOL 2→3px(手写 2px 过严),同步刷 SKILL/executor-base/spec_lock_reference 表述与测试值;30 测试全绿
- 新增诊断:diag_toolfail_scan(跑 scan_tool_failures 出聚集)/ diag_svgguard_breakdown / diag_malformed_samples
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
d5854e3337
commit
a02d1aa5d8
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
> 配合 `DESIGN.md`。本文件只记 phase 状态、决策偏差、文件量、下一步。每条 1-2 句:做了啥 + 关键判断;细节查 `git log` / `git diff` / `DESIGN §7.9`。
|
||||
|
||||
最后更新:2026-07-19(文件预览背景改毛玻璃虚化、观感再细化,bump 0.58.44)
|
||||
最后更新:2026-07-20(工具失败聚集巡检:修 glob 绝对路径崩溃 + PPT 页脚漂移治本 A+C,bump 0.58.45)
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -23,6 +23,8 @@
|
|||
|
||||
### 2026-07
|
||||
|
||||
- **07-20 / 0.58.45 / glob 绝对路径崩溃(工具失败聚集 #2)**:巡检近7天工具失败聚集,`glob` 排第2(3次/3task/3用户、近24h 仍在发)。根因:`tools/fs.py` GlobTool 直接 `base.glob(pattern)`,模型传绝对 pattern(沙箱 cwd 就是 `/workspace`,自然写 `/workspace/**/*.py`)时 pathlib 抛 `NotImplementedError: Non-relative patterns are unsupported`,整个调用崩返 `[Error executing glob]`。改:绝对 pattern 先 `_split_abs_glob` 拆成「存在的静态前缀目录 + 相对 glob 串」(左起吃掉不含 `*?[` 的组件当 base)再匹配;补首字符 `/`\`\\` 判定(POSIX 前导斜杠在 Windows 宿主 `is_absolute()` 为 False,沙箱是 Linux,两处对齐)。顺手删掉原 `if "**"`/`else` 两分支代码完全相同的死重构。`/sandbox/tools/` 是 Dockerfile `COPY tools/` 拷贝,**需重建沙箱镜像**才生效。
|
||||
- **07-20 / 0.58.45 / PPT 页脚漂移治本(工具失败聚集 #1,A+C)**:`shell/exit` 排第1(近7天16条 exit-1、近24h 6条,9task/7用户),实测拆解 15/16 是**逐页手写绝对坐标偏离 `spec_lock.layout_grid` 锁定值 2–16px 的「想对齐没对齐」**(footer_y 近失 27行为最大头,margin_x/content_top 各7行,content_bottom 溢出12行),字体/spec_lock 只是 WARN 不 fail。关键发现:`executor-base.md §180-185` 早有「HARD—checker-enforced」的 snap 纪律却仍被漂移,加 prose 无用,治本须机械化。**A**:spec_lock_reference 新增 `## footer` 锁定片段——x/y 用 `margin_x`/`footer_y` 预填好、页码 `text-anchor="end"` 在 `canvas_w−margin_x`、留 `{PAGE}` 占位,executor 每页**逐字粘贴只换页码**,把 footer_y 这个跨页漂移第一大头从「每页手算」收敛成「一次编写」(executor-base 加对应硬规则,SKILL.md 同步);冒烟验证锁定片段过质检 errors=0、无 footer_y/margin_x 报错。**C**:`svg_quality_checker._ALIGN_TOL` 2→3px(2px 对手写实测过严),同步刷 SKILL/spec_lock_reference/executor-base 的「2–15px/2–12px」表述与测试值(margin drift 用例 60/63/66→60/64/68,30 测试全绿)。content_bottom 溢出(装太多项,另一性质)与 grid-snap 后处理(B)本轮未做。
|
||||
- **07-19 / 0.58.43**:文件预览遮罩改整屏、消除"上暗下亮"割裂——旧实现用 `#file-preview-modal { bottom: var(--preview-bottom-inset) }` 把遮罩顶到 chat-form 之上,好让预览时能边看边打字,代价是底部整条(输入区+左栏入口+右栏+状态)全不压暗,与上方压暗区形成一条硬边界的通亮带,很割裂。改为遮罩铺满整屏(`bottom`→`padding-bottom`,仍让居中卡片停在输入区之上不遮挡),底部随整屏统一压暗;仅 chat-form 由新 `body.fp-open` 类 `position:relative;z-index:95`(压过遮罩 z90,层叠上下文已核:`#app`/`#pane-mid` 均不建 context)+ 上向阴影,上浮成"悬浮活动条",继续可打字。预览时左栏/右栏/状态点击落到遮罩=关预览(符合"背景不可用"预期)。改 `web/static/dev.html` + `web/static/js/preview.js`(open/close 加/去 `fp-open`)。
|
||||
- **07-19 / 0.58.44**:文件预览背景再细化——0.58.43 的整屏纯深色遮罩(`rgba(0,0,0,.5)`)仍偏硬,悬浮输入条亮在深黑上略突兀。改为**毛玻璃**:浅压暗 `rgba(20,22,28,.22)` + `backdrop-filter: blur(6px)`(带 `-webkit-` 前缀,老浏览器不支持则退化为纯浅压暗、不崩),背景虚化推远聚焦到卡片又不刺眼;chat-form 悬浮阴影同步调柔(`0 -10px 28px/.22`→`0 -6px 20px/.12`)。只动 `web/static/dev.html`。
|
||||
- **07-19 / 0.58.42**:页内弹框替代原生 alert/confirm/prompt——用户反馈原生浏览器弹框(顶部丑框)应改成页内样式,且区分两类(对标 Element `dialog`/`message`)。新增 `web/static/js/dialog.js`:`dialogConfirm`/`dialogPrompt`(遮罩+卡片 modal,复用改密码那套头/体/脚布局,Promise 化以一比一替换同步的 `confirm`/`prompt`,Esc/点遮罩=取消、Enter=确认、danger 变体红键)+ `message`(顶部居中轻量 toast,success/error/info 三态,自动消失、点即消)。dev.html 加 `#app-dialog` 容器 + `#app-messages` toast + 对应 CSS。全量替换主应用 8 文件调用点(chat/files/crons/procs/wechat/skills/media/auth,共 30 处);`admin.js`(独立 admin-only 页、独立 `<style>`、无 CSS 变量)刻意保留原生 alert 避免缺样式。纯前端加法,不动后端/接口。
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
# zcbot 版本号单一事实源:web/app.py 的 FastAPI version、/healthz 返回、前端展示都引这里。
|
||||
# 改版本只动这一行。
|
||||
__version__ = "0.58.44"
|
||||
__version__ = "0.58.45"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,29 @@
|
|||
"""拉 edit/write malformed 的原始损坏参数样本,判断是「两调用拼接」还是「截断」。"""
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
env = Path(__file__).resolve().parent.parent / ".env"
|
||||
for line in env.read_text(encoding="utf-8").splitlines():
|
||||
s = line.strip()
|
||||
if s.startswith("ZCBOT_DB_URL=") and not s.startswith("#"):
|
||||
os.environ["ZCBOT_DB_URL"] = s.split("=", 1)[1].strip()
|
||||
from sqlalchemy import create_engine, text # noqa: E402
|
||||
|
||||
tool = sys.argv[1] if len(sys.argv) > 1 else "edit"
|
||||
eng = create_engine(os.environ["ZCBOT_DB_URL"])
|
||||
with eng.connect() as c:
|
||||
rows = c.execute(text(
|
||||
"select task_id, model_profile, units->>'err' err, "
|
||||
"units->>'head' head, units->>'tail' tail "
|
||||
"from usage_events "
|
||||
"where kind='tool_malformed' and units->>'tool'=:t "
|
||||
"and created_at >= now() - interval '7 days' "
|
||||
"order by created_at desc limit 8"
|
||||
), {"t": tool}).fetchall()
|
||||
|
||||
for i, (tid, mp, err, head, tail) in enumerate(rows, 1):
|
||||
print(f"\n=== {i} task={str(tid)[:8]} model={mp} ===")
|
||||
print(f" err : {err}")
|
||||
print(f" head: {(head or '')[:220]}")
|
||||
print(f" tail: {(tail or '')[-220:]}")
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
"""拆 #1 SVG 质检门 exit-1 的 ERROR 具体子类,定位治本靶点。"""
|
||||
import os
|
||||
import re
|
||||
from collections import Counter
|
||||
from pathlib import Path
|
||||
|
||||
env = Path(__file__).resolve().parent.parent / ".env"
|
||||
for line in env.read_text(encoding="utf-8").splitlines():
|
||||
s = line.strip()
|
||||
if s.startswith("ZCBOT_DB_URL=") and not s.startswith("#"):
|
||||
os.environ["ZCBOT_DB_URL"] = s.split("=", 1)[1].strip()
|
||||
from sqlalchemy import create_engine, text # noqa: E402
|
||||
|
||||
eng = create_engine(os.environ["ZCBOT_DB_URL"])
|
||||
with eng.connect() as c:
|
||||
rows = c.execute(text(
|
||||
"select m.payload->>'content' content "
|
||||
"from messages m "
|
||||
"where m.created_at >= now() - interval '7 days' "
|
||||
" and m.payload->>'role'='tool' and m.payload->>'name'='shell' "
|
||||
" and m.payload->>'content' like '%[SCAN] Checking%SVG%' "
|
||||
" and m.payload->>'content' like '%[exit 1]%'"
|
||||
)).fetchall()
|
||||
|
||||
print(f"SVG 质检门 exit-1 结果:{len(rows)} 条\n")
|
||||
|
||||
|
||||
def classify(line: str) -> str:
|
||||
if "off the locked footer_y" in line:
|
||||
return "footer_y 近失(footer/页码 baseline 差2-16px)"
|
||||
if "spills past the locked content_bottom" in line:
|
||||
return "content_bottom 溢出(内容装不下)"
|
||||
if "off the locked margin_x" in line:
|
||||
return "margin_x 近失(卡片/文本左缘)"
|
||||
if "off the locked content_top" in line:
|
||||
return "content_top 近失(卡片/图标顶)"
|
||||
if "below" in line and "baseline" in line:
|
||||
return "出画布(baseline 超底边)"
|
||||
if "overlap" in line or "叠压" in line or "overlaps" in line:
|
||||
return "CJK/元素叠压"
|
||||
if "icon" in line.lower() and ("text" in line.lower() or "文字" in line):
|
||||
return "图标压字"
|
||||
if "px off the" in line and "px" in line:
|
||||
return "其他网格近失"
|
||||
return "其他: " + line[:60]
|
||||
|
||||
|
||||
cat = Counter()
|
||||
per_result_hit = Counter()
|
||||
for (content,) in rows:
|
||||
if not content:
|
||||
continue
|
||||
hits = set()
|
||||
for m in re.finditer(r"\[ERROR\]\s*([^\n]+)", content):
|
||||
line = m.group(1).strip()
|
||||
if not line or line.startswith("With errors") or "Failed" in line:
|
||||
continue
|
||||
k = classify(line)
|
||||
cat[k] += 1
|
||||
hits.add(k)
|
||||
for k in hits:
|
||||
per_result_hit[k] += 1
|
||||
|
||||
print("=== 每类 ERROR 出现的结果数(一条结果可命中多类)===")
|
||||
for k, n in per_result_hit.most_common():
|
||||
print(f" {n:>3} 条结果 {k}")
|
||||
print("\n=== ERROR 行总数(含同结果多行)===")
|
||||
for k, n in cat.most_common():
|
||||
print(f" {n:>3} {k}")
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
"""跑 core.toolfail.scan_tool_failures,打印近 N 天工具失败聚集 TOP。
|
||||
|
||||
用法:.venv/Scripts/python.exe scripts/diag_toolfail_scan.py [days] [top]
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
env = Path(__file__).resolve().parent.parent / ".env"
|
||||
for line in env.read_text(encoding="utf-8").splitlines():
|
||||
s = line.strip()
|
||||
if s.startswith("ZCBOT_DB_URL=") and not s.startswith("#"):
|
||||
os.environ["ZCBOT_DB_URL"] = s.split("=", 1)[1].strip()
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||
from core.toolfail import scan_tool_failures # noqa: E402
|
||||
|
||||
days = float(sys.argv[1]) if len(sys.argv) > 1 else 7
|
||||
top = int(sys.argv[2]) if len(sys.argv) > 2 else 10
|
||||
|
||||
# 用低阈值,拿到完整聚集再自己排 —— 巡检默认 min_count=5/min_tasks=2
|
||||
# scan 内部已按 (count_24h, count) 降序 = 近24h活跃在前
|
||||
clusters = scan_tool_failures(days=days, min_count=3, min_tasks=1)
|
||||
print(f"近 {days:g} 天:命中 {len(clusters)} 类聚集(阈值 count>=3, tasks>=1)\n")
|
||||
hdr = f"{'#':>2} {'tool':<14} {'kind':<10} {'cnt':>5} {'24h':>4} {'tsk':>4} {'usr':>4} signature"
|
||||
print(hdr)
|
||||
print("-" * len(hdr))
|
||||
for i, c in enumerate(clusters[:top], 1):
|
||||
print(
|
||||
f"{i:>2} {c['tool'][:14]:<14} {c['kind']:<10} {c['count']:>5} "
|
||||
f"{c['count_24h']:>4} {c['task_count']:>4} {c['user_count']:>4} "
|
||||
f"{c['signature'][:80]}"
|
||||
)
|
||||
|
||||
print("\n===== 前 3 聚集详情 =====")
|
||||
for i, c in enumerate(clusters[:3], 1):
|
||||
print(f"\n--- #{i} [{c['tool']}/{c['kind']}] x{c['count']} "
|
||||
f"(近24h {c['count_24h']}, task {c['task_count']}, user {c['user_count']}) ---")
|
||||
print(f" 首次: {c['first_at']} 最近: {c['last_at']}")
|
||||
print(f" daily(旧→新): {c['daily']}")
|
||||
print(f" 签名: {c['signature']}")
|
||||
print(f" 样例: {c['sample'][:500]}")
|
||||
|
|
@ -105,7 +105,7 @@ model: glm.pro52
|
|||
|
||||
**确认后产出两份引擎契约**(按骨架填,**只填实际用到的行**):
|
||||
- `<project_dir>/design_spec.md` —— 人读叙事(I–XI 节,见 design_spec_reference.md)
|
||||
- `<project_dir>/spec_lock.md` —— 机读执行锁(canvas/**layout_grid**/mode/visual_style/colors/typography/icons/images/page_rhythm/page_layouts/page_charts/forbidden,见 spec_lock_reference.md)。**executor 每页重读它**,是长 deck 抗漂移的命门。`layout_grid`(margin_x/content_top/footer_y/gutter)是跨页对齐的锚 —— 手写绝对坐标没有锁定基线必漂,质检会硬卡偏离网格 2–15px 的"想对齐没对齐"。
|
||||
- `<project_dir>/spec_lock.md` —— 机读执行锁(canvas/**layout_grid**/mode/visual_style/colors/typography/icons/images/page_rhythm/page_layouts/page_charts/forbidden,见 spec_lock_reference.md)。**executor 每页重读它**,是长 deck 抗漂移的命门。`layout_grid`(margin_x/content_top/footer_y/gutter)是跨页对齐的锚 —— 手写绝对坐标没有锁定基线必漂,质检会硬卡偏离网格 3–15px 的"想对齐没对齐"。页脚/页码用 spec_lock.footer 的锁定片段逐字粘贴(只换 {PAGE}),别每页手写 footer 坐标 —— 那是跨页漂移第一大来源。
|
||||
|
||||
> 公式策略 mixed/render-all 且有公式 → 写 `images/formula_manifest.json` 后渲染(ppt-master 的 latex_render 未搬;zcbot 可用现有公式渲染或转图后按 `images` 行登记)。
|
||||
|
||||
|
|
@ -140,7 +140,7 @@ references/visual-styles/<locked-style>.md # 锁定的视觉风格
|
|||
```
|
||||
.venv/Scripts/python.exe <skill_dir>/scripts/svg_quality_checker.py <project_dir>
|
||||
```
|
||||
- **任何 `error`(禁用特性 / viewBox 不符 / spec_lock 漂移 / **图标压在文字上、文字基线超出画布、CJK 文字互相叠压**(Geometry 检测,几何精确)/ **兄弟卡片错位 2–12px、偏离 layout_grid 网格、正文越过 content_bottom 侵入页脚区、spec 指派了 page_charts 该页却零图形(图表被退化成文字)**(Alignment 检测,几何精确)/ **锁了图标 inventory 却全 deck 0 图标** / **内容 deck 全是文字方块(≥6 页且零 `<path>`/`<polygon>`/`<polyline>`/`<image>`)** / **≥4 页同版式指纹(单调门,含两栏裸文字列表)** 等)必须改:回阶段三重写该页再跑**,不放过。
|
||||
- **任何 `error`(禁用特性 / viewBox 不符 / spec_lock 漂移 / **图标压在文字上、文字基线超出画布、CJK 文字互相叠压**(Geometry 检测,几何精确)/ **兄弟卡片错位 3–12px、偏离 layout_grid 网格、正文越过 content_bottom 侵入页脚区、spec 指派了 page_charts 该页却零图形(图表被退化成文字)**(Alignment 检测,几何精确)/ **锁了图标 inventory 却全 deck 0 图标** / **内容 deck 全是文字方块(≥6 页且零 `<path>`/`<polygon>`/`<polyline>`/`<image>`)** / **≥4 页同版式指纹(单调门,含两栏裸文字列表)** 等)必须改:回阶段三重写该页再跑**,不放过。
|
||||
- `warning`(低分辨率图 / 非 PPT 安全字体等):能顺手改就改,否则知会后放行。**例外:`Geometry:` 开头的文字重叠 warning 不许无视** —— 它给了精确坐标,是"大字压说明 / 同行文字互侵"的高嫌疑点(估宽无法区分擦边与压字,所以只报 warn)。**必须回源 SVG 对着该坐标核对**:算一下两段文字的实际占位(字号 × 字数 vs 间距),确实互侵就改坐标/缩字号/换行,擦边误报则知会后放行。
|
||||
- 跑 `svg_output/`(不要在 finalize 后跑 —— finalize 改写 SVG 会掩盖源级违规)。
|
||||
- ⚠️ **别用 `| head` / `| tail` 截断质检输出**:管道会把脚本的非零退出码换成 `head` 的 0(门形同虚设),`head` 还会截掉打在**最后**的 deck 级门结论(如零图标 `[ERROR]`)。原样跑,读完整输出、认它的退出码。
|
||||
|
|
|
|||
|
|
@ -178,7 +178,8 @@ Before drawing each page, look up its entry in `page_charts` to decide which cha
|
|||
- **Proximity**: group related elements with tight spacing; separate unrelated groups
|
||||
- **Spec adherence**: follow color, layout, canvas format, and typography in the spec
|
||||
- **Grid & alignment discipline (HARD — checker-enforced)**: hand-written absolute coordinates drift; the fix is snapping, not eyeballing.
|
||||
- **Snap to `layout_grid`**: header title, content blocks, and footer left edges sit exactly at `spec_lock.layout_grid.margin_x`; body content starts at `content_top`; footer baseline at `footer_y`. Never write a "close enough" coordinate (63 when the grid says 60) — the quality checker errors on 2–15px deviations. Breaking the grid on purpose (full-bleed, asymmetric hero) means clearing it by ≥16px, not by a few px.
|
||||
- **Snap to `layout_grid`**: header title, content blocks, and footer left edges sit exactly at `spec_lock.layout_grid.margin_x`; body content starts at `content_top`; footer baseline at `footer_y`. Never write a "close enough" coordinate (63 when the grid says 60) — the quality checker errors on 3–15px deviations. Breaking the grid on purpose (full-bleed, asymmetric hero) means clearing it by ≥16px, not by a few px.
|
||||
- **Footer = paste the locked snippet, don't hand-author it**: when `spec_lock.footer.footer_snippet` exists, paste it **verbatim** on every content page and swap only `{PAGE}` for the page number. Its x/y are already pinned to `margin_x`/`footer_y` — hand-writing the footer `y` per page is the #1 drift source (checker errors on footer baselines a few px off `footer_y`). Structural pages (cover / chapter / TOC / ending) carry no footer.
|
||||
- **Sibling cards align exactly**: cards in one row share the same `y` and `height`; cards in one column share the same `x`; gaps in a row all equal `layout_grid.gutter`. Compute one set of constants per grid (`x = margin_x + i * (card_w + gutter)`) instead of placing each card by feel. Deliberate stagger (masonry) offsets by ≥16px.
|
||||
- **Two-column layouts resolve the bottom edge**: either both columns end at the same y, or the shorter column is deliberately closed (vertical centering, a filler visual, a closing rule) — never one column dangling far above the other with dead whitespace.
|
||||
- **No glued glyphs on one line**: adjacent inline elements (arrow + number, numeral + unit, badge + label) keep ≥0.3em horizontal gap. An arrow touching a digit ("→02") reads as a typo.
|
||||
|
|
|
|||
|
|
@ -1882,11 +1882,11 @@ class SVGQualityChecker:
|
|||
# deck's margin line page by page, and the same card/icon-grid archetype
|
||||
# repeated until the deck reads monotone. Rect / icon coordinates are
|
||||
# exact (no width estimation), so near-miss offsets are reliable: a
|
||||
# 2-12px offset between row-mates is virtually never design intent —
|
||||
# 3-12px offset between row-mates is virtually never design intent —
|
||||
# deliberate stagger clears 16px — which is why those land error-tier,
|
||||
# unlike the estimated text boxes above.
|
||||
|
||||
_ALIGN_TOL = 2.0 # <= : aligned (authoring rounding slack)
|
||||
_ALIGN_TOL = 3.0 # <= : aligned (authoring rounding slack;手写像素实测 2px 过严)
|
||||
_ALIGN_ERR = 12.0 # (tol, err] : hard misalignment → error
|
||||
_ALIGN_INTENT = 16.0 # (err, intent): borderline → warning; >= intent: deliberate
|
||||
_CARD_MIN_W = 60.0 # smaller rects are chips / accent bars, not cards
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@
|
|||
- footer_y: 688
|
||||
- gutter: 24
|
||||
|
||||
> Deck-wide layout constants, in canvas px. Executor snaps every content page to these; `svg_quality_checker.py` hard-checks them (a content block whose left edge lands 2–15px off `margin_x` is an error — "meant to align, didn't"). This section exists because hand-written absolute coordinates drift across pages without a locked baseline — the #1 source of cross-page misalignment.
|
||||
> Deck-wide layout constants, in canvas px. Executor snaps every content page to these; `svg_quality_checker.py` hard-checks them (a content block whose left edge lands 3–15px off `margin_x` is an error — "meant to align, didn't"). This section exists because hand-written absolute coordinates drift across pages without a locked baseline — the #1 source of cross-page misalignment.
|
||||
>
|
||||
> - `margin_x` — left/right content margin. Header title, content blocks, and footer on content pages all share this left edge.
|
||||
> - `content_top` — top edge of the body content zone on content pages (below the header band). Structural pages (cover / chapter / TOC / ending) are exempt.
|
||||
|
|
@ -30,6 +30,25 @@
|
|||
> Strategist derives values from canvas + visual_style (defaults above suit 1280×720; scale proportionally for other canvases — e.g. 4:3 `margin_x: 50`). A page may deliberately break the grid (full-bleed image, asymmetric breathing page) — breaking means clearing it by ≥16px or going full-bleed, never a few-px "almost".
|
||||
>
|
||||
> **Missing section** → legacy deck; checker falls back to cross-page clustering (warning-tier only). New decks MUST fill it.
|
||||
|
||||
## footer
|
||||
- footer_snippet: |
|
||||
<text x="60" y="688" font-family="Microsoft YaHei, sans-serif" font-size="12" fill="#94A3B8">材料总院 · 内部资料</text>
|
||||
<text x="1220" y="688" text-anchor="end" font-family="Microsoft YaHei, sans-serif" font-size="12" fill="#94A3B8">{PAGE}</text>
|
||||
|
||||
> Locked footer + page-number block, **geometry pre-filled from `layout_grid` + canvas**.
|
||||
> This section exists because the footer/page-number baseline is the #1 cross-page drift
|
||||
> source — hand-writing `y=` a few px off `footer_y` on every page is what the checker
|
||||
> errors on most. Consolidating it into one authored snippet kills the per-page drift.
|
||||
>
|
||||
> - Strategist fills x/y ONCE: left run at `x=margin_x y=footer_y`; page number right-aligned
|
||||
> at `x=(canvas_w − margin_x) y=footer_y` with `text-anchor="end"` and the literal `{PAGE}`
|
||||
> placeholder. Font stack MUST end on a PPT-safe family (Microsoft YaHei / SimSun / Arial …);
|
||||
> size = footnote role; color = footnote/muted color from `colors`.
|
||||
> - Executor **pastes the snippet verbatim on every content page, swapping only `{PAGE}`
|
||||
> for the page number** — never hand-authors footer coordinates. Structural pages
|
||||
> (cover / chapter / TOC / ending) carry no footer.
|
||||
> - **Omit the whole section** if the deck has no footer (then `layout_grid.footer_y` is also omitted).
|
||||
- mode: pyramid
|
||||
|
||||
> Strategist: the deck's narrative skeleton, locked at confirmation `d` Layer 1. One of `pyramid` / `narrative` / `instructional` / `showcase` / `briefing` — see [`references/modes/_index.md`](../references/modes/_index.md). Executor reads only the locked mode's file. Deck-wide. Or the literal `custom` for a bespoke direction no preset captures (a special cadence, a multi-mode fusion, a particular posture) — user-requested or Strategist-recommended (user confirms, like every lock). Then add a sibling `- mode_behavior:` paragraph (how the argument advances, title voice, page rhythm, register) that the Executor follows in place of a preset file. One deck locks one value; don't default to `custom` when a preset fits.
|
||||
|
|
|
|||
|
|
@ -172,10 +172,11 @@ class DeckAggregationTests(unittest.TestCase):
|
|||
return "".join(_text(margin, 100 + 40 * i, f"第{i}行") for i in range(4))
|
||||
|
||||
def test_margin_drift_across_pages_warns(self):
|
||||
# 边距间距须 > _ALIGN_TOL(3px)才算漂移;用 4px 步长明确越过容差
|
||||
pages = {
|
||||
"01_a.svg": self._content_page(60),
|
||||
"02_b.svg": self._content_page(63),
|
||||
"03_c.svg": self._content_page(66),
|
||||
"02_b.svg": self._content_page(64),
|
||||
"03_c.svg": self._content_page(68),
|
||||
"04_d.svg": self._content_page(60),
|
||||
}
|
||||
_, out = self._run_deck(pages)
|
||||
|
|
@ -189,8 +190,8 @@ class DeckAggregationTests(unittest.TestCase):
|
|||
def test_locked_grid_disables_drift_fallback(self):
|
||||
pages = {
|
||||
"01_a.svg": self._content_page(60),
|
||||
"02_b.svg": self._content_page(63),
|
||||
"03_c.svg": self._content_page(66),
|
||||
"02_b.svg": self._content_page(64),
|
||||
"03_c.svg": self._content_page(68),
|
||||
"04_d.svg": self._content_page(60),
|
||||
}
|
||||
# margin 值刻意与页面无关:fallback 该关闭,页级 error 才是出口
|
||||
|
|
|
|||
35
tools/fs.py
35
tools/fs.py
|
|
@ -113,18 +113,41 @@ class GlobTool(Tool):
|
|||
}
|
||||
|
||||
def execute(self, pattern: str, path: str = ".") -> str:
|
||||
base = self._resolve(path)
|
||||
# 绝对 pattern(模型常传 `/workspace/**/*.py`,沙箱 cwd 就是 /workspace)
|
||||
# 不能直接喂 Path.glob —— pathlib 抛 NotImplementedError: Non-relative
|
||||
# patterns are unsupported。拆成「静态前缀目录 + 相对 glob 串」再匹配。
|
||||
pat = Path(pattern)
|
||||
# POSIX 前导斜杠在 Windows 宿主上 is_absolute()==False,但沙箱是 Linux、
|
||||
# 模型传的就是 `/workspace/...`;补判首字符,让两处行为一致。
|
||||
if pat.is_absolute() or pattern[:1] in ("/", "\\"):
|
||||
base, rel = self._split_abs_glob(pat)
|
||||
else:
|
||||
base, rel = self._resolve(path), pattern
|
||||
if not base.exists():
|
||||
return f"[Error] base path not found: {self._display(base)}"
|
||||
# 把 '**/' 前缀的递归交给 rglob,其他用 glob
|
||||
if "**" in pattern:
|
||||
matches = sorted(self._display(p) for p in base.glob(pattern))
|
||||
else:
|
||||
matches = sorted(self._display(p) for p in base.glob(pattern))
|
||||
if not rel: # 绝对路径里没有通配符 —— 命中即它本身
|
||||
return self._display(base) if base.exists() else f"[no matches for '{pattern}']"
|
||||
matches = sorted(self._display(p) for p in base.glob(rel))
|
||||
if not matches:
|
||||
return f"[no matches for '{pattern}' under {self._display(base)}]"
|
||||
return "\n".join(matches[:200])
|
||||
|
||||
@staticmethod
|
||||
def _split_abs_glob(pat: Path) -> "tuple[Path, str]":
|
||||
"""绝对 pattern → (存在的静态前缀目录, 相对 glob 串)。
|
||||
左起吃掉不含通配符(* ? [)的组件当 base,首个含通配符的组件起为相对 pattern。
|
||||
例:/workspace/**/*.py → (Path('/workspace'), '**/*.py')。
|
||||
"""
|
||||
parts = pat.parts # ('/', 'workspace', '**', '*.py')
|
||||
i = len(parts)
|
||||
for j, seg in enumerate(parts):
|
||||
if any(c in seg for c in "*?["):
|
||||
i = j
|
||||
break
|
||||
base = Path(*parts[:i]) if i else pat
|
||||
rel = str(Path(*parts[i:])) if i < len(parts) else ""
|
||||
return base, rel
|
||||
|
||||
|
||||
class GrepTool(Tool):
|
||||
name = "grep"
|
||||
|
|
|
|||
Loading…
Reference in New Issue