@xiaobuyu/nodesign 0.0.6 → 0.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (135) hide show
  1. package/package.json +1 -1
  2. package/server/api/assets/helpers.js +18 -0
  3. package/server/api/assets/notes.js +50 -0
  4. package/server/api/assets.js +11 -6
  5. package/server/api/instruction.js +3 -3
  6. package/server/engine/agent/events.js +11 -1
  7. package/server/engine/agent/hooks/failure.js +11 -1
  8. package/server/engine/agent/hooks/lifecycle.js +10 -39
  9. package/server/engine/agent/hooks/post-guidance.js +4 -38
  10. package/server/engine/agent/hooks/user-prompt-submit.js +11 -40
  11. package/server/engine/agent/hooks.js +4 -8
  12. package/server/engine/agent/memory-config.js +61 -0
  13. package/server/engine/agent/prompts/nodesign-prelude.md +14 -12
  14. package/server/engine/agent/prompts/tools/generate-image-cookbook.md +24 -7
  15. package/server/engine/agent/prompts/tools/site-reference.md +13 -3
  16. package/server/engine/agent/session-loop.js +29 -18
  17. package/server/engine/agents/explorer.md +1 -1
  18. package/server/engine/agents/index.js +1 -1
  19. package/server/engine/mcp/index.js +4 -11
  20. package/server/engine/mcp/tools/arrange-on-board.js +3 -1
  21. package/server/engine/mcp/tools/browse-computer.js +2 -1
  22. package/server/engine/mcp/tools/browse-find-batch.js +20 -3
  23. package/server/engine/mcp/tools/browse.js +24 -2
  24. package/server/engine/mcp/tools/crystallize-skill.js +1 -1
  25. package/server/engine/mcp/tools/generate-image.js +50 -70
  26. package/server/engine/mcp/tools/helpers/codex-imagegen.js +139 -0
  27. package/server/engine/mcp/tools/helpers/rembg.js +7 -2
  28. package/server/engine/mcp/tools/helpers/shot-pipeline.js +109 -0
  29. package/server/engine/mcp/tools/list-pages.js +11 -5
  30. package/server/engine/mcp/tools/organize-board.js +26 -1
  31. package/server/engine/mcp/tools/pin-to-board.js +26 -17
  32. package/server/engine/mcp/tools/read-board.js +19 -5
  33. package/server/engine/mcp/tools/relate-on-board.js +42 -4
  34. package/server/engine/mcp/tools/report-issue.js +14 -7
  35. package/server/engine/mcp/tools/screenshot.js +17 -7
  36. package/server/engine/mcp/tools/sketch-on-board.js +13 -5
  37. package/server/engine/mcp/tools/web-search.js +11 -15
  38. package/server/engine/mcp/tools/write-on-board.js +15 -6
  39. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/SKILL.md +3 -4
  40. package/server/engine/plugins/nodesign/skills/docx-craft/SKILL.md +10 -1
  41. package/server/engine/plugins/nodesign/skills/docx-craft/references/token-schema.md +16 -0
  42. package/server/engine/plugins/nodesign/skills/site-craft/SKILL.md +4 -4
  43. package/server/index.js +0 -2
  44. package/server/lib/artifact-target.js +31 -14
  45. package/server/lib/board-kind-sizes.js +2 -3
  46. package/server/lib/canvas-id.js +29 -4
  47. package/server/lib/export-collect.js +3 -1
  48. package/server/lib/issues-store.js +3 -1
  49. package/server/lib/kinds/docx.js +11 -4
  50. package/server/lib/kinds/index.js +6 -16
  51. package/server/lib/kinds/site.js +40 -7
  52. package/server/lib/rewrite-refs.js +110 -0
  53. package/server/lib/ssrf-guard.js +9 -0
  54. package/server/lib/task-scan.js +18 -0
  55. package/server/projects/board-store.js +42 -1
  56. package/server/projects/memory-migration.js +85 -0
  57. package/server/projects/move-entry.js +11 -1
  58. package/server/projects/ui-config.js +2 -1
  59. package/server/projects/workspace-templates.js +13 -10
  60. package/server/projects/workspace.js +15 -15
  61. package/server/services/rembg-launcher.js +5 -2
  62. package/web/dist/assets/{BrowserWindow-P_vIdh-G.js → BrowserWindow-BoOBaIyz.js} +1 -1
  63. package/web/dist/assets/DeckWindow-Bg3rgkvi.js +11 -0
  64. package/web/dist/assets/{DocxWindow-DSAU1-tD.js → DocxWindow-B_u6aX9o.js} +1 -1
  65. package/web/dist/assets/{MermaidBlock-DR1GikCv.js → MermaidBlock-BO-46Du5.js} +6 -6
  66. package/web/dist/assets/{SiteWindow-92uBuRRR.js → SiteWindow-ConPmzdM.js} +1 -1
  67. package/web/dist/assets/{abnfDiagram-VCTEODGH-uA8Esix4.js → abnfDiagram-VCTEODGH-FCO9yf-n.js} +1 -1
  68. package/web/dist/assets/{arc-D_khH_xN.js → arc-BLcEOSRX.js} +1 -1
  69. package/web/dist/assets/{architectureDiagram-5GKGNRK7-DydJHqOt.js → architectureDiagram-5GKGNRK7-kxiX4TeN.js} +1 -1
  70. package/web/dist/assets/{blockDiagram-NRAW4CY4-B7wBbKiW.js → blockDiagram-NRAW4CY4-CptFksix.js} +1 -1
  71. package/web/dist/assets/{c4Diagram-UCG6FXSJ-6mrH0x7Y.js → c4Diagram-UCG6FXSJ-Dxmxj34F.js} +1 -1
  72. package/web/dist/assets/channel-DFyiuGL0.js +1 -0
  73. package/web/dist/assets/{chunk-2Q5K7J3B-Cuikho5Q.js → chunk-2Q5K7J3B-D9inx5oo.js} +1 -1
  74. package/web/dist/assets/{chunk-5VM5RSS4-3-KZ5AUz.js → chunk-5VM5RSS4-DydfHlEc.js} +1 -1
  75. package/web/dist/assets/{chunk-F27PBJKO-Bkwq2gGU.js → chunk-F27PBJKO-BO7GxhSv.js} +1 -1
  76. package/web/dist/assets/{chunk-G27WJ6UU-BLyQzdGE.js → chunk-G27WJ6UU-BzP6YiQ_.js} +1 -1
  77. package/web/dist/assets/{chunk-JWPE2WC7-B7HxxQds.js → chunk-JWPE2WC7-CHcgcd0s.js} +1 -1
  78. package/web/dist/assets/{chunk-LCL6LL3I-CtXrnUwF.js → chunk-LCL6LL3I-D2Wg5JtX.js} +1 -1
  79. package/web/dist/assets/{chunk-POPQ4Y6H-dxddZlXt.js → chunk-POPQ4Y6H-B2fCRfpM.js} +1 -1
  80. package/web/dist/assets/{chunk-SVP7TREG-DpkmrgHn.js → chunk-SVP7TREG-JSLcsRlZ.js} +1 -1
  81. package/web/dist/assets/{chunk-XXDRQBXY-BwZ7RbpQ.js → chunk-XXDRQBXY-BCjvaeHm.js} +1 -1
  82. package/web/dist/assets/classDiagram-DTDB5LWJ-Aw1f8z-b.js +1 -0
  83. package/web/dist/assets/classDiagram-v2-JRS7N3AN-Aw1f8z-b.js +1 -0
  84. package/web/dist/assets/{cose-bilkent-JH36ORCC-DGFOsMUt.js → cose-bilkent-JH36ORCC-0uKv6oJ7.js} +1 -1
  85. package/web/dist/assets/{cynefin-OW5HDTMX-DUwn3ehS.js → cynefin-OW5HDTMX-Bvt-gEmA.js} +1 -1
  86. package/web/dist/assets/{cynefinDiagram-5FMLGOSQ-DlLAOZ9x.js → cynefinDiagram-5FMLGOSQ-C9Ht02qC.js} +1 -1
  87. package/web/dist/assets/{dagre-3AP2YEHR-CyourLfz.js → dagre-3AP2YEHR-BfxZny4s.js} +1 -1
  88. package/web/dist/assets/{diagram-S7CK7UJ4-BTMbx2np.js → diagram-S7CK7UJ4-CrE0ZETE.js} +1 -1
  89. package/web/dist/assets/{diagram-UQ7AKVKN-UK94_1-P.js → diagram-UQ7AKVKN-BO8VQ4Fx.js} +1 -1
  90. package/web/dist/assets/{diagram-VSXAHHWV-C7zS-v9v.js → diagram-VSXAHHWV-CHUZnR9F.js} +1 -1
  91. package/web/dist/assets/{diagram-VX7I27RA-DKJRa6CK.js → diagram-VX7I27RA-DYQAz1Vu.js} +1 -1
  92. package/web/dist/assets/{diagram-Z3DM3KII-kUbAfGAp.js → diagram-Z3DM3KII-BDjivgLr.js} +1 -1
  93. package/web/dist/assets/{ebnfDiagram-PWID7BFC-CVW16_vb.js → ebnfDiagram-PWID7BFC-Bpe_QVbH.js} +1 -1
  94. package/web/dist/assets/{erDiagram-SSCWMZ5O-BRQMFQ5y.js → erDiagram-SSCWMZ5O-BxhwuAu1.js} +1 -1
  95. package/web/dist/assets/{flowDiagram-A5DVABFB-S34LpLtK.js → flowDiagram-A5DVABFB-CCw2H08-.js} +1 -1
  96. package/web/dist/assets/{ganttDiagram-EL5Y4UJY-BFiqk1-G.js → ganttDiagram-EL5Y4UJY-DtTc7MyI.js} +1 -1
  97. package/web/dist/assets/{gitGraphDiagram-WWUBYQGX-LZsrdyyv.js → gitGraphDiagram-WWUBYQGX-BvORxMd2.js} +1 -1
  98. package/web/dist/assets/index-DGG5wIZs.js +2118 -0
  99. package/web/dist/assets/{infoDiagram-RXCK75RN-0DvN6KIp.js → infoDiagram-RXCK75RN-DMcrkypD.js} +1 -1
  100. package/web/dist/assets/{ishikawaDiagram-5VMMS53U-CaY3A-Bx.js → ishikawaDiagram-5VMMS53U-CZkTIbe-.js} +1 -1
  101. package/web/dist/assets/{journeyDiagram-EYS64GPL-BBeiaCrj.js → journeyDiagram-EYS64GPL-DR0XazdE.js} +1 -1
  102. package/web/dist/assets/{kanban-definition-3QL26DDD-ux42DRXQ.js → kanban-definition-3QL26DDD-DN8VbwVu.js} +1 -1
  103. package/web/dist/assets/{layout-CnXiF_NB.js → layout-9_paCbyt.js} +1 -1
  104. package/web/dist/assets/{linear-BMW0u6G6.js → linear-g6Ct012e.js} +1 -1
  105. package/web/dist/assets/{mindmap-definition-FBJOCRG2-hUxSpdKn.js → mindmap-definition-FBJOCRG2-BGt4omYC.js} +1 -1
  106. package/web/dist/assets/{pegDiagram-XKGWAZYB-aCaQPH0w.js → pegDiagram-XKGWAZYB-uSfS-PBE.js} +1 -1
  107. package/web/dist/assets/{pending-edit-apply-AcdOQ9Ij.js → pending-edit-apply-uP4uvrOD.js} +1 -1
  108. package/web/dist/assets/{pieDiagram-E7YTZNPT-CmOEZ8zg.js → pieDiagram-E7YTZNPT-DpeI935H.js} +1 -1
  109. package/web/dist/assets/{quadrantDiagram-AXDQQJYC-VW6fAhsV.js → quadrantDiagram-AXDQQJYC-B7IuNvWa.js} +1 -1
  110. package/web/dist/assets/{railroadDiagram-O6MQD6OU-CKJ7Ba5K.js → railroadDiagram-O6MQD6OU-ARzYpPQI.js} +1 -1
  111. package/web/dist/assets/{requirementDiagram-EFPCY7ZU-pNGkq9Vw.js → requirementDiagram-EFPCY7ZU-Cj4upkAo.js} +1 -1
  112. package/web/dist/assets/{rotate-cw-KAfChbGV.js → rotate-cw-CXJcyHWk.js} +1 -1
  113. package/web/dist/assets/{sankeyDiagram-P5KCCOFB-BIMmDTfZ.js → sankeyDiagram-P5KCCOFB-OaVwSnLP.js} +1 -1
  114. package/web/dist/assets/{sequenceDiagram-WJ2MYXX4-BBW7wzKY.js → sequenceDiagram-WJ2MYXX4-1yrUbq4t.js} +1 -1
  115. package/web/dist/assets/{sizeCapture-X5ZJPWSS-DdkXmLYR.js → sizeCapture-X5ZJPWSS-nYH5p9D6.js} +1 -1
  116. package/web/dist/assets/{square-dashed-mouse-pointer-CDUlhUP7.js → square-dashed-mouse-pointer-CTtTVKoH.js} +1 -1
  117. package/web/dist/assets/{stateDiagram-HBIQ2CUA-W313Uz16.js → stateDiagram-HBIQ2CUA-Dj5HvyNq.js} +1 -1
  118. package/web/dist/assets/stateDiagram-v2-4QOOHH4V-kJ1m_zJT.js +1 -0
  119. package/web/dist/assets/{swimlanes-XN3QIQJK-C29TL26K.js → swimlanes-XN3QIQJK-umeT0HpB.js} +1 -1
  120. package/web/dist/assets/swimlanesDiagram-VK2B7HYN-DPSkMbbl.js +8 -0
  121. package/web/dist/assets/{timeline-definition-24CTP7MA-CF0JMObX.js → timeline-definition-24CTP7MA-MqnQfHKB.js} +1 -1
  122. package/web/dist/assets/{vennDiagram-4TSXK5OY-DrDFsaH3.js → vennDiagram-4TSXK5OY-b2liyZra.js} +1 -1
  123. package/web/dist/assets/{wardleyDiagram-VM6X3IG4-CVrnRIS_.js → wardleyDiagram-VM6X3IG4-QMK-zNh1.js} +1 -1
  124. package/web/dist/assets/{xychartDiagram-S5SC5T6Z-BvkN1zsT.js → xychartDiagram-S5SC5T6Z-D5Mc6tfA.js} +1 -1
  125. package/web/dist/index.html +1 -1
  126. package/server/api/memory.js +0 -185
  127. package/server/engine/agent/prompts/tools/generate-image-cookbook.gemini-gateway.md +0 -583
  128. package/server/engine/mcp/tools/record-decision.js +0 -140
  129. package/web/dist/assets/DeckWindow-BpLv5082.js +0 -16
  130. package/web/dist/assets/channel-DGkccMO1.js +0 -1
  131. package/web/dist/assets/classDiagram-DTDB5LWJ-BGscw3OD.js +0 -1
  132. package/web/dist/assets/classDiagram-v2-JRS7N3AN-BGscw3OD.js +0 -1
  133. package/web/dist/assets/index-GAamh1p4.js +0 -2152
  134. package/web/dist/assets/stateDiagram-v2-4QOOHH4V-DDhJBZzf.js +0 -1
  135. package/web/dist/assets/swimlanesDiagram-VK2B7HYN-BfN-Xdmu.js +0 -8
@@ -31,18 +31,24 @@ export function makeReportIssueTool({ projectId, sessionId, ctx }) {
31
31
  return tool(
32
32
  'report_issue',
33
33
  `Write to the maintainer of this HARNESS — the tools and environment you work
34
- in. This goes to their issue list, not to the user. Three kinds:
34
+ in. This goes to their issue list, not to the user.
35
+
36
+ This harness is in BETA and you are one of its primary testers: the maintainer
37
+ actively wants these reports and reads every one. When you hesitate between
38
+ reporting and not reporting — report. Three kinds:
35
39
 
36
40
  - "bug": something in the harness behaved WRONGLY — a tool crashed on valid
37
41
  input, returned corrupt output, or contradicted its own description.
38
42
  - "friction": it worked, but the path was painful — a missing parameter, a
39
- return shape you had to fight, a workaround you have now done more than once
40
- (the workaround IS the report).
43
+ return shape you had to fight, a workaround (one detour is enough to report;
44
+ the workaround IS the report).
41
45
  - "idea": nothing is broken, but while working you saw something the harness
42
46
  could do better or do at all — a tool that should exist, a flag that would
43
- collapse three steps into one, a default that fights real usage. The
44
- maintainer reads every one of these and good ones get built if a concrete
45
- improvement occurred to you, report it rather than let it evaporate.
47
+ collapse three steps into one, a default that fights real usage, or a
48
+ METHODOLOGY you worked out on the job (a technique, a judgment rule, a
49
+ sequence that reliably works) that the harness should teach every session,
50
+ bake into a skill, or automate. Good ones get built — report it rather than
51
+ let it evaporate.
46
52
 
47
53
  This is NOT a substitute for doing the work. Finish the task the way you would
48
54
  have anyway — route around the problem, deliver the result — and log this on
@@ -50,7 +56,8 @@ the side. Never report and then hand back less than you could have.
50
56
 
51
57
  Do NOT report:
52
58
  - Your own mistakes (wrong path, malformed edit) — not harness issues
53
- - One-off flakes that a retry fixed
59
+ - A one-off flake that a single retry fixed (the automatic layer counts those);
60
+ a flake that RECURS is friction — report it
54
61
  - Anything about the design work itself (that belongs in the conversation)
55
62
  - Praise or vague sentiment ("the tools are great", "X could be better
56
63
  somehow") — a report with no actionable content is noise
@@ -19,7 +19,7 @@ import { can } from '../../../lib/kinds/index.js';
19
19
  import { screenshotDocx } from './screenshot-docx.js';
20
20
  import { SITE_DEVICE_W } from './helpers/perception-page.js';
21
21
  import { acquireArtifactPage, LIVE_PARAM_DESC } from './helpers/acquire-page.js';
22
- import { normalizeShot, detectPaintTransform, attachPageDiagnostics, runWaitFor, runBeforeShot } from './helpers/shot-pipeline.js';
22
+ import { normalizeShot, detectPaintTransform, attachPageDiagnostics, runWaitFor, runBeforeShot, shotWithFallback, longPageSheet, clipShotWithFallback } from './helpers/shot-pipeline.js';
23
23
  import { recordMotion, pickNearestFrames, composeSheet, encodeWebm, motionCaptionLines } from './helpers/motion-lab.js';
24
24
  import { wheelScroll, elementMotionReport, elementMotionLines } from './helpers/motion-scroll.js';
25
25
 
@@ -428,6 +428,8 @@ Do NOT use this tool when:
428
428
  // (系统 fit script 包 frame + scroll-snap),locator.screenshot 自动
429
429
  // 拿目标 section 的 bbox,不需要再操作 DOM。
430
430
  let buf;
431
+ let shotDegraded = false;
432
+ let longPageNote = null;
431
433
  let captureMode;
432
434
  const targetSelector = selector
433
435
  || (pageIndex ? `section[data-page="${pageIndex}"]` : null);
@@ -476,13 +478,19 @@ Do NOT use this tool when:
476
478
  };
477
479
  clip.width = Math.max(1, Math.min(rect.width, rect.docW - clip.x));
478
480
  clip.height = Math.max(1, Math.min(rect.height, rect.docH - clip.y));
479
- buf = await page.screenshot({ fullPage: true, clip, type: 'png' });
481
+ ({ buf, degraded: shotDegraded } = await clipShotWithFallback(page, { clip, selector: targetSelector }));
480
482
  captureMode = `selector="${targetSelector}"`;
481
483
  } else {
482
- buf = await page.screenshot({ fullPage: fp, type: 'png' });
483
- captureMode = isSite
484
- ? `site ${device || 'desktop'} ${vp.width}px, fullPage=${fp}`
485
- : `fullPage=${fp}`;
484
+ // 超长页 fullPage 滚动联络表(阈值与理由见 shot-pipeline.longPageSheet)
485
+ const sheet = fp ? await longPageSheet(page).catch(() => null) : null;
486
+ if (sheet) {
487
+ buf = sheet.buf; captureMode = sheet.mode; longPageNote = sheet.note;
488
+ } else {
489
+ ({ buf, degraded: shotDegraded } = await shotWithFallback(page, { fullPage: fp, type: 'png' }));
490
+ captureMode = isSite
491
+ ? `site ${device || 'desktop'} ${vp.width}px, fullPage=${fp}`
492
+ : `fullPage=${fp}`;
493
+ }
486
494
  }
487
495
  // live 页的 DPR 是会话的(1),detail:'normal' 的 0.6 光栅在这里事后缩
488
496
  if (acq.live && rasterScale < 1) {
@@ -537,7 +545,9 @@ Do NOT use this tool when:
537
545
  const shot = await normalizeShot(buf);
538
546
 
539
547
  const captionParts = [
540
- `Screenshot of ${target.relPath} (layout ${vp.width}x${vp.height} @${rasterScale}x raster, ${captureMode})`,
548
+ `Screenshot of ${target.relPath} (layout ${vp.width}x${vp.height} @${rasterScale}x raster, ${captureMode})`
549
+ + (shotDegraded ? '(常规截图等稳定帧超时——页面在持续动画(WebGL/rAF),已改抓当前帧:画面是真实的某一瞬间,动画中间态属正常)' : '')
550
+ + (longPageNote ? `(${longPageNote})` : ''),
541
551
  ];
542
552
  // 加载通道写进 caption:agent 不用再靠"把 location.protocol 写进 DOM 再截一张"
543
553
  // 去反推自己被什么方式打开了(问题库 iss_msxk2oci_0v0v 就是这么查了四轮)
@@ -19,10 +19,11 @@ import { tool } from '@anthropic-ai/claude-agent-sdk';
19
19
  import { z } from 'zod';
20
20
  import { readBoard, patchBoard, commitStaging, removeByTag, TEXT_FONTS } from '../../../projects/board-store.js';
21
21
  import { estimateSizeOn } from '../../../lib/board-kind-sizes.js';
22
- import { layerOf, normalizeCanvasId } from '../../../lib/canvas-id.js';
22
+ import { layerOf, normalizeCanvasId, tagEnvelope } from '../../../lib/canvas-id.js';
23
23
  import { BINDING_TYPE_IDS, BINDING_MATERIALS } from '../../../lib/binding-types.js';
24
24
  import { UNIT, SKETCH_FIT, SKETCH_MAX, textBox, shapePath, layoutNodes, bboxOf, findSpot, fitFor } from '../../../lib/sketch-layout.js';
25
25
  import { getViewpoint } from '../../../projects/viewpoint-store.js';
26
+ import { Events } from '../../agent/events.js';
26
27
 
27
28
  const MAX_NODES = 40;
28
29
  const MAX_SHAPES = 30;
@@ -248,9 +249,16 @@ Per sketch: ≤${MAX_NODES} nodes, ${MAX_SHAPES} shapes, ${MAX_EDGES} edges (spl
248
249
  if (args.near) {
249
250
  const nid = normalizeCanvasId(args.near);
250
251
  const e = nid ? board.objects?.[nid] : null;
251
- if (!e || !Number.isFinite(e.x)) return err(`锚点 ${args.near} 还没有座位(read_board 里看不到就锚不上)。`);
252
- zone = layerOf(nid, e, known);
253
- anchor = { x: e.x, y: e.y, ...estimateSizeOn(after, nid, e) };
252
+ if (e && Number.isFinite(e.x)) {
253
+ zone = layerOf(nid, e, known);
254
+ anchor = { x: e.x, y: e.y, ...estimateSizeOn(after, nid, e) };
255
+ } else {
256
+ // near 也认 tag(08-23 案,同 write_on_board)
257
+ const env = tagEnvelope(board, args.near, (id2, e2) => estimateSizeOn(after, id2, e2));
258
+ if (!env) return err(`锚点 ${args.near} 还没有座位,也不是板上任何 tag(read_board 里看不到就锚不上)。`);
259
+ zone = layerOf(env.anchorId, board.objects[env.anchorId], known);
260
+ anchor = { x: env.x, y: env.y, w: env.w, h: env.h };
261
+ }
254
262
  }
255
263
  const obstacles = [];
256
264
  let contentBottom = 0;
@@ -293,7 +301,7 @@ Per sketch: ≤${MAX_NODES} nodes, ${MAX_SHAPES} shapes, ${MAX_EDGES} edges (spl
293
301
  try {
294
302
  ctx?.emit?.({ type: 'board.updated', sessionId: null, summary: `画了一张草图 #${tag}` });
295
303
  // 镜头提示(前端只在黑板模式跟随;不是劫持,是"黑板是主窗口"的约定)
296
- ctx?.emit?.({ type: 'board.focus', sessionId: null, tag, rect: world, layer: zone });
304
+ ctx?.emit?.(Events.boardFocus(world, { tag, layer: zone }));
297
305
  } catch { /* fail-soft */ }
298
306
  const lines = [
299
307
  `Sketch #${tag} landed${staging ? ' as STAGING (半透明)' : ''}: ${Object.keys(objects).length - shapes.length} nodes, ${shapes.length} shapes, ${Object.keys(bindings).length} lines; layout ${tpl}; at world (${world.x},${world.y}) ${world.w}x${world.h}${anchor ? ` (${spot.side} of ${args.near})` : ' (below current content)'}.`,
@@ -393,6 +393,7 @@ Suggested flow for image-led pages:
393
393
  // - 普通模式:tavily/exa/baidu/zhipu auto-route by language(CJK→baidu, EN→tavily)
394
394
  // - include_images:zhipu 不支持图,从候选中剔除;其余按语言 auto-route
395
395
  let providerId;
396
+ let providerNote = '';
396
397
  if (include_images) {
397
398
  if (provider === 'zhipu') {
398
399
  return {
@@ -418,15 +419,13 @@ Suggested flow for image-led pages:
418
419
  };
419
420
  }
420
421
  } else {
422
+ // 点名的 provider 没配 key:换配了的顶上别硬失败(结果>供应商身份,iss_mt5z4s8j)
421
423
  providerId = provider;
422
424
  if (!getKey(providerId)) {
423
- return {
424
- content: [{
425
- type: 'text',
426
- text: `web_search failed: ${PROVIDERS[providerId].keyEnv} not set in .env.`,
427
- }],
428
- isError: true,
429
- };
425
+ const alt = ((looksChinese(query) ? PRIORITY_CJK : PRIORITY_NON_CJK).filter(id => id !== 'zhipu')).find(id => getKey(id)) || null;
426
+ if (!alt) return { content: [{ type: 'text', text: `web_search failed: ${PROVIDERS[providerId].keyEnv} not set, and no other image-capable provider configured.` }], isError: true };
427
+ providerNote = `(provider "${provider}" 没配 key,已换用 ${alt})`;
428
+ providerId = alt;
430
429
  }
431
430
  }
432
431
  } else {
@@ -443,13 +442,10 @@ Suggested flow for image-led pages:
443
442
  };
444
443
  }
445
444
  if (!getKey(providerId)) {
446
- return {
447
- content: [{
448
- type: 'text',
449
- text: `web_search failed: ${PROVIDERS[providerId].keyEnv} not set in .env.`,
450
- }],
451
- isError: true,
452
- };
445
+ const alt = autoSelectProvider(query); // 只挑配了 key 的
446
+ if (!alt || !getKey(alt)) return { content: [{ type: 'text', text: `web_search failed: ${PROVIDERS[providerId].keyEnv} not set, and no other provider configured.` }], isError: true };
447
+ providerNote = `(provider "${providerId}" 没配 key,已换用 ${alt})`;
448
+ providerId = alt;
453
449
  }
454
450
  }
455
451
 
@@ -518,7 +514,7 @@ Suggested flow for image-led pages:
518
514
  // count 张 + base64 体积。已在上一步通过 count 截断控量。
519
515
  const out = [{
520
516
  type: 'text',
521
- text: formatMarkdown(query, providerId, hits, { images: downloadedImages }),
517
+ text: `${providerNote ? `${providerNote}\n` : ''}${formatMarkdown(query, providerId, hits, { images: downloadedImages })}`,
522
518
  }];
523
519
  for (const img of downloadedImages) {
524
520
  if (img.base64) {
@@ -17,10 +17,11 @@ import { tool } from '@anthropic-ai/claude-agent-sdk';
17
17
  import { z } from 'zod';
18
18
  import { readBoard, patchBoard } from '../../../projects/board-store.js';
19
19
  import { estimateSizeOn } from '../../../lib/board-kind-sizes.js';
20
- import { layerOf, normalizeCanvasId } from '../../../lib/canvas-id.js';
20
+ import { layerOf, normalizeCanvasId, tagEnvelope } from '../../../lib/canvas-id.js';
21
21
  import { textBox, findSpot, SKETCH_FIT } from '../../../lib/sketch-layout.js';
22
22
  import { getViewpoint } from '../../../projects/viewpoint-store.js';
23
23
  import { renderChalk, chalkFileName, writeChalkFile, CHALK_DIR } from '../../../lib/chalk.js';
24
+ import { Events } from '../../agent/events.js';
24
25
 
25
26
  let seq = 0;
26
27
  const stamp = () => `${Date.now().toString(36)}${(seq++ % 1000).toString(36)}`;
@@ -91,10 +92,17 @@ Keep the chat reply short — a line pointing at the board is enough.`,
91
92
  } else if (near) {
92
93
  const nid = normalizeCanvasId(near);
93
94
  const e = nid ? board.objects?.[nid] : null;
94
- if (!e || !Number.isFinite(e.x)) return err(`锚点 ${near} 还没有座位(read_board 里看不到就锚不上)。`);
95
- anchorId = nid; zone = layerOf(nid, e, known);
96
- const as = estimateSizeOn(board, nid, e);
97
- pos = avoid({ x: e.x + as.w + 24, y: e.y }, obstaclesOf(zone), box.w, box.h);
95
+ if (e && Number.isFinite(e.x)) {
96
+ anchorId = nid; zone = layerOf(nid, e, known);
97
+ const as = estimateSizeOn(board, nid, e);
98
+ pos = avoid({ x: e.x + as.w + 24, y: e.y }, obstaclesOf(zone), box.w, box.h);
99
+ } else {
100
+ // near 也认 tag(08-23 案):落到那片东西的包络右侧,锚线连最右那个
101
+ const env = tagEnvelope(board, near, (id2, e2) => estimateSizeOn(board, id2, e2));
102
+ if (!env) return err(`锚点 ${near} 还没有座位,也不是板上任何 tag(read_board 里看不到就锚不上)。`);
103
+ anchorId = env.anchorId; zone = layerOf(env.anchorId, board.objects[env.anchorId], known);
104
+ pos = avoid({ x: env.x + env.w + 24, y: env.y }, obstaclesOf(zone), box.w, box.h);
105
+ }
98
106
  } else {
99
107
  const obstacles = obstaclesOf('');
100
108
  let bottom = 0; for (const o of obstacles) bottom = Math.max(bottom, o.y + o.h);
@@ -118,7 +126,8 @@ Keep the chat reply short — a line pointing at the board is enough.`,
118
126
  const rect = { x: Math.round(pos.x), y: Math.round(pos.y), w: box.w, h: box.h };
119
127
  try {
120
128
  ctx?.emit?.({ type: 'board.updated', sessionId: null, summary: parentId ? '回了一条板书' : '写了一条板书' });
121
- ctx?.emit?.({ type: 'board.focus', sessionId: null, tag: tag || null, rect, layer: zone, soft: true, chalk: rel });
129
+ // chalk 字段进精灵追踪链(08-24):形状钉在 Events.boardFocus,别再内联对象
130
+ ctx?.emit?.(Events.boardFocus(rect, { tag: tag || null, layer: zone, soft: true, chalk: rel }));
122
131
  } catch { /* */ }
123
132
  const big = box.h > SKETCH_FIT.h * 0.6;
124
133
  return { content: [{ type: 'text', text:
@@ -116,10 +116,9 @@ IP、角色、真人、某个具体产品型号、小众品牌 —— 那种情
116
116
  用户点头之后,把这一份固化下来,它就是后面所有页的基准:
117
117
 
118
118
  - `<style id="design-tokens">` 的 `:root` 整组写死(色号、字号、字体链)
119
- - `record_decision` 落锚:`title` **名字打头**(`风格锚 = 拼贴`),取值写进 `rationale`
120
- "纸纹底 + 微旋转 + 白边 + 手写体标注 + 一点樱桃红")。⚠️ 入参是 title / rationale
121
- (都必填)/ scope / alternatives,**没有 `topic`**
122
- - 风格锚定之后顺手更新 `agent-memory/brand/memory.md`(风格档案),参照图留在
119
+ - 落锚进长期记忆:写一条 `type: project` 记忆(名字打头,如「风格锚 = 拼贴」,
120
+ 取值写进正文:"纸纹底 + 微旋转 + 白边 + 手写体标注 + 一点樱桃红");项目级
121
+ 定案还该进根 `CLAUDE.md` 的「风格档案」节(用户点头后)。参照图留在
123
122
  `assets/references/` 别删 —— 后面几页跑偏时靠它回锚
124
123
 
125
124
  锚说不出名字,说明还没定下来,回去再试一轮。
@@ -38,6 +38,13 @@ docx 的版式谱系跟网页不是一套,常用的这些:
38
38
  | 简历 / 投递 | **单页履历体** | 窄边距、姓名区大字号、条目左对齐悬挂缩进、零装饰 |
39
39
  | 手册 / 说明书 | **技术文档体** | 编号标题、等宽代码块、表格密排 |
40
40
 
41
+ ⚠️ 公文体的坑(实测反馈,08-21 案):严格国标的三级标题**同缩进、纯换字体**
42
+ (黑/楷/仿宋),而预览的替身字体会吃掉楷体与仿宋的对比度 —— 三级在预览里几乎
43
+ 不可辨,用户大概率抱怨"层级看不出来"。两条路先问一句:a) 严格国标(对方单位
44
+ 真按 9704 收文时用);b) **层级强化变体**(用户没点名国标就默认它)——
45
+ Heading1 加 `spacing.beforePt: 8`、Heading2 加 `indent.leftChars: 200` + `bold: true`,
46
+ 层级立刻一眼可辨且不破坏 exact 行距的页数稳定性。
47
+
41
48
  内置词典条目有三个(`build_docx` 的 `preset`):`办公标准` / `公文` / `学术论文`。
42
49
  **它们是起点不是菜单** —— 上表其余的用 preset 起手再覆写 tokens,或者干脆不写
43
50
  preset 给完整 token 表。自造词作废("科技感文档"不是版式名),但凡是真实存在、
@@ -60,7 +67,9 @@ preset 给完整 token 表。自造词作废("科技感文档"不是版式名
60
67
  新旧并排),⛔ 别覆盖旧版 —— 版本是给用户看的,不是给你腾地方的。
61
68
 
62
69
  起手文件 `文档.template.json` 已经拷进工作区了 —— 照着它改,别从空白开始。
63
- **字段全集 Read `references/token-schema.md`** —— 键名、值域、单位、块的形状、
70
+ **字段全集 Read `references/token-schema.md`**(⚠️ 相对**本 SKILL 文件所在目录**,
71
+ 不相对工作区 —— 用加载本 skill 时给出的 SKILL.md 路径拼出绝对路径去 Read,
72
+ 别在工作区里找、更别 find / 全盘扫)—— 键名、值域、单位、块的形状、
64
73
  常见错法对照全在那里,别靠猜,也别去读引擎源码。
65
74
 
66
75
  **schema 是闭合的**:写了没登记过的字段名,`build_docx` 直接报错。这是故意的 ——
@@ -262,6 +262,22 @@
262
262
  写成 `"para": {...}` **会被拒**(2026-08-18 起 content 和页眉页脚都过闭合校验)。
263
263
  在那之前它是"不报错也不生效"——最难发现的一类错,起手模板自己教错过一次。
264
264
 
265
+ ⭐ **对象型键(`spacing` / `indent` / `borders`)与样式里的同名键是属性级深合并**,
266
+ 跟 tokens 对 preset 的合并规则一致:引擎只把你写的属性写进这一段的 pPr,
267
+ 没写的属性照常从样式继承(OOXML 的属性级联就是这个粒度;2026-08-24 真渲染
268
+ 实测证实——样式 `line:10/lineRule:"exact"` 的段,块级只写 `beforePt:30`,
269
+ 行距不掉):
270
+
271
+ ```jsonc
272
+ { "t": "p", "style": "要点", "spacing": { "beforePt": 3 } }
273
+ // ✅ 只多 3pt 段前距;样式里的 line/lineRule 原样保留,不必重抄
274
+ ```
275
+
276
+ 唯一的例外是 `indent`:`firstLine*` 和 `hanging*` 互斥(见上),而深合并抹不掉
277
+ 从样式继承来的属性——所以"样式带首行缩进的段想改成悬挂"**不要**靠块级开小灶
278
+ (继承的 firstLine 还在,两个属性并存后哪个生效随渲染器),正路是给这类段落
279
+ 单独立一个不带首行缩进的样式。
280
+
265
281
  ### `{"t": "table"}` 表格
266
282
 
267
283
  ```jsonc
@@ -125,10 +125,10 @@ IP、角色、真人、某个具体产品型号、小众品牌 —— 那种情
125
125
  2-4 个带小样的候选,不是一张风格大全。摊开菜单就退回模板思维了。
126
126
 
127
127
  他已经点了名字("我要拼贴风")的,跳过前两步直接进试作;直接甩了参考图的,
128
- 读图读出那是什么风格、跟他确认一下名字,再进试作。名字定下来之后调 `record_decision` 落锚:`title` 名字打头
129
- (`风格锚 = 拼贴`),取值写进 `rationale`;⚠️ 入参是 title / rationale(都必填)/
130
- scope / alternatives,**没有 `topic`**。参照图留在 `assets/references/` 别删 ——
131
- 铺到第五页跑偏时靠它回锚。
128
+ 读图读出那是什么风格、跟他确认一下名字,再进试作。名字定下来之后落锚进长期
129
+ 记忆(一条 `type: project` 记忆,名字打头如「风格锚 = 拼贴」,取值写进正文);
130
+ 项目级定案经用户点头后进根 `CLAUDE.md` 的「风格档案」节。参照图留在
131
+ `assets/references/` 别删 —— 铺到第五页跑偏时靠它回锚。
132
132
 
133
133
  ### 翻译词典 —— 你的词汇量,不是给用户的菜单
134
134
 
package/server/index.js CHANGED
@@ -33,7 +33,6 @@ import turnRouter from './api/turn.js';
33
33
  import exportsRouter from './api/exports.js';
34
34
  import sessionsRouter from './api/sessions.js';
35
35
  import instructionRouter from './api/instruction.js';
36
- import memoryRouter from './api/memory.js';
37
36
  import boardRouter from './api/board.js';
38
37
  import pendingChangesRouter from './api/pending-changes.js';
39
38
  import browseRouter from './api/browse.js';
@@ -118,7 +117,6 @@ app.use('/api/projects', turnRouter);
118
117
  app.use('/api/projects', exportsRouter);
119
118
  app.use('/api/projects', sessionsRouter);
120
119
  app.use('/api/projects', instructionRouter);
121
- app.use('/api/projects', memoryRouter);
122
120
  app.use('/api/projects', boardRouter);
123
121
  app.use('/api/projects', pendingChangesRouter); // C4: 用户直接编辑 + 评论 buffer
124
122
  app.use('/api/projects', browseRouter); // 刷新后拿回浏览器窗/求助状态
@@ -95,7 +95,10 @@ export function clearActiveArtifact(sessionId) {
95
95
  }
96
96
 
97
97
  function normalizeRel(p) {
98
- return String(p).replace(/\\/g, '/').replace(/^\.\//, '');
98
+ // 尾部 ?query / #hash 一并剥掉:agent 常把预览 URL 原样拿来当 path
99
+ //('dist/index.html?t=18&panel=0' 落 not found,08-24 清账两案)。
100
+ // 磁盘文件名里合法出现 ? # 的场景在这套产物约定里不存在。
101
+ return String(p).replace(/\\/g, '/').replace(/^\.\//, '').replace(/[?#].*$/, '');
99
102
  }
100
103
 
101
104
  async function exists(p) {
@@ -108,13 +111,9 @@ function insideWorkspace(workspaceRoot, absPath) {
108
111
  return absPath === root || absPath.startsWith(root + path.sep);
109
112
  }
110
113
 
111
- /**
112
- * 站点的页面清单(相对产物根)。
113
- */
114
- export async function listSitePages(workspaceRoot) {
115
- const m = await KINDS[KIND_SITE].manifest(workspaceRoot, null);
116
- return m.pages;
117
- }
114
+ // (listSitePages 2026-08-24 拆除:调用 KINDS.site.manifest 这个不存在的方法,
115
+ // 一调必 TypeError,全仓无调用方 —— 死代码假装公开 API。页面清单读
116
+ // taskManifest().artifacts[].pages。)
118
117
 
119
118
  /**
120
119
  * 推断某个 html 路径的产物形态。
@@ -209,11 +208,28 @@ export async function resolveArtifactTarget(workspaceRoot, relPath, sessionId) {
209
208
  const hit = await tryPath(relPath);
210
209
  if (hit) { setActiveArtifact(sessionId, hit.relPath, hit.kind); return hit; }
211
210
  const abs = path.resolve(workspaceRoot, relPath);
211
+ if (!insideWorkspace(workspaceRoot, abs)) return { ok: false, message: 'path escapes workspace' };
212
+ // 回退:agent 常拿**产物根相对**或**源目录相对**路径来喂('index.html'、
213
+ // 'dist/index.html'),而这里收的是工作区相对('jet-engine/dist/index.html')。
214
+ // 没命中时按各站点实例的 root / srcRoot 补前缀再试一轮,省一次注定失败的
215
+ // 往返(08-24 案:三个工具三种写法全 not found)。
216
+ const m = await taskManifest(workspaceRoot);
217
+ const rel = normalizeRel(relPath);
218
+ const prefixes = new Set();
219
+ for (const a of (m?.artifacts || [])) {
220
+ if (a.kind !== KIND_SITE || a.single) continue;
221
+ if (a.root) prefixes.add(a.root);
222
+ if (a.srcRoot && a.srcRoot !== a.root) prefixes.add(a.srcRoot);
223
+ }
224
+ for (const pre of prefixes) {
225
+ const hit2 = await tryPath(`${pre}/${rel}`);
226
+ if (hit2) { setActiveArtifact(sessionId, hit2.relPath, hit2.kind); return hit2; }
227
+ }
228
+ const candidates = (m?.artifacts || []).map(a => a.entryRel).filter(Boolean);
212
229
  return {
213
230
  ok: false,
214
- message: insideWorkspace(workspaceRoot, abs)
215
- ? `${relPath} not found. Write it first, or check the path.`
216
- : 'path escapes workspace',
231
+ message: `${relPath} not found. Write it first, or check the path (workspace-relative).`
232
+ + (candidates.length ? `\nExisting artifact entries:\n${candidates.map(c => `- ${c}`).join('\n')}` : ''),
217
233
  };
218
234
  }
219
235
 
@@ -272,9 +288,10 @@ export function requireBrowsable(target) {
272
288
 
273
289
  /** 给各工具复用的 path 参数描述(保持措辞一致) */
274
290
  export const ARTIFACT_PATH_DESC =
275
- 'Relative path of the artifact entry (deck: "canvas.html", site: "index.html" '
276
- + 'or its built output like "dist/index.html", word: "文档.docx"). '
277
- + 'Omit to use the artifact you are currently working on.';
291
+ 'WORKSPACE-relative path of the artifact entry (deck: "canvas.html", '
292
+ + 'root site: "index.html" or "dist/index.html", site in a folder: full path '
293
+ + 'like "my-site/dist/index.html", word: "文档.docx"). All artifact tools share '
294
+ + 'this one convention. Omit to use the artifact you are currently working on.';
278
295
 
279
296
  // ── 兼容层:老名字继续可用,内部全部走上面的实现 ────────────────────────────
280
297
  export const CANVAS_PATH_DESC = ARTIFACT_PATH_DESC;
@@ -19,7 +19,6 @@ export const ARTIFACT_PREVIEW_H = { deck: 360, site: 400, docx: 420, browse: 360
19
19
  // file 是 224x32 的细条卡(parity 测试上岗第一天就逮住我猜成 160x120 ——
20
20
  // 那是涂鸦的默认身位。别猜,抄表)
21
21
  export const KIND_SIZES = {
22
- doc: { w: 200, h: 96 },
23
22
  image: { w: 200, h: 176 },
24
23
  video: { w: 240, h: 160 },
25
24
  note: { w: 200, h: 148 },
@@ -44,7 +43,6 @@ export function estimateSize(id, entry) {
44
43
  return { w: entry.w, h: entry.h };
45
44
  }
46
45
  const s = String(id || '');
47
- if (s.startsWith('doc:')) return KIND_SIZES.doc;
48
46
  // 浏览器卡是**单例**,id 就是 'browse'(没有路径可挂 —— 它背后不是文件)。
49
47
  // 不给这条分支的话它会掉到最后的 file 兜底(224×32 的细条),agent 摆位时
50
48
  // 就会拿一个错的矩形去算"挨着谁摆、会不会压到谁"。
@@ -58,7 +56,8 @@ export function estimateSize(id, entry) {
58
56
  }
59
57
  if (IMG_EXT.test(s)) return KIND_SIZES.image;
60
58
  if (VIDEO_EXT.test(s)) return KIND_SIZES.video;
61
- if (/\.(md|txt)$/i.test(s)) return KIND_SIZES.note;
59
+ // 文本类文件卡带内容预览(08-24:md/json 等升级出预览体),身位=note
60
+ if (/\.(md|markdown|txt|json|csv|ya?ml)$/i.test(s)) return KIND_SIZES.note;
62
61
  return KIND_SIZES.file;
63
62
  }
64
63
 
@@ -10,21 +10,46 @@
10
10
  export function normalizeCanvasId(raw) {
11
11
  let id = String(raw || '').trim().replace(/\\/g, '/').replace(/^\.\//, '').replace(/^\/+|\/+$/g, '');
12
12
  if (!id || id.includes('..')) return null;
13
- if (id.endsWith('agent-memory/brand/memory.md')) return 'doc:brand';
14
- if (id.endsWith('agent-memory/memory.md')) return 'doc:_root';
13
+ // (doc:brand / doc:_root 映射 2026-08-24 拆除:项目文档并进根 CLAUDE.md,
14
+ // 记忆住 记忆/,都是普通画布文件,没有特殊 id 了)
15
15
  if (!/^(deck|site|doc|text|scribble):/.test(id) && /\.html?$/i.test(id)) return `deck:${id}`;
16
16
  return id;
17
17
  }
18
18
 
19
+ /**
20
+ * 一个 tag 下所有座位的包络(08-23 案:near 只认节点 id 不认 tag,可 tag 本来
21
+ * 就是"一片东西的名字"——刚用 tag 画完草图,把 tag 传给 near 是自然写法)。
22
+ * 返回 { x, y, w, h, anchorId, ids } 或 null;anchorId = 最右那个(贴右侧摆用)。
23
+ * sizeOf(id, entry) 由调用方给(estimateSizeOn 绑着 board,不在这层 import)。
24
+ */
25
+ export function tagEnvelope(board, tag, sizeOf) {
26
+ const hits = Object.entries(board?.objects || {})
27
+ .filter(([, e]) => e?.tag === tag && Number.isFinite(e?.x));
28
+ if (!hits.length) return null;
29
+ let x1 = Infinity; let y1 = Infinity; let x2 = -Infinity; let y2 = -Infinity;
30
+ let anchorId = null; let rightEdge = -Infinity;
31
+ for (const [id, e] of hits) {
32
+ const s = sizeOf(id, e) || { w: 0, h: 0 };
33
+ x1 = Math.min(x1, e.x); y1 = Math.min(y1, e.y);
34
+ x2 = Math.max(x2, e.x + s.w); y2 = Math.max(y2, e.y + s.h);
35
+ if (e.x + s.w > rightEdge) { rightEdge = e.x + s.w; anchorId = id; }
36
+ }
37
+ return { x: x1, y: y1, w: x2 - x1, h: y2 - y1, anchorId, ids: hits.map(([id]) => id) };
38
+ }
39
+
19
40
  /**
20
41
  * 它住在哪一层(服务端近似版):显式 zone 字段优先,其次沿路径往上找第一个
21
42
  * 已知文件夹。已知集 = board.zones 的 key —— 比前端少了"服务端扫出来的任务
22
43
  * 目录"这一路,没摆过的深层目录会归到根。read_board 的输出里写明这是近似。
23
44
  */
24
45
  export function layerOf(id, entry, knownFolders) {
25
- if (entry && typeof entry.zone === 'string') return entry.zone;
46
+ // 显式 zone 只认**真实存在的层**('' zones 里有的文件夹)。历史上
47
+ // pin_to_board 往 zone 里写过 'assets/generated' 这类前端根本不当层渲染的
48
+ // 路径(iss_mt5t487g:pin 上来的图被判在"assets 层",arrange 跟根层的草图
49
+ // 节点一比就拒)——错标签直接落回按路径推,存量脏数据顺带自愈。
50
+ if (entry && typeof entry.zone === 'string'
51
+ && (entry.zone === '' || knownFolders?.has?.(entry.zone))) return entry.zone;
26
52
  const s = String(id);
27
- if (s.startsWith('doc:')) return '';
28
53
  const c = s.indexOf(':');
29
54
  const p = (c > 0 && /^[a-z]+$/.test(s.slice(0, c))) ? s.slice(c + 1) : s;
30
55
  let d = p.includes('/') ? p.slice(0, p.lastIndexOf('/')) : '';
@@ -145,7 +145,9 @@ export async function collectCard({ workspaceRoot, cardId }) {
145
145
  const m = await taskManifest(workspaceRoot).catch(() => null);
146
146
  const rootSite = (m?.artifacts || []).find(a => a.kind === 'site' && !a.single && !a.srcRoot);
147
147
  for (const pg of (rootSite?.pages || [])) {
148
- const pRel = String(pg).replace(/\\/g, '/');
148
+ // ⚠️ pages 是**产物根相对**(构建型根站 root='dist' 时是 'index.html' 不是
149
+ // 'dist/index.html')——对着工作区根解析会抓到源文件或落空(08-24 对齐)
150
+ const pRel = (rootSite.root ? `${rootSite.root}/` : '') + String(pg).replace(/\\/g, '/');
149
151
  const pAbs = await safeResolve(workspaceRoot, pRel);
150
152
  if (!pAbs) continue;
151
153
  try { if ((await fs.stat(pAbs)).isFile()) files.push({ abs: pAbs, rel: pRel }); } catch { /* 页面刚被删 */ }
@@ -59,7 +59,9 @@ db.exec(`
59
59
  */
60
60
  export function signatureOf(text) {
61
61
  const norm = String(text || '')
62
- .replace(/[/~][\w.\-/@ 一-鿿]+/g, '<path>') // 路径
62
+ // 字符类里不能有空格:不然一个孤立的 "/" 会贪婪吞掉后面一整串词(08-24 案,
63
+ // artifact_find 输出里的 "→ https://…" 之后的文字被不定长吃掉,指纹漂移不聚合)
64
+ .replace(/[/~][\w.\-/@一-鿿]+/g, '<path>') // 路径
63
65
  .replace(/\b[0-9a-f]{8,}\b/gi, '<hash>') // id / hash
64
66
  .replace(/\d+/g, 'N') // 行号、字节数、耗时
65
67
  .replace(/\s+/g, ' ')
@@ -264,16 +264,23 @@ export default {
264
264
  async describe(taskDir, artifact) {
265
265
  const label = `word ${artifact.entryRel}`;
266
266
  const src = artifact.sourceFile;
267
+ // 文件夹里的**全部**成员都要点名(08-19 案:目录里 v3/v4 两份,清单只报
268
+ // 默认那份,agent 对着过时的 v3 干了一整轮 —— 注入的清单让人默认它是全的,
269
+ // 半份状态比没状态更误导)
270
+ const others = (artifact.members || [])
271
+ .map(m => m.file)
272
+ .filter(f => f && f !== artifact.entryRel);
273
+ const memberNote = others.length ? ` · 同夹还有:${others.join(' / ')}` : '';
267
274
  try {
268
275
  const stat = await fs.stat(path.join(taskDir, artifact.entryRel));
269
276
  const kb = (stat.size / 1024).toFixed(0);
270
277
  return src
271
- ? `${label} · ${kb}KB · 源 ${src}(改源重建,别直接改 .docx)`
272
- : `${label} · ${kb}KB · 外来文档,没有 token 源`;
278
+ ? `${label} · ${kb}KB · 源 ${src}(改源重建,别直接改 .docx)${memberNote}`
279
+ : `${label} · ${kb}KB · 外来文档,没有 token 源${memberNote}`;
273
280
  } catch {
274
281
  return src
275
- ? `${label} · 还没构建(源 ${src} 已就位)`
276
- : `${label} · 入口读不到`;
282
+ ? `${label} · 还没构建(源 ${src} 已就位)${memberNote}`
283
+ : `${label} · 入口读不到${memberNote}`;
277
284
  }
278
285
  },
279
286
 
@@ -30,7 +30,9 @@ import docx from './docx.js';
30
30
 
31
31
  export const KINDS = Object.freeze({ [deck.id]: deck, [site.id]: site, [docx.id]: docx });
32
32
 
33
- export { RESERVED_DIRS, isReservedFile } from '../task-scan.js';
33
+ import { readTaskMarker } from '../task-scan.js';
34
+
35
+ export { RESERVED_DIRS, isReservedFile, readTaskMarker } from '../task-scan.js';
34
36
 
35
37
  export function kindDef(kind) {
36
38
  return KINDS[kind] || null;
@@ -57,21 +59,9 @@ export function can(kind, cap) {
57
59
  return !!KINDS[kind]?.capabilities?.includes(cap);
58
60
  }
59
61
 
60
- /**
61
- * 读产物标记(`.nd-project.json`)。没有 / 读不动 → null。
62
- *
63
- * 2026-08-07 从 `.nd-task.json` 改名。旧文件里有两个字段:`sessionId`(这个
64
- * 任务属于哪次对话 —— 正是要废的那条绑定,迁移时直接删)和 `root`(构建型
65
- * 站点显式声明产物根,agent 刚写完源还没 build 的窗口期靠它认出"站已经在了")。
66
- * 后者还有用,所以标记本身留着,只是不再承载归属。
67
- */
68
- export async function readTaskMarker(root) {
69
- try {
70
- const raw = await fs.readFile(path.join(root, '.nd-project.json'), 'utf8');
71
- const parsed = JSON.parse(raw);
72
- return (parsed && typeof parsed === 'object') ? parsed : null;
73
- } catch { return null; }
74
- }
62
+ // readTaskMarker 本体 2026-08-24 迁去 task-scan.js(site.js 要按子目录读 marker,
63
+ // 住这里会循环 import)。上面 re-export 保住老调用方。历史注释一并搬走:
64
+ // `.nd-project.json` 2026-08-07 从 `.nd-task.json` 改名,只剩 `root` 字段有用。
75
65
 
76
66
  /**
77
67
  * 任务的完整 manifest —— 服务端唯一的解析器,前端、感知工具、导出都吃这份。