@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.
- package/package.json +1 -1
- package/server/api/assets/helpers.js +18 -0
- package/server/api/assets/notes.js +50 -0
- package/server/api/assets.js +11 -6
- package/server/api/instruction.js +3 -3
- package/server/engine/agent/events.js +11 -1
- package/server/engine/agent/hooks/failure.js +11 -1
- package/server/engine/agent/hooks/lifecycle.js +10 -39
- package/server/engine/agent/hooks/post-guidance.js +4 -38
- package/server/engine/agent/hooks/user-prompt-submit.js +11 -40
- package/server/engine/agent/hooks.js +4 -8
- package/server/engine/agent/memory-config.js +61 -0
- package/server/engine/agent/prompts/nodesign-prelude.md +14 -12
- package/server/engine/agent/prompts/tools/generate-image-cookbook.md +24 -7
- package/server/engine/agent/prompts/tools/site-reference.md +13 -3
- package/server/engine/agent/session-loop.js +29 -18
- package/server/engine/agents/explorer.md +1 -1
- package/server/engine/agents/index.js +1 -1
- package/server/engine/mcp/index.js +4 -11
- package/server/engine/mcp/tools/arrange-on-board.js +3 -1
- package/server/engine/mcp/tools/browse-computer.js +2 -1
- package/server/engine/mcp/tools/browse-find-batch.js +20 -3
- package/server/engine/mcp/tools/browse.js +24 -2
- package/server/engine/mcp/tools/crystallize-skill.js +1 -1
- package/server/engine/mcp/tools/generate-image.js +50 -70
- package/server/engine/mcp/tools/helpers/codex-imagegen.js +139 -0
- package/server/engine/mcp/tools/helpers/rembg.js +7 -2
- package/server/engine/mcp/tools/helpers/shot-pipeline.js +109 -0
- package/server/engine/mcp/tools/list-pages.js +11 -5
- package/server/engine/mcp/tools/organize-board.js +26 -1
- package/server/engine/mcp/tools/pin-to-board.js +26 -17
- package/server/engine/mcp/tools/read-board.js +19 -5
- package/server/engine/mcp/tools/relate-on-board.js +42 -4
- package/server/engine/mcp/tools/report-issue.js +14 -7
- package/server/engine/mcp/tools/screenshot.js +17 -7
- package/server/engine/mcp/tools/sketch-on-board.js +13 -5
- package/server/engine/mcp/tools/web-search.js +11 -15
- package/server/engine/mcp/tools/write-on-board.js +15 -6
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/SKILL.md +3 -4
- package/server/engine/plugins/nodesign/skills/docx-craft/SKILL.md +10 -1
- package/server/engine/plugins/nodesign/skills/docx-craft/references/token-schema.md +16 -0
- package/server/engine/plugins/nodesign/skills/site-craft/SKILL.md +4 -4
- package/server/index.js +0 -2
- package/server/lib/artifact-target.js +31 -14
- package/server/lib/board-kind-sizes.js +2 -3
- package/server/lib/canvas-id.js +29 -4
- package/server/lib/export-collect.js +3 -1
- package/server/lib/issues-store.js +3 -1
- package/server/lib/kinds/docx.js +11 -4
- package/server/lib/kinds/index.js +6 -16
- package/server/lib/kinds/site.js +40 -7
- package/server/lib/rewrite-refs.js +110 -0
- package/server/lib/ssrf-guard.js +9 -0
- package/server/lib/task-scan.js +18 -0
- package/server/projects/board-store.js +42 -1
- package/server/projects/memory-migration.js +85 -0
- package/server/projects/move-entry.js +11 -1
- package/server/projects/ui-config.js +2 -1
- package/server/projects/workspace-templates.js +13 -10
- package/server/projects/workspace.js +15 -15
- package/server/services/rembg-launcher.js +5 -2
- package/web/dist/assets/{BrowserWindow-P_vIdh-G.js → BrowserWindow-BoOBaIyz.js} +1 -1
- package/web/dist/assets/DeckWindow-Bg3rgkvi.js +11 -0
- package/web/dist/assets/{DocxWindow-DSAU1-tD.js → DocxWindow-B_u6aX9o.js} +1 -1
- package/web/dist/assets/{MermaidBlock-DR1GikCv.js → MermaidBlock-BO-46Du5.js} +6 -6
- package/web/dist/assets/{SiteWindow-92uBuRRR.js → SiteWindow-ConPmzdM.js} +1 -1
- package/web/dist/assets/{abnfDiagram-VCTEODGH-uA8Esix4.js → abnfDiagram-VCTEODGH-FCO9yf-n.js} +1 -1
- package/web/dist/assets/{arc-D_khH_xN.js → arc-BLcEOSRX.js} +1 -1
- package/web/dist/assets/{architectureDiagram-5GKGNRK7-DydJHqOt.js → architectureDiagram-5GKGNRK7-kxiX4TeN.js} +1 -1
- package/web/dist/assets/{blockDiagram-NRAW4CY4-B7wBbKiW.js → blockDiagram-NRAW4CY4-CptFksix.js} +1 -1
- package/web/dist/assets/{c4Diagram-UCG6FXSJ-6mrH0x7Y.js → c4Diagram-UCG6FXSJ-Dxmxj34F.js} +1 -1
- package/web/dist/assets/channel-DFyiuGL0.js +1 -0
- package/web/dist/assets/{chunk-2Q5K7J3B-Cuikho5Q.js → chunk-2Q5K7J3B-D9inx5oo.js} +1 -1
- package/web/dist/assets/{chunk-5VM5RSS4-3-KZ5AUz.js → chunk-5VM5RSS4-DydfHlEc.js} +1 -1
- package/web/dist/assets/{chunk-F27PBJKO-Bkwq2gGU.js → chunk-F27PBJKO-BO7GxhSv.js} +1 -1
- package/web/dist/assets/{chunk-G27WJ6UU-BLyQzdGE.js → chunk-G27WJ6UU-BzP6YiQ_.js} +1 -1
- package/web/dist/assets/{chunk-JWPE2WC7-B7HxxQds.js → chunk-JWPE2WC7-CHcgcd0s.js} +1 -1
- package/web/dist/assets/{chunk-LCL6LL3I-CtXrnUwF.js → chunk-LCL6LL3I-D2Wg5JtX.js} +1 -1
- package/web/dist/assets/{chunk-POPQ4Y6H-dxddZlXt.js → chunk-POPQ4Y6H-B2fCRfpM.js} +1 -1
- package/web/dist/assets/{chunk-SVP7TREG-DpkmrgHn.js → chunk-SVP7TREG-JSLcsRlZ.js} +1 -1
- package/web/dist/assets/{chunk-XXDRQBXY-BwZ7RbpQ.js → chunk-XXDRQBXY-BCjvaeHm.js} +1 -1
- package/web/dist/assets/classDiagram-DTDB5LWJ-Aw1f8z-b.js +1 -0
- package/web/dist/assets/classDiagram-v2-JRS7N3AN-Aw1f8z-b.js +1 -0
- package/web/dist/assets/{cose-bilkent-JH36ORCC-DGFOsMUt.js → cose-bilkent-JH36ORCC-0uKv6oJ7.js} +1 -1
- package/web/dist/assets/{cynefin-OW5HDTMX-DUwn3ehS.js → cynefin-OW5HDTMX-Bvt-gEmA.js} +1 -1
- package/web/dist/assets/{cynefinDiagram-5FMLGOSQ-DlLAOZ9x.js → cynefinDiagram-5FMLGOSQ-C9Ht02qC.js} +1 -1
- package/web/dist/assets/{dagre-3AP2YEHR-CyourLfz.js → dagre-3AP2YEHR-BfxZny4s.js} +1 -1
- package/web/dist/assets/{diagram-S7CK7UJ4-BTMbx2np.js → diagram-S7CK7UJ4-CrE0ZETE.js} +1 -1
- package/web/dist/assets/{diagram-UQ7AKVKN-UK94_1-P.js → diagram-UQ7AKVKN-BO8VQ4Fx.js} +1 -1
- package/web/dist/assets/{diagram-VSXAHHWV-C7zS-v9v.js → diagram-VSXAHHWV-CHUZnR9F.js} +1 -1
- package/web/dist/assets/{diagram-VX7I27RA-DKJRa6CK.js → diagram-VX7I27RA-DYQAz1Vu.js} +1 -1
- package/web/dist/assets/{diagram-Z3DM3KII-kUbAfGAp.js → diagram-Z3DM3KII-BDjivgLr.js} +1 -1
- package/web/dist/assets/{ebnfDiagram-PWID7BFC-CVW16_vb.js → ebnfDiagram-PWID7BFC-Bpe_QVbH.js} +1 -1
- package/web/dist/assets/{erDiagram-SSCWMZ5O-BRQMFQ5y.js → erDiagram-SSCWMZ5O-BxhwuAu1.js} +1 -1
- package/web/dist/assets/{flowDiagram-A5DVABFB-S34LpLtK.js → flowDiagram-A5DVABFB-CCw2H08-.js} +1 -1
- package/web/dist/assets/{ganttDiagram-EL5Y4UJY-BFiqk1-G.js → ganttDiagram-EL5Y4UJY-DtTc7MyI.js} +1 -1
- package/web/dist/assets/{gitGraphDiagram-WWUBYQGX-LZsrdyyv.js → gitGraphDiagram-WWUBYQGX-BvORxMd2.js} +1 -1
- package/web/dist/assets/index-DGG5wIZs.js +2118 -0
- package/web/dist/assets/{infoDiagram-RXCK75RN-0DvN6KIp.js → infoDiagram-RXCK75RN-DMcrkypD.js} +1 -1
- package/web/dist/assets/{ishikawaDiagram-5VMMS53U-CaY3A-Bx.js → ishikawaDiagram-5VMMS53U-CZkTIbe-.js} +1 -1
- package/web/dist/assets/{journeyDiagram-EYS64GPL-BBeiaCrj.js → journeyDiagram-EYS64GPL-DR0XazdE.js} +1 -1
- package/web/dist/assets/{kanban-definition-3QL26DDD-ux42DRXQ.js → kanban-definition-3QL26DDD-DN8VbwVu.js} +1 -1
- package/web/dist/assets/{layout-CnXiF_NB.js → layout-9_paCbyt.js} +1 -1
- package/web/dist/assets/{linear-BMW0u6G6.js → linear-g6Ct012e.js} +1 -1
- package/web/dist/assets/{mindmap-definition-FBJOCRG2-hUxSpdKn.js → mindmap-definition-FBJOCRG2-BGt4omYC.js} +1 -1
- package/web/dist/assets/{pegDiagram-XKGWAZYB-aCaQPH0w.js → pegDiagram-XKGWAZYB-uSfS-PBE.js} +1 -1
- package/web/dist/assets/{pending-edit-apply-AcdOQ9Ij.js → pending-edit-apply-uP4uvrOD.js} +1 -1
- package/web/dist/assets/{pieDiagram-E7YTZNPT-CmOEZ8zg.js → pieDiagram-E7YTZNPT-DpeI935H.js} +1 -1
- package/web/dist/assets/{quadrantDiagram-AXDQQJYC-VW6fAhsV.js → quadrantDiagram-AXDQQJYC-B7IuNvWa.js} +1 -1
- package/web/dist/assets/{railroadDiagram-O6MQD6OU-CKJ7Ba5K.js → railroadDiagram-O6MQD6OU-ARzYpPQI.js} +1 -1
- package/web/dist/assets/{requirementDiagram-EFPCY7ZU-pNGkq9Vw.js → requirementDiagram-EFPCY7ZU-Cj4upkAo.js} +1 -1
- package/web/dist/assets/{rotate-cw-KAfChbGV.js → rotate-cw-CXJcyHWk.js} +1 -1
- package/web/dist/assets/{sankeyDiagram-P5KCCOFB-BIMmDTfZ.js → sankeyDiagram-P5KCCOFB-OaVwSnLP.js} +1 -1
- package/web/dist/assets/{sequenceDiagram-WJ2MYXX4-BBW7wzKY.js → sequenceDiagram-WJ2MYXX4-1yrUbq4t.js} +1 -1
- package/web/dist/assets/{sizeCapture-X5ZJPWSS-DdkXmLYR.js → sizeCapture-X5ZJPWSS-nYH5p9D6.js} +1 -1
- package/web/dist/assets/{square-dashed-mouse-pointer-CDUlhUP7.js → square-dashed-mouse-pointer-CTtTVKoH.js} +1 -1
- package/web/dist/assets/{stateDiagram-HBIQ2CUA-W313Uz16.js → stateDiagram-HBIQ2CUA-Dj5HvyNq.js} +1 -1
- package/web/dist/assets/stateDiagram-v2-4QOOHH4V-kJ1m_zJT.js +1 -0
- package/web/dist/assets/{swimlanes-XN3QIQJK-C29TL26K.js → swimlanes-XN3QIQJK-umeT0HpB.js} +1 -1
- package/web/dist/assets/swimlanesDiagram-VK2B7HYN-DPSkMbbl.js +8 -0
- package/web/dist/assets/{timeline-definition-24CTP7MA-CF0JMObX.js → timeline-definition-24CTP7MA-MqnQfHKB.js} +1 -1
- package/web/dist/assets/{vennDiagram-4TSXK5OY-DrDFsaH3.js → vennDiagram-4TSXK5OY-b2liyZra.js} +1 -1
- package/web/dist/assets/{wardleyDiagram-VM6X3IG4-CVrnRIS_.js → wardleyDiagram-VM6X3IG4-QMK-zNh1.js} +1 -1
- package/web/dist/assets/{xychartDiagram-S5SC5T6Z-BvkN1zsT.js → xychartDiagram-S5SC5T6Z-D5Mc6tfA.js} +1 -1
- package/web/dist/index.html +1 -1
- package/server/api/memory.js +0 -185
- package/server/engine/agent/prompts/tools/generate-image-cookbook.gemini-gateway.md +0 -583
- package/server/engine/mcp/tools/record-decision.js +0 -140
- package/web/dist/assets/DeckWindow-BpLv5082.js +0 -16
- package/web/dist/assets/channel-DGkccMO1.js +0 -1
- package/web/dist/assets/classDiagram-DTDB5LWJ-BGscw3OD.js +0 -1
- package/web/dist/assets/classDiagram-v2-JRS7N3AN-BGscw3OD.js +0 -1
- package/web/dist/assets/index-GAamh1p4.js +0 -2152
- package/web/dist/assets/stateDiagram-v2-4QOOHH4V-DDhJBZzf.js +0 -1
- package/web/dist/assets/swimlanesDiagram-VK2B7HYN-BfN-Xdmu.js +0 -8
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* helpers/codex-imagegen.js — codex 生图桥的 prompt 组装 + 子进程执行
|
|
3
|
+
* (2026-08-24 从 generate-image.js 迁出:给棘轮腾行 + prompt 组装层归位一处)
|
|
4
|
+
*
|
|
5
|
+
* 两层各管一事:
|
|
6
|
+
* - buildVariationPrompt:变体模式(variationOf)的 prompt 骨架。把"要保持的
|
|
7
|
+
* 东西逐项列出"从 agent 手写咒语变成参数 —— 32 张换装立绘实战里,漏写一项
|
|
8
|
+
* (袜子)整行就坏,这套骨架把踩过的坑固化成默认禁止项。
|
|
9
|
+
* - buildCodexBridgePrompt / runCodexImageGen:codex CLI 桥(原样迁出,
|
|
10
|
+
* 仅第 3 条在变体模式下改口径:参考图第 1 张是编辑基底,不是风格参照)。
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import fs from 'node:fs/promises';
|
|
14
|
+
import { spawn } from 'node:child_process';
|
|
15
|
+
|
|
16
|
+
export const CODEX_BIN = process.env.NODESIGN_CODEX_BIN || 'codex';
|
|
17
|
+
export const CODEX_IMAGE_TIMEOUT_MS = Number(process.env.NODESIGN_CODEX_IMAGE_TIMEOUT_MS) || 240_000;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 变体模式的 preserve 词表。键是 agent 传的枚举值,值是展开进 prompt 的英文短语。
|
|
21
|
+
* 短语必须把"缺席也是要保持的"写死(no socks 的参考图,结果也必须 no socks)——
|
|
22
|
+
* 这是实战里最容易漂的一类。
|
|
23
|
+
*/
|
|
24
|
+
export const PRESERVE_PHRASES = {
|
|
25
|
+
pose: 'the exact same pose and body position',
|
|
26
|
+
framing: 'the same framing, camera distance and position of the subject in the frame',
|
|
27
|
+
proportions: 'the same height and body proportions',
|
|
28
|
+
face: 'the same face and the same expression',
|
|
29
|
+
hair: 'the same hairstyle and hair color',
|
|
30
|
+
hands: 'the same hand positions, holding the same things (empty hands stay empty)',
|
|
31
|
+
top: 'the same top garment — identical cut, color, sleeves, neckline and every detail',
|
|
32
|
+
bottom: 'the same bottom garment (skirt / trousers) — identical in every detail',
|
|
33
|
+
shoes: 'the same footwear, identical in every detail',
|
|
34
|
+
socks: 'the same socks / legwear (if the reference has none, the result has none)',
|
|
35
|
+
accessories: 'the same accessories — nothing added, nothing removed',
|
|
36
|
+
background: 'the same background',
|
|
37
|
+
lighting: 'the same lighting and color grading',
|
|
38
|
+
style: 'the same art style and rendering technique',
|
|
39
|
+
};
|
|
40
|
+
export const PRESERVE_KEYS = Object.keys(PRESERVE_PHRASES);
|
|
41
|
+
|
|
42
|
+
// 不论 preserve 传什么都追加的通用军规 —— 全部是真实翻过车的:凭空长出背带、
|
|
43
|
+
// 白袜整行消失、取景悄悄拉近。
|
|
44
|
+
const VARIATION_HARD_RULES = [
|
|
45
|
+
'Do NOT invent anything that is not in the reference: no added straps, suspenders,'
|
|
46
|
+
+ ' belts, hair ornaments, jewelry or extra garments.',
|
|
47
|
+
'Absences are part of the reference: whatever the reference does not have, the result must not have.',
|
|
48
|
+
'Do NOT change the subject\'s height, body proportions, or position in the frame.',
|
|
49
|
+
].join('\n');
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* variationOf 模式:把 change + preserve 展开成"只改一处、其余照搬"的固定骨架。
|
|
53
|
+
* @param {object} o
|
|
54
|
+
* @param {string} o.change 唯一要改的那一处(自然语言)
|
|
55
|
+
* @param {string[]} [o.preserve] PRESERVE_KEYS 的子集;缺省 = 全部
|
|
56
|
+
* @param {string} [o.extra] 附注(agent 想额外说明的,放在骨架之后)
|
|
57
|
+
*/
|
|
58
|
+
export function buildVariationPrompt({ change, preserve, extra }) {
|
|
59
|
+
const keys = (preserve && preserve.length > 0) ? preserve : PRESERVE_KEYS;
|
|
60
|
+
const phrases = keys.map((k) => PRESERVE_PHRASES[k]).filter(Boolean);
|
|
61
|
+
return [
|
|
62
|
+
'Reproduce the reference illustration (the first reference image) with ONE single change.',
|
|
63
|
+
`THE ONLY CHANGE: ${change}`,
|
|
64
|
+
`Everything else must be identical to the reference: ${phrases.join('; ')}.`,
|
|
65
|
+
VARIATION_HARD_RULES,
|
|
66
|
+
...(extra ? [extra] : []),
|
|
67
|
+
].join('\n');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* codex 桥接 prompt。必须写死"逐字传递零改写"——codex agent 默认会按自己的
|
|
72
|
+
* Augmentation rules 润色 prompt。变体模式下第 3 条改口径:第 1 张参考图是
|
|
73
|
+
* **编辑基底**,必须除指定改动外逐细节复现("风格参照"的软口径会被当成
|
|
74
|
+
* style reference,收敛不住)。
|
|
75
|
+
*/
|
|
76
|
+
export function buildCodexBridgePrompt({ prompt, aspectRatio, absOut, refCount, variation = false }) {
|
|
77
|
+
let refLine;
|
|
78
|
+
if (refCount > 0 && variation) {
|
|
79
|
+
refLine = `3. 本消息附带 ${refCount} 张参考图。第 1 张是**编辑基底**:除 <image-prompt> 指定的那一处改动外,`
|
|
80
|
+
+ '必须逐细节复现它的全部内容;其余参考图(如有)才是风格 / 一致性参照。';
|
|
81
|
+
} else if (refCount > 0) {
|
|
82
|
+
refLine = `3. 本消息附带 ${refCount} 张参考图,把它们作为图像生成的参考输入(风格 / 主体一致性参照)。`;
|
|
83
|
+
} else {
|
|
84
|
+
refLine = '3. 本次无参考图。';
|
|
85
|
+
}
|
|
86
|
+
return [
|
|
87
|
+
'你是图像生成管道的执行端,只做下面几件事,不做任何多余动作:',
|
|
88
|
+
'1. 调用你的图像生成工具生成一张图。<image-prompt> 标签内的内容必须逐字作为生成 prompt,禁止改写、增删、翻译或润色。',
|
|
89
|
+
`2. 输出比例:${aspectRatio}。优先用工具的比例/尺寸参数;工具没有对应参数时,作为补充说明传给工具,但不修改 <image-prompt> 原文。`,
|
|
90
|
+
refLine,
|
|
91
|
+
`4. 生成后把图片文件复制到精确路径 ${absOut}(目录已存在)。`,
|
|
92
|
+
'5. 最后只回复该绝对路径。',
|
|
93
|
+
'<image-prompt>',
|
|
94
|
+
prompt,
|
|
95
|
+
'</image-prompt>',
|
|
96
|
+
].join('\n');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* 跑一次 codex exec 生图,以目标文件落盘为成功标准(codex 的文本回复不可信),
|
|
101
|
+
* 失败自动重试一次。abort signal / 超时都 SIGKILL 子进程。
|
|
102
|
+
*/
|
|
103
|
+
export async function runCodexImageGen({ bridgePrompt, refPaths, cwd, signal, expectFile, timeoutMs = CODEX_IMAGE_TIMEOUT_MS }) {
|
|
104
|
+
const args = ['exec', '--skip-git-repo-check', '-s', 'workspace-write', '-C', cwd, bridgePrompt];
|
|
105
|
+
for (const p of refPaths) args.push('-i', p);
|
|
106
|
+
|
|
107
|
+
const runOnce = () => new Promise((resolve, reject) => {
|
|
108
|
+
const child = spawn(CODEX_BIN, args, { stdio: ['ignore', 'pipe', 'pipe'], env: process.env });
|
|
109
|
+
let stderrTail = '';
|
|
110
|
+
child.stdout.on('data', () => { /* 排空防背压 */ });
|
|
111
|
+
child.stderr.on('data', (d) => { stderrTail = (stderrTail + d.toString()).slice(-2000); });
|
|
112
|
+
const killTimer = setTimeout(() => {
|
|
113
|
+
try { child.kill('SIGKILL'); } catch { /* */ }
|
|
114
|
+
reject(new Error(`codex exec timeout after ${Math.round(timeoutMs / 1000)}s`));
|
|
115
|
+
}, timeoutMs);
|
|
116
|
+
const onAbort = () => { try { child.kill('SIGKILL'); } catch { /* */ } };
|
|
117
|
+
signal?.addEventListener?.('abort', onAbort, { once: true });
|
|
118
|
+
child.on('error', (err) => { clearTimeout(killTimer); reject(err); });
|
|
119
|
+
child.on('close', (code) => {
|
|
120
|
+
clearTimeout(killTimer);
|
|
121
|
+
signal?.removeEventListener?.('abort', onAbort);
|
|
122
|
+
if (signal?.aborted) return reject(new Error('aborted'));
|
|
123
|
+
if (code !== 0) return reject(new Error(`codex exec exited ${code}: ${stderrTail.slice(-300) || 'no stderr'}`));
|
|
124
|
+
resolve();
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
for (let attempt = 1; attempt <= 2; attempt++) {
|
|
129
|
+
try {
|
|
130
|
+
await runOnce();
|
|
131
|
+
const st = await fs.stat(expectFile).catch(() => null);
|
|
132
|
+
if (st && st.size > 0) return;
|
|
133
|
+
throw new Error(`codex finished but target file missing/empty: ${expectFile}`);
|
|
134
|
+
} catch (err) {
|
|
135
|
+
if (attempt === 2 || signal?.aborted) throw err;
|
|
136
|
+
console.warn(`[generate-image] codex attempt ${attempt} failed (${err.message}), retrying once`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
@@ -39,13 +39,18 @@ export const REMBG_SETUP_HINT = IS_WIN
|
|
|
39
39
|
: 'cd server && python3 -m venv .venv-rembg && .venv-rembg/bin/python3 -m pip install rembg onnxruntime';
|
|
40
40
|
const DEFAULT_PYTHON = REMBG_VENV_PYTHON;
|
|
41
41
|
const DEFAULT_HELPER = path.join(__dirname, 'rembg-bridge.py');
|
|
42
|
-
const DEFAULT_SOCKET = '/tmp/nodesign-rembg.sock';
|
|
43
42
|
// fallback spawn 的 timeout——首次冷启 + 模型 load 留余量
|
|
44
43
|
const DEFAULT_FALLBACK_TIMEOUT_MS = 60_000;
|
|
45
44
|
|
|
46
45
|
function resolvePython() { return process.env.NODESIGN_REMBG_PYTHON || DEFAULT_PYTHON; }
|
|
47
46
|
function resolveHelper() { return process.env.NODESIGN_REMBG_HELPER || DEFAULT_HELPER; }
|
|
48
|
-
|
|
47
|
+
// socket 默认按**端口**派生(08-24 案:prod/exp 共用一个默认路径,exp 启动把生产的
|
|
48
|
+
// 抠图服务当 stale 杀掉再自己占坑)。端口一实例一个,是现成的实例身份。
|
|
49
|
+
// 这里是全仓唯一一份真相源,rembg-launcher.js import 它。
|
|
50
|
+
export function resolveRembgSocket() {
|
|
51
|
+
return process.env.NODESIGN_REMBG_SOCKET || `/tmp/nodesign-rembg-${process.env.PORT || '4001'}.sock`;
|
|
52
|
+
}
|
|
53
|
+
function resolveSocket() { return resolveRembgSocket(); }
|
|
49
54
|
|
|
50
55
|
// service health check 缓存(避免每次 request 都探一遍 /health)
|
|
51
56
|
let serviceHealthCache = { ok: false, checkedAt: 0 };
|
|
@@ -278,3 +278,112 @@ export async function runBeforeShot(page, beforeShot) {
|
|
|
278
278
|
return `beforeShot error: ${err?.message || err}`;
|
|
279
279
|
}
|
|
280
280
|
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* 带兜底的整页/裁剪截图(iss_mt6tru7p:WebGL 持续动画页 + device:'mobile' 下
|
|
284
|
+
* page.screenshot 等稳定帧/字体 30s 必超时——rAF 每帧都在重绘,"稳定"永远
|
|
285
|
+
* 等不到)。先给 playwright 一次机会(15s,够正常页),超时改走 CDP
|
|
286
|
+
* Page.captureScreenshot:**抓当前帧不等任何东西**。持续动画页抓哪一帧都是
|
|
287
|
+
* 对的——它本来就没有"稳定帧"这回事。
|
|
288
|
+
*
|
|
289
|
+
* @returns {Promise<{buf: Buffer, degraded: boolean}>} degraded=true 走了 CDP 兜底
|
|
290
|
+
*/
|
|
291
|
+
export async function shotWithFallback(page, opts = {}) {
|
|
292
|
+
try {
|
|
293
|
+
return { buf: await page.screenshot({ ...opts, timeout: 15_000 }), degraded: false };
|
|
294
|
+
} catch (err) {
|
|
295
|
+
if (!/Timeout/i.test(String(err?.message))) throw err;
|
|
296
|
+
const cdp = await page.context().newCDPSession(page);
|
|
297
|
+
try {
|
|
298
|
+
const params = { format: 'png', captureBeyondViewport: !!opts.fullPage };
|
|
299
|
+
// CDP 的 clip:captureBeyondViewport 下是文档坐标,与我们 fullPage+clip 的语义一致
|
|
300
|
+
if (opts.clip) params.clip = { x: opts.clip.x, y: opts.clip.y, width: opts.clip.width, height: opts.clip.height, scale: 1 };
|
|
301
|
+
const { data } = await cdp.send('Page.captureScreenshot', params);
|
|
302
|
+
return { buf: Buffer.from(data, 'base64'), degraded: true };
|
|
303
|
+
} finally {
|
|
304
|
+
await cdp.detach().catch(() => {});
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* 超长页 fullPage → 滚动位置联络表(iss_mt0365b8):48000px 的整页压进 API 长边
|
|
311
|
+
* 上限后只剩 47px 宽,什么都看不见——工具自己承认不可读还是把废图塞给了模型。
|
|
312
|
+
* 超过阈值改成:按滚动位置取 5 帧视口,拼一张带位置标注的联络表(跟胶片条同一
|
|
313
|
+
* 套版式,横轴从时间换成滚动位置)。一次调用看清整页节奏;要某段的原尺寸用
|
|
314
|
+
* scrollTo 参数去截那一段。
|
|
315
|
+
*/
|
|
316
|
+
export const LONG_PAGE_LIMIT = 12_000;
|
|
317
|
+
|
|
318
|
+
export async function longPageSheet(page, { positions = [0, 0.22, 0.45, 0.68, 0.9] } = {}) {
|
|
319
|
+
const dims = await page.evaluate(() => ({
|
|
320
|
+
docH: Math.max(document.documentElement.scrollHeight, document.body?.scrollHeight || 0),
|
|
321
|
+
vpH: window.innerHeight, vpW: window.innerWidth, y0: window.scrollY,
|
|
322
|
+
}));
|
|
323
|
+
if (!(dims.docH > LONG_PAGE_LIMIT)) return null;
|
|
324
|
+
const shots = [];
|
|
325
|
+
for (const p of positions) {
|
|
326
|
+
const y = Math.round(p * (dims.docH - dims.vpH));
|
|
327
|
+
await page.evaluate((yy) => window.scrollTo(0, yy), y).catch(() => {});
|
|
328
|
+
await page.waitForTimeout(300); // 给懒加载/reveal 一点点时间,不求全到
|
|
329
|
+
const { buf } = await shotWithFallback(page, { type: 'png' });
|
|
330
|
+
shots.push({ buf, label: `${Math.round(p * 100)}% y=${y}` });
|
|
331
|
+
}
|
|
332
|
+
await page.evaluate((yy) => window.scrollTo(0, yy), dims.y0).catch(() => {});
|
|
333
|
+
|
|
334
|
+
const { default: sharp } = await import('sharp');
|
|
335
|
+
const { sheetLayout } = await import('./motion-lab.js');
|
|
336
|
+
const meta = await sharp(shots[0].buf).metadata();
|
|
337
|
+
const L = sheetLayout(shots.length, meta.width, meta.height);
|
|
338
|
+
const composites = []; const labels = [];
|
|
339
|
+
for (let i = 0; i < shots.length; i += 1) {
|
|
340
|
+
const col = i % L.cols; const row = Math.floor(i / L.cols);
|
|
341
|
+
const left = L.gap + col * (L.cellW + L.gap);
|
|
342
|
+
const top = L.gap + row * (L.cellH + L.gap);
|
|
343
|
+
composites.push({ input: await sharp(shots[i].buf).resize(L.cellW, L.cellH, { fit: 'fill' }).png().toBuffer(), left, top });
|
|
344
|
+
labels.push({ left, top, label: shots[i].label });
|
|
345
|
+
}
|
|
346
|
+
const fontH = Math.max(13, Math.round(L.cellH * 0.055));
|
|
347
|
+
const escXml = (t) => String(t).replace(/[<>&]/g, (c) => ({ '<': '<', '>': '>', '&': '&' }[c]));
|
|
348
|
+
const svg = [`<svg xmlns="http://www.w3.org/2000/svg" width="${L.sheetW}" height="${L.sheetH}">`,
|
|
349
|
+
...labels.map((l) => `<rect x="${l.left}" y="${l.top}" width="${(l.label.length * fontH * 0.62 + 12).toFixed(0)}" height="${fontH + 10}" fill="#000" fill-opacity="0.72"/>`
|
|
350
|
+
+ `<text x="${l.left + 6}" y="${l.top + fontH + 2}" font-size="${fontH}" fill="#fff" font-family="monospace">${escXml(l.label)}</text>`),
|
|
351
|
+
'</svg>'].join('');
|
|
352
|
+
const buf = await sharp({ create: { width: L.sheetW, height: L.sheetH, channels: 3, background: '#15150f' } })
|
|
353
|
+
.composite([...composites, { input: Buffer.from(svg), left: 0, top: 0 }]).png().toBuffer();
|
|
354
|
+
return {
|
|
355
|
+
buf,
|
|
356
|
+
mode: `long-page contact sheet (doc ${dims.docH}px, ${shots.length} viewports)`,
|
|
357
|
+
note: `整页高 ${dims.docH}px,fullPage 会压成不可读的细条 —— 已改为 ${shots.length} 帧滚动位置联络表(标注=滚动百分比与 y)。要某一段的原尺寸,用 scrollTo 参数(像素/百分比/选择器)单独截那一屏。`,
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* fullPage+clip 的"Clipped area is either empty or outside the resulting image"
|
|
363
|
+
* 兜底(iss_mszxp0zy):Chromium 整页光栅有高度上限(~16384px 纹理),元素落在
|
|
364
|
+
* 超高页更深处时文档坐标 clip 会落到成图之外。兜底=滚到元素处,改用**视口坐标**
|
|
365
|
+
* clip 截当前屏(clip 语义两套:fullPage 下文档坐标 / 非 fullPage 视口坐标,
|
|
366
|
+
* 本机探针 08-19 定过案)。
|
|
367
|
+
*/
|
|
368
|
+
export async function clipShotWithFallback(page, { clip, selector }) {
|
|
369
|
+
try {
|
|
370
|
+
return await shotWithFallback(page, { fullPage: true, clip, type: 'png' });
|
|
371
|
+
} catch (err) {
|
|
372
|
+
if (!/Clipped area/i.test(String(err?.message)) || !selector) throw err;
|
|
373
|
+
await page.locator(selector).first().scrollIntoViewIfNeeded({ timeout: 5000 }).catch(() => {});
|
|
374
|
+
const r = await page.evaluate((sel) => {
|
|
375
|
+
const el = document.querySelector(sel);
|
|
376
|
+
if (!el) return null;
|
|
377
|
+
const b = el.getBoundingClientRect();
|
|
378
|
+
return { x: b.x, y: b.y, width: b.width, height: b.height, vw: window.innerWidth, vh: window.innerHeight };
|
|
379
|
+
}, selector);
|
|
380
|
+
if (!r) throw err;
|
|
381
|
+
const vClip = {
|
|
382
|
+
x: Math.max(0, r.x), y: Math.max(0, r.y),
|
|
383
|
+
width: Math.max(1, Math.min(r.width, r.vw - Math.max(0, r.x))),
|
|
384
|
+
height: Math.max(1, Math.min(r.height, r.vh - Math.max(0, r.y))),
|
|
385
|
+
};
|
|
386
|
+
const out = await shotWithFallback(page, { clip: vClip, type: 'png' });
|
|
387
|
+
return { ...out, degraded: true };
|
|
388
|
+
}
|
|
389
|
+
}
|
|
@@ -152,8 +152,8 @@ async function listSiteStructure(target) {
|
|
|
152
152
|
return {
|
|
153
153
|
content: [{
|
|
154
154
|
type: 'text',
|
|
155
|
-
text:
|
|
156
|
-
+ (art?.root ? '
|
|
155
|
+
text: `这个站点(${art?.root ? `产物根 ${art.root}/` : '任务根'})还没有 html 页面。`
|
|
156
|
+
+ (art?.root ? '(源写完了的话,先跑构建让产物落进去;构建后确认产物真的落在这个目录。)' : ''),
|
|
157
157
|
}],
|
|
158
158
|
};
|
|
159
159
|
}
|
|
@@ -186,6 +186,9 @@ async function listSiteStructure(target) {
|
|
|
186
186
|
});
|
|
187
187
|
}
|
|
188
188
|
|
|
189
|
+
// 输出统一成**工作区相对**路径:screenshot_canvas / artifact_open 收的就是这种。
|
|
190
|
+
// 以前 file 是产物根相对(index.html),agent 原样回喂必然 not found(08-24 案)
|
|
191
|
+
const prefix = art?.root ? `${art.root}/` : '';
|
|
189
192
|
const known = new Set(pages);
|
|
190
193
|
const broken = [];
|
|
191
194
|
for (const p of out) {
|
|
@@ -199,7 +202,7 @@ async function listSiteStructure(target) {
|
|
|
199
202
|
if (seg === '..') stack.pop();
|
|
200
203
|
else stack.push(seg);
|
|
201
204
|
}
|
|
202
|
-
if (!known.has(stack.join('/'))) broken.push(`${p.file} → ${href}`);
|
|
205
|
+
if (!known.has(stack.join('/'))) broken.push(`${prefix}${p.file} → ${href}`);
|
|
203
206
|
}
|
|
204
207
|
}
|
|
205
208
|
const brokenNote = broken.length
|
|
@@ -212,11 +215,14 @@ async function listSiteStructure(target) {
|
|
|
212
215
|
? `\n\n同任务的其他产物(平等,各自寻址):${siblings.map(a => a.entryRel).join(' / ')}`
|
|
213
216
|
: '';
|
|
214
217
|
|
|
218
|
+
const outWs = prefix ? out.map(p => ({ ...p, file: prefix + p.file })) : out;
|
|
219
|
+
|
|
215
220
|
return {
|
|
216
221
|
content: [{
|
|
217
222
|
type: 'text',
|
|
218
|
-
text:
|
|
219
|
-
+
|
|
223
|
+
text: `站点${art?.title ? ` ${art.title}` : ''} · ${out.length} 个页面(入口 ${prefix}${pages[0]})${rootNote}`
|
|
224
|
+
+ ',file 是工作区相对路径,可直接传给 screenshot_canvas / artifact_open:\n\n'
|
|
225
|
+
+ `${JSON.stringify(outWs, null, 2)}${brokenNote}${siblingNote}`,
|
|
220
226
|
}],
|
|
221
227
|
};
|
|
222
228
|
}
|
|
@@ -16,6 +16,8 @@
|
|
|
16
16
|
import { tool } from '@anthropic-ai/claude-agent-sdk';
|
|
17
17
|
import { z } from 'zod';
|
|
18
18
|
import { moveEntry, MoveError } from '../../../projects/move-entry.js';
|
|
19
|
+
import { rewriteWorkspaceRefs } from '../../../lib/rewrite-refs.js';
|
|
20
|
+
import { getSharedDir } from '../../../projects/workspace.js';
|
|
19
21
|
import { Events } from '../../agent/events.js';
|
|
20
22
|
|
|
21
23
|
export function makeOrganizeBoardTool({ projectId, ctx }) {
|
|
@@ -26,20 +28,29 @@ export function makeOrganizeBoardTool({ projectId, ctx }) {
|
|
|
26
28
|
Use for: grouping generated images into a folder, collecting a site's materials into <site>/assets/, un-cluttering the desktop root. Sticky notes (notes/*.md) may move too, but they become plain .md file cards outside notes/ — lose the flippable sticky form.
|
|
27
29
|
Not for: site roots as destination (they are artifacts, not storage — a site takes materials in its assets/ subfolder).
|
|
28
30
|
|
|
31
|
+
After moving, references across the workspace (src/href in html/css/md pointing
|
|
32
|
+
at the moved items) are rewritten to the new paths automatically — the count of
|
|
33
|
+
rewritten spots is reported per file so you can verify. Pass rewrite_refs:false
|
|
34
|
+
to move only.
|
|
35
|
+
|
|
29
36
|
Batch: up to 16 items, moved in order, stops at first failure.`,
|
|
30
37
|
{
|
|
31
38
|
items: z.array(z.string().min(1)).min(1).max(16)
|
|
32
39
|
.describe('Workspace-relative paths to move (files or folders), e.g. ["assets/generated/a.png", "旧稿.html"]'),
|
|
33
40
|
into: z.string()
|
|
34
41
|
.describe('Destination folder (workspace-relative), e.g. "素材" or "观察日志/assets". Created if missing. "" = workspace root (un-nest).'),
|
|
42
|
+
rewrite_refs: z.boolean().optional()
|
|
43
|
+
.describe('Also rewrite references to the moved items across workspace text files (default true).'),
|
|
35
44
|
},
|
|
36
|
-
async ({ items, into }) => {
|
|
45
|
+
async ({ items, into, rewrite_refs: rewriteRefs }) => {
|
|
37
46
|
const lines = [];
|
|
47
|
+
const moves = [];
|
|
38
48
|
let moved = 0;
|
|
39
49
|
for (const item of items) {
|
|
40
50
|
try {
|
|
41
51
|
const out = await moveEntry(projectId, item, into, { createFolder: true });
|
|
42
52
|
moved += 1;
|
|
53
|
+
if (out.moved) moves.push({ from: out.from, to: out.to });
|
|
43
54
|
lines.push(out.moved ? `✓ ${out.from} → ${out.to}` : `· ${out.from}(已在原地)`);
|
|
44
55
|
if (out.moved) {
|
|
45
56
|
try {
|
|
@@ -55,6 +66,20 @@ Batch: up to 16 items, moved in order, stops at first failure.`,
|
|
|
55
66
|
break;
|
|
56
67
|
}
|
|
57
68
|
}
|
|
69
|
+
// 「新建文件夹然后改个索引」的后半件(iss_mt38uih6):把全工作区指向
|
|
70
|
+
// 被搬条目的引用改到新路径。改了多少处逐文件上报 —— 自动改写用户内容
|
|
71
|
+
// 必须可核对;报 0 也说一声,agent 不用再自己 grep。
|
|
72
|
+
if (moves.length > 0 && rewriteRefs !== false) {
|
|
73
|
+
try {
|
|
74
|
+
const rw = await rewriteWorkspaceRefs(getSharedDir(projectId), moves);
|
|
75
|
+
lines.push(rw.hits > 0
|
|
76
|
+
? `引用改写:${rw.hits} 处 / ${rw.files} 个文件`
|
|
77
|
+
: '引用改写:全工作区没有指向这些条目的文本引用(0 处)');
|
|
78
|
+
lines.push(...rw.lines.slice(0, 12));
|
|
79
|
+
} catch (err) {
|
|
80
|
+
lines.push(`⚠️ 引用改写失败(文件已搬成,引用要自己 grep 修):${err?.message || err}`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
58
83
|
try {
|
|
59
84
|
if (moved > 0) ctx?.emit?.({ type: 'board.updated', sessionId: null, summary: `归纳了 ${moved} 件到 ${into || '桌面根'}` });
|
|
60
85
|
} catch { /* fail-soft */ }
|
|
@@ -8,7 +8,6 @@
|
|
|
8
8
|
* - 普通文件(图片 / 便签 / 数据):路径本身,`assets/generated/a.webp`
|
|
9
9
|
* - deck:`deck:<路径>`,如 `deck:稿件/主稿.html`
|
|
10
10
|
* - 站点:`site:<目录>`
|
|
11
|
-
* - 项目文档:`doc:_root`(记忆)/ `doc:brand`(品牌档案)
|
|
12
11
|
*
|
|
13
12
|
* ## 2026-08-13:这个工具的职权范围缩小了
|
|
14
13
|
*
|
|
@@ -30,16 +29,11 @@ import { tool } from '@anthropic-ai/claude-agent-sdk';
|
|
|
30
29
|
import { z } from 'zod';
|
|
31
30
|
import path from 'path';
|
|
32
31
|
import { promises as fs } from 'fs';
|
|
33
|
-
import { pinToZone } from '../../../projects/board-store.js';
|
|
32
|
+
import { pinToZone, readBoard } from '../../../projects/board-store.js';
|
|
33
|
+
import { layerOf } from '../../../lib/canvas-id.js';
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
if (objectId.startsWith('doc:')) return '';
|
|
38
|
-
const c = objectId.indexOf(':');
|
|
39
|
-
const p = (c > 0 && /^[a-z]+$/.test(objectId.slice(0, c))) ? objectId.slice(c + 1) : objectId;
|
|
40
|
-
const i = p.lastIndexOf('/');
|
|
41
|
-
return i > 0 ? p.slice(0, i) : '';
|
|
42
|
-
}
|
|
35
|
+
// (folderOfObjectId 08-24 拆除:它按 dirname 硬算,会给 assets/generated 这类
|
|
36
|
+
// 前端不当层渲染的路径发"层"标签 —— 改问 layerOf + board.zones,跟渲染同口径)
|
|
43
37
|
|
|
44
38
|
/**
|
|
45
39
|
* @param {object} deps
|
|
@@ -55,7 +49,7 @@ folder it lives in. The canvas is their desktop: whatever you write appears
|
|
|
55
49
|
there automatically — you do NOT need this tool for your own outputs.
|
|
56
50
|
Use it only to deliberately surface something:
|
|
57
51
|
|
|
58
|
-
- Pull a reference (an uploaded asset,
|
|
52
|
+
- Pull a reference (an uploaded asset, a memory note, an older image) into view
|
|
59
53
|
- Restore something the user dragged off-screen, when they ask for it back
|
|
60
54
|
|
|
61
55
|
This does NOT change which folder the item belongs to — that is decided by
|
|
@@ -64,8 +58,7 @@ where the file is on disk. To move it, \`mv\` the file; the canvas follows.
|
|
|
64
58
|
Paths are workspace-relative, exactly as they are on disk. Accepted forms:
|
|
65
59
|
- any file path: 'assets/generated/hero.webp', 'notes/灵感.md', '稿件/数据.csv'
|
|
66
60
|
- a deck: 'deck:<path>.html' a site: 'site:<dir>'
|
|
67
|
-
(a bare '<path>.html' is read as a deck)
|
|
68
|
-
- '.claude/agent-memory/memory.md' (project memory) / '.../brand/memory.md' (brand doc)`,
|
|
61
|
+
(a bare '<path>.html' is read as a deck)`,
|
|
69
62
|
{
|
|
70
63
|
path: z
|
|
71
64
|
.string()
|
|
@@ -84,9 +77,7 @@ Paths are workspace-relative, exactly as they are on disk. Accepted forms:
|
|
|
84
77
|
if (!objectId || objectId.includes('..')) {
|
|
85
78
|
return { content: [{ type: 'text', text: 'Invalid path.' }], isError: true };
|
|
86
79
|
}
|
|
87
|
-
if (
|
|
88
|
-
else if (objectId.endsWith('agent-memory/memory.md')) objectId = 'doc:_root';
|
|
89
|
-
else if (!/^(deck|site|doc):/.test(objectId)) {
|
|
80
|
+
if (!/^(deck|site):/.test(objectId)) {
|
|
90
81
|
// 裸路径:.html 是一份 deck,其余(图片 / 便签 / 数据文件)id 就是路径。
|
|
91
82
|
// 站点的目录要显式写 `site:` —— 光看路径分不出
|
|
92
83
|
// "一个收纳文件夹"和"一件目录型产物",猜错了钉上去的是个不存在的 id。
|
|
@@ -102,7 +93,25 @@ Paths are workspace-relative, exactly as they are on disk. Accepted forms:
|
|
|
102
93
|
if (/\.html?$/i.test(objectId)) objectId = `deck:${objectId}`;
|
|
103
94
|
}
|
|
104
95
|
|
|
105
|
-
|
|
96
|
+
// 可见性预检(08-21 案:钉 assets/ 深处的图报成功,用户画布上根本没有)。
|
|
97
|
+
// 画布扫描只认 assets 顶层 / assets/generated / assets/notes 这三个口,
|
|
98
|
+
// 更深的(references/web/… 之类)有座位也不渲染 —— 钉了等于没钉。
|
|
99
|
+
if (/^assets\//.test(objectId)
|
|
100
|
+
&& !/^assets\/[^/]+$/.test(objectId)
|
|
101
|
+
&& !/^assets\/(generated|notes)\/[^/]+$/.test(objectId)) {
|
|
102
|
+
return {
|
|
103
|
+
content: [{
|
|
104
|
+
type: 'text',
|
|
105
|
+
text: `${objectId} 在画布扫描范围之外(assets/ 只有顶层、generated/、notes/ 三个口上墙),pin 了用户也看不到。`
|
|
106
|
+
+ '先把文件 cp 到工作区根或某个文件夹里再 pin(用户要看的素材放看得见的地方)。',
|
|
107
|
+
}],
|
|
108
|
+
isError: true,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
// 层归属按**真实存在的层**算('' 或 zones 里的文件夹)——跟前端渲染同口径。
|
|
112
|
+
// dirname 硬算的老写法会把 assets/generated 当层写进 zone,arrange 就此拒摆
|
|
113
|
+
const boardNow = await readBoard(projectId);
|
|
114
|
+
const zoneId = layerOf(objectId, null, new Set(Object.keys(boardNow.zones || {})));
|
|
106
115
|
const { zone: placedZone, placed } = await pinToZone(projectId, { objectId, zoneId });
|
|
107
116
|
|
|
108
117
|
try {
|
|
@@ -22,11 +22,13 @@ import { groupObjects, asciiMinimap, bboxOfRects, relationOf, columnsOf, viewpor
|
|
|
22
22
|
import { getViewpoint } from '../../../projects/viewpoint-store.js';
|
|
23
23
|
import { chalkExcerpts, CHALK_DIR } from '../../../lib/chalk.js';
|
|
24
24
|
import { getSharedDir } from '../../../projects/workspace.js';
|
|
25
|
+
import { promises as fs } from 'node:fs';
|
|
26
|
+
import path from 'node:path';
|
|
25
27
|
|
|
26
28
|
/** 同一"行"的 y 容差:入座算法一行内顶对齐,40 世界像素内视作同行 */
|
|
27
29
|
const ROW_TOLERANCE = 40;
|
|
28
30
|
|
|
29
|
-
function describeEntry(board, id, entry, glyph = null, excerpts = null) {
|
|
31
|
+
function describeEntry(board, id, entry, glyph = null, excerpts = null, staleIds = null) {
|
|
30
32
|
const sz = estimateSizeOn(board, id, entry);
|
|
31
33
|
const at = `@(${Math.round(entry.x)},${Math.round(entry.y)}) ${Math.round(sz.w)}x${Math.round(sz.h)}`;
|
|
32
34
|
const g = glyph ? `[${glyph}] ` : '';
|
|
@@ -39,7 +41,9 @@ function describeEntry(board, id, entry, glyph = null, excerpts = null) {
|
|
|
39
41
|
return `- ${g}[${md}] 「${t}」 ${at} (id: ${id})${entry.by === 'agent' ? ' ·你写的' : ''}${flags}`;
|
|
40
42
|
}
|
|
41
43
|
if (entry.kind === 'scribble') return `- ${g}[涂鸦] ${at} (id: ${id})${entry.by === 'agent' ? ' ·你画的' : ''}${flags}`;
|
|
42
|
-
|
|
44
|
+
// 过期座位要明说(iss_mt38ucyq:旧路径条目被 agent 当"失效卡"差点建议删素材母版)
|
|
45
|
+
const stale = staleIds?.has(id) ? ' 〔⚠️磁盘上已无此路径 —— 多半被移动/改名了,以磁盘为准,别据此判失效或建议删除〕' : '';
|
|
46
|
+
return `- ${g}${id} ${at}${entry.by === 'agent' ? ' ·你摆的' : ''}${flags}${stale}`;
|
|
43
47
|
}
|
|
44
48
|
|
|
45
49
|
export function makeReadBoardTool({ projectId }) {
|
|
@@ -86,6 +90,16 @@ on the minimap and listed with what is inside it.`,
|
|
|
86
90
|
.filter(({ id }) => !id.startsWith(`${CHALK_DIR}/`) || excerpts.has(id))
|
|
87
91
|
.sort((a, b) => (a.entry.y - b.entry.y) || (a.entry.x - b.entry.x));
|
|
88
92
|
const entryOf = new Map(items.map(it => [it.id, it.entry]));
|
|
93
|
+
// 座位 vs 磁盘对账(iss_mt38ucyq):文件挪走后旧座位可能还挂几十秒
|
|
94
|
+
// (改名对账/前端回写有时差)。查一遍真身,过期的在条目上点名。
|
|
95
|
+
const root = getSharedDir(projectId);
|
|
96
|
+
const staleIds = new Set();
|
|
97
|
+
await Promise.all(items.map(async ({ id, entry }) => {
|
|
98
|
+
if (entry.kind === 'text' || entry.kind === 'scribble' || id === 'browse') return;
|
|
99
|
+
const bare = String(id).replace(/^(deck|site|docx):/, '');
|
|
100
|
+
if (!bare || bare.includes('..') || /^(text|scribble|b):/.test(bare)) return;
|
|
101
|
+
try { await fs.access(path.resolve(root, bare)); } catch { staleIds.add(id); }
|
|
102
|
+
}));
|
|
89
103
|
|
|
90
104
|
// 小地图(用户视口画框)
|
|
91
105
|
const vp = getViewpoint(projectId);
|
|
@@ -129,7 +143,7 @@ on the minimap and listed with what is inside it.`,
|
|
|
129
143
|
lines.push('', `组 ${i + 1}${tags ? ` ${tags}` : ''}(${g.members.length} 件 ${g.edges.length} 线${staging ? ',草稿' : ''}):`);
|
|
130
144
|
const sorted = g.members.map(id => ({ id, entry: entryOf.get(id) }))
|
|
131
145
|
.sort((a, b) => (a.entry.y - b.entry.y) || (a.entry.x - b.entry.x));
|
|
132
|
-
for (const { id, entry } of sorted) lines.push(describeEntry(board, id, entry, glyphOf.get(id), excerpts));
|
|
146
|
+
for (const { id, entry } of sorted) lines.push(describeEntry(board, id, entry, glyphOf.get(id), excerpts, staleIds));
|
|
133
147
|
for (const bid of g.edges.slice(0, 12)) lines.push(` ${bindingLine(board.bindings[bid], board)} (line id: ${bid})`);
|
|
134
148
|
if (g.edges.length > 12) lines.push(` …还有 ${g.edges.length - 12} 条线`);
|
|
135
149
|
});
|
|
@@ -147,7 +161,7 @@ on the minimap and listed with what is inside it.`,
|
|
|
147
161
|
rowY = entry.y;
|
|
148
162
|
lines.push(`— 行 y≈${Math.round(rowY)} —`);
|
|
149
163
|
}
|
|
150
|
-
lines.push(describeEntry(board, id, entry, glyphOf.get(id), excerpts));
|
|
164
|
+
lines.push(describeEntry(board, id, entry, glyphOf.get(id), excerpts, staleIds));
|
|
151
165
|
}
|
|
152
166
|
}
|
|
153
167
|
}
|
|
@@ -183,7 +197,7 @@ on the minimap and listed with what is inside it.`,
|
|
|
183
197
|
} catch { /* 关系读不到不挡座次 */ }
|
|
184
198
|
}
|
|
185
199
|
|
|
186
|
-
lines.push('', '
|
|
200
|
+
lines.push('', '(口径:稀疏表只列摆过的;层归属为服务端近似;尺寸为形态估算;带⚠️的条目=座位与磁盘对不上账)');
|
|
187
201
|
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
|
188
202
|
},
|
|
189
203
|
);
|
|
@@ -16,10 +16,26 @@
|
|
|
16
16
|
* 线不存坐标:端点是物件 id 或工作区 id,端点一移动线自己跟着走。
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
+
import path from 'node:path';
|
|
20
|
+
import fs from 'node:fs/promises';
|
|
19
21
|
import { tool } from '@anthropic-ai/claude-agent-sdk';
|
|
20
22
|
import { z } from 'zod';
|
|
21
|
-
import { patchBoard } from '../../../projects/board-store.js';
|
|
23
|
+
import { patchBoard, readBoard } from '../../../projects/board-store.js';
|
|
22
24
|
import { BINDING_TYPES, BINDING_TYPE_IDS, BINDING_MATERIALS } from '../../../lib/binding-types.js';
|
|
25
|
+
import { normalizeCanvasId } from '../../../lib/canvas-id.js';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 端点校验(08-23 案:不是 canvas id 的字符串照收,board.json 留悬空线)。
|
|
29
|
+
* 合法端点 = 板上有座位的 id,或者真实存在的工作区路径(带不带 kind 前缀都行)。
|
|
30
|
+
* write_on_board 的锚点早就这么查了,同一件事两个工具一个查一个不查是口径病。
|
|
31
|
+
*/
|
|
32
|
+
async function endpointExists(id, board, sharedRoot) {
|
|
33
|
+
if (board.objects?.[id]) return true;
|
|
34
|
+
if (/^doc:/.test(id)) return true; // 项目文档:固定名额
|
|
35
|
+
const bare = id.replace(/^(deck|site|docx|text|scribble):/, '');
|
|
36
|
+
if (!bare || bare.includes('..')) return false;
|
|
37
|
+
try { await fs.access(path.join(sharedRoot, bare)); return true; } catch { return false; }
|
|
38
|
+
}
|
|
23
39
|
|
|
24
40
|
/** 词汇表渲染成工具描述里的一段表,免得两处各写各的 */
|
|
25
41
|
const VOCAB = BINDING_TYPE_IDS
|
|
@@ -29,7 +45,7 @@ const VOCAB = BINDING_TYPE_IDS
|
|
|
29
45
|
let seq = 0;
|
|
30
46
|
const nextId = () => `b:a${Date.now().toString(36)}${(seq++ % 1000).toString(36)}`;
|
|
31
47
|
|
|
32
|
-
export function makeRelateOnBoardTool({ projectId, ctx }) {
|
|
48
|
+
export function makeRelateOnBoardTool({ sharedRoot, projectId, ctx }) {
|
|
33
49
|
return tool(
|
|
34
50
|
'relate_on_board',
|
|
35
51
|
`Draw a relationship line between two things on the workbench canvas.
|
|
@@ -76,14 +92,36 @@ where the files live.`,
|
|
|
76
92
|
isError: true,
|
|
77
93
|
};
|
|
78
94
|
}
|
|
95
|
+
// 端点先归一再校验:不存在的端点会在 board.json 里留一条悬空线,
|
|
96
|
+
// 画布上凭空多出从无到有的线(08-23 录 GIF 时真踩)
|
|
97
|
+
const nFrom = normalizeCanvasId(from);
|
|
98
|
+
const nTo = normalizeCanvasId(to);
|
|
99
|
+
if (!nFrom || !nTo) {
|
|
100
|
+
return { content: [{ type: 'text', text: `Bad endpoint id: ${!nFrom ? from : to}` }], isError: true };
|
|
101
|
+
}
|
|
102
|
+
const board = await readBoard(projectId);
|
|
103
|
+
for (const [raw, nid] of [[from, nFrom], [to, nTo]]) {
|
|
104
|
+
if (!(await endpointExists(nid, board, sharedRoot))) {
|
|
105
|
+
const seats = Object.keys(board.objects || {}).slice(0, 20);
|
|
106
|
+
return {
|
|
107
|
+
content: [{
|
|
108
|
+
type: 'text',
|
|
109
|
+
text: `端点 ${raw} 不在板上,也不是存在的工作区路径 —— 线没画。`
|
|
110
|
+
+ `(read_board 可看全部;现有座位举例:${seats.join(', ') || '无'})`,
|
|
111
|
+
}],
|
|
112
|
+
isError: true,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
from = nFrom; to = nTo;
|
|
79
117
|
const id = nextId();
|
|
80
118
|
// by:'agent' 不只是记录出处 —— 用户画的线 agent 不该擅自删,反过来也一样
|
|
81
119
|
const binding = { type, from, to, by: 'agent', ...(label ? { label } : {}), ...(material && material !== 'ink' ? { material } : {}) };
|
|
82
|
-
const
|
|
120
|
+
const patched = await patchBoard(projectId, { bindings: { [id]: binding } });
|
|
83
121
|
|
|
84
122
|
// 服务端会丢掉非法的线(未知 type / 自环)。**丢了要说**,否则 agent 以为
|
|
85
123
|
// 画上了,用户看不到,两边都不知道发生了什么。
|
|
86
|
-
if (!
|
|
124
|
+
if (!patched.bindings?.[id]) {
|
|
87
125
|
return {
|
|
88
126
|
content: [{ type: 'text', text: `Relationship rejected by the board (bad type or endpoints): ${type} ${from} -> ${to}` }],
|
|
89
127
|
isError: true,
|