@ptengine/inline-editor 0.0.0-stage → 0.1.0

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 (169) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +286 -2
  3. package/dist/InlineEditor.d.ts +342 -0
  4. package/dist/InlineEditor.js +867 -0
  5. package/dist/PagePreview.d.ts +83 -0
  6. package/dist/PagePreview.js +111 -0
  7. package/dist/assets/AssetsPanel.d.ts +19 -0
  8. package/dist/assets/AssetsPanel.js +23 -0
  9. package/dist/assets/componentLibrary.d.ts +23 -0
  10. package/dist/assets/componentLibrary.js +12 -0
  11. package/dist/canvas-ui/index.d.ts +17 -0
  12. package/dist/canvas-ui/index.js +7 -0
  13. package/dist/canvas-ui/scope.d.ts +13 -0
  14. package/dist/canvas-ui/scope.js +1 -0
  15. package/dist/canvas-ui/selection/classNames.d.ts +30 -0
  16. package/dist/canvas-ui/selection/classNames.js +15 -0
  17. package/dist/canvas-ui/selection/tokens.css +204 -0
  18. package/dist/canvas-ui/tree/TreePanel.d.ts +47 -0
  19. package/dist/canvas-ui/tree/TreePanel.js +132 -0
  20. package/dist/canvas-ui/tree/classNames.d.ts +21 -0
  21. package/dist/canvas-ui/tree/classNames.js +8 -0
  22. package/dist/canvas-ui/tree/order.d.ts +38 -0
  23. package/dist/canvas-ui/tree/order.js +38 -0
  24. package/dist/canvas-ui/tree/tree.css +131 -0
  25. package/dist/chrome/EditorChrome.d.ts +83 -0
  26. package/dist/chrome/EditorChrome.js +10 -0
  27. package/dist/chrome/VersionMenu.d.ts +26 -0
  28. package/dist/chrome/VersionMenu.js +86 -0
  29. package/dist/chrome/topbarPrimitives.d.ts +69 -0
  30. package/dist/chrome/topbarPrimitives.js +39 -0
  31. package/dist/chrome/useDismissOnOutside.d.ts +23 -0
  32. package/dist/chrome/useDismissOnOutside.js +27 -0
  33. package/dist/dialogs/ClickActionDialog.d.ts +24 -0
  34. package/dist/dialogs/ClickActionDialog.js +35 -0
  35. package/dist/dialogs/HtmlEditDialog.d.ts +23 -0
  36. package/dist/dialogs/HtmlEditDialog.js +23 -0
  37. package/dist/dialogs/ImageEditDialog.d.ts +30 -0
  38. package/dist/dialogs/ImageEditDialog.js +57 -0
  39. package/dist/dialogs/ImageSettingDialog.d.ts +16 -0
  40. package/dist/dialogs/ImageSettingDialog.js +17 -0
  41. package/dist/dialogs/InsertElementDialog.d.ts +23 -0
  42. package/dist/dialogs/InsertElementDialog.js +28 -0
  43. package/dist/dialogs/ReplaceVideoDialog.d.ts +18 -0
  44. package/dist/dialogs/ReplaceVideoDialog.js +16 -0
  45. package/dist/dialogs/actionTypes.d.ts +53 -0
  46. package/dist/dialogs/actionTypes.js +74 -0
  47. package/dist/dialogs/htmlValidation.d.ts +24 -0
  48. package/dist/dialogs/htmlValidation.js +37 -0
  49. package/dist/dialogs/imageEdit.d.ts +49 -0
  50. package/dist/dialogs/imageEdit.js +30 -0
  51. package/dist/index.d.ts +101 -0
  52. package/dist/index.js +53 -0
  53. package/dist/kernel/CanvasContextBar.d.ts +23 -0
  54. package/dist/kernel/CanvasContextBar.js +12 -0
  55. package/dist/kernel/CanvasFrame.d.ts +56 -0
  56. package/dist/kernel/CanvasFrame.js +53 -0
  57. package/dist/kernel/ai/aiInlineApply.d.ts +326 -0
  58. package/dist/kernel/ai/aiInlineApply.js +461 -0
  59. package/dist/kernel/ai/imageSource.d.ts +24 -0
  60. package/dist/kernel/ai/imageSource.js +19 -0
  61. package/dist/kernel/ai/insertHtmlGate.d.ts +71 -0
  62. package/dist/kernel/ai/insertHtmlGate.js +123 -0
  63. package/dist/kernel/ai/postApplyCheck.d.ts +56 -0
  64. package/dist/kernel/ai/postApplyCheck.js +74 -0
  65. package/dist/kernel/ai/replaceHtmlGate.d.ts +71 -0
  66. package/dist/kernel/ai/replaceHtmlGate.js +147 -0
  67. package/dist/kernel/blocks/BlockOverlay.d.ts +16 -0
  68. package/dist/kernel/blocks/BlockOverlay.js +66 -0
  69. package/dist/kernel/blocks/annotateAndTile.d.ts +106 -0
  70. package/dist/kernel/blocks/annotateAndTile.js +122 -0
  71. package/dist/kernel/blocks/resolveBlocks.d.ts +153 -0
  72. package/dist/kernel/blocks/resolveBlocks.js +233 -0
  73. package/dist/kernel/canvasCapabilities.d.ts +31 -0
  74. package/dist/kernel/canvasCapabilities.js +39 -0
  75. package/dist/kernel/canvasCommands.d.ts +126 -0
  76. package/dist/kernel/canvasCommands.js +146 -0
  77. package/dist/kernel/canvasErrors.d.ts +1 -0
  78. package/dist/kernel/canvasErrors.js +14 -0
  79. package/dist/kernel/captureGeometry.d.ts +35 -0
  80. package/dist/kernel/captureGeometry.js +25 -0
  81. package/dist/kernel/captureOrchestration.d.ts +48 -0
  82. package/dist/kernel/captureOrchestration.js +43 -0
  83. package/dist/kernel/captureSlices.d.ts +36 -0
  84. package/dist/kernel/captureSlices.js +20 -0
  85. package/dist/kernel/changelist/ChangeListPanel.d.ts +13 -0
  86. package/dist/kernel/changelist/ChangeListPanel.js +47 -0
  87. package/dist/kernel/changelist/ChangeListPopover.d.ts +20 -0
  88. package/dist/kernel/changelist/ChangeListPopover.js +10 -0
  89. package/dist/kernel/changelist/builders.d.ts +89 -0
  90. package/dist/kernel/changelist/builders.js +81 -0
  91. package/dist/kernel/changelist/changeSummary.d.ts +19 -0
  92. package/dist/kernel/changelist/changeSummary.js +85 -0
  93. package/dist/kernel/changelist/clipboard.d.ts +16 -0
  94. package/dist/kernel/changelist/clipboard.js +4 -0
  95. package/dist/kernel/changelist/exportForSave.d.ts +30 -0
  96. package/dist/kernel/changelist/exportForSave.js +11 -0
  97. package/dist/kernel/changelist/mergeForSave.d.ts +79 -0
  98. package/dist/kernel/changelist/mergeForSave.js +40 -0
  99. package/dist/kernel/changelist/moveObserverSafety.d.ts +70 -0
  100. package/dist/kernel/changelist/moveObserverSafety.js +39 -0
  101. package/dist/kernel/changelist/overrides.d.ts +22 -0
  102. package/dist/kernel/changelist/overrides.js +63 -0
  103. package/dist/kernel/changelist/useChangeList.d.ts +185 -0
  104. package/dist/kernel/changelist/useChangeList.js +259 -0
  105. package/dist/kernel/config.d.ts +29 -0
  106. package/dist/kernel/config.js +10 -0
  107. package/dist/kernel/dataUrlToBlob.d.ts +8 -0
  108. package/dist/kernel/dataUrlToBlob.js +17 -0
  109. package/dist/kernel/linkHref.d.ts +25 -0
  110. package/dist/kernel/linkHref.js +16 -0
  111. package/dist/kernel/mobileFit.d.ts +46 -0
  112. package/dist/kernel/mobileFit.js +44 -0
  113. package/dist/kernel/moveState.d.ts +35 -0
  114. package/dist/kernel/moveState.js +14 -0
  115. package/dist/kernel/selection/SelectionOverlay.d.ts +10 -0
  116. package/dist/kernel/selection/SelectionOverlay.js +25 -0
  117. package/dist/kernel/selection/SelectionToolbar.d.ts +30 -0
  118. package/dist/kernel/selection/SelectionToolbar.js +82 -0
  119. package/dist/kernel/selection/SelectorPathBar.d.ts +27 -0
  120. package/dist/kernel/selection/SelectorPathBar.js +55 -0
  121. package/dist/kernel/selection/buildDescendantShelf.d.ts +40 -0
  122. package/dist/kernel/selection/buildDescendantShelf.js +50 -0
  123. package/dist/kernel/selection/buildRootShelf.d.ts +26 -0
  124. package/dist/kernel/selection/buildRootShelf.js +149 -0
  125. package/dist/kernel/selection/elementAiContext.d.ts +313 -0
  126. package/dist/kernel/selection/elementAiContext.js +122 -0
  127. package/dist/kernel/selection/elementGeometryForAi.d.ts +9 -0
  128. package/dist/kernel/selection/elementGeometryForAi.js +15 -0
  129. package/dist/kernel/selection/geometry.d.ts +55 -0
  130. package/dist/kernel/selection/geometry.js +18 -0
  131. package/dist/kernel/selection/resolveAnchors.d.ts +116 -0
  132. package/dist/kernel/selection/resolveAnchors.js +173 -0
  133. package/dist/kernel/selection/shelfRow.d.ts +33 -0
  134. package/dist/kernel/selection/shelfRow.js +19 -0
  135. package/dist/kernel/selection/tagLabels.d.ts +9 -0
  136. package/dist/kernel/selection/tagLabels.js +46 -0
  137. package/dist/kernel/selection/toolbarButtons.d.ts +60 -0
  138. package/dist/kernel/selection/toolbarButtons.js +105 -0
  139. package/dist/kernel/selection/useAiSelectionContext.d.ts +26 -0
  140. package/dist/kernel/selection/useAiSelectionContext.js +26 -0
  141. package/dist/kernel/selection/useCanvasGeometry.d.ts +32 -0
  142. package/dist/kernel/selection/useCanvasGeometry.js +47 -0
  143. package/dist/kernel/selection/useSelectionRestore.d.ts +18 -0
  144. package/dist/kernel/selection/useSelectionRestore.js +25 -0
  145. package/dist/kernel/shrinkImageForUpload.d.ts +45 -0
  146. package/dist/kernel/shrinkImageForUpload.js +61 -0
  147. package/dist/kernel/useCanvasBridge.d.ts +36 -0
  148. package/dist/kernel/useCanvasBridge.js +41 -0
  149. package/dist/kernel/useCanvasGate.d.ts +22 -0
  150. package/dist/kernel/useCanvasGate.js +35 -0
  151. package/dist/kernel/useCanvasSession.d.ts +44 -0
  152. package/dist/kernel/useCanvasSession.js +182 -0
  153. package/dist/kernel/useCanvasStream.d.ts +39 -0
  154. package/dist/kernel/useCanvasStream.js +45 -0
  155. package/dist/kernel/useMobileFit.d.ts +40 -0
  156. package/dist/kernel/useMobileFit.js +139 -0
  157. package/dist/locales.d.ts +1094 -0
  158. package/dist/locales.js +545 -0
  159. package/dist/panels/StylePanel.d.ts +24 -0
  160. package/dist/panels/StylePanel.js +40 -0
  161. package/dist/panels/styleFields.d.ts +39 -0
  162. package/dist/panels/styleFields.js +91 -0
  163. package/dist/tree/LayersPanel.d.ts +30 -0
  164. package/dist/tree/LayersPanel.js +15 -0
  165. package/dist/tree/domTreeAdapter.d.ts +63 -0
  166. package/dist/tree/domTreeAdapter.js +90 -0
  167. package/dist/useInlineEditor.d.ts +158 -0
  168. package/dist/useInlineEditor.js +343 -0
  169. package/package.json +58 -3
@@ -0,0 +1,313 @@
1
+ /**
2
+ * 「问 AI」交给宿主的元素上下文。
3
+ *
4
+ * 包只负责**采集**,怎么送进对话是宿主的事(包不知道产品里有没有 AI 面板、长什么样)——
5
+ * 与 `uploadImage` / `libraryComponents` 同一套分工。
6
+ */
7
+ import type { CanvasDevice } from '@ptengine/inline-core/protocol';
8
+ import type { ResolvedAnchor } from './resolveAnchors.js';
9
+ /** outerHTML 进 prompt 的预算。 */
10
+ export declare const AI_HTML_BUDGET_BYTES = 4096;
11
+ /**
12
+ * 选中元素的几何。
13
+ *
14
+ * 存在的理由:没有它 agent 只能**盲猜布局** —— 真机上它要在 banner 上叠一个绝对定位的
15
+ * 覆盖层,只好说「如果和我预估的不一致,你告诉我微调偏移量」。有了宽高与视口,它能给出
16
+ * 合理的尺寸/字号,也能判断这是不是一条通栏(元素宽 ≈ 视口宽)。
17
+ *
18
+ * ⚠️ **不含 x**:横向位置对「改这个元素」没有信息量,而它在轮播/横滚容器里还会变。
19
+ * 纵向给的是 `topInPage`(文档坐标,不随滚动变),因为「在不在首屏」是个真会被问到的问题。
20
+ */
21
+ export interface AiElementGeometry {
22
+ width: number;
23
+ height: number;
24
+ /** 元素顶边在**整页文档**里的位置(已加回 scrollY,不随滚动变)。 */
25
+ topInPage: number;
26
+ viewportWidth: number;
27
+ viewportHeight: number;
28
+ device: CanvasDevice;
29
+ }
30
+ /** 货架的一项。字段刻意少 —— 它要进 prompt,多一个字段就是每次对话的固定开销。 */
31
+ export interface AiDescendant {
32
+ /** 直接可用的 selector(逐字抄,不要改写)。 */
33
+ selector: string;
34
+ tag: string;
35
+ /** 直接文本的摘要(截断)。空 = 这一项自己不持有文字,只是层级。 */
36
+ text: string;
37
+ /** 有子元素 —— 决定 `EDIT_TEXT` 是否适用(容器上改文字会换掉整棵子树)。 */
38
+ hasChildren: boolean;
39
+ /**
40
+ * 这个区块里第一个 h1-h4 的文字(≤80 字符)。**只在容器行上、且新 agent 给了
41
+ * `heading` 时**出现——AI 靠它认「哪个区块是"雨の日も"那一段」,而不是靠 selector 猜。
42
+ * 老 agent(树上没 rect)产出的货架永远没有这个键。
43
+ */
44
+ heading?: string;
45
+ /**
46
+ * 文档坐标的顶边(px,取整)。同上,只有新 agent(`includeRect`)的树才有;
47
+ * AI 用它判断「这是页面上半部还是下半部的区块」、以及区块的先后顺序。
48
+ */
49
+ top?: number;
50
+ /**
51
+ * 这一行**自己**是可换的图片位:`'img'` = `<img>`,`'background'` = CSS 背景图(PageFly
52
+ * 首屏照片常是类名设的背景,html 里看不到)。来自树节点的 `image`(新扩展 + includeRect);
53
+ * 都不是、或老扩展时没有这个键。宿主写成行尾的 `[image: …]`。
54
+ */
55
+ image?: 'img' | 'background';
56
+ /**
57
+ * 这一行**自己**是链接:点下去去哪(树节点的 `href`,新扩展 + includeRect;`<a href>` 或 PageFly
58
+ * 图片链接 / 商品卡的 `data-href`)。原样存,宿主写成行尾的 `[link→路径]`(`linkTag`,查询串去掉)。
59
+ * 不是链接、或老扩展时没有这个键。
60
+ */
61
+ href?: string;
62
+ /**
63
+ * 层级缩进:这一行上面有几个**出了行**的祖先(跳过的包装层不计)。只用来排版
64
+ * (`formatShelfRow`),不进授权判断。顶层行不写这个键(条件写入,与 `image` 同一条纪律)。
65
+ */
66
+ depth?: number;
67
+ }
68
+ /**
69
+ * 区块级货架取树的口径(`<page-block>`、方案锚点子树、选中元素货架三处共用)。
70
+ *
71
+ * `maxDepth` 用扩展的硬上限(`domTree.ts MAX_DEPTH_CEILING`),**不再按层截**:真实站点的标题
72
+ * 在第 5-10 层都常见(PageFly:Section > div > Row > .pf-c > Column > h1),按 4 截的结果是
73
+ * 货架里只剩容器(2026-09-30 真机)。真正的总量闸是 `maxNodes`;货架行数另有
74
+ * `AI_DESCENDANT_LIMIT` / 各调用方的上限,由 `buildDescendantShelf` 按「值不值得出行」挑。
75
+ */
76
+ export declare const SHELF_TREE_MAX_DEPTH = 64;
77
+ export declare const SHELF_TREE_MAX_NODES = 1000;
78
+ export interface ElementAiContext {
79
+ /** 发命令用的 selector,也是 AI 定位这个元素的唯一凭据。 */
80
+ selector: string;
81
+ tag: string;
82
+ /**
83
+ * 有直接子元素 —— **纯显示用**:宿主按它决定芯片上的图标(有子节点=容器图标,
84
+ * 叶子才按类型给)。刻意**不进** `<page-element>` 块:AI 从 outerHtml 就能看出结构,
85
+ * 多一行是噪音。
86
+ *
87
+ * 来自 `ElementCapabilities.childElementCount`,老 agent 不发那个字段时为 false。
88
+ */
89
+ hasChildren: boolean;
90
+ /** 祖先选择器路径(由近及远的反序,即从 body 到自己)。只给 selector,不给它们的 HTML。 */
91
+ ancestors: string[];
92
+ /**
93
+ * **可编辑的后代货架** —— 选中元素子树里的元素,每项一个可直接使用的 selector。
94
+ *
95
+ * 存在的理由:用户常常选的是一个**容器**(一个 section),而真正要改的文字在它的某个
96
+ * 子元素里。只给容器的 selector 时 agent 只能回「请你重新点选那个按钮」——
97
+ * 而它手上明明有 outerHTML、看得见那个按钮。
98
+ *
99
+ * ⚠️ 货架是**授权清单**,不是参考资料:作用域闸只接受「选中元素本身」或货架里逐字
100
+ * 命中的某一项。agent 因此永远不需要、也不允许自己拼子选择器 —— 拼错的 selector
101
+ * 改错元素**不报错**,用户只看到页面花了。
102
+ */
103
+ descendants: AiDescendant[];
104
+ /**
105
+ * **方案锚点的解析结果** —— 只有整页上下文(宿主传了 `anchors`)才有。
106
+ *
107
+ * 存在的理由:AI 建的实验来自一条点名了区块的假设(「B2 首屏的徽章」),而货架是一张
108
+ * 扁平的 selector 表,没有区块身份 —— agent 只能按关键词猜,猜错了也没有任何一道闸会
109
+ * 拦(实测:把首屏那条改到了页底另一个提到同样关键词的段落上)。
110
+ *
111
+ * ⚠️ `resolved: false` 的那几条**也要交出来**。不交的话 agent 以为锚点还在页面上,
112
+ * 会拿一个已经失效的 selector 反复重试;交出来它才知道要在货架里按标题重新找。
113
+ */
114
+ anchors?: ResolvedAnchor[];
115
+ /**
116
+ * 选中元素的几何。**可能缺**:几何帧还没到(刚打开画布)、或元素已不在文档里。
117
+ * 缺的时候 agent 就该避免给绝对定位/固定尺寸的东西 —— 见 skill。
118
+ */
119
+ geometry?: AiElementGeometry;
120
+ /** 已剥 script/noscript 的 outerHTML,超预算则截断。 */
121
+ outerHtml: string;
122
+ /**
123
+ * `'anchors'` = `outerHtml` **不是这个元素本身**,而是方案锚点所在区块的 outerHTML +
124
+ * 页面链接清单的组合(见 `AiHtmlFragment`)。只在首版(body 为根、有锚点)出现;
125
+ * 元素级上下文没有这个键 —— 那份 `outerHtml` 就是选中元素自己。
126
+ * 宿主据此在 `<page-element>` 里写一行 `html-scope:`,agent 才不会把片段当整页读。
127
+ */
128
+ htmlScope?: 'anchors';
129
+ /**
130
+ * 区块 / 元素里的真实图片(见 `AiPageImage`)。宿主写成 `anchor-image:`(整页首版)或
131
+ * `element-image:`(元素级)行。老扩展不认识 `getElementImages`、或这一块里没有图时
132
+ * 这个键不存在。
133
+ */
134
+ images?: AiPageImage[];
135
+ /**
136
+ * **换图会落在哪一支** —— `'img'` = 改 `<img>` 的 src,`'background'` = 改
137
+ * `background-image`。两者都不是(普通文字元素)时**没有这个键**。
138
+ *
139
+ * ⚠️ 为什么必须给:块里只有 outerHTML,而 PageFly / styled-components 这类页面的照片是
140
+ * **CSS 类**设的背景图 —— html 里既没有 `<img>` 也没有内联样式,agent 只能猜自己的
141
+ * `CHANGE_IMAGE` 会不会被闸打回(真机 2026-09-22:它猜对了分支,但全程是在赌)。
142
+ * 这个值来自选中那一刻就已经在手上的能力位(`isImg` / `hasBackgroundImage`),
143
+ * 与闸判分支用的是**同一个函数**(`resolveImageBranch`),不会两边漂移。
144
+ */
145
+ imageTarget?: 'img' | 'background';
146
+ /**
147
+ * `true` = 上面那段被截断了。
148
+ *
149
+ * ⚠️ **必须一并送给 AI**。不说的话它会拿半截 HTML 当全部去下结论 —— 比如断言
150
+ * 「这个容器里只有一个按钮」,而其实后面还有十个被截掉了。
151
+ */
152
+ htmlTruncated: boolean;
153
+ /** 原始字节数(截断时才有意义,用于让 AI 知道少看了多少)。 */
154
+ htmlBytes: number;
155
+ }
156
+ /**
157
+ * 按预算截断 HTML。
158
+ *
159
+ * **从中间截**而不是只留开头:一个元素的开头是标签和属性、结尾是闭合与末尾子节点,
160
+ * 只留开头会让 AI 看到一个永远没有闭合的片段,更容易被误读成「结构不完整的坏 HTML」。
161
+ */
162
+ export declare function truncateHtml(rawHtml: string, budget?: number): {
163
+ text: string;
164
+ truncated: boolean;
165
+ bytes: number;
166
+ };
167
+ /**
168
+ * **首版 html 片段**:方案锚点所在区块的 outerHTML、以及页面已有链接清单。
169
+ *
170
+ * 存在的理由(真机 2026-09-20,vivaia,243KB 的 PageFly 页):首版上下文以 body 为根,整页
171
+ * outerHTML 按 4KB 从中间截,agent 拿到的是 `<div class="route-loader">…` 和页脚 ——
172
+ * 它要改的首屏一个字节都不在。结果是「无法确定购买链接」(CTA 落成不可点的 span)、
173
+ * 颜色/字号只能从截图猜。锚点区块的 outerHTML 才是它要改的东西;链接清单是它被允许
174
+ * 使用的 href 的唯一来源。
175
+ */
176
+ export interface AiHtmlFragment {
177
+ /** 注释头里的名字:区块号(`B1`)或 `links on page`。 */
178
+ label: string;
179
+ /** 这段 HTML 来自哪个元素(链接清单写 `(scan)`)。 */
180
+ selector: string;
181
+ html: string;
182
+ }
183
+ /**
184
+ * 首版时每个锚点区块的裁切截图。与整页截图一起附上:整页图告诉 agent 区块在哪、周围是什么,
185
+ * 裁切图才看得清区块内文字的颜色、字重、按钮的圆角 —— 这些是它要"融进去"的东西。
186
+ */
187
+ export type AnchorShot = {
188
+ blockId?: string;
189
+ label: string;
190
+ /** 实际截的元素(区块容器,见 `blockContainerFor`),不一定等于锚点 selector。 */
191
+ selector: string;
192
+ } & ({
193
+ url: string;
194
+ error?: never;
195
+ }
196
+ /**
197
+ * 没截到:失败原因原文。宿主把它写进 `<page-element>`(`not captured: …`)——
198
+ * 真机 9/21、9/23 有锚点却没有近景图,原因只闪在编辑器提示条上,trace 里无处可查;
199
+ * agent 也因此知道这张图没有,而不是以为自己看全了。
200
+ */
201
+ | {
202
+ error: string;
203
+ url?: never;
204
+ });
205
+ /**
206
+ * 锚点区块里真实图片的 URL(首版专用)。`<img src>` 与 CSS 里的 `background-image` 都在——
207
+ * PageFly 页的首屏照片是类名设的背景,outerHTML 里根本没有,只有计算样式里有(`getElementImages`
208
+ * 命令)。agent 换图时用它当 `referenceImageUrl`(当前这张图的构图/色调),而不是叠着文字的区块截图。
209
+ */
210
+ export interface AiPageImage {
211
+ blockId?: string;
212
+ /** 承载这张图的元素(`<img>` 自己,或设了 background-image 的容器)。 */
213
+ selector: string;
214
+ kind: 'img' | 'background';
215
+ url: string;
216
+ }
217
+ /** 每个片段的预算。一个首屏区块几十行标记通常 1-3KB;塞了 base64 图的会在块内截。 */
218
+ export declare const AI_FRAGMENT_BUDGET_BYTES = 3072;
219
+ /** 所有片段合计的预算 —— 首版是一次性的消息,可以比元素级的 4KB 大,但不能无上限。 */
220
+ export declare const AI_FRAGMENTS_TOTAL_BUDGET_BYTES = 8192;
221
+ /**
222
+ * 给 AI 看的 html 里,把 `srcset` 的值换成 `(N sizes, omitted)`。
223
+ *
224
+ * 那是**同一张图**的一串不同尺寸地址(浏览器按屏宽 / 像素密度挑一张),对 AI 没有信息量,却很占
225
+ * 预算 —— 真机 2026-09-24:B2 商品卡区块 4.6KB 里约 2/3 是 srcset,结构(「今すぐ購入」那段)被截掉。
226
+ * 实际显示的是哪张由 `anchor-image` / `element-image`(currentSrc)与截图给。**属性名保留**:AI 仍看得出这里是响应式图,
227
+ * `<picture><source media>` 这种「不同屏幕显示不同图」的结构与 media 条件也原样在(换图时编辑器与 SDK
228
+ * 都会清掉 srcset / source,所有屏宽统一显示新图 —— 这件事 AI 得看得出来)。只影响给 AI 的文本,
229
+ * 页面与换图行为不受影响。
230
+ */
231
+ export declare function compactSrcsetForAi(html: string): string;
232
+ /**
233
+ * 把片段组合成一段给 agent 读的 html:每块一行注释头(label / selector / 原始字节),块之间
234
+ * 空一行;单块超预算在**块内**从中间截(同 `truncateHtml` 的理由:头是标签和属性、尾是闭合,
235
+ * 只留头会被读成坏 HTML);总预算用完后面的块整块不进,并写明省略了几块 —— 不说的话 agent
236
+ * 会以为方案点名的另一个区块"在页面上不存在"。
237
+ */
238
+ export declare function composeAnchorHtml(fragments: AiHtmlFragment[], opts?: {
239
+ perFragment?: number;
240
+ total?: number;
241
+ }): {
242
+ text: string;
243
+ truncated: boolean;
244
+ bytes: number;
245
+ };
246
+ /**
247
+ * 从整页 html 里抽"可指认的链接":`<a href>` + 可见文本。这是 agent 被允许用于 `href` /
248
+ * `SET_LINK` 的唯一来源(它不许编 URL),而整页 html 本身进不了 prompt。
249
+ *
250
+ * 剔掉:`javascript:` / 纯锚点 `#…`(不是跳转)、没有文本的(只有图的 LINE 浮标之类,
251
+ * agent 认不出是什么)、文本过长的(文章标题不是 CTA)。按 href 去重,保留首次出现的文本。
252
+ */
253
+ export declare function extractPageLinks(html: string, opts?: {
254
+ limit?: number;
255
+ maxTextChars?: number;
256
+ }): Array<{
257
+ href: string;
258
+ text: string;
259
+ }>;
260
+ /** 链接清单 → 一个片段;没有链接就 null(别送一段空 html 让 agent 猜它是什么意思)。 */
261
+ export declare function linksFragment(links: Array<{
262
+ href: string;
263
+ text: string;
264
+ }>): AiHtmlFragment | null;
265
+ /** 货架上限。放太多就是把整棵 DOM 树塞进每轮对话;30 项够覆盖一个区块的可改点。 */
266
+ export declare const AI_DESCENDANT_LIMIT = 30;
267
+ /**
268
+ * **整页**上下文(`onPageContextReady`,以 body 为根)的货架上限。
269
+ *
270
+ * 与元素级那档**刻意分开、也刻意更大**:元素级是「用户已经选好了一个区块,只要它里面的
271
+ * 可改点」;整页那份是 AI 自己挑改哪儿(首版生成),30 项连一个长落地页的区块标题都盖不全,
272
+ * agent 只能去编 selector —— 而编出来的 selector 会被作用域闸原样拒掉。
273
+ *
274
+ * 60 → 100:货架改成「区块优先」(`buildRootShelf`)之后,每个区块是 1 行容器 + 最多 6 行
275
+ * 可改点,一个 10 个区块的落地页就是 70 行;60 会把最后几个区块整段切掉,AI 又回到
276
+ * 「找不到要改的那一块」。100 = 约 13 个区块,长落地页也够;header/nav/footer 各封 5 行,
277
+ * 不会被它们吃掉。
278
+ *
279
+ * 也不能顺手把 `AI_DESCENDANT_LIMIT` 一起调大:那份是**每轮对话**的固定开销,
280
+ * 整页这份一次会话只算一次。
281
+ */
282
+ export declare const ROOT_SHELF_LIMIT = 100;
283
+ /** 货架里文字摘要的长度。够认出「是哪个元素」就行,正文在 outerHtml 里。 */
284
+ export declare const AI_DESCENDANT_TEXT_CHARS = 40;
285
+ export declare function buildElementAiContext(input: {
286
+ selector: string;
287
+ tag: string;
288
+ ancestors: string[];
289
+ outerHtml: string;
290
+ hasChildren?: boolean;
291
+ descendants?: AiDescendant[];
292
+ geometry?: AiElementGeometry;
293
+ budget?: number;
294
+ /**
295
+ * 货架上限。默认元素级那档;整页上下文传 `ROOT_SHELF_LIMIT`。
296
+ *
297
+ * ⚠️ 有锚点行时**必须把它们的条数加上去**:下面这一刀切的是 `descendants`,
298
+ * 锚点行落在刀口之后的话,它们仍然被授权、却在 prompt 里消失了 ——
299
+ * 这是一种不报错的错配(agent 手上没有那个 selector,只能自己编,然后整批被拒)。
300
+ */
301
+ descendantLimit?: number;
302
+ /** 方案锚点的解析结果(整页上下文才有)。空数组 = 宿主给了锚点但一条都没命中。 */
303
+ anchors?: ResolvedAnchor[];
304
+ /**
305
+ * 首版专用:锚点区块的 outerHTML + 链接清单。**非空时取代 `outerHtml`**(整页那段一个字节
306
+ * 都不进 prompt),并标 `htmlScope: 'anchors'`。空数组 = 没解析到任何锚点,等同没传。
307
+ */
308
+ fragments?: AiHtmlFragment[];
309
+ /** 锚点区块 / 选中元素里的真实图片。空数组 = 没有 / 老扩展不认识命令,等同没传。 */
310
+ images?: AiPageImage[];
311
+ /** 换图落在哪一支(见 `ElementAiContext.imageTarget`)。不是图片目标时不传。 */
312
+ imageTarget?: 'img' | 'background';
313
+ }): ElementAiContext;
@@ -0,0 +1,122 @@
1
+ export const AI_HTML_BUDGET_BYTES = 4096;
2
+ export const SHELF_TREE_MAX_DEPTH = 64;
3
+ export const SHELF_TREE_MAX_NODES = 1000;
4
+ export function truncateHtml(rawHtml, budget = AI_HTML_BUDGET_BYTES) {
5
+ const html = compactSrcsetForAi(rawHtml);
6
+ const bytes = new TextEncoder().encode(html).length;
7
+ if (bytes <= budget)
8
+ return { text: html, truncated: false, bytes };
9
+ const half = Math.floor(budget / 2);
10
+ const head = html.slice(0, half);
11
+ const tail = html.slice(-half);
12
+ return {
13
+ text: `${head}\n<!-- …已截断,原始 ${bytes} 字节… -->\n${tail}`,
14
+ truncated: true,
15
+ bytes
16
+ };
17
+ }
18
+ export const AI_FRAGMENT_BUDGET_BYTES = 3072;
19
+ export const AI_FRAGMENTS_TOTAL_BUDGET_BYTES = 8192;
20
+ const SRCSET_ATTR = /(\s(?:srcset|imagesrcset|data-srcset))\s*=\s*(?:"([^"]*)"|'([^']*)')/gi;
21
+ function countSrcsetCandidates(value) {
22
+ const described = value.match(/\S\s+\d+(?:\.\d+)?[wx]\b/g)?.length ?? 0;
23
+ if (described)
24
+ return described;
25
+ return value.split(',').filter(part => part.trim()).length || 1;
26
+ }
27
+ export function compactSrcsetForAi(html) {
28
+ return html.replace(SRCSET_ATTR, (_match, name, dq, sq) => {
29
+ const quote = dq !== undefined ? '"' : "'";
30
+ const n = countSrcsetCandidates(dq ?? sq ?? '');
31
+ return `${name}=${quote}(${n} ${n === 1 ? 'size' : 'sizes'}, omitted)${quote}`;
32
+ });
33
+ }
34
+ export function composeAnchorHtml(fragments, opts = {}) {
35
+ const perFragment = opts.perFragment ?? AI_FRAGMENT_BUDGET_BYTES;
36
+ const total = opts.total ?? AI_FRAGMENTS_TOTAL_BUDGET_BYTES;
37
+ const parts = [];
38
+ let used = 0;
39
+ let truncated = false;
40
+ let bytes = 0;
41
+ let omitted = 0;
42
+ for (const f of fragments) {
43
+ const cut = truncateHtml(f.html, perFragment);
44
+ bytes += cut.bytes;
45
+ if (cut.truncated)
46
+ truncated = true;
47
+ const block = `<!-- ${f.label}: ${f.selector} (${cut.bytes} bytes${cut.truncated ? ', truncated' : ''}) -->\n${cut.text}`;
48
+ const size = new TextEncoder().encode(block).length;
49
+ if (parts.length && used + size > total) {
50
+ omitted++;
51
+ continue;
52
+ }
53
+ parts.push(block);
54
+ used += size;
55
+ }
56
+ if (omitted) {
57
+ truncated = true;
58
+ parts.push(`<!-- …${omitted} more fragment${omitted > 1 ? 's' : ''} omitted (budget)… -->`);
59
+ }
60
+ return { text: parts.join('\n\n'), truncated, bytes };
61
+ }
62
+ const PAGE_LINK_TEXT_CHARS = 40;
63
+ const PAGE_LINK_LIMIT = 8;
64
+ export function extractPageLinks(html, opts = {}) {
65
+ const limit = opts.limit ?? PAGE_LINK_LIMIT;
66
+ const maxText = opts.maxTextChars ?? PAGE_LINK_TEXT_CHARS;
67
+ const out = [];
68
+ const seen = new Set();
69
+ const re = /<a\b([^>]*?)\bhref\s*=\s*(?:"([^"]*)"|'([^']*)')[^>]*>([\s\S]*?)<\/a>/gi;
70
+ let m;
71
+ while ((m = re.exec(html)) && out.length < limit) {
72
+ const href = (m[2] ?? m[3] ?? '').trim();
73
+ if (!href || href.startsWith('#') || /^javascript:/i.test(href) || /^data:/i.test(href))
74
+ continue;
75
+ if (seen.has(href))
76
+ continue;
77
+ const text = m[4]
78
+ .replace(/<[^>]*>/g, ' ')
79
+ .replace(/&nbsp;/g, ' ')
80
+ .replace(/\s+/g, ' ')
81
+ .trim();
82
+ if (!text || text.length > maxText)
83
+ continue;
84
+ seen.add(href);
85
+ out.push({ href, text });
86
+ }
87
+ return out;
88
+ }
89
+ export function linksFragment(links) {
90
+ if (!links.length)
91
+ return null;
92
+ return {
93
+ label: 'links on page',
94
+ selector: '(scan)',
95
+ html: links.map(l => `<a href="${l.href}">${l.text}</a>`).join('\n')
96
+ };
97
+ }
98
+ export const AI_DESCENDANT_LIMIT = 30;
99
+ export const ROOT_SHELF_LIMIT = 100;
100
+ export const AI_DESCENDANT_TEXT_CHARS = 40;
101
+ export function buildElementAiContext(input) {
102
+ const fragments = input.fragments ?? [];
103
+ const images = input.images ?? [];
104
+ const { text, truncated, bytes } = fragments.length
105
+ ? composeAnchorHtml(fragments)
106
+ : truncateHtml(input.outerHtml, input.budget);
107
+ return {
108
+ ...(fragments.length ? { htmlScope: 'anchors' } : {}),
109
+ ...(images.length ? { images } : {}),
110
+ ...(input.imageTarget ? { imageTarget: input.imageTarget } : {}),
111
+ selector: input.selector,
112
+ tag: input.tag,
113
+ hasChildren: input.hasChildren ?? false,
114
+ ancestors: input.ancestors,
115
+ descendants: (input.descendants ?? []).slice(0, input.descendantLimit ?? AI_DESCENDANT_LIMIT),
116
+ ...(input.anchors ? { anchors: input.anchors } : {}),
117
+ ...(input.geometry ? { geometry: input.geometry } : {}),
118
+ outerHtml: text,
119
+ htmlTruncated: truncated,
120
+ htmlBytes: bytes
121
+ };
122
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * 几何帧 → AI 上下文里的几何。
3
+ *
4
+ * 单独一个纯函数而不是内联:这里有两处**容易写错且不报错**的判断 ——
5
+ * 「元素不可见时不给几何」与「纵向要换成文档坐标」。
6
+ */
7
+ import type { CanvasDevice, CanvasGeometryPayload } from '@ptengine/inline-core/protocol';
8
+ import type { AiElementGeometry } from './elementAiContext.js';
9
+ export declare function elementGeometryForAi(payload: CanvasGeometryPayload | null | undefined, device: CanvasDevice): AiElementGeometry | undefined;
@@ -0,0 +1,15 @@
1
+ export function elementGeometryForAi(payload, device) {
2
+ const rect = payload?.selectedViewportRect;
3
+ if (!payload || !rect)
4
+ return undefined;
5
+ if (rect.width <= 0 || rect.height <= 0)
6
+ return undefined;
7
+ return {
8
+ width: Math.round(rect.width),
9
+ height: Math.round(rect.height),
10
+ topInPage: Math.round(rect.y + payload.scrollY),
11
+ viewportWidth: Math.round(payload.viewportWidth),
12
+ viewportHeight: Math.round(payload.viewportHeight),
13
+ device
14
+ };
15
+ }
@@ -0,0 +1,55 @@
1
+ import type { ViewportRect } from '@ptengine/inline-core/protocol';
2
+ /**
3
+ * frame 视口坐标 → 产品页视口坐标。
4
+ *
5
+ * ⚠️ **必须从零写,engage-editor 没有可参考代码,而且它那几处都是坑**:
6
+ * - 它把缩放做在 iframe 内部 `<html>` 的 transform 上并显式设 `frameScale = 1`
7
+ * (`PageIframe.vue:557,565`),overlay 侧**零补偿** —— 所以它根本没有这个问题;
8
+ * - `dom_utils_overlayProperties` 的 scale 分支**只乘 w/h 不乘 left/top**
9
+ * (`goog$modules.util.js:446-455`),照搬会让缩放后位置整体偏移,缩放比越小偏得越多;
10
+ * - `getZoom()` 的终止条件用父页面 document(`:405-418`),跨文档时行为不对。
11
+ *
12
+ * 我们的做法:`zoom` 加在**父页面容器**上,agent 上报的是**未缩放的 frame 视口坐标**,
13
+ * 所以 x/y/w/h **四个都要乘 zoom**,然后叠加 iframe 自身在页面里的偏移
14
+ * (偏移是产品页坐标,**不参与缩放**)。
15
+ */
16
+ export interface FrameToProductContext {
17
+ /** iframe 元素的 `getBoundingClientRect()`(产品页视口坐标)。 */
18
+ iframeRect: {
19
+ left: number;
20
+ top: number;
21
+ };
22
+ zoom: number;
23
+ }
24
+ /**
25
+ * 量出换算所需的上下文。
26
+ *
27
+ * ⚠️ **必须量 iframe 元素本身,不能量它的容器。** `CanvasFrame` 把 iframe 放在一个
28
+ * `justify-content: center` 的 flex 容器里,所以当渲染宽度小于容器宽度时,iframe 是
29
+ * **居中**的、左边留白 `(容器宽 − 渲染宽)/2`。
30
+ *
31
+ * PC 端看不出差别:`zoom = 面板宽 / 1440`,渲染宽恰好等于面板宽、没有留白。
32
+ * **SP 端 `zoom` 被「只缩不放」夹成 1**,渲染宽 390 < 面板宽,于是留白出现 ——
33
+ * 量容器就会让选中框整体左偏正好那么多。这个偏移只在移动端出现,且不报任何错。
34
+ *
35
+ * `zoom` 由调用方给(它与 `CanvasFrame` 内部用同一个公式,见 InlineEditor 的注释)。
36
+ */
37
+ export declare function measureFrameContext(iframe: {
38
+ getBoundingClientRect(): {
39
+ left: number;
40
+ top: number;
41
+ };
42
+ } | null | undefined, zoom: number): FrameToProductContext;
43
+ export declare function frameRectToProductRect(rect: ViewportRect, ctx: FrameToProductContext): ViewportRect;
44
+ /**
45
+ * 反向:产品页视口坐标 → frame 视口坐标。
46
+ * 目前没有消费方(命中测试在 frame 内做),但与正向成对定义能防止将来有人
47
+ * 只写一半、或把除法写成乘法。有 round-trip 测试守着。
48
+ */
49
+ export declare function productPointToFramePoint(point: {
50
+ x: number;
51
+ y: number;
52
+ }, ctx: FrameToProductContext): {
53
+ x: number;
54
+ y: number;
55
+ };
@@ -0,0 +1,18 @@
1
+ export function measureFrameContext(iframe, zoom) {
2
+ const rect = iframe?.getBoundingClientRect();
3
+ return { iframeRect: { left: rect?.left ?? 0, top: rect?.top ?? 0 }, zoom };
4
+ }
5
+ export function frameRectToProductRect(rect, ctx) {
6
+ return {
7
+ x: rect.x * ctx.zoom + ctx.iframeRect.left,
8
+ y: rect.y * ctx.zoom + ctx.iframeRect.top,
9
+ width: rect.width * ctx.zoom,
10
+ height: rect.height * ctx.zoom
11
+ };
12
+ }
13
+ export function productPointToFramePoint(point, ctx) {
14
+ return {
15
+ x: (point.x - ctx.iframeRect.left) / ctx.zoom,
16
+ y: (point.y - ctx.iframeRect.top) / ctx.zoom
17
+ };
18
+ }
@@ -0,0 +1,116 @@
1
+ /**
2
+ * 方案锚点(宿主给的「这一版要改哪个区块」)→ 货架行 + 授权表。
3
+ *
4
+ * 为什么需要这一层:AI 建的 inline 实验来自一条**点名了区块**的假设(「B2 首屏的徽章」),
5
+ * 而编辑器交给 agent 的只有一张扁平的 selector 货架 —— 上面没有任何区块身份。真机 trace 里
6
+ * agent 把「首屏徽章(B2,top:78)」静默重锚到了 `div.pf-62_`(top:1231,B4),只因为那段
7
+ * 文字里出现了同样的关键词;没有任何一道闸会拒绝它,用户只看到改动落在了页面下半部。
8
+ *
9
+ * 这里做两件事:
10
+ * ① 把宿主存下来的锚点**在活页面上重新解析**(selector 优先,失败按 label 在树上找回来);
11
+ * ② 把命中的锚点**连同它的子树**产出成货架行 —— 货架是授权清单,孩子不在货架里的话
12
+ * agent 想改容器里的某个按钮就会整批 `OUT_OF_SCOPE`。
13
+ *
14
+ * ⚠️ 货架与 `ancestorsOf` **同一个出口**(与 `buildRootShelf` 同一条纪律):每一行都在表里、
15
+ * 表里也只有货架里的行。两边一旦漂移就是「同一个 selector 有时能移有时不能,且不报错」。
16
+ */
17
+ import type { DomTreeNode } from '@ptengine/inline-core/protocol';
18
+ import { type AiDescendant, type AiPageImage } from './elementAiContext.js';
19
+ /** 宿主存下来的一个锚点。与 backend `contracts.ts` 的 `InlinePlanAnchor` 逐字同形。 */
20
+ export interface InlinePlanAnchor {
21
+ /** 作者写方案那一刻的内核 selector。 */
22
+ selector: string;
23
+ /** 区块标题 / 首句 —— selector 失效时按它找回来。 */
24
+ label: string;
25
+ /** 页面模型里的区块号(B2…)。纯展示:让 agent 与用户说的是同一个区块。 */
26
+ blockId?: string;
27
+ /**
28
+ * 方案卡上写下的**落法**(2026-09-30):`op` 是编辑器要执行的那个 op;`mode` 只对
29
+ * INSERT_ELEMENT 有意义(before/after = 进锚点的父级、按父级的排法摆;insert/append = 进锚点
30
+ * 自己);`placement` 是一句人话(「h1 正下方、同列」)。
31
+ *
32
+ * 此前锚点只有 selector,意图只活在模型二次转述的 brief 自由文本里 —— 首版把「标题下方」
33
+ * 做成了 after 一个 flex 横排里的 Column。三个都可选:老方案没有;宿主只原样打印给 agent。
34
+ */
35
+ op?: string;
36
+ mode?: string;
37
+ placement?: string;
38
+ /**
39
+ * 该区块(B{n})在活页面上的 member 容器 selector,外层优先。**只在内存里**(宿主从页面模型
40
+ * 补进来),不落库、不与 backend 同形。
41
+ *
42
+ * 存在的理由:锚点常常是区块里的一个叶子/小列(真机:首屏 B1 的锚点是只有 h1+h2 的
43
+ * `div.pf-503_`,照片挂在旁边的 `div.pf-505_` 上)。真实图片与裁切截图要按**区块**取,
44
+ * 否则查到的是"这个文字列里没有图"。html 片段仍按锚点容器取(区块整段太大)。
45
+ */
46
+ blockSelectors?: string[];
47
+ /**
48
+ * 这是用户**最近一次选中的方案**的锚点(只在内存里,宿主补进来)。宿主写成 `latest pick`:打开编辑器
49
+ * 是为了把那个方案加成新版本时,模型要知道改的是这一块,而不是 `plan:` 里旧版本的区块。
50
+ */
51
+ latestPick?: boolean;
52
+ }
53
+ /** 解析结果。`resolved: false` 也要交给宿主 —— 不说的话 agent 以为它还在。 */
54
+ export interface ResolvedAnchor extends InlinePlanAnchor {
55
+ resolved: boolean;
56
+ /** 哪条路成功的。`label` = 原 selector 已失效(客户改版),值得让 agent 知道。 */
57
+ via?: 'selector' | 'label';
58
+ }
59
+ export interface AnchorShelfResult {
60
+ /** 锚点容器行在前、其子树行在后,按锚点顺序。 */
61
+ shelf: AiDescendant[];
62
+ /** 每一行的祖先链,**绝对到 body**(由近及远,不含自己)。 */
63
+ ancestorsOf: Map<string, string[]>;
64
+ anchors: ResolvedAnchor[];
65
+ }
66
+ /**
67
+ * 首版 html 片段该取哪个元素的 outerHTML —— 锚点所在的**区块容器**,不是锚点自己。
68
+ *
69
+ * 锚点是叶子(`h2.pf-119_`,EDIT_TEXT 的落点)时,只给那个 `<h2>` agent 看不到旁边的
70
+ * 证言卡片和按钮,等于没给;锚点本身就是容器(INSERT_ELEMENT 进 `div.pf-23_`)时就是它。
71
+ * 上溯只认**货架里有的容器行**(被授权的区块),跳过中间那些包装 div;`body` 不算区块 ——
72
+ * 走到它就退回锚点自己(宁可给窄的,不给整页)。
73
+ */
74
+ export declare function blockContainerFor(anchor: InlinePlanAnchor | ResolvedAnchor, shelf: AiDescendant[], ancestorsOf: Map<string, string[]>): string;
75
+ /**
76
+ * 锚点往外最近一个**自身**带 CSS 背景图的元素(货架行 `image: 'background'`),到 body 为止。
77
+ *
78
+ * 用途:锚点没有区块容器(`blockSelectors` —— 页面模型没了:刷新过页面 / 不是从分析画布进的编辑器)
79
+ * 时,找真实图片也要够到它。PageFly 页的首屏照片是外层 section 的类名背景,锚点多是里面的文字列
80
+ * (真机 2026-09-24:vivaia 锚点 `div.pf-504_` / `h1.pf-502_`,照片在 `div.pf-505_`),只查锚点
81
+ * 子树就一张图都没有。锚点自己就带背景 → null(它本来就在查找范围里)。
82
+ */
83
+ export declare function backgroundImageAncestorFor(selector: string, shelf: AiDescendant[], ancestorsOf: Map<string, string[]>): string | null;
84
+ /**
85
+ * 把首版找到的真实图片元素(`anchor-image:` 那几行)并进授权货架。
86
+ *
87
+ * 为什么:编辑器只放行货架上**逐字出现**的 selector,而承载图片的元素常常不在货架里 ——
88
+ * PageFly 首屏照片是区块外层容器的 CSS 背景,锚点子树(4 层)与整页货架(按区块挑)都够不到。
89
+ * 首版照「新图:」brief 换首屏图时(2026-09-30 起首版允许生图),`CHANGE_IMAGE` 打在
90
+ * `anchor-image:` 点名的元素上会整条 `OUT_OF_SCOPE`,模型却以为给出的就是合法落点。
91
+ *
92
+ * - 货架里已有的行:补上 `image` 标记(没有才补,不覆盖树上读到的);
93
+ * - 不在的:追加成一行,排在锚点行之后、整页货架之前 —— 与锚点行同一个理由,排在刀口
94
+ * (`descendantLimit`)之后就是「授权了、prompt 里却看不到」。
95
+ *
96
+ * 纯函数、不碰祖先表:调用方先给每个新元素取真实祖先链(货架每一行都必须在表里),只把取到链的
97
+ * 图片交进来。返回 `added` = 新增行数,调用方据此把 `descendantLimit` 抬高同样的量。
98
+ */
99
+ export declare function withImageRows(shelf: AiDescendant[], anchorRowCount: number, images: AiPageImage[]): {
100
+ shelf: AiDescendant[];
101
+ added: number;
102
+ };
103
+ export declare function resolveInlinePlanAnchors(input: {
104
+ anchors: InlinePlanAnchor[];
105
+ /** 已经拉到的整页树(锚点的绝对祖先链、label 回落都从它上面取,不再多一次往返)。 */
106
+ rootTree: DomTreeNode;
107
+ /** 探活。未命中是**正常分支**(resolve 一个 `matched:false`),不是异常。 */
108
+ resolveSelector: (selector: string) => Promise<{
109
+ matched: boolean;
110
+ matchCount: number;
111
+ }>;
112
+ /** 取锚点子树(宿主用 `getDomTree(rootSelector, …)` 包一层)。 */
113
+ getSubtree: (rootSelector: string) => Promise<DomTreeNode>;
114
+ /** 每个锚点最多产出几行子树(不含容器行)。 */
115
+ limitPerAnchor?: number;
116
+ }): Promise<AnchorShelfResult>;