e10-ebuilder-prototype 0.5.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.
- package/README.md +113 -0
- package/dist/api.d.ts +12 -0
- package/dist/api.js +125 -0
- package/dist/application.d.ts +7 -0
- package/dist/application.js +16 -0
- package/dist/archive.d.ts +130 -0
- package/dist/archive.js +151 -0
- package/dist/capture.d.ts +15 -0
- package/dist/capture.js +440 -0
- package/dist/common.d.ts +20 -0
- package/dist/common.js +87 -0
- package/dist/dom.d.mts +1 -0
- package/dist/dom.mjs +58 -0
- package/dist/form-context.d.ts +3 -0
- package/dist/form-context.js +58 -0
- package/dist/form-runtime.d.mts +2 -0
- package/dist/form-runtime.mjs +149 -0
- package/dist/forms.d.ts +51 -0
- package/dist/forms.js +603 -0
- package/dist/html.d.ts +22 -0
- package/dist/html.js +427 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +370 -0
- package/dist/menus.d.ts +32 -0
- package/dist/menus.js +330 -0
- package/dist/model.d.ts +164 -0
- package/dist/model.js +8 -0
- package/dist/offline-store.d.mts +5 -0
- package/dist/offline-store.mjs +80 -0
- package/dist/platform.d.ts +10 -0
- package/dist/platform.js +123 -0
- package/dist/readiness.d.ts +124 -0
- package/dist/readiness.js +529 -0
- package/dist/runtime-support.d.mts +52 -0
- package/dist/runtime-support.mjs +279 -0
- package/dist/site.d.ts +34 -0
- package/dist/site.js +195 -0
- package/dist/store.d.ts +90 -0
- package/dist/store.js +296 -0
- package/dist/templates/form-guide.md +539 -0
- package/dist/templates/index.html +803 -0
- package/dist/templates/placeholder.html +143 -0
- package/dist/templates/workflow-guide.md +95 -0
- package/dist/templates/workflow-presets.json +89 -0
- package/dist/temporary-records.d.ts +15 -0
- package/dist/temporary-records.js +286 -0
- package/dist/vendor/environment-auth.d.ts +61 -0
- package/dist/vendor/environment-auth.js +455 -0
- package/dist/workflow-runtime.d.mts +2 -0
- package/dist/workflow-runtime.mjs +298 -0
- package/dist/workflows.d.ts +28 -0
- package/dist/workflows.js +90 -0
- package/docs/PROTOCOL.md +299 -0
- package/package.json +45 -0
|
@@ -0,0 +1,529 @@
|
|
|
1
|
+
import { CaptureError, digest, sleep } from './common.js';
|
|
2
|
+
// Global search history does not render inside the E10 page (verified 2026-09-11).
|
|
3
|
+
const ignored = /^(?:\/api\/front\/monitor\/|\/api\/em\/msg\/getTopSearchList$)/;
|
|
4
|
+
export class Monitor {
|
|
5
|
+
page;
|
|
6
|
+
origin;
|
|
7
|
+
expectedPage;
|
|
8
|
+
generation = 0;
|
|
9
|
+
lastSample;
|
|
10
|
+
requestStarted = new Map();
|
|
11
|
+
entranceAt = 0;
|
|
12
|
+
pending = new Set();
|
|
13
|
+
processing = new Set();
|
|
14
|
+
processingRequests = new Map();
|
|
15
|
+
lastActivity = Date.now();
|
|
16
|
+
epoch = 0;
|
|
17
|
+
issues = [];
|
|
18
|
+
pageConfig;
|
|
19
|
+
entrance = false;
|
|
20
|
+
constructor(page, origin, expectedPage) {
|
|
21
|
+
this.page = page;
|
|
22
|
+
this.origin = origin;
|
|
23
|
+
this.expectedPage = expectedPage;
|
|
24
|
+
this.pageConfig = expectedPage;
|
|
25
|
+
page.on('request', (r) => {
|
|
26
|
+
if (r.isNavigationRequest() && r.frame() === page.mainFrame()) {
|
|
27
|
+
this.generation++;
|
|
28
|
+
this.pending.clear();
|
|
29
|
+
this.requestStarted.clear();
|
|
30
|
+
this.processing.clear();
|
|
31
|
+
this.processingRequests.clear();
|
|
32
|
+
this.issues = [];
|
|
33
|
+
this.entrance = false;
|
|
34
|
+
this.pageConfig = expectedPage;
|
|
35
|
+
this.touch();
|
|
36
|
+
}
|
|
37
|
+
if (this.blocking(r)) {
|
|
38
|
+
this.pending.add(r);
|
|
39
|
+
this.requestStarted.set(r, Date.now());
|
|
40
|
+
this.touch();
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
page.on('requestfinished', (r) => {
|
|
44
|
+
if (this.pending.delete(r))
|
|
45
|
+
this.touch();
|
|
46
|
+
this.requestStarted.delete(r);
|
|
47
|
+
});
|
|
48
|
+
page.on('requestfailed', (r) => {
|
|
49
|
+
const processing = this.processingRequests.get(r);
|
|
50
|
+
if (processing)
|
|
51
|
+
this.processing.delete(processing);
|
|
52
|
+
this.processingRequests.delete(r);
|
|
53
|
+
if (this.relevant(r))
|
|
54
|
+
this.issue(r, 'NETWORK_FAILED');
|
|
55
|
+
if (this.pending.delete(r)) {
|
|
56
|
+
this.touch();
|
|
57
|
+
this.requestStarted.delete(r);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
page.on('response', (r) => {
|
|
61
|
+
const req = r.request();
|
|
62
|
+
if (!this.relevant(req))
|
|
63
|
+
return;
|
|
64
|
+
if (r.status() >= 400)
|
|
65
|
+
this.issue(req, `HTTP_${r.status()}`);
|
|
66
|
+
if (new URL(r.url()).origin !== origin ||
|
|
67
|
+
!['xhr', 'fetch'].includes(req.resourceType()) ||
|
|
68
|
+
!/json/i.test(r.headers()['content-type'] || ''))
|
|
69
|
+
return;
|
|
70
|
+
const generation = this.generation;
|
|
71
|
+
const promise = (async () => {
|
|
72
|
+
const url = new URL(r.url());
|
|
73
|
+
if (url.origin === origin &&
|
|
74
|
+
['xhr', 'fetch'].includes(req.resourceType()) &&
|
|
75
|
+
/json/i.test(r.headers()['content-type'] || '')) {
|
|
76
|
+
const j = await r.json().catch(() => null);
|
|
77
|
+
if (generation !== this.generation)
|
|
78
|
+
return;
|
|
79
|
+
if (j && typeof j === 'object') {
|
|
80
|
+
if ('code' in j &&
|
|
81
|
+
(j.status === false ||
|
|
82
|
+
j.fail === true ||
|
|
83
|
+
![0, 200, '0', '200', null].includes(j.code)))
|
|
84
|
+
this.issue(req, `BUSINESS_${String(j.code ?? 'FAIL').slice(0, 20)}`);
|
|
85
|
+
if (url.pathname === '/api/ebuilder/page/view/entrance') {
|
|
86
|
+
const terminalMessage = /当前页面不支持(?:PC|移动)端访问/.test(j.msg || '')
|
|
87
|
+
? j.msg
|
|
88
|
+
: undefined;
|
|
89
|
+
if (j.data?.page || terminalMessage) {
|
|
90
|
+
this.entrance = true;
|
|
91
|
+
this.entranceAt = Date.now();
|
|
92
|
+
this.pageConfig = {
|
|
93
|
+
id: j.data?.page?.id,
|
|
94
|
+
layoutType: j.data?.page?.layoutType,
|
|
95
|
+
componentCount: Array.isArray(j.data?.page?.comps)
|
|
96
|
+
? j.data.page.comps.length
|
|
97
|
+
: undefined,
|
|
98
|
+
terminalMessage,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
})()
|
|
105
|
+
.catch(() => {
|
|
106
|
+
if (generation === this.generation)
|
|
107
|
+
this.issue(req, 'RESPONSE_UNREADABLE');
|
|
108
|
+
})
|
|
109
|
+
.finally(() => {
|
|
110
|
+
this.processing.delete(promise);
|
|
111
|
+
this.processingRequests.delete(req);
|
|
112
|
+
if (generation === this.generation && this.blocking(req)) {
|
|
113
|
+
this.touch();
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
if (this.blocking(req)) {
|
|
117
|
+
this.processing.add(promise);
|
|
118
|
+
this.processingRequests.set(req, promise);
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
touch() {
|
|
123
|
+
this.lastActivity = Date.now();
|
|
124
|
+
this.epoch++;
|
|
125
|
+
}
|
|
126
|
+
relevant(r) {
|
|
127
|
+
const u = new URL(r.url());
|
|
128
|
+
return (['document', 'script', 'stylesheet', 'font', 'image', 'xhr', 'fetch'].includes(r.resourceType()) && !ignored.test(u.pathname));
|
|
129
|
+
}
|
|
130
|
+
blocking(r) {
|
|
131
|
+
const u = new URL(r.url());
|
|
132
|
+
let embeddedRequest = false;
|
|
133
|
+
try {
|
|
134
|
+
embeddedRequest = r.frame().url().startsWith(`${this.origin}/sp/chtml/`);
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
/* service workers have no frame */
|
|
138
|
+
}
|
|
139
|
+
// The page's own data and custom components must settle. Global application
|
|
140
|
+
// background requests do not decide whether the displayed page is ready.
|
|
141
|
+
return (this.relevant(r) &&
|
|
142
|
+
u.origin === this.origin &&
|
|
143
|
+
((r.isNavigationRequest() && r.frame() === this.page.mainFrame()) ||
|
|
144
|
+
embeddedRequest ||
|
|
145
|
+
(r.isNavigationRequest() && u.pathname.startsWith('/sp/chtml/')) ||
|
|
146
|
+
(['xhr', 'fetch'].includes(r.resourceType()) &&
|
|
147
|
+
u.pathname.startsWith('/api/bs/ebuilder/form/')) ||
|
|
148
|
+
(['xhr', 'fetch'].includes(r.resourceType()) && u.pathname.startsWith('/api/ebuilder/')) ||
|
|
149
|
+
(['script', 'stylesheet'].includes(r.resourceType()) &&
|
|
150
|
+
u.pathname.startsWith('/ecodestatic/'))));
|
|
151
|
+
}
|
|
152
|
+
issue(r, kind) {
|
|
153
|
+
const u = new URL(r.url());
|
|
154
|
+
const critical = u.origin === this.origin &&
|
|
155
|
+
['document', 'script', 'stylesheet', 'font', 'xhr', 'fetch'].includes(r.resourceType());
|
|
156
|
+
if (this.issues.length < 100 &&
|
|
157
|
+
!this.issues.some((i) => i.path === u.origin + u.pathname && i.kind === kind))
|
|
158
|
+
this.issues.push({ path: u.origin + u.pathname, kind, critical });
|
|
159
|
+
}
|
|
160
|
+
evidence() {
|
|
161
|
+
return {
|
|
162
|
+
lastSample: this.lastSample,
|
|
163
|
+
pending: [...this.pending].map((r) => new URL(r.url()).pathname).slice(0, 20),
|
|
164
|
+
processing: this.processing.size,
|
|
165
|
+
issues: this.issues,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
check() {
|
|
169
|
+
const failures = this.issues.filter((i) => i.critical);
|
|
170
|
+
if (failures.some((i) => /^(HTTP|BUSINESS)_(401|403)$/.test(i.kind)))
|
|
171
|
+
throw new CaptureError('E10_LOGIN_REQUIRED', '页面认证失效,请重新执行 auth set');
|
|
172
|
+
// Stable page error states are screenshot content; preserve them as warnings.
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
export async function sample(page, viewId, requireEntrance, monitor) {
|
|
176
|
+
const data = await page.evaluate(({ viewId, pageConfig }) => {
|
|
177
|
+
const ordinaryRoot = document.getElementById(`ebpage_${viewId}`);
|
|
178
|
+
const placeholders = [...document.querySelectorAll('.ebcoms-empty,.ui-empty')];
|
|
179
|
+
const emptyRoot = pageConfig?.id === viewId && pageConfig.componentCount === 0
|
|
180
|
+
? placeholders.find((e) => e.innerText.trim() === '暂无内容')
|
|
181
|
+
: undefined;
|
|
182
|
+
const terminalRoot = pageConfig?.terminalMessage
|
|
183
|
+
? placeholders.find((e) => e.innerText.includes(pageConfig.terminalMessage))
|
|
184
|
+
: undefined;
|
|
185
|
+
const codeRoot = pageConfig?.id === viewId
|
|
186
|
+
? document.querySelector('.weapp-de-sourcecode-render')
|
|
187
|
+
: null;
|
|
188
|
+
const codeContent = codeRoot &&
|
|
189
|
+
(codeRoot.innerText.trim() || codeRoot.querySelector('iframe,canvas,svg,img,table'));
|
|
190
|
+
const root = ordinaryRoot || emptyRoot || terminalRoot || (codeContent ? codeRoot : null);
|
|
191
|
+
const pageState = emptyRoot && !ordinaryRoot
|
|
192
|
+
? 'empty'
|
|
193
|
+
: terminalRoot && !ordinaryRoot
|
|
194
|
+
? 'terminal-unavailable'
|
|
195
|
+
: 'rendered';
|
|
196
|
+
const visible = (e) => {
|
|
197
|
+
const r = e.getBoundingClientRect();
|
|
198
|
+
let left = r.left, right = r.right, top = r.top, bottom = r.bottom;
|
|
199
|
+
if (r.width <= 0 || r.height <= 0)
|
|
200
|
+
return false;
|
|
201
|
+
for (let p = e; p; p = p.parentElement) {
|
|
202
|
+
const style = getComputedStyle(p);
|
|
203
|
+
if (style.visibility === 'hidden' ||
|
|
204
|
+
style.display === 'none' ||
|
|
205
|
+
Number(style.opacity) === 0)
|
|
206
|
+
return false;
|
|
207
|
+
if (p !== e && p !== document.documentElement && p !== document.body) {
|
|
208
|
+
const b = p.getBoundingClientRect();
|
|
209
|
+
if (/hidden|clip|auto|scroll/.test(style.overflowX)) {
|
|
210
|
+
left = Math.max(left, b.left);
|
|
211
|
+
right = Math.min(right, b.right);
|
|
212
|
+
}
|
|
213
|
+
if (/hidden|clip|auto|scroll/.test(style.overflowY)) {
|
|
214
|
+
top = Math.max(top, b.top);
|
|
215
|
+
bottom = Math.min(bottom, b.bottom);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return right - left > 0.5 && bottom - top > 0.5;
|
|
220
|
+
};
|
|
221
|
+
const inside = (e) => {
|
|
222
|
+
const r = e.getBoundingClientRect();
|
|
223
|
+
return (visible(e) && r.bottom > 0 && r.top < innerHeight && r.right > 0 && r.left < innerWidth);
|
|
224
|
+
};
|
|
225
|
+
const loading = [
|
|
226
|
+
...document.querySelectorAll('[aria-busy="true"],.ui-spin-spinning,.ant-spin-spinning,.ui-loading,.ebpage-loading,.ui-skeleton,[data-capture-loading]'),
|
|
227
|
+
].filter(visible).length;
|
|
228
|
+
const error = [
|
|
229
|
+
...document.querySelectorAll('.ui-message-error,.ant-message-error,.ebcom-error,[data-capture-error]'),
|
|
230
|
+
].filter(visible).length;
|
|
231
|
+
const elements = root ? [root, ...root.querySelectorAll('*')] : [];
|
|
232
|
+
const geometry = elements.filter(visible).map((e) => {
|
|
233
|
+
const r = e.getBoundingClientRect();
|
|
234
|
+
return [
|
|
235
|
+
e.tagName,
|
|
236
|
+
e.id,
|
|
237
|
+
e.getAttribute('data-type'),
|
|
238
|
+
Math.round(r.x),
|
|
239
|
+
Math.round(r.y),
|
|
240
|
+
Math.round(r.width),
|
|
241
|
+
Math.round(r.height),
|
|
242
|
+
e.childElementCount === 0 ? e.textContent : '',
|
|
243
|
+
];
|
|
244
|
+
});
|
|
245
|
+
const components = elements
|
|
246
|
+
.filter((e) => e.matches('[data-type][data-id],.ebcom[id]') && visible(e))
|
|
247
|
+
.map((e) => ({
|
|
248
|
+
id: e.id || e.getAttribute('data-id'),
|
|
249
|
+
type: e.getAttribute('data-type') ||
|
|
250
|
+
[...e.classList].find((c) => c.startsWith('ebcom-')) ||
|
|
251
|
+
'component',
|
|
252
|
+
}));
|
|
253
|
+
const canvases = [...document.querySelectorAll('canvas')].filter(visible).map((c) => {
|
|
254
|
+
try {
|
|
255
|
+
const dst = document.createElement('canvas');
|
|
256
|
+
dst.width = 48;
|
|
257
|
+
dst.height = 48;
|
|
258
|
+
const ctx = dst.getContext('2d');
|
|
259
|
+
ctx.drawImage(c, 0, 0, 48, 48);
|
|
260
|
+
const data = ctx.getImageData(0, 0, 48, 48).data;
|
|
261
|
+
let h = 2166136261;
|
|
262
|
+
for (const n of data)
|
|
263
|
+
h = Math.imul(h ^ n, 16777619);
|
|
264
|
+
return `${c.width}x${c.height}:${h >>> 0}`;
|
|
265
|
+
}
|
|
266
|
+
catch {
|
|
267
|
+
return 'UNREADABLE';
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
const images = [...document.images].filter(inside);
|
|
271
|
+
const broken = images.filter((i) => i.complete && !i.naturalWidth && !!i.currentSrc).length;
|
|
272
|
+
const pendingImages = images.filter((i) => !i.complete).length;
|
|
273
|
+
const finiteAnimations = document
|
|
274
|
+
.getAnimations()
|
|
275
|
+
.filter((a) => a.playState === 'running' && a.effect?.getComputedTiming().iterations !== Infinity).length;
|
|
276
|
+
const virtual = [
|
|
277
|
+
...document.querySelectorAll('[data-capture-virtual],.ReactVirtualized__Grid,.react-window,[class*=virtual-list-holder]'),
|
|
278
|
+
].filter(visible).length;
|
|
279
|
+
const rootSize = root
|
|
280
|
+
? { width: root.getBoundingClientRect().width, height: root.getBoundingClientRect().height }
|
|
281
|
+
: null;
|
|
282
|
+
return {
|
|
283
|
+
timeOrigin: performance.timeOrigin,
|
|
284
|
+
origin: location.origin,
|
|
285
|
+
pathname: location.pathname,
|
|
286
|
+
root: !!root && visible(root),
|
|
287
|
+
boundContent: !!ordinaryRoot ||
|
|
288
|
+
!!(codeContent &&
|
|
289
|
+
pageConfig?.id === viewId &&
|
|
290
|
+
location.pathname.startsWith(`/sp/ebdpage/view/${viewId}/`)),
|
|
291
|
+
pageState,
|
|
292
|
+
rootSize,
|
|
293
|
+
readyState: document.readyState,
|
|
294
|
+
loading,
|
|
295
|
+
error,
|
|
296
|
+
geometry,
|
|
297
|
+
components,
|
|
298
|
+
canvases,
|
|
299
|
+
broken,
|
|
300
|
+
pendingImages,
|
|
301
|
+
fonts: document.fonts.status,
|
|
302
|
+
finiteAnimations,
|
|
303
|
+
virtual,
|
|
304
|
+
width: Math.max(document.documentElement.scrollWidth, document.body.scrollWidth),
|
|
305
|
+
height: Math.max(document.documentElement.scrollHeight, document.body.scrollHeight),
|
|
306
|
+
frames: [...document.querySelectorAll('iframe')].filter(visible).length,
|
|
307
|
+
};
|
|
308
|
+
}, { viewId, pageConfig: monitor.pageConfig });
|
|
309
|
+
for (const r of monitor.pending) {
|
|
310
|
+
const start = monitor.requestStarted.get(r);
|
|
311
|
+
if (start && start < data.timeOrigin) {
|
|
312
|
+
monitor.pending.delete(r);
|
|
313
|
+
monitor.requestStarted.delete(r);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
if (data.origin !== monitor.origin || /\/(login|passport)(\/|$)/i.test(data.pathname))
|
|
317
|
+
throw new CaptureError('E10_LOGIN_REQUIRED', '页面已跳转登录或其他环境');
|
|
318
|
+
monitor.check();
|
|
319
|
+
// A settled business error or a broken image is captured as displayed, with warnings.
|
|
320
|
+
if (data.canvases.includes('UNREADABLE'))
|
|
321
|
+
throw new CaptureError('CANVAS_UNVERIFIABLE', 'Canvas 无法读取,不能验证渲染稳定性');
|
|
322
|
+
if (data.virtual)
|
|
323
|
+
throw new CaptureError('VIRTUAL_CONTENT_UNSUPPORTED', '检测到虚拟滚动列表,无法证明整段内容已渲染');
|
|
324
|
+
const frameStates = [];
|
|
325
|
+
if (data.frames)
|
|
326
|
+
for (const frame of page.frames().slice(1)) {
|
|
327
|
+
const element = await frame.frameElement();
|
|
328
|
+
const shown = await element
|
|
329
|
+
.evaluate((node) => {
|
|
330
|
+
const e = node;
|
|
331
|
+
const r = e.getBoundingClientRect();
|
|
332
|
+
let left = r.left, right = r.right, top = r.top, bottom = r.bottom;
|
|
333
|
+
for (let p = e; p; p = p.parentElement) {
|
|
334
|
+
const s = getComputedStyle(p), b = p.getBoundingClientRect();
|
|
335
|
+
if (s.display === 'none' || s.visibility === 'hidden' || Number(s.opacity) === 0)
|
|
336
|
+
return false;
|
|
337
|
+
if (p !== e && p !== document.body && p !== document.documentElement) {
|
|
338
|
+
if (/hidden|clip|auto|scroll/.test(s.overflowX)) {
|
|
339
|
+
left = Math.max(left, b.left);
|
|
340
|
+
right = Math.min(right, b.right);
|
|
341
|
+
}
|
|
342
|
+
if (/hidden|clip|auto|scroll/.test(s.overflowY)) {
|
|
343
|
+
top = Math.max(top, b.top);
|
|
344
|
+
bottom = Math.min(bottom, b.bottom);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
return right - left > 0.5 && bottom - top > 0.5;
|
|
349
|
+
})
|
|
350
|
+
.finally(() => element.dispose());
|
|
351
|
+
if (!shown)
|
|
352
|
+
continue;
|
|
353
|
+
const state = await frame.evaluate(() => ({
|
|
354
|
+
state: document.readyState,
|
|
355
|
+
fonts: document.fonts.status,
|
|
356
|
+
text: document.body?.innerText || '',
|
|
357
|
+
geometry: [...document.querySelectorAll('*')].flatMap((e) => {
|
|
358
|
+
const r = e.getBoundingClientRect(), s = getComputedStyle(e);
|
|
359
|
+
return r.width > 0 &&
|
|
360
|
+
r.height > 0 &&
|
|
361
|
+
r.bottom > 0 &&
|
|
362
|
+
r.top < innerHeight &&
|
|
363
|
+
s.display !== 'none' &&
|
|
364
|
+
s.visibility !== 'hidden'
|
|
365
|
+
? [
|
|
366
|
+
[
|
|
367
|
+
e.tagName,
|
|
368
|
+
...[r.x, r.y, r.width, r.height].map(Math.round),
|
|
369
|
+
e.childElementCount ? '' : e.textContent,
|
|
370
|
+
],
|
|
371
|
+
]
|
|
372
|
+
: [];
|
|
373
|
+
}),
|
|
374
|
+
width: document.documentElement.scrollWidth,
|
|
375
|
+
height: document.documentElement.scrollHeight,
|
|
376
|
+
images: [...document.images].map((i) => [i.complete, i.naturalWidth]),
|
|
377
|
+
loading: !!document.querySelector('[aria-busy="true"],.ui-spin-spinning'),
|
|
378
|
+
}));
|
|
379
|
+
frameStates.push({
|
|
380
|
+
signature: digest(JSON.stringify(state)),
|
|
381
|
+
ready: state.state === 'complete' &&
|
|
382
|
+
state.fonts === 'loaded' &&
|
|
383
|
+
!state.loading &&
|
|
384
|
+
state.images.every((i) => i[0]),
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
const warnings = [
|
|
388
|
+
...monitor.issues
|
|
389
|
+
.filter((i) => i.critical)
|
|
390
|
+
.map((i) => ({ code: 'PAGE_RESOURCE_ERROR', message: `${i.kind}: ${i.path}` })),
|
|
391
|
+
...(data.error
|
|
392
|
+
? [{ code: 'VISIBLE_PAGE_ERROR', message: '页面本身显示业务错误,按原样截图' }]
|
|
393
|
+
: []),
|
|
394
|
+
...(data.broken
|
|
395
|
+
? [{ code: 'BROKEN_IMAGE', message: `页面存在 ${data.broken} 张可见失效图片,按原样截图` }]
|
|
396
|
+
: []),
|
|
397
|
+
...(data.pageState === 'terminal-unavailable'
|
|
398
|
+
? [
|
|
399
|
+
{
|
|
400
|
+
code: 'TERMINAL_UNAVAILABLE',
|
|
401
|
+
message: '此页面 URL 在当前终端显示不支持访问,已保留原页面提示',
|
|
402
|
+
},
|
|
403
|
+
]
|
|
404
|
+
: []),
|
|
405
|
+
...(data.frames && monitor.pageConfig?.layoutType !== 'ECODE_HTML'
|
|
406
|
+
? [{ code: 'IFRAME_VIEWPORT', message: 'iframe 保留页面设计的显示区域,不展开内嵌外部网站' }]
|
|
407
|
+
: []),
|
|
408
|
+
];
|
|
409
|
+
const { geometry, ...evidence } = data;
|
|
410
|
+
return {
|
|
411
|
+
evidence: { ...evidence, warnings },
|
|
412
|
+
contentSignature: digest(JSON.stringify({
|
|
413
|
+
content: geometry.map((g) => [g[0], g[1], g[2], g[7]]),
|
|
414
|
+
canvases: data.canvases,
|
|
415
|
+
})),
|
|
416
|
+
signature: digest(JSON.stringify({
|
|
417
|
+
geometry,
|
|
418
|
+
canvases: data.canvases,
|
|
419
|
+
frameStates,
|
|
420
|
+
width: data.width,
|
|
421
|
+
height: data.height,
|
|
422
|
+
})),
|
|
423
|
+
ready: data.root &&
|
|
424
|
+
data.readyState !== 'loading' &&
|
|
425
|
+
!data.loading &&
|
|
426
|
+
frameStates.every((f) => f.ready) &&
|
|
427
|
+
!data.pendingImages &&
|
|
428
|
+
data.fonts === 'loaded' &&
|
|
429
|
+
!data.finiteAnimations &&
|
|
430
|
+
(!requireEntrance ||
|
|
431
|
+
data.boundContent ||
|
|
432
|
+
(monitor.entrance && monitor.entranceAt >= data.timeOrigin)) &&
|
|
433
|
+
!monitor.pending.size &&
|
|
434
|
+
!monitor.processing.size,
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
export async function ready(page, viewId, monitor, deadline, stableMs, requireEntrance = true) {
|
|
438
|
+
let previous = '', since = Date.now(), last;
|
|
439
|
+
while (Date.now() < deadline) {
|
|
440
|
+
try {
|
|
441
|
+
last = await sample(page, viewId, requireEntrance, monitor);
|
|
442
|
+
}
|
|
443
|
+
catch (e) {
|
|
444
|
+
if (/Execution context was destroyed|Frame (?:has been|was) detached|Cannot find context with specified id/.test(String(e))) {
|
|
445
|
+
since = Date.now();
|
|
446
|
+
await sleep(150);
|
|
447
|
+
continue;
|
|
448
|
+
}
|
|
449
|
+
throw e;
|
|
450
|
+
}
|
|
451
|
+
monitor.lastSample = {
|
|
452
|
+
...last.evidence,
|
|
453
|
+
ready: last.ready,
|
|
454
|
+
stableForMs: Date.now() - since,
|
|
455
|
+
networkIdleForMs: Date.now() - monitor.lastActivity,
|
|
456
|
+
entrance: monitor.entrance,
|
|
457
|
+
};
|
|
458
|
+
if (last.signature !== previous || !last.ready) {
|
|
459
|
+
previous = last.signature;
|
|
460
|
+
since = Date.now();
|
|
461
|
+
}
|
|
462
|
+
if (last.ready &&
|
|
463
|
+
Date.now() - since >= stableMs &&
|
|
464
|
+
Date.now() - monitor.lastActivity >= stableMs) {
|
|
465
|
+
await page.evaluate(async () => {
|
|
466
|
+
await document.fonts.ready;
|
|
467
|
+
await Promise.all([...document.images]
|
|
468
|
+
.filter((i) => i.complete && i.naturalWidth)
|
|
469
|
+
.map((i) => i.decode().catch(() => { })));
|
|
470
|
+
await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(() => resolve())));
|
|
471
|
+
});
|
|
472
|
+
const confirmed = await sample(page, viewId, requireEntrance, monitor);
|
|
473
|
+
if (confirmed.ready && confirmed.signature === last.signature)
|
|
474
|
+
return confirmed;
|
|
475
|
+
since = Date.now();
|
|
476
|
+
}
|
|
477
|
+
await sleep(150);
|
|
478
|
+
}
|
|
479
|
+
throw new CaptureError('READINESS_TIMEOUT', '等待页面加载/渲染稳定超时', {
|
|
480
|
+
...monitor.evidence(),
|
|
481
|
+
last: last?.evidence,
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
export async function lazyScroll(page, waitReady) {
|
|
485
|
+
const count = await page.evaluate(() => {
|
|
486
|
+
const root = document.scrollingElement || document.documentElement;
|
|
487
|
+
const regions = [
|
|
488
|
+
...(root.scrollHeight > root.clientHeight + 3 ? [root] : []),
|
|
489
|
+
...[...document.querySelectorAll('*')].filter((e) => e !== root &&
|
|
490
|
+
e.clientHeight > 60 &&
|
|
491
|
+
e.clientWidth > 60 &&
|
|
492
|
+
getComputedStyle(e).visibility !== 'hidden' &&
|
|
493
|
+
/(auto|scroll)/.test(getComputedStyle(e).overflowY) &&
|
|
494
|
+
e.scrollHeight > e.clientHeight + 3),
|
|
495
|
+
];
|
|
496
|
+
window.__e10CaptureScrollers = regions;
|
|
497
|
+
return regions.length;
|
|
498
|
+
});
|
|
499
|
+
for (let i = 0; i < count; i++) {
|
|
500
|
+
let endCount = 0;
|
|
501
|
+
for (let step = 0; step < 100; step++) {
|
|
502
|
+
const before = await page.evaluate((i) => {
|
|
503
|
+
const e = window.__e10CaptureScrollers[i];
|
|
504
|
+
const old = e.scrollTop;
|
|
505
|
+
e.style.setProperty('scroll-behavior', 'auto', 'important');
|
|
506
|
+
e.scrollTop = Math.min(e.scrollTop + Math.max(100, e.clientHeight * 0.8), e.scrollHeight);
|
|
507
|
+
return { old, top: e.scrollTop, height: e.scrollHeight };
|
|
508
|
+
}, i);
|
|
509
|
+
await waitReady();
|
|
510
|
+
const after = await page.evaluate((i) => {
|
|
511
|
+
const e = window.__e10CaptureScrollers[i];
|
|
512
|
+
return { end: e.scrollTop + e.clientHeight >= e.scrollHeight - 3, height: e.scrollHeight };
|
|
513
|
+
}, i);
|
|
514
|
+
endCount = after.end && after.height === before.height ? endCount + 1 : 0;
|
|
515
|
+
if (endCount >= 2)
|
|
516
|
+
break;
|
|
517
|
+
if (step === 99 || (!after.end && before.old === before.top))
|
|
518
|
+
throw new CaptureError('SCROLL_LIMIT', '滚动未抵达稳定底部');
|
|
519
|
+
}
|
|
520
|
+
await page.evaluate((i) => {
|
|
521
|
+
const e = window.__e10CaptureScrollers[i];
|
|
522
|
+
e.scrollTop = 0;
|
|
523
|
+
e.scrollLeft = 0;
|
|
524
|
+
}, i);
|
|
525
|
+
}
|
|
526
|
+
if (count)
|
|
527
|
+
await waitReady();
|
|
528
|
+
return count;
|
|
529
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
export function environmentValue(env: any, name: any, platform?: NodeJS.Platform): any;
|
|
2
|
+
export function productStateRoot(env?: NodeJS.ProcessEnv, platform?: NodeJS.Platform, homeDirectory?: string): string;
|
|
3
|
+
export function npmEnvironment(env?: NodeJS.ProcessEnv): {
|
|
4
|
+
[x: string]: string | undefined;
|
|
5
|
+
TZ?: string;
|
|
6
|
+
};
|
|
7
|
+
export function resolveNpmPath({ platform, env, nodePath, fileExists, }?: {
|
|
8
|
+
platform?: NodeJS.Platform | undefined;
|
|
9
|
+
env?: NodeJS.ProcessEnv | undefined;
|
|
10
|
+
nodePath?: string | undefined;
|
|
11
|
+
fileExists?: typeof existsSync | undefined;
|
|
12
|
+
}): string;
|
|
13
|
+
export function processInvocation(command: any, args: any, platform?: NodeJS.Platform, env?: NodeJS.ProcessEnv, nodePath?: string): {
|
|
14
|
+
command: any;
|
|
15
|
+
args: any;
|
|
16
|
+
shell: boolean;
|
|
17
|
+
env: NodeJS.ProcessEnv;
|
|
18
|
+
windowsVerbatimArguments?: undefined;
|
|
19
|
+
} | {
|
|
20
|
+
command: any;
|
|
21
|
+
args: string[];
|
|
22
|
+
shell: boolean;
|
|
23
|
+
env: {
|
|
24
|
+
[x: string]: string | undefined;
|
|
25
|
+
TZ?: string;
|
|
26
|
+
};
|
|
27
|
+
windowsVerbatimArguments: boolean;
|
|
28
|
+
};
|
|
29
|
+
export function processTreeTerminationInvocation(pid: any, platform?: NodeJS.Platform): {
|
|
30
|
+
command: string;
|
|
31
|
+
args: string[];
|
|
32
|
+
shell: boolean;
|
|
33
|
+
} | null;
|
|
34
|
+
export function processAlive(pid: any): boolean;
|
|
35
|
+
export function terminateProcessTree(pid: any): Promise<void>;
|
|
36
|
+
export function runChild(command: any, args: any, { cwd, stdio, timeout, env }?: {
|
|
37
|
+
stdio?: string | undefined;
|
|
38
|
+
timeout?: number | undefined;
|
|
39
|
+
env?: NodeJS.ProcessEnv | undefined;
|
|
40
|
+
}): Promise<any>;
|
|
41
|
+
export function chromeExecutable({ platform, env, homeDirectory, fileExists, }?: {
|
|
42
|
+
platform?: NodeJS.Platform | undefined;
|
|
43
|
+
env?: NodeJS.ProcessEnv | undefined;
|
|
44
|
+
homeDirectory?: string | undefined;
|
|
45
|
+
fileExists?: typeof existsSync | undefined;
|
|
46
|
+
}): string;
|
|
47
|
+
export function renameWithRetry(source: any, target: any, rename?: typeof fs.rename): Promise<void>;
|
|
48
|
+
export function renameWithRetrySync(source: any, target: any): void;
|
|
49
|
+
export function reservedWindowsName(name: any): boolean;
|
|
50
|
+
export function cleanupOwnedDirectory(target: any, parent: any, prefix: any): Promise<void>;
|
|
51
|
+
import { existsSync } from 'node:fs';
|
|
52
|
+
import fs from 'node:fs/promises';
|