@ptengine/inline-editor 0.1.4 → 0.1.5

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/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.5
4
+
5
+ **变更**
6
+ - 截图之后把画布滚回截图前的位置。截图只能拍可视区(元素截图先滚过去、整页图一屏屏滚到底),此前 agent 拍完不滚回来 ——
7
+ 进编辑器首版自动拍整页图 + 锚点近景后,页面停在最后一张近景那里。`captureSelectedElement`、`InlineEditorHandle.capturePage`、
8
+ `PagePreviewHandle.capturePage` 都会滚回;锚点近景几张连拍,只在最后滚回一次。
9
+
10
+ **新增**
11
+ - `useInlineEditor().commands.preserveScroll(run)`:跑完 `run` 把画布滚回开始前的位置(取几何帧的 `scrollY`)。
12
+ 嵌套时只有最外层还原;几何帧还没到(不知道原位置)就不滚;滚回失败不抛。
13
+
3
14
  ## 0.1.4
4
15
 
5
16
  **变更**
@@ -561,20 +561,21 @@ export function InlineEditor({ url, device, onDeviceChange, title, versionLabel
561
561
  fragments.push(links);
562
562
  }
563
563
  const captureAnchors = anchorBlocks.length
564
- ? async () => {
565
- const shots = [];
566
- for (const { anchor, selector } of anchorBlocks) {
567
- const shotSelector = anchor.blockSelectors?.[0] ?? selector;
568
- const shot = await captureAiShot(shotSelector);
569
- const base = {
570
- ...(anchor.blockId ? { blockId: anchor.blockId } : {}),
571
- label: anchor.label,
572
- selector: shotSelector
573
- };
574
- shots.push('url' in shot ? { ...base, url: shot.url } : { ...base, error: shot.error });
575
- }
576
- return shots;
577
- }
564
+ ?
565
+ () => commandsRef.current.preserveScroll(async () => {
566
+ const shots = [];
567
+ for (const { anchor, selector } of anchorBlocks) {
568
+ const shotSelector = anchor.blockSelectors?.[0] ?? selector;
569
+ const shot = await captureAiShot(shotSelector);
570
+ const base = {
571
+ ...(anchor.blockId ? { blockId: anchor.blockId } : {}),
572
+ label: anchor.label,
573
+ selector: shotSelector
574
+ };
575
+ shots.push('url' in shot ? { ...base, url: shot.url } : { ...base, error: shot.error });
576
+ }
577
+ return shots;
578
+ })
578
579
  : undefined;
579
580
  const frame = geometryRef.current;
580
581
  onPageContextReady(buildElementAiContext({
@@ -1,4 +1,4 @@
1
- import { ELEMENT_CONTENT_MODE, type ApplyChangeResult, type CaptureElementResult, type CaptureSlice, type PrepareElementCaptureResult, type SetFixedHiddenResult, type ScrollToSelectorResult, type DomTreeNode, type GetDomTreeParams, type ElementCapabilities, type GetAncestorChainResult, type GetOuterHtmlResult, type ResolveSelectorResult, type ExperienceCode, type SetCodeResult, type MeasureDocumentParams, type MeasureDocumentResult, type SetStyleLayerResult, type ObserveDocumentSizeParams, type ObserveDocumentSizeResult, type QueryElementsParams, type QueryElementsResult, type ElementsAtPointsParams, type ElementsAtPointsResult, type ConfigureParams, type ConfigureResult, type ResolveHeatmapSelectorResult, type ElementsInBandResult, type GetElementImagesResult } from '@ptengine/inline-core/protocol';
1
+ import { ELEMENT_CONTENT_MODE, type ApplyChangeResult, type CaptureElementResult, type CaptureSlice, type PrepareElementCaptureResult, type SetFixedHiddenResult, type ScrollFrameResult, type ScrollToSelectorResult, type DomTreeNode, type GetDomTreeParams, type ElementCapabilities, type GetAncestorChainResult, type GetOuterHtmlResult, type ResolveSelectorResult, type ExperienceCode, type SetCodeResult, type MeasureDocumentParams, type MeasureDocumentResult, type SetStyleLayerResult, type ObserveDocumentSizeParams, type ObserveDocumentSizeResult, type QueryElementsParams, type QueryElementsResult, type ElementsAtPointsParams, type ElementsAtPointsResult, type ConfigureParams, type ConfigureResult, type ResolveHeatmapSelectorResult, type ElementsInBandResult, type GetElementImagesResult } from '@ptengine/inline-core/protocol';
2
2
  import type { InlineChange } from '@ptengine/inline-core/change-format';
3
3
  /**
4
4
  * 六条 frame 命令(含 D9 的 setCode)的产品侧封装。全部走既有的 `sendCanvasCommand`(`chrome.runtime.sendMessage`
@@ -64,6 +64,12 @@ export declare function setHoverTarget(sessionId: string, selector: string): Pro
64
64
  * 算坐标只有 agent 侧摸得到 DOM。
65
65
  */
66
66
  export declare function scrollToSelector(sessionId: string, selector: string): Promise<ScrollToSelectorResult>;
67
+ /**
68
+ * 把画布 frame 滚到文档 y(agent 回报实际滚到哪,到底会被夹住)。
69
+ *
70
+ * 截图分屏由 SW 直接发这条;产品页用它在截图之后把页面滚回截图前的位置(见 `preserveScroll`)。
71
+ */
72
+ export declare function scrollFrame(sessionId: string, y: number): Promise<ScrollFrameResult>;
67
73
  /** 把一条**明文形态**的 change 应用到画布(DEC-22:转译只发生在落盘那一步)。 */
68
74
  export declare function applyChange(sessionId: string, changeId: string, change: InlineChange): Promise<ApplyChangeResult>;
69
75
  export declare function revertChange(sessionId: string, changeId: string): Promise<{
@@ -72,6 +72,9 @@ export function scrollToSelector(sessionId, selector) {
72
72
  selector
73
73
  });
74
74
  }
75
+ export function scrollFrame(sessionId, y) {
76
+ return sendCanvasCommand({ command: 'scrollFrame', sessionId, y });
77
+ }
75
78
  export function applyChange(sessionId, changeId, change) {
76
79
  return sendCanvasCommand({
77
80
  command: 'applyChange',
@@ -131,8 +131,19 @@ export interface UseInlineEditorResult {
131
131
  * 由 agent 的 `textEditCommit` 事件回报,本 hook 收到后自动落一条 change。
132
132
  */
133
133
  startTextEdit: (selector?: string) => Promise<void>;
134
- /** 截选中元素并返回图像字节。**不上传** —— 上传是宿主的事(包里不碰后端端点)。 */
134
+ /**
135
+ * 截选中元素并返回图像字节。**不上传** —— 上传是宿主的事(包里不碰后端端点)。
136
+ * 截完把页面滚回截图前的位置(自带 `preserveScroll`)。
137
+ */
135
138
  captureSelectedElement: (selector: string) => Promise<CaptureOutcome>;
139
+ /**
140
+ * 跑完 `run` 把画布滚回开始前的位置。截图只能拍可视区,所以要滚过去拍、整页图要一屏屏滚到底,
141
+ * 而 agent 拍完不滚回来 —— 进编辑器首版自动拍图时,用户看到页面自己跑到了别处(2026-10-20)。
142
+ *
143
+ * 嵌套时**只有最外层还原**:连拍几张(锚点近景)包一层,页面只在最后滚回一次,不在每张之间来回跳。
144
+ * 几何帧还没到(不知道原位置)就不还原;还原失败不抛。
145
+ */
146
+ preserveScroll: <T>(run: () => Promise<T>) => Promise<T>;
136
147
  /** 复制元素(存 outerHTML 到剪贴板,不产生 change)。 */
137
148
  copyElement: (selector: string) => Promise<void>;
138
149
  /** 粘贴到某个元素的指定位置。**粘贴前会重写防撞号**。 */
@@ -9,7 +9,7 @@ import { runCapture } from './kernel/captureOrchestration.js';
9
9
  import { adaptDomTree, findIdBySelector, graftSubtree } from './tree/domTreeAdapter.js';
10
10
  import { pasteHtml } from './kernel/changelist/clipboard.js';
11
11
  import { isReady, pickTarget, shouldCapturePick, startMove as startMove_ } from './kernel/moveState.js';
12
- import { setPickMode, getAncestorChain, getDomTree, getElementCapabilities, getOuterHtml, captureElement, prepareElementCapture, setFixedHidden, setHoverTarget, scrollToSelector, setSelection, setTextEditing } from './kernel/canvasCommands.js';
12
+ import { setPickMode, getAncestorChain, getDomTree, getElementCapabilities, getOuterHtml, captureElement, prepareElementCapture, scrollFrame, setFixedHidden, setHoverTarget, scrollToSelector, setSelection, setTextEditing } from './kernel/canvasCommands.js';
13
13
  const PICK_MODE_MAX_RETRIES = 2;
14
14
  const PICK_MODE_RETRY_MS = 400;
15
15
  export function useInlineEditor(input) {
@@ -214,7 +214,24 @@ export function useInlineEditor(input) {
214
214
  useEffect(() => {
215
215
  onChangesChange?.(changes);
216
216
  }, [changes, onChangesChange]);
217
+ const latestGeometryRef = useRef(geometry.latest);
218
+ latestGeometryRef.current = geometry.latest;
219
+ const scrollKeepRef = useRef({ depth: 0, y: null });
220
+ const preserveScroll = useCallback(async (run) => {
221
+ const keep = scrollKeepRef.current;
222
+ if (keep.depth++ === 0)
223
+ keep.y = latestGeometryRef.current?.scrollY ?? null;
224
+ try {
225
+ return await run();
226
+ }
227
+ finally {
228
+ if (--keep.depth === 0 && sessionId && keep.y !== null) {
229
+ await scrollFrame(sessionId, keep.y).catch(() => { });
230
+ }
231
+ }
232
+ }, [sessionId]);
217
233
  const commands = useMemo(() => ({
234
+ preserveScroll,
218
235
  select: async (selector) => {
219
236
  if (!sessionId)
220
237
  return;
@@ -292,7 +309,7 @@ export function useInlineEditor(input) {
292
309
  const iframe = input.iframeRef?.current;
293
310
  if (!iframe)
294
311
  throw new Error('no canvas iframe');
295
- return runCapture(selector, {
312
+ return preserveScroll(() => runCapture(selector, {
296
313
  prepare: s => prepareElementCapture(sessionId, s),
297
314
  capture: (rect, slices) => captureElement(sessionId, rect, slices),
298
315
  hideOverlays: async () => {
@@ -312,9 +329,9 @@ export function useInlineEditor(input) {
312
329
  devicePixelRatio: window.devicePixelRatio || 1
313
330
  };
314
331
  }
315
- });
332
+ }));
316
333
  }
317
- }), [graftAt, input.iframeRef, refresh, sessionId]);
334
+ }), [graftAt, input.iframeRef, preserveScroll, refresh, sessionId]);
318
335
  return useMemo(() => ({
319
336
  session,
320
337
  frameProps: session.frameProps,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ptengine/inline-editor",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "description": "Ptengine Inline 编辑器:画布会话编排、图层树、属性面板与组件库(含内部 canvas-ui)",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,