@xiaobuyu/nodesign 0.0.2 → 0.0.3
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 +20 -0
- package/server/api/assets/notes.js +50 -0
- package/server/api/assets.js +6 -11
- package/server/api/board.js +78 -1
- package/server/engine/agent/events.js +1 -0
- package/server/engine/agent/hooks/user-prompt-submit.js +43 -0
- package/server/engine/agent/prompts/nodesign-prelude.md +53 -1
- package/server/engine/agent/session-loop.js +11 -0
- package/server/engine/mcp/index.js +12 -0
- package/server/engine/mcp/tools/arrange-on-board.js +4 -4
- package/server/engine/mcp/tools/create-on-board.js +9 -12
- package/server/engine/mcp/tools/edit-sketch.js +167 -0
- package/server/engine/mcp/tools/look-at-board.js +124 -0
- package/server/engine/mcp/tools/read-board.js +121 -34
- package/server/engine/mcp/tools/read-user-view.js +59 -0
- package/server/engine/mcp/tools/relate-on-board.js +5 -3
- package/server/engine/mcp/tools/sketch-on-board.js +337 -0
- package/server/engine/mcp/tools/write-on-board.js +130 -0
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/SKILL.md +7 -0
- package/server/engine/plugins/nodesign/skills/docx-craft/SKILL.md +7 -0
- package/server/engine/plugins/nodesign/skills/rp-craft/SKILL.md +7 -0
- package/server/engine/plugins/nodesign/skills/site-craft/SKILL.md +7 -0
- package/server/engine/runs/board-tasklist.js +130 -0
- package/server/index.js +3 -0
- package/server/lib/binding-types.js +12 -0
- package/server/lib/board-graph-export.js +191 -0
- package/server/lib/board-groups.js +167 -0
- package/server/lib/board-hero.js +76 -0
- package/server/lib/board-kind-sizes.js +23 -0
- package/server/lib/board-relations.js +1 -1
- package/server/lib/chalk.js +121 -0
- package/server/lib/sketch-layout.js +277 -0
- package/server/projects/board-limits.js +12 -0
- package/server/projects/board-sanitize.js +252 -0
- package/server/projects/board-store.js +72 -205
- package/server/projects/ui-config.js +6 -0
- package/server/projects/viewpoint-store.js +78 -0
- package/server/ws/broker.js +3 -0
- package/web/dist/assets/{BrowserWindow-BYCgizHw.js → BrowserWindow-P_vIdh-G.js} +2 -2
- package/web/dist/assets/{DeckWindow-pNydEM4L.js → DeckWindow-BpLv5082.js} +3 -3
- package/web/dist/assets/{DocxWindow-PPgzh3Gq.js → DocxWindow-DSAU1-tD.js} +4 -4
- package/web/dist/assets/MermaidBlock-DR1GikCv.js +313 -0
- package/web/dist/assets/SiteWindow-92uBuRRR.js +18 -0
- package/web/dist/assets/abnfDiagram-VCTEODGH-uA8Esix4.js +1 -0
- package/web/dist/assets/arc-D_khH_xN.js +1 -0
- package/web/dist/assets/architectureDiagram-5GKGNRK7-DydJHqOt.js +36 -0
- package/web/dist/assets/blockDiagram-NRAW4CY4-B7wBbKiW.js +129 -0
- package/web/dist/assets/c4Diagram-UCG6FXSJ-6mrH0x7Y.js +38 -0
- package/web/dist/assets/channel-DGkccMO1.js +1 -0
- package/web/dist/assets/chunk-2Q5K7J3B-Cuikho5Q.js +1 -0
- package/web/dist/assets/chunk-5VM5RSS4-3-KZ5AUz.js +15 -0
- package/web/dist/assets/chunk-F27PBJKO-Bkwq2gGU.js +1 -0
- package/web/dist/assets/chunk-G27WJ6UU-BLyQzdGE.js +231 -0
- package/web/dist/assets/chunk-JWPE2WC7-B7HxxQds.js +1 -0
- package/web/dist/assets/chunk-LCL6LL3I-CtXrnUwF.js +206 -0
- package/web/dist/assets/chunk-POPQ4Y6H-dxddZlXt.js +1 -0
- package/web/dist/assets/chunk-SVP7TREG-DpkmrgHn.js +88 -0
- package/web/dist/assets/chunk-XXDRQBXY-BwZ7RbpQ.js +1 -0
- package/web/dist/assets/classDiagram-DTDB5LWJ-BGscw3OD.js +1 -0
- package/web/dist/assets/classDiagram-v2-JRS7N3AN-BGscw3OD.js +1 -0
- package/web/dist/assets/cose-bilkent-JH36ORCC-DGFOsMUt.js +1 -0
- package/web/dist/assets/cynefin-OW5HDTMX-DUwn3ehS.js +178 -0
- package/web/dist/assets/cynefinDiagram-5FMLGOSQ-DlLAOZ9x.js +62 -0
- package/web/dist/assets/cytoscape.esm-BjAbmLdb.js +331 -0
- package/web/dist/assets/dagre-3AP2YEHR-CyourLfz.js +4 -0
- package/web/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
- package/web/dist/assets/diagram-S7CK7UJ4-BTMbx2np.js +30 -0
- package/web/dist/assets/diagram-UQ7AKVKN-UK94_1-P.js +41 -0
- package/web/dist/assets/diagram-VSXAHHWV-C7zS-v9v.js +3 -0
- package/web/dist/assets/diagram-VX7I27RA-DKJRa6CK.js +24 -0
- package/web/dist/assets/diagram-Z3DM3KII-kUbAfGAp.js +24 -0
- package/web/dist/assets/ebnfDiagram-PWID7BFC-CVW16_vb.js +1 -0
- package/web/dist/assets/erDiagram-SSCWMZ5O-BRQMFQ5y.js +99 -0
- package/web/dist/assets/flowDiagram-A5DVABFB-S34LpLtK.js +168 -0
- package/web/dist/assets/ganttDiagram-EL5Y4UJY-BFiqk1-G.js +292 -0
- package/web/dist/assets/gitGraphDiagram-WWUBYQGX-LZsrdyyv.js +106 -0
- package/web/dist/assets/index-GAamh1p4.js +2152 -0
- package/web/dist/assets/infoDiagram-RXCK75RN-0DvN6KIp.js +2 -0
- package/web/dist/assets/init-Gi6I4Gst.js +1 -0
- package/web/dist/assets/ishikawaDiagram-5VMMS53U-CaY3A-Bx.js +70 -0
- package/web/dist/assets/journeyDiagram-EYS64GPL-BBeiaCrj.js +139 -0
- package/web/dist/assets/kanban-definition-3QL26DDD-ux42DRXQ.js +89 -0
- package/web/dist/assets/layout-CnXiF_NB.js +1 -0
- package/web/dist/assets/linear-BMW0u6G6.js +1 -0
- package/web/dist/assets/mindmap-definition-FBJOCRG2-hUxSpdKn.js +96 -0
- package/web/dist/assets/ordinal-Cboi1Yqb.js +1 -0
- package/web/dist/assets/pegDiagram-XKGWAZYB-aCaQPH0w.js +1 -0
- package/web/dist/assets/{pending-edit-apply-C0I13CbD.js → pending-edit-apply-AcdOQ9Ij.js} +6 -6
- package/web/dist/assets/pieDiagram-E7YTZNPT-CmOEZ8zg.js +39 -0
- package/web/dist/assets/quadrantDiagram-AXDQQJYC-VW6fAhsV.js +7 -0
- package/web/dist/assets/railroadDiagram-O6MQD6OU-CKJ7Ba5K.js +1 -0
- package/web/dist/assets/requirementDiagram-EFPCY7ZU-pNGkq9Vw.js +84 -0
- package/web/dist/assets/{rotate-cw-B1UQ8Hld.js → rotate-cw-KAfChbGV.js} +1 -1
- package/web/dist/assets/sankeyDiagram-P5KCCOFB-BIMmDTfZ.js +40 -0
- package/web/dist/assets/sequenceDiagram-WJ2MYXX4-BBW7wzKY.js +162 -0
- package/web/dist/assets/sizeCapture-X5ZJPWSS-DdkXmLYR.js +1 -0
- package/web/dist/assets/{square-dashed-mouse-pointer-8lrgqmfv.js → square-dashed-mouse-pointer-CDUlhUP7.js} +1 -1
- package/web/dist/assets/stateDiagram-HBIQ2CUA-W313Uz16.js +1 -0
- package/web/dist/assets/stateDiagram-v2-4QOOHH4V-DDhJBZzf.js +1 -0
- package/web/dist/assets/swimlanes-XN3QIQJK-C29TL26K.js +1 -0
- package/web/dist/assets/swimlanesDiagram-VK2B7HYN-BfN-Xdmu.js +8 -0
- package/web/dist/assets/timeline-definition-24CTP7MA-CF0JMObX.js +120 -0
- package/web/dist/assets/vennDiagram-4TSXK5OY-DrDFsaH3.js +34 -0
- package/web/dist/assets/wardleyDiagram-VM6X3IG4-CVrnRIS_.js +78 -0
- package/web/dist/assets/xychartDiagram-S5SC5T6Z-BvkN1zsT.js +7 -0
- package/web/dist/index.html +1 -1
- package/web/dist/assets/SiteWindow-DM0LQPCO.js +0 -18
- package/web/dist/assets/index-BHTBJlpY.js +0 -2125
|
@@ -20,16 +20,16 @@
|
|
|
20
20
|
import { promises as fs } from 'fs';
|
|
21
21
|
import path from 'path';
|
|
22
22
|
import { getSharedDir, ensureProjectWorkspace, gitRenamesSince } from './workspace.js';
|
|
23
|
-
import {
|
|
23
|
+
import { CHALK_DIR } from '../lib/chalk.js';
|
|
24
24
|
|
|
25
|
-
export
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
export
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
25
|
+
export {
|
|
26
|
+
DEFAULT_BOARD_SIZE, MAX_BOARD_BYTES, MAX_OBJECTS, MAX_ZONES, MAX_BINDINGS,
|
|
27
|
+
} from './board-limits.js';
|
|
28
|
+
export { TEXT_FONTS } from './board-sanitize.js';
|
|
29
|
+
import { DEFAULT_BOARD_SIZE, MAX_BOARD_BYTES, MAX_OBJECTS, MAX_ZONES, MAX_BINDINGS } from './board-limits.js';
|
|
30
|
+
import {
|
|
31
|
+
clampNum, sanitizeSize, sanitizeTag, sanitizeObject, sanitizeBinding, sanitizeZone, sanitizeBoard, isSafeCanvasId,
|
|
32
|
+
} from './board-sanitize.js';
|
|
33
33
|
|
|
34
34
|
// 分区自动铺位常数 —— 与前端 BoardCanvas 的 ZONE_* 保持一致(数值约定,非共享代码)
|
|
35
35
|
// ZONE_DEFAULTS 已删(#14):它是"文件夹=版面上一整条带"时代的默认矩形,
|
|
@@ -48,200 +48,6 @@ function withBoardLock(pid, fn) {
|
|
|
48
48
|
return run;
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
function clampNum(v, min, max, fallback) {
|
|
52
|
-
const n = Number(v);
|
|
53
|
-
if (!Number.isFinite(n)) return fallback;
|
|
54
|
-
return Math.min(max, Math.max(min, Math.round(n)));
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function sanitizeSize(raw) {
|
|
58
|
-
return {
|
|
59
|
-
w: clampNum(raw?.w, 1000, 20000, DEFAULT_BOARD_SIZE.w),
|
|
60
|
-
h: clampNum(raw?.h, 800, 20000, DEFAULT_BOARD_SIZE.h),
|
|
61
|
-
};
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* **画布原生**物件的形态白名单。
|
|
66
|
-
*
|
|
67
|
-
* 绝大多数画布物件是磁盘产物的影子:board.json 只存它摆在哪,本体是文件
|
|
68
|
-
* (所以 agent 读得到、能进上下文、删文件即消失)。涂鸦不一样 —— 它没有
|
|
69
|
-
* 有意义的文件形态,board.json 就是它的**本体**。这类物件必须显式登记,
|
|
70
|
-
* 否则任何人往 objects 里塞一个 kind 就能造出一个不受形态表管的东西。
|
|
71
|
-
*
|
|
72
|
-
* 2026-08-08 加进 `text`。在那之前画布上打的字一律落成 `.md` 便签,理由是
|
|
73
|
-
* "agent 读得到"。但用户要的是**白板**:在工程文件旁边随手写一句、画一笔,
|
|
74
|
-
* 那是给自己的记号,不是给 agent 的输入。想让 agent 看见的写便利贴 ——
|
|
75
|
-
* 那条路还在,挪到了右键菜单里。
|
|
76
|
-
*/
|
|
77
|
-
const CANVAS_NATIVE_KINDS = new Set(['scribble', 'text']);
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* 坐标夹持上限。画布 2026-08-13 起全向无限,坐标不再由 board.size 夹住 ——
|
|
81
|
-
* 这个数只挡非有限值和纯属事故的数字(±100 万世界像素之外没有正常操作能到)。
|
|
82
|
-
*/
|
|
83
|
-
const COORD_LIMIT = 1e6;
|
|
84
|
-
|
|
85
|
-
/** 涂鸦路径串上限。一条随手画的线约 300~800 字符,8000 够长且撑不爆 board.json */
|
|
86
|
-
const MAX_SCRIBBLE_PATH = 8000;
|
|
87
|
-
/**
|
|
88
|
-
* 画布文字的字数上限。
|
|
89
|
-
*
|
|
90
|
-
* 它是"写在白板上的一句话",不是文档 —— 长东西该写成 .md(那是便利贴,
|
|
91
|
-
* agent 读得到)。2000 字够写一段说明,也撑不爆 board.json。
|
|
92
|
-
*/
|
|
93
|
-
const MAX_TEXT_LEN = 2000;
|
|
94
|
-
|
|
95
|
-
/** 画布文字可选的字体。**白名单而不是自由字符串** —— 这个值会进 CSS */
|
|
96
|
-
export const TEXT_FONTS = ['pen', 'kai', 'sans', 'serif', 'mono'];
|
|
97
|
-
const TEXT_SIZES = ['sm', 'md', 'lg', 'xl'];
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* 变换字段(2026-08-13,选中态控制器)。缺省不落字段 —— 没转过没缩过的物件
|
|
101
|
-
* 别背两个恒等值,board.json 的 diff 要能一眼看出"谁被动过"。
|
|
102
|
-
*/
|
|
103
|
-
function sanitizeTransform(data) {
|
|
104
|
-
const out = {};
|
|
105
|
-
const rot = Number(data?.rotation);
|
|
106
|
-
if (Number.isFinite(rot) && rot !== 0) out.rotation = clampNum(rot, -360, 360, 0);
|
|
107
|
-
const sc = Number(data?.scale);
|
|
108
|
-
if (Number.isFinite(sc) && sc !== 1) out.scale = clampNum(sc, 0.2, 10, 1);
|
|
109
|
-
return out;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
function sanitizeCanvasData(kind, data) {
|
|
113
|
-
if (kind === 'text') {
|
|
114
|
-
const t = typeof data?.t === 'string' ? data.t.slice(0, MAX_TEXT_LEN) : '';
|
|
115
|
-
if (!t.trim()) return null;
|
|
116
|
-
return {
|
|
117
|
-
t,
|
|
118
|
-
font: TEXT_FONTS.includes(data?.font) ? data.font : 'kai',
|
|
119
|
-
size: TEXT_SIZES.includes(data?.size) ? data.size : 'md',
|
|
120
|
-
color: ['ink', 'red', 'pencil', 'brass'].includes(data?.color) ? data.color : 'ink',
|
|
121
|
-
...sanitizeTransform(data),
|
|
122
|
-
};
|
|
123
|
-
}
|
|
124
|
-
if (kind !== 'scribble') return null;
|
|
125
|
-
const d = typeof data?.d === 'string' ? data.d.slice(0, MAX_SCRIBBLE_PATH) : '';
|
|
126
|
-
// 只收 SVG path 里合法的那几个字符,挡住任何往 DOM 里塞东西的尝试
|
|
127
|
-
if (!d || !/^[\dMLQCZ ,.\-eE]+$/.test(d)) return null;
|
|
128
|
-
return {
|
|
129
|
-
d,
|
|
130
|
-
color: ['ink', 'red', 'pencil', 'brass'].includes(data.color) ? data.color : 'ink',
|
|
131
|
-
width: clampNum(data.width, 1, 24, 2),
|
|
132
|
-
...sanitizeTransform(data),
|
|
133
|
-
};
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
function sanitizeObject(o, size) {
|
|
137
|
-
if (!o || typeof o !== 'object') return null;
|
|
138
|
-
const kind = typeof o.kind === 'string' && CANVAS_NATIVE_KINDS.has(o.kind) ? o.kind : null;
|
|
139
|
-
const data = kind ? sanitizeCanvasData(kind, o.data) : null;
|
|
140
|
-
// 登记了 kind 却给不出合法内容 → 整条丢弃。留一个空壳会在画布上变成
|
|
141
|
-
// 一个看不见也删不掉的幽灵物件。
|
|
142
|
-
if (kind && !data) return null;
|
|
143
|
-
return {
|
|
144
|
-
// 画布 2026-08-13 起全向无限:坐标不再被桌面尺寸夹持(原来非 native 只许
|
|
145
|
-
// 正区间、native 也出不了 ±size)。这里只挡非有限值和纯属事故的数字 ——
|
|
146
|
-
// 夹得再紧一点都意味着"用户摆在那儿的东西刷新后跳走",那是静默数据损坏。
|
|
147
|
-
x: clampNum(o.x, -COORD_LIMIT, COORD_LIMIT, 0),
|
|
148
|
-
y: clampNum(o.y, -COORD_LIMIT, COORD_LIMIT, 0),
|
|
149
|
-
z: clampNum(o.z, 0, 1e6, 0),
|
|
150
|
-
...(Number.isFinite(Number(o.w)) ? { w: clampNum(o.w, 4, COORD_LIMIT, 200) } : {}),
|
|
151
|
-
...(Number.isFinite(Number(o.h)) ? { h: clampNum(o.h, 4, COORD_LIMIT, 200) } : {}),
|
|
152
|
-
...(o.expanded ? { expanded: true } : {}),
|
|
153
|
-
// 显式归属:'' = 明确无归属(覆盖 sid 派生),非空 = 所属工作区 id
|
|
154
|
-
...(typeof o.zone === 'string' && o.zone.length <= 300 ? { zone: o.zone } : {}),
|
|
155
|
-
// 出处(2026-08-14 agent 摆位/建元素):agent 落的座/写的字标出来,
|
|
156
|
-
// 用户的是默认不标 —— 跟关系线的 by 同一条纪律
|
|
157
|
-
...(o.by === 'agent' ? { by: 'agent' } : {}),
|
|
158
|
-
...(kind ? { kind, data } : {}),
|
|
159
|
-
};
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
/**
|
|
163
|
-
* 关系线。**不存坐标** —— 端点是 object id 或 zone id,线跟着端点走。
|
|
164
|
-
*
|
|
165
|
-
* 词汇表在 `server/lib/binding-types.js`(前端画线那份视觉映射要跟它对齐,
|
|
166
|
-
* 有 parity 断言看着)。不认识的 type 一律丢弃:宁可少画一条线,也不要在
|
|
167
|
-
* 画布上留一条没人知道什么意思的连线。
|
|
168
|
-
*
|
|
169
|
-
* 自环(from === to)也丢:它画不出来,且多半是 agent 传错了 id。
|
|
170
|
-
*/
|
|
171
|
-
function sanitizeBinding(b) {
|
|
172
|
-
if (!b || typeof b !== 'object') return null;
|
|
173
|
-
if (!isBindingType(b.type)) return null;
|
|
174
|
-
const from = typeof b.from === 'string' ? b.from.slice(0, 300) : '';
|
|
175
|
-
const to = typeof b.to === 'string' ? b.to.slice(0, 300) : '';
|
|
176
|
-
if (!from || !to || from === to) return null;
|
|
177
|
-
return {
|
|
178
|
-
type: b.type,
|
|
179
|
-
from,
|
|
180
|
-
to,
|
|
181
|
-
// 线上的字。没写就渲染时回落到词汇表的默认词,不在这里补 —— 存了默认词
|
|
182
|
-
// 之后改词汇表就改不动存量了。
|
|
183
|
-
...(typeof b.label === 'string' && b.label.trim()
|
|
184
|
-
? { label: b.label.trim().slice(0, 60) }
|
|
185
|
-
: {}),
|
|
186
|
-
// 谁画的。用户画的线 agent 不该擅自删,反过来也一样;auto = 机器可证的
|
|
187
|
-
// 引用关系(auto-relations.js 对账层专属,只有它增删自家 b:auto:* 条目)。
|
|
188
|
-
...(b.by === 'agent' || b.by === 'user' || b.by === 'auto' ? { by: b.by } : {}),
|
|
189
|
-
};
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
/**
|
|
193
|
-
* zones 一行只剩坐标(2026-08-13 瘦身,#14)。
|
|
194
|
-
*
|
|
195
|
-
* 逐字段的下场:
|
|
196
|
-
* - `w`/`h` —— 文件夹变方卡后前端视图**强制** FOLDER_CARD 尺寸,存的数字
|
|
197
|
-
* 没人读,还会成为"画布上 288 宽、存档里 1340"那种自相矛盾的证据。
|
|
198
|
-
* - `title` —— 名字从路径读(id 就是路径),存一份就是第二个真相源,
|
|
199
|
-
* 改名后立刻过期(实测过)。
|
|
200
|
-
* - `collapsed` —— 收起/展开两态 2026-08-13 随"当前目录"模型退役。
|
|
201
|
-
* - `pinned` —— 纵向堆叠 2026-08-08 退役,字段没有了对立面。
|
|
202
|
-
* 存量数据里这些字段读进来直接丢,下次写盘自然消失。
|
|
203
|
-
*/
|
|
204
|
-
function sanitizeZone(z) {
|
|
205
|
-
if (!z || typeof z !== 'object') return null;
|
|
206
|
-
return {
|
|
207
|
-
// 同 sanitizeObject:无限画布,文件夹卡也能摆在任何地方(含负坐标)
|
|
208
|
-
x: clampNum(z.x, -COORD_LIMIT, COORD_LIMIT, 0),
|
|
209
|
-
y: clampNum(z.y, -COORD_LIMIT, COORD_LIMIT, 0),
|
|
210
|
-
};
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
function sanitizeBoard(raw) {
|
|
214
|
-
const size = sanitizeSize(raw?.size);
|
|
215
|
-
const objects = {};
|
|
216
|
-
const zones = {};
|
|
217
|
-
const bindings = {};
|
|
218
|
-
let count = 0;
|
|
219
|
-
for (const [id, o] of Object.entries(raw?.objects && typeof raw.objects === 'object' ? raw.objects : {})) {
|
|
220
|
-
if (count >= MAX_OBJECTS) break;
|
|
221
|
-
if (typeof id !== 'string' || id.length > 300) continue;
|
|
222
|
-
const s = sanitizeObject(o, size);
|
|
223
|
-
if (s) { objects[id] = s; count += 1; }
|
|
224
|
-
}
|
|
225
|
-
let zCount = 0;
|
|
226
|
-
for (const [id, z] of Object.entries(raw?.zones && typeof raw.zones === 'object' ? raw.zones : {})) {
|
|
227
|
-
if (zCount >= MAX_ZONES) break;
|
|
228
|
-
if (typeof id !== 'string' || id.length > 300) continue;
|
|
229
|
-
const s = sanitizeZone(z);
|
|
230
|
-
if (s) { zones[id] = s; zCount += 1; }
|
|
231
|
-
}
|
|
232
|
-
let bCount = 0;
|
|
233
|
-
for (const [id, b] of Object.entries(raw?.bindings && typeof raw.bindings === 'object' ? raw.bindings : {})) {
|
|
234
|
-
if (bCount >= MAX_BINDINGS) break;
|
|
235
|
-
if (typeof id !== 'string' || id.length > 300) continue;
|
|
236
|
-
const s = sanitizeBinding(b);
|
|
237
|
-
if (s) { bindings[id] = s; bCount += 1; }
|
|
238
|
-
}
|
|
239
|
-
// 主角覆盖(2026-08-14 agent 摆位):显式立的主角压过前端 pickHero 的推断。
|
|
240
|
-
// 存 id 不存理由 —— 理由在会话里,画布只要知道谁站 C 位
|
|
241
|
-
const hero = typeof raw?.hero === 'string' && raw.hero.length <= 300 ? raw.hero : null;
|
|
242
|
-
return { size, zones, objects, bindings, ...(hero ? { hero } : {}) };
|
|
243
|
-
}
|
|
244
|
-
|
|
245
51
|
export async function readBoard(pid) {
|
|
246
52
|
try {
|
|
247
53
|
const raw = await fs.readFile(boardPath(pid), 'utf8');
|
|
@@ -289,7 +95,7 @@ export function patchBoard(pid, patch) {
|
|
|
289
95
|
const fwd = (id) => forwardId(pid, id);
|
|
290
96
|
if (patch?.zones && typeof patch.zones === 'object') {
|
|
291
97
|
for (const [rawId, z] of Object.entries(patch.zones)) {
|
|
292
|
-
if (
|
|
98
|
+
if (!isSafeCanvasId(rawId)) continue;
|
|
293
99
|
const id = fwd(rawId);
|
|
294
100
|
if (z === null) { delete board.zones[id]; removed.add(id); continue; }
|
|
295
101
|
const s = sanitizeZone(z);
|
|
@@ -298,7 +104,7 @@ export function patchBoard(pid, patch) {
|
|
|
298
104
|
}
|
|
299
105
|
if (patch?.objects && typeof patch.objects === 'object') {
|
|
300
106
|
for (const [rawId, o] of Object.entries(patch.objects)) {
|
|
301
|
-
if (
|
|
107
|
+
if (!isSafeCanvasId(rawId)) continue;
|
|
302
108
|
const id = fwd(rawId);
|
|
303
109
|
if (o === null) { delete board.objects[id]; removed.add(id); continue; }
|
|
304
110
|
const s = sanitizeObject(o, board.size);
|
|
@@ -362,6 +168,67 @@ export function patchBoard(pid, patch) {
|
|
|
362
168
|
* 进程重启、以及 agent 在画布背后自己 mv 的情况,由 git 改名对账兜底
|
|
363
169
|
* (见 workspace.js 的 reconcileBoardRenames)。
|
|
364
170
|
*/
|
|
171
|
+
/**
|
|
172
|
+
* 草稿落定(2026-08-23 黑板):把 staging 位清掉,tag 留着当逻辑分组。
|
|
173
|
+
* 不传 tag = 这个项目上所有草稿一起落定(回合结束的兜底用它)。
|
|
174
|
+
* 返回落定了几件,0 = 没草稿,不写盘。
|
|
175
|
+
*/
|
|
176
|
+
export function commitStaging(pid, { tag = null } = {}) {
|
|
177
|
+
return withBoardLock(pid, async () => {
|
|
178
|
+
const board = await readBoard(pid);
|
|
179
|
+
let n = 0;
|
|
180
|
+
const hit = (e) => e?.staging === true && (!tag || e.tag === tag);
|
|
181
|
+
for (const o of Object.values(board.objects || {})) if (hit(o)) { delete o.staging; n += 1; }
|
|
182
|
+
for (const b of Object.values(board.bindings || {})) if (hit(b)) { delete b.staging; n += 1; }
|
|
183
|
+
if (n) await writeBoard(pid, board);
|
|
184
|
+
return { board, committed: n };
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* 按标签整组删除(物件 + 线)。黑板擦 —— 用户或 agent 把一次头脑风暴整块抹掉。
|
|
190
|
+
* 删画布原生物件(text/scribble)和这组的板书文件(notes/板书/,它们是这组自己的话);
|
|
191
|
+
* 带同一标签的产物卡只摘标签不删座位,产物的本体不归黑板擦管。
|
|
192
|
+
*/
|
|
193
|
+
export function removeByTag(pid, tag) {
|
|
194
|
+
const t = sanitizeTag(tag);
|
|
195
|
+
if (!t) return Promise.resolve({ board: null, removed: 0 });
|
|
196
|
+
return withBoardLock(pid, async () => {
|
|
197
|
+
const board = await readBoard(pid);
|
|
198
|
+
let removed = 0;
|
|
199
|
+
const gone = new Set();
|
|
200
|
+
for (const [id, o] of Object.entries(board.objects || {})) {
|
|
201
|
+
if (o.tag !== t) continue;
|
|
202
|
+
if (o.kind) { delete board.objects[id]; gone.add(id); removed += 1; continue; }
|
|
203
|
+
// 板书(notes/板书/*.md)是这组自己的话,擦组连文件一起删;其它文件只摘标签。
|
|
204
|
+
// ⛔ 删前按绝对路径断言落在 notes/板书/ 里且是单段文件名(fable 08-23 审出:id 里塞 ../ 能
|
|
205
|
+
// 以进程身份删任意文件;sanitize 现在也拒 `..`,这里是第二道闸)
|
|
206
|
+
if (id.startsWith(`${CHALK_DIR}/`)) {
|
|
207
|
+
const abs = chalkAbsPath(pid, id);
|
|
208
|
+
if (abs) { try { await fs.unlink(abs); } catch { /* 已经没了 */ } }
|
|
209
|
+
delete board.objects[id]; gone.add(id); removed += 1; continue;
|
|
210
|
+
}
|
|
211
|
+
delete o.tag;
|
|
212
|
+
}
|
|
213
|
+
for (const [id, b] of Object.entries(board.bindings || {})) {
|
|
214
|
+
if (b.tag === t || gone.has(b.from) || gone.has(b.to)) { delete board.bindings[id]; removed += 1; }
|
|
215
|
+
}
|
|
216
|
+
if (board.hero && gone.has(board.hero)) delete board.hero;
|
|
217
|
+
await writeBoard(pid, board);
|
|
218
|
+
return { board, removed };
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** 板书 id → 绝对路径,越界/多段/非 .md 一律 null(删与读两处共用) */
|
|
223
|
+
export function chalkAbsPath(pid, id) {
|
|
224
|
+
if (typeof id !== 'string' || !id.startsWith(`${CHALK_DIR}/`)) return null;
|
|
225
|
+
const name = id.slice(CHALK_DIR.length + 1);
|
|
226
|
+
if (!name || name.includes('/') || name.includes('\\') || name.includes('..') || name.startsWith('.') || !name.endsWith('.md')) return null;
|
|
227
|
+
const base = path.resolve(getSharedDir(pid), CHALK_DIR);
|
|
228
|
+
const abs = path.resolve(base, name);
|
|
229
|
+
return abs.startsWith(base + path.sep) ? abs : null;
|
|
230
|
+
}
|
|
231
|
+
|
|
365
232
|
const RENAME_TTL_MS = 5 * 60 * 1000;
|
|
366
233
|
/** @type {Map<string, Map<string, { to: string, at: number }>>} pid → (旧 id → 新 id) */
|
|
367
234
|
const renameJournal = new Map();
|
|
@@ -22,6 +22,12 @@ const LEGACY_NAME = 'session-config.json';
|
|
|
22
22
|
|
|
23
23
|
export const DEFAULT_UI_CONFIG = Object.freeze({
|
|
24
24
|
tweaks_mode_enabled: true,
|
|
25
|
+
/**
|
|
26
|
+
* 黑板模式(2026-08-23):用户专注头脑风暴时,画布取代侧栏成为主交互窗口 ——
|
|
27
|
+
* agent 每轮的主体内容落画布(sketch_on_board / edit_sketch),聊天里只留一两句;
|
|
28
|
+
* 草图落定时服务端广播 board.focus,前端把镜头带过去。
|
|
29
|
+
*/
|
|
30
|
+
blackboard_mode: false,
|
|
25
31
|
});
|
|
26
32
|
|
|
27
33
|
/**
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* server/projects/viewpoint-store.js —— 用户视点(2026-08-23 黑板)
|
|
3
|
+
*
|
|
4
|
+
* 服务端在这之前对「用户此刻看着哪里」一无所知:相机在哪、哪扇窗开着、选中了
|
|
5
|
+
* 谁,全在前端。agent 于是只能猜「这个」「这里」指什么。前端按节流上报一份
|
|
6
|
+
* 视点,这里存**每个项目最近一份**(进程内存,不落盘 —— 它是"此刻",重启即作废)。
|
|
7
|
+
*
|
|
8
|
+
* 读者:read_board(小地图画视口框 + 列视口里有什么)、UserPromptSubmit 注入
|
|
9
|
+
* (只报变化)、read_user_view 工具。
|
|
10
|
+
*
|
|
11
|
+
* 只存最近一份而不是按用户存:一个项目同时几个人看是例外不是常态,而 agent
|
|
12
|
+
* 要的是"跟它说话的那个人在看哪"——发消息的人上一拍上报的视点就是它。
|
|
13
|
+
* 写入带 userId,以后要拆按用户拆读法就行。
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const TTL_MS = 10 * 60 * 1000; // 十分钟没上报就当离开了
|
|
17
|
+
const store = new Map(); // pid → viewpoint
|
|
18
|
+
|
|
19
|
+
function num(v, min, max) {
|
|
20
|
+
const n = Number(v);
|
|
21
|
+
return Number.isFinite(n) ? Math.min(max, Math.max(min, n)) : null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function sanitizeViewpoint(raw, userId) {
|
|
25
|
+
if (!raw || typeof raw !== 'object') return null;
|
|
26
|
+
const cam = raw.camera && typeof raw.camera === 'object' ? raw.camera : null;
|
|
27
|
+
const camera = cam ? {
|
|
28
|
+
x: num(cam.x, -1e6, 1e6), y: num(cam.y, -1e6, 1e6),
|
|
29
|
+
w: num(cam.w, 1, 1e5), h: num(cam.h, 1, 1e5),
|
|
30
|
+
} : null;
|
|
31
|
+
if (camera && Object.values(camera).some(v => v === null)) return null;
|
|
32
|
+
const str = (v, n) => (typeof v === 'string' && v.length <= n ? v : null);
|
|
33
|
+
return {
|
|
34
|
+
userId: userId || null,
|
|
35
|
+
camera,
|
|
36
|
+
zoom: num(raw.zoom, 0.01, 100),
|
|
37
|
+
layer: str(raw.layer, 300) || '',
|
|
38
|
+
openWindow: str(raw.openWindow, 300),
|
|
39
|
+
openPage: str(raw.openPage, 120),
|
|
40
|
+
selected: Array.isArray(raw.selected) ? raw.selected.filter(s => typeof s === 'string' && s.length <= 300).slice(0, 24) : [],
|
|
41
|
+
at: Date.now(),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function setViewpoint(pid, raw, userId = null) {
|
|
46
|
+
const v = sanitizeViewpoint(raw, userId);
|
|
47
|
+
if (!v) return null;
|
|
48
|
+
store.set(pid, v);
|
|
49
|
+
return v;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function getViewpoint(pid) {
|
|
53
|
+
const v = store.get(pid);
|
|
54
|
+
if (!v) return null;
|
|
55
|
+
if (Date.now() - v.at > TTL_MS) { store.delete(pid); return null; }
|
|
56
|
+
return v;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** 一行人话(注入/工具共用一份措辞)。rects 可选:给了就报视口里有什么 */
|
|
60
|
+
export function describeViewpoint(v, rects = null) {
|
|
61
|
+
if (!v) return null;
|
|
62
|
+
const bits = [];
|
|
63
|
+
if (v.layer) bits.push(`在文件夹「${v.layer}」里`);
|
|
64
|
+
if (v.camera) {
|
|
65
|
+
bits.push(`视口 (${Math.round(v.camera.x)},${Math.round(v.camera.y)}) ${Math.round(v.camera.w)}x${Math.round(v.camera.h)}${v.zoom ? ` 缩放 ${Number(v.zoom).toFixed(2)}` : ''}`);
|
|
66
|
+
if (rects) {
|
|
67
|
+
const c = v.camera;
|
|
68
|
+
const inside = rects.filter(r => !(r.x + r.w < c.x || r.x > c.x + c.w || r.y + r.h < c.y || r.y > c.y + c.h)).map(r => r.id);
|
|
69
|
+
if (inside.length) bits.push(`视口里有 ${inside.length} 件:${inside.slice(0, 10).join('、')}${inside.length > 10 ? ' 等' : ''}`);
|
|
70
|
+
else bits.push('视口里是空地');
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (v.openWindow) bits.push(`开着窗:${v.openWindow}${v.openPage ? `(${v.openPage})` : ''}`);
|
|
74
|
+
if (v.selected?.length) bits.push(`选中:${v.selected.slice(0, 8).join('、')}${v.selected.length > 8 ? ' 等' : ''}`);
|
|
75
|
+
return bits.join(';') || null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function _resetViewpoints() { store.clear(); }
|
package/server/ws/broker.js
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
import { EventBus } from '../engine/agent/events.js';
|
|
12
12
|
import { attachLiveTurnTracker } from '../engine/runs/live-turn.js';
|
|
13
|
+
import { attachBoardTasklist } from '../engine/runs/board-tasklist.js';
|
|
13
14
|
|
|
14
15
|
/** @type {Map<string, EventBus>} */
|
|
15
16
|
const projectBuses = new Map();
|
|
@@ -22,6 +23,8 @@ export function getProjectBus(projectId) {
|
|
|
22
23
|
// live-turn 快照折叠器:进行中 turn 的事件物化成可恢复状态,
|
|
23
24
|
// WS 重连走"hydrate + ws.live_turn 快照 + 尾随"三段协议。见 live-turn.js
|
|
24
25
|
attachLiveTurnTracker(bus);
|
|
26
|
+
// 步骤清单镜像成板书 + 每步产物连线(2026-08-23 黑板文化,harness 做不靠 agent 记得)
|
|
27
|
+
attachBoardTasklist(bus, projectId);
|
|
25
28
|
projectBuses.set(projectId, bus);
|
|
26
29
|
}
|
|
27
30
|
return bus;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{c as G,r as i,
|
|
1
|
+
import{c as G,r as i,B as z,H as ee,j as t,I as te,F as y,a as $,G as u,b as ne,A as ie,C as N,d as c,e as j,L as W,f as se,g as re,h as oe}from"./index-GAamh1p4.js";import{A as ae,R as le}from"./rotate-cw-KAfChbGV.js";/**
|
|
2
2
|
* @license lucide-react v0.468.0 - ISC
|
|
3
3
|
*
|
|
4
4
|
* This source code is licensed under the ISC license.
|
|
@@ -8,4 +8,4 @@ import{c as G,r as i,au as L,av as ee,j as t,a0 as te,F as y,b as $,G as u,aw as
|
|
|
8
8
|
*
|
|
9
9
|
* This source code is licensed under the ISC license.
|
|
10
10
|
* See the LICENSE file in the root directory of this source tree.
|
|
11
|
-
*/const ce=G("PowerOff",[["path",{d:"M18.36 6.64A9 9 0 0 1 20.77 15",key:"dxknvb"}],["path",{d:"M6.16 6.16a9 9 0 1 0 12.68 12.68",key:"1x7qb5"}],["path",{d:"M12 2v4",key:"3427ic"}],["path",{d:"m2 2 20 20",key:"1ooewy"}]]);function de(o){try{return decodeURI(o)}catch{return o}}function _(o,
|
|
11
|
+
*/const ce=G("PowerOff",[["path",{d:"M18.36 6.64A9 9 0 0 1 20.77 15",key:"dxknvb"}],["path",{d:"M6.16 6.16a9 9 0 1 0 12.68 12.68",key:"1x7qb5"}],["path",{d:"M12 2v4",key:"3427ic"}],["path",{d:"m2 2 20 20",key:"1ooewy"}]]);function de(o){try{return decodeURI(o)}catch{return o}}function _(o,h){const x=o.getBoundingClientRect();return{rx:Math.min(1,Math.max(0,(h.clientX-x.left)/x.width)),ry:Math.min(1,Math.max(0,(h.clientY-x.top)/x.height))}}function fe({projectId:o,url:h,help:x,onClose:w,onToolbarGroups:J}){const[v,d]=i.useState("connecting"),[C,g]=i.useState(null),[b,O]=i.useState(h||""),[a,H]=i.useState(!1),[M,X]=i.useState(!1),[B,T]=i.useState(null),[k,D]=i.useState(!1),[A,K]=i.useState([]),[F,V]=i.useState(null),[E,Y]=i.useState(!0),R=i.useRef(null),L=i.useRef(null),q=i.useRef(!1);q.current=a;const l=i.useCallback(e=>{const n=L.current;n&&n.readyState===1&&n.send(JSON.stringify(e))},[]),Z=i.useCallback(async()=>{D(!0),g(null);try{const e=await z.open(o);O(e.url||""),d("connecting"),l({type:"subscribe"})}catch(e){d("error"),g((e==null?void 0:e.message)||"打不开")}finally{D(!1)}},[o,l]);i.useEffect(()=>{const e=window.location.protocol==="https:"?"wss:":"ws:",n=new WebSocket(`${e}//${window.location.host}/ws/projects/${encodeURIComponent(o)}/browser`);n.binaryType="arraybuffer",L.current=n,n.onopen=()=>{d("connecting"),n.send(JSON.stringify({type:"subscribe"}))},n.onclose=p=>{d(p.code===4401?"error":"closed"),p.code===4401&&g("没有权限")},n.onerror=()=>d("error"),n.onmessage=async p=>{var r,P;if(typeof p.data!="string"){const m=R.current;if(!m)return;try{const f=await createImageBitmap(new Blob([p.data],{type:"image/jpeg"}));(m.width!==f.width||m.height!==f.height)&&(m.width=f.width,m.height=f.height),(r=m.getContext("2d"))==null||r.drawImage(f,0,0),(P=f.close)==null||P.call(f),X(!0),d("live")}catch{}return}let s;try{s=JSON.parse(p.data)}catch{return}s.type==="subscribed"&&(d("live"),s.url&&O(s.url)),s.type==="hello"&&s.help&&T(s.help),s.type==="released"&&T(null),s.type==="idle"&&(d("idle"),g(s.reason)),s.type==="error"&&(d("error"),g(s.reason)),s.type==="closed"&&(d("closed"),g(s.reason)),s.type==="url"&&O(s.url||"")};const S=()=>l({type:document.hidden?"unsubscribe":"subscribe"});return document.addEventListener("visibilitychange",S),()=>{document.removeEventListener("visibilitychange",S);try{n.send(JSON.stringify({type:"unsubscribe"}))}catch{}n.close(),L.current=null}},[o,l]),i.useEffect(()=>{h&&O(h)},[h]),i.useEffect(()=>{let e=!0;return z.state(o).then(n=>{e&&K(Array.isArray(n==null?void 0:n.sites)?n.sites:[])}).catch(()=>{}),()=>{e=!1}},[o]),i.useEffect(()=>{var s;if(!a)return;const e=R.current;if(!e)return;const n=r=>{r.preventDefault(),l({type:"input",kind:"click",..._(e,r)})},S=r=>{r.preventDefault(),l({type:"input",kind:"wheel",..._(e,r),dx:r.deltaX,dy:r.deltaY})},p=r=>{if(!r.isComposing){if(r.key.length===1){r.preventDefault(),l({type:"input",kind:"key",text:r.key});return}["Enter","Backspace","Tab","Escape","ArrowUp","ArrowDown","ArrowLeft","ArrowRight"].includes(r.key)&&(r.preventDefault(),l({type:"input",kind:"key",key:r.key}))}};return e.addEventListener("click",n),e.addEventListener("wheel",S,{passive:!1}),e.addEventListener("keydown",p),(s=e.focus)==null||s.call(e),()=>{e.removeEventListener("click",n),e.removeEventListener("wheel",S),e.removeEventListener("keydown",p)}},[a,l]);const Q=i.useMemo(()=>[{id:"addr",node:t.jsxs("span",{style:{display:"inline-flex",alignItems:"center",gap:6,padding:`0 ${u.sm}px`,maxWidth:420,fontFamily:$,fontSize:y.xs,color:te.text,whiteSpace:"nowrap",overflow:"hidden",textOverflow:"ellipsis",userSelect:"text"},title:b||"",children:[t.jsx(ne,{size:12,style:{flexShrink:0,opacity:.6}}),b?de(b):"(还没打开页面)"]})},{id:"nav",items:[{id:"back",icon:ae,title:"后退",onClick:()=>l({type:"nav",action:"back"})},{id:"reload",icon:le,title:"刷新",onClick:()=>l({type:"nav",action:"reload"})}]},{id:"end",items:[{id:"end",icon:ce,title:"不逛了 —— 关掉浏览器并把桌面上那张卡收走(在站点上的登录留着)",onClick:async()=>{try{await z.end(o)}catch{}w==null||w()}}]},{id:"takeover",items:[{id:"hand",icon:a?U:ee,title:a?"好了继续 —— 把控制权交回 agent(它正等着)":"我来接手 —— 点击/滚动/打字会直接发到那个浏览器里",active:a,onClick:()=>{a?(l({type:"release"}),H(!1)):H(!0)}}]}],[b,a,l,o,w]),I={connecting:"连接中…",live:null,idle:C||"agent 还没开始浏览",error:C||"出错了",closed:C||"浏览器已经关了"}[v];return t.jsxs(ie,{kind:"browse",title:"agent 的浏览器",subtitle:a?"你在接手":v==="live"?"实时":null,onClose:w,groups:Q,onToolbarGroups:J,banner:x||B?t.jsxs("span",{children:[t.jsx("b",{children:"agent 需要你帮个手"}),":",x||B,"—— 点工具栏上的",t.jsx("b",{children:"手形按钮"}),"接手,弄完再点一次(变成 ▶)把控制权交回去。 它正停在那儿等你。"]}):null,contentStyle:{background:N.paper},children:[t.jsxs("div",{style:{flex:1,minHeight:0,width:"100%",display:"flex",alignItems:"center",justifyContent:"center",padding:u.md,boxSizing:"border-box",position:"relative"},children:[t.jsx("canvas",{ref:R,tabIndex:a?0:-1,title:a?"接手中:点一下这块画面再打字(键盘只在这里生效)":void 0,style:{maxWidth:"100%",maxHeight:"100%",display:M?"block":"none",background:"#fff",borderRadius:2,boxShadow:"0 2px 12px rgba(43,39,35,.18)",cursor:a?"pointer":"default",outline:a?`2px solid ${c.accent||"#8a4b2d"}`:"none",outlineOffset:3}}),(!M||I)&&t.jsxs("div",{style:{position:"absolute",inset:0,display:"flex",flexDirection:"column",alignItems:"center",justifyContent:"center",gap:u.sm,fontFamily:j,fontSize:y.sm,color:c.text2,textAlign:"center"},children:[v==="connecting"&&t.jsx(W,{size:16,style:{animation:"spin 1s linear infinite"}}),t.jsx("span",{children:I||"等第一帧…"}),(v==="idle"||v==="closed")&&t.jsxs(t.Fragment,{children:[b&&t.jsxs("button",{type:"button",disabled:k,onClick:Z,style:{display:"inline-flex",alignItems:"center",gap:6,padding:`${u.xs}px ${u.md}px`,cursor:k?"default":"pointer",fontFamily:j,fontSize:y.sm,color:c.text,background:N.paper,border:`1px solid ${c.border}`,borderRadius:3,opacity:k?.6:1},children:[k?t.jsx(W,{size:13,style:{animation:"spin 1s linear infinite"}}):t.jsx(U,{size:13}),k?"正在打开…":"打开上次那一页"]}),t.jsxs("span",{style:{color:c.sub,fontSize:y.xs,maxWidth:360,lineHeight:1.7},children:[b?"浏览器空闲 5 分钟会自己休息(登录态留着)。打开之后你可以直接接手操作,agent 下次也接着用这一页。":"让 agent 去看一个站(它有 browser_navigate),这里就会亮起来。",t.jsx("br",{}),"静止的页面不会一直传帧 —— 画面不动是正常的,不是卡住了。"]})]})]})]}),!!A.length&&t.jsxs("div",{style:{flexShrink:0,borderTop:`1px solid ${c.border}`,background:c.bgCard,maxHeight:E?220:30,overflow:"hidden",transition:"max-height .18s ease"},children:[t.jsxs("button",{type:"button",onClick:()=>Y(e=>!e),style:{display:"flex",alignItems:"center",gap:6,width:"100%",padding:`4px ${u.md}px`,cursor:"pointer",background:"transparent",border:0,color:c.text2,fontFamily:j,fontSize:y.xs},children:[t.jsx(se,{size:12}),"采到的东西 · ",A.length," 个站 · ",A.reduce((e,n)=>e+n.count,0)," 件",t.jsx(re,{size:12,style:{marginLeft:"auto",opacity:.6,transform:E?"none":"rotate(-90deg)",transition:"transform .18s ease"}})]}),E&&t.jsx("div",{style:{display:"flex",gap:u.sm,padding:`0 ${u.md}px ${u.sm}px`,overflowX:"auto"},children:A.map(e=>t.jsxs("div",{style:{flexShrink:0,width:150},children:[t.jsxs("button",{type:"button",title:`${e.dir}(${e.count} 件)`,onClick:()=>V(F===e.site?null:e.site),style:{display:"block",width:"100%",padding:0,cursor:"pointer",background:N.paper,border:`1px solid ${F===e.site?c.text:c.border}`,borderRadius:2,overflow:"hidden"},children:[e.cover?t.jsx("img",{alt:"",loading:"lazy",src:oe.artifactFileUrl(o,e.cover),style:{width:"100%",height:84,objectFit:"cover",objectPosition:"top center",display:"block"}}):t.jsx("div",{style:{height:84,display:"flex",alignItems:"center",justifyContent:"center",color:c.sub,fontFamily:j,fontSize:y.xxs},children:"没有截图"}),t.jsx("div",{style:{padding:"3px 5px",textAlign:"left",fontFamily:$,fontSize:y.xxs,color:c.text2,whiteSpace:"nowrap",overflow:"hidden",textOverflow:"ellipsis"},children:e.site})]}),F===e.site&&t.jsxs("div",{style:{paddingTop:3,fontFamily:j,fontSize:y.xxs,color:c.sub,lineHeight:1.6},children:[t.jsxs("span",{style:{fontFamily:$},children:[e.dir,"/"]}),t.jsx("br",{}),e.count," 件(截图 / 调色板 / 字体 / 结构 / CSS,类别在文件名里)"]})]},e.site))})]})]})}export{fe as default};
|