flower-trellis 0.5.2 → 0.5.4
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/enhancements/0.6/.agents/skills/trellis-release/SKILL.md +1 -1
- package/enhancements/0.6/.claude/skills/trellis-release/SKILL.md +1 -1
- package/enhancements/MANIFEST.json +2 -2
- package/enhancements/common/.common/.claude/skills/craft-rpa/SKILL.md +82 -32
- package/enhancements/common/.common/.claude/skills/craft-rpa/recorder/browser-controller.js +772 -0
- package/enhancements/common/.common/.claude/skills/craft-rpa/recorder/config.js +54 -0
- package/enhancements/common/.common/.claude/skills/craft-rpa/recorder/dashboard.html +16 -13
- package/enhancements/common/.common/.claude/skills/craft-rpa/recorder/inject.js +84 -78
- package/enhancements/common/.common/.claude/skills/craft-rpa/recorder/launch.js +103 -212
- package/enhancements/common/.common/.claude/skills/craft-rpa/recorder/logger.js +256 -179
- package/enhancements/common/.common/.claude/skills/craft-rpa/recorder/network-capture.js +339 -0
- package/enhancements/common/.common/.claude/skills/craft-rpa/recorder/package.json +2 -1
- package/enhancements/common/.common/.claude/skills/craft-rpa/recorder/test/browser-controller.test.js +90 -0
- package/enhancements/common/.common/.claude/skills/craft-rpa/recorder/test/browser-smoke.test.js +121 -0
- package/enhancements/common/.common/.claude/skills/craft-rpa/recorder/test/control-http.test.js +100 -0
- package/enhancements/common/.common/.claude/skills/craft-rpa/recorder/test/logger.test.js +14 -0
- package/enhancements/common/.common/.claude/skills/craft-rpa/recorder/test/network-capture.test.js +199 -0
- package/enhancements/common/.common/.claude/skills/craft-rpa/recorder/test/runtime-layout.test.js +96 -0
- package/enhancements/common/.common/.claude/skills/craft-rpa/recorder/test/trace.test.js +36 -0
- package/enhancements/common/.common/.claude/skills/craft-rpa/scripts/control-client.js +95 -0
- package/enhancements/common/.common/.claude/skills/craft-rpa/scripts/jsonl-to-trace.js +122 -5
- package/enhancements/common/.common/.claude/skills/craft-rpa/scripts/package.json +3 -0
- package/enhancements/common/.common/.claude/skills/craft-rpa/scripts/run.sh +107 -40
- package/enhancements/common/.common/.codex/skills/craft-rpa/SKILL.md +82 -32
- package/enhancements/common/.common/.codex/skills/craft-rpa/recorder/browser-controller.js +772 -0
- package/enhancements/common/.common/.codex/skills/craft-rpa/recorder/config.js +54 -0
- package/enhancements/common/.common/.codex/skills/craft-rpa/recorder/dashboard.html +16 -13
- package/enhancements/common/.common/.codex/skills/craft-rpa/recorder/inject.js +84 -78
- package/enhancements/common/.common/.codex/skills/craft-rpa/recorder/launch.js +103 -212
- package/enhancements/common/.common/.codex/skills/craft-rpa/recorder/logger.js +256 -179
- package/enhancements/common/.common/.codex/skills/craft-rpa/recorder/network-capture.js +339 -0
- package/enhancements/common/.common/.codex/skills/craft-rpa/recorder/package.json +2 -1
- package/enhancements/common/.common/.codex/skills/craft-rpa/recorder/test/browser-controller.test.js +90 -0
- package/enhancements/common/.common/.codex/skills/craft-rpa/recorder/test/browser-smoke.test.js +121 -0
- package/enhancements/common/.common/.codex/skills/craft-rpa/recorder/test/control-http.test.js +100 -0
- package/enhancements/common/.common/.codex/skills/craft-rpa/recorder/test/logger.test.js +14 -0
- package/enhancements/common/.common/.codex/skills/craft-rpa/recorder/test/network-capture.test.js +199 -0
- package/enhancements/common/.common/.codex/skills/craft-rpa/recorder/test/runtime-layout.test.js +96 -0
- package/enhancements/common/.common/.codex/skills/craft-rpa/recorder/test/trace.test.js +36 -0
- package/enhancements/common/.common/.codex/skills/craft-rpa/scripts/control-client.js +95 -0
- package/enhancements/common/.common/.codex/skills/craft-rpa/scripts/jsonl-to-trace.js +122 -5
- package/enhancements/common/.common/.codex/skills/craft-rpa/scripts/package.json +3 -0
- package/enhancements/common/.common/.codex/skills/craft-rpa/scripts/run.sh +107 -40
- package/package.json +3 -3
- package/src/builtin-plugins/skill-garden/content-adapter.js +97 -4
- package/src/commands/init.js +13 -2
- package/src/commands/plugin.js +8 -4
- package/src/commands/update.js +7 -1
- package/src/lib/banner.js +10 -1
- package/src/lib/command-completion.js +43 -0
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
const DEFAULT_PORT = 7777;
|
|
2
|
+
const DEFAULT_HOST = '0.0.0.0';
|
|
3
|
+
const DEFAULT_CLIENT_HOST = 'localhost';
|
|
4
|
+
const MAX_NETWORK_BODY_BYTES = 20 * 1024 * 1024;
|
|
5
|
+
const MAX_FALLBACK_PAYLOAD_BYTES = 60 * 1024;
|
|
6
|
+
const MAX_OBSERVE_TEXT_BYTES = 64 * 1024;
|
|
7
|
+
const MAX_OBSERVE_ARIA_BYTES = 64 * 1024;
|
|
8
|
+
const MAX_OBSERVE_METADATA_BYTES = 4 * 1024;
|
|
9
|
+
const MAX_OBSERVE_DOM_BYTES = 5 * 1024 * 1024;
|
|
10
|
+
const MAX_OBSERVE_SCREENSHOT_BYTES = 20 * 1024 * 1024;
|
|
11
|
+
const MAX_OBSERVE_ELEMENTS = 500;
|
|
12
|
+
const MAX_OBSERVE_FRAMES = 100;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* 解析 recorder 端口,非法值回退到默认端口。
|
|
16
|
+
*
|
|
17
|
+
* @param {string|number|undefined} value 候选端口。
|
|
18
|
+
* @return {number} 可用端口。
|
|
19
|
+
*/
|
|
20
|
+
function resolvePort(value = process.env.CRAFT_RPA_PORT) {
|
|
21
|
+
const port = Number.parseInt(String(value || ''), 10);
|
|
22
|
+
return Number.isInteger(port) && port > 0 && port <= 65535 ? port : DEFAULT_PORT;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* 返回浏览器或本机客户端访问 logger 的 HTTP origin。
|
|
27
|
+
*
|
|
28
|
+
* @param {Object} [options] 配置项。
|
|
29
|
+
* @param {string} [options.host] 客户端访问主机名。
|
|
30
|
+
* @param {number} [options.port] logger 端口。
|
|
31
|
+
* @return {string} HTTP origin。
|
|
32
|
+
*/
|
|
33
|
+
function getLoggerOrigin(options = {}) {
|
|
34
|
+
const host = options.host || process.env.CRAFT_RPA_CLIENT_HOST || DEFAULT_CLIENT_HOST;
|
|
35
|
+
const port = resolvePort(options.port);
|
|
36
|
+
return `http://${host}:${port}`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
module.exports = {
|
|
40
|
+
DEFAULT_PORT,
|
|
41
|
+
DEFAULT_HOST,
|
|
42
|
+
DEFAULT_CLIENT_HOST,
|
|
43
|
+
MAX_NETWORK_BODY_BYTES,
|
|
44
|
+
MAX_FALLBACK_PAYLOAD_BYTES,
|
|
45
|
+
MAX_OBSERVE_TEXT_BYTES,
|
|
46
|
+
MAX_OBSERVE_ARIA_BYTES,
|
|
47
|
+
MAX_OBSERVE_METADATA_BYTES,
|
|
48
|
+
MAX_OBSERVE_DOM_BYTES,
|
|
49
|
+
MAX_OBSERVE_SCREENSHOT_BYTES,
|
|
50
|
+
MAX_OBSERVE_ELEMENTS,
|
|
51
|
+
MAX_OBSERVE_FRAMES,
|
|
52
|
+
resolvePort,
|
|
53
|
+
getLoggerOrigin,
|
|
54
|
+
};
|
|
@@ -1347,7 +1347,7 @@ body::before {
|
|
|
1347
1347
|
<span id="last-update"></span>
|
|
1348
1348
|
</div>
|
|
1349
1349
|
<div class="center" id="filter-info">显示 0 / 0</div>
|
|
1350
|
-
<div class="right" id="endpoint-info"
|
|
1350
|
+
<div class="right" id="endpoint-info">本机 recorder · session.jsonl</div>
|
|
1351
1351
|
</footer>
|
|
1352
1352
|
|
|
1353
1353
|
<div class="toast" id="toast"></div>
|
|
@@ -1358,6 +1358,7 @@ body::before {
|
|
|
1358
1358
|
初次进入:优先读取 localStorage;没有则跟随系统 prefers-color-scheme
|
|
1359
1359
|
============================================================ */
|
|
1360
1360
|
const THEME_KEY = 'recorder-theme';
|
|
1361
|
+
document.getElementById('endpoint-info').textContent = `${location.host} · session.jsonl`;
|
|
1361
1362
|
|
|
1362
1363
|
/**
|
|
1363
1364
|
* 应用主题到 <html> 上
|
|
@@ -1394,7 +1395,7 @@ const state = {
|
|
|
1394
1395
|
events: [], // 全部已收事件,按 serverTime 顺序
|
|
1395
1396
|
pages: [], // 浏览器当前打开的 tab 列表
|
|
1396
1397
|
activeTabIdx: 0, // 当前活跃 tab(focus 后更新)
|
|
1397
|
-
|
|
1398
|
+
lastCursor: 0, // 增量拉取游标(logger 生成的单调序号)
|
|
1398
1399
|
filter: 'all', // kind 过滤
|
|
1399
1400
|
search: '', // 关键词搜索
|
|
1400
1401
|
paused: false, // 是否暂停轮询
|
|
@@ -1506,7 +1507,7 @@ async function pollOnce() {
|
|
|
1506
1507
|
if (state.paused) return;
|
|
1507
1508
|
try {
|
|
1508
1509
|
const [eventsResp, pagesResp] = await Promise.all([
|
|
1509
|
-
fetch(state.
|
|
1510
|
+
fetch(state.lastCursor ? `/events?cursor=${encodeURIComponent(state.lastCursor)}` : '/events'),
|
|
1510
1511
|
fetch('/control/pages').catch(() => null),
|
|
1511
1512
|
]);
|
|
1512
1513
|
|
|
@@ -1516,7 +1517,7 @@ async function pollOnce() {
|
|
|
1516
1517
|
if (events.length > 0) {
|
|
1517
1518
|
const startIdx = state.events.length;
|
|
1518
1519
|
state.events.push(...events);
|
|
1519
|
-
state.
|
|
1520
|
+
state.lastCursor = events[events.length - 1]._cursor || state.lastCursor;
|
|
1520
1521
|
// 标记本批为"新事件"用于入场动画
|
|
1521
1522
|
for (let i = 0; i < events.length; i++) {
|
|
1522
1523
|
state.newEventIdxs.add(startIdx + i);
|
|
@@ -1846,10 +1847,15 @@ function selectEvent(idx) {
|
|
|
1846
1847
|
if (e.kind === 'network') {
|
|
1847
1848
|
const items = [];
|
|
1848
1849
|
items.push(['METHOD', e.method || '?']);
|
|
1849
|
-
items.push(['STATUS', String(e.status
|
|
1850
|
+
items.push(['STATUS', String(e.status ?? 'ERR')]);
|
|
1850
1851
|
if (e.requestUrl) items.push(['URL', e.requestUrl]);
|
|
1851
1852
|
if (e.durationMs) items.push(['DURATION', e.durationMs + ' ms']);
|
|
1852
|
-
if (e.
|
|
1853
|
+
if (e.source) items.push(['SOURCE', e.source]);
|
|
1854
|
+
if (e.requestContentType) items.push(['REQUEST TYPE', e.requestContentType]);
|
|
1855
|
+
if (e.requestSize !== undefined) items.push(['REQUEST SIZE', e.requestSize + ' bytes']);
|
|
1856
|
+
if (e.responseContentType) items.push(['RESPONSE TYPE', e.responseContentType]);
|
|
1857
|
+
if (e.responseSize !== undefined) items.push(['RESPONSE SIZE', e.responseSize + ' bytes']);
|
|
1858
|
+
if (e.failure?.errorText || e.error) items.push(['ERROR', e.failure?.errorText || e.error]);
|
|
1853
1859
|
html += `
|
|
1854
1860
|
<div class="detail-section">
|
|
1855
1861
|
<div class="section-label">请求信息</div>
|
|
@@ -1912,7 +1918,7 @@ async function doControl(path, body) {
|
|
|
1912
1918
|
const resp = await fetch('/control/' + path, opts);
|
|
1913
1919
|
if (!resp.ok) {
|
|
1914
1920
|
const err = await resp.json().catch(() => ({}));
|
|
1915
|
-
throw new Error(err.error || `HTTP ${resp.status}`);
|
|
1921
|
+
throw new Error(err.error?.message || err.error || `HTTP ${resp.status}`);
|
|
1916
1922
|
}
|
|
1917
1923
|
return await resp.json();
|
|
1918
1924
|
} catch (e) {
|
|
@@ -2010,17 +2016,14 @@ $('btn-export').onclick = () => {
|
|
|
2010
2016
|
toast('暂无事件可导出');
|
|
2011
2017
|
return;
|
|
2012
2018
|
}
|
|
2013
|
-
const jsonl = state.events.map(e => JSON.stringify(e)).join('\n');
|
|
2014
|
-
const blob = new Blob([jsonl], { type: 'application/x-ndjson' });
|
|
2015
2019
|
const a = document.createElement('a');
|
|
2016
|
-
a.href =
|
|
2020
|
+
a.href = '/session';
|
|
2017
2021
|
a.download = `recorder-${new Date().toISOString().replace(/[:.]/g, '-')}.jsonl`;
|
|
2018
2022
|
a.click();
|
|
2019
|
-
|
|
2020
|
-
toast(`⬇ 已导出 ${state.events.length} 条事件`, 'success');
|
|
2023
|
+
toast(`已请求导出完整 JSONL(${state.events.length} 条摘要)`, 'success');
|
|
2021
2024
|
};
|
|
2022
2025
|
|
|
2023
|
-
// 清空显示(仅前端,磁盘文件不变;保留
|
|
2026
|
+
// 清空显示(仅前端,磁盘文件不变;保留 lastCursor 以免刷出旧事件)
|
|
2024
2027
|
$('btn-clear').onclick = () => {
|
|
2025
2028
|
if (!confirm('清空当前显示?\n\n磁盘文件不受影响,只有新事件会出现。')) return;
|
|
2026
2029
|
state.events = [];
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* 浏览器端事件监听注入脚本 —— RPA 增强版
|
|
3
3
|
*
|
|
4
4
|
* 通过 Playwright 的 addInitScript 在每个页面加载前注入。
|
|
5
|
-
*
|
|
5
|
+
* 监听用户交互、路由变化和异常,优先通过 Playwright binding 写入本地 logger。
|
|
6
6
|
*
|
|
7
7
|
* RPA 友好的元素详情包括:
|
|
8
8
|
* - 多种选择器策略(id / data-testid / aria / role+name / text / css / xpath)
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* 1. 幂等:window.__loggerInjected 防重复绑定。
|
|
19
19
|
* 2. 跳过 about:blank:浏览器内嵌的空白 iframe 没有有用事件,避免噪音。
|
|
20
20
|
* 3. 批量+延迟发送:500ms 或攒满 10 条触发一次 flush。
|
|
21
|
-
* 4.
|
|
21
|
+
* 4. 传输主路径:Playwright binding;不可用时才用 sendBeacon / 原生 fetch 回退。
|
|
22
22
|
* 5. 敏感字段标记:命中 password / 含 password|secret|token|captcha 字段名的元素在 target.sensitive=true;
|
|
23
23
|
* value 默认透传原值(RPA 流程参考所需);设 REDACT_SENSITIVE=true 才替换成 [REDACTED len=N]。
|
|
24
24
|
*/
|
|
@@ -34,7 +34,10 @@
|
|
|
34
34
|
|
|
35
35
|
// ============ 常量 ============
|
|
36
36
|
|
|
37
|
-
const
|
|
37
|
+
const CONFIG = globalThis.__CRAFT_RPA_CONFIG__ || {};
|
|
38
|
+
const LOGGER = CONFIG.loggerUrl || 'http://localhost:7777/log';
|
|
39
|
+
const MAX_FALLBACK_PAYLOAD_BYTES = CONFIG.maxFallbackPayloadBytes || 60 * 1024;
|
|
40
|
+
const NATIVE_FETCH = typeof window.fetch === 'function' ? window.fetch.bind(window) : null;
|
|
38
41
|
|
|
39
42
|
/** 会话 ID:用于把同一个页面 / SPA 流程的事件串起来。整页跳转会产生新的 sessionId(属正常) */
|
|
40
43
|
const SESSION_ID =
|
|
@@ -92,35 +95,98 @@
|
|
|
92
95
|
}
|
|
93
96
|
|
|
94
97
|
/**
|
|
95
|
-
*
|
|
96
|
-
*
|
|
98
|
+
* 通过 HTTP 回退发送事件,并按 keepalive/beacon 的实际限制拆批。
|
|
99
|
+
*
|
|
100
|
+
* @param {Object[]} events 待发送事件。
|
|
101
|
+
*/
|
|
102
|
+
function sendHttpFallback(events) {
|
|
103
|
+
const batches = [];
|
|
104
|
+
let current = [];
|
|
105
|
+
for (const event of events) {
|
|
106
|
+
const candidate = current.concat(event);
|
|
107
|
+
const candidateBody = JSON.stringify(candidate);
|
|
108
|
+
if (new TextEncoder().encode(candidateBody).length <= MAX_FALLBACK_PAYLOAD_BYTES) {
|
|
109
|
+
current = candidate;
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (current.length > 0) batches.push(current);
|
|
113
|
+
const singleBody = JSON.stringify([event]);
|
|
114
|
+
if (new TextEncoder().encode(singleBody).length <= MAX_FALLBACK_PAYLOAD_BYTES) {
|
|
115
|
+
current = [event];
|
|
116
|
+
} else {
|
|
117
|
+
console.warn('[inject] dropped oversized fallback event:', event.kind, event.type);
|
|
118
|
+
current = [{
|
|
119
|
+
sessionId: SESSION_ID,
|
|
120
|
+
clientTime: new Date().toISOString(),
|
|
121
|
+
url: location.href,
|
|
122
|
+
frame: getFrameContext(),
|
|
123
|
+
kind: 'error',
|
|
124
|
+
type: 'transport.drop',
|
|
125
|
+
message: 'fallback event exceeded payload limit',
|
|
126
|
+
droppedKind: event.kind,
|
|
127
|
+
droppedType: event.type,
|
|
128
|
+
}];
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if (current.length > 0) batches.push(current);
|
|
132
|
+
|
|
133
|
+
for (const batch of batches) {
|
|
134
|
+
const body = JSON.stringify(batch);
|
|
135
|
+
try {
|
|
136
|
+
const sent = navigator.sendBeacon
|
|
137
|
+
? navigator.sendBeacon(LOGGER, new Blob([body], { type: 'application/json' }))
|
|
138
|
+
: false;
|
|
139
|
+
if (!sent && NATIVE_FETCH) {
|
|
140
|
+
void NATIVE_FETCH(LOGGER, {
|
|
141
|
+
method: 'POST',
|
|
142
|
+
headers: { 'Content-Type': 'application/json' },
|
|
143
|
+
body,
|
|
144
|
+
keepalive: true,
|
|
145
|
+
}).catch(error => {
|
|
146
|
+
console.warn('[inject] fallback fetch failed:', error.message);
|
|
147
|
+
});
|
|
148
|
+
} else if (!sent) {
|
|
149
|
+
console.warn('[inject] fallback transport unavailable:', batch.length, 'events');
|
|
150
|
+
}
|
|
151
|
+
} catch (e) {
|
|
152
|
+
// 回退失败只能写浏览器控制台,不能把异常抛进业务页面。
|
|
153
|
+
console.warn('[inject] fallback transport failed:', e.message);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* 把队列里的事件发送到 Playwright binding,失败或卸载时走 HTTP 回退。
|
|
160
|
+
*
|
|
161
|
+
* @param {{unloading?:boolean}} [options] 卸载阶段必须使用同步发起的回退通道。
|
|
97
162
|
*/
|
|
98
|
-
function flush() {
|
|
163
|
+
function flush(options = {}) {
|
|
99
164
|
if (queue.length === 0) return;
|
|
100
165
|
const batch = queue.splice(0);
|
|
101
166
|
if (flushTimer) {
|
|
102
167
|
clearTimeout(flushTimer);
|
|
103
168
|
flushTimer = null;
|
|
104
169
|
}
|
|
105
|
-
const body = JSON.stringify(batch);
|
|
106
170
|
try {
|
|
107
|
-
if (
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
171
|
+
if (options.unloading) {
|
|
172
|
+
sendHttpFallback(batch);
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
if (typeof window.__craftRpaAppendEvents === 'function') {
|
|
176
|
+
void Promise.resolve(window.__craftRpaAppendEvents(batch)).catch(error => {
|
|
177
|
+
console.warn('[inject] binding transport failed:', error.message);
|
|
178
|
+
sendHttpFallback(batch);
|
|
115
179
|
});
|
|
180
|
+
} else {
|
|
181
|
+
sendHttpFallback(batch);
|
|
116
182
|
}
|
|
117
183
|
} catch (e) {
|
|
118
|
-
|
|
184
|
+
sendHttpFallback(batch);
|
|
119
185
|
}
|
|
120
186
|
}
|
|
121
187
|
|
|
122
|
-
window.addEventListener('beforeunload', flush);
|
|
123
|
-
window.addEventListener('pagehide', flush);
|
|
188
|
+
window.addEventListener('beforeunload', () => flush({ unloading: true }));
|
|
189
|
+
window.addEventListener('pagehide', () => flush({ unloading: true }));
|
|
124
190
|
|
|
125
191
|
// ============ 上下文:iframe / 视口 ============
|
|
126
192
|
|
|
@@ -653,66 +719,6 @@
|
|
|
653
719
|
});
|
|
654
720
|
}, true);
|
|
655
721
|
|
|
656
|
-
// ============ 网络请求拦截 ============
|
|
657
|
-
|
|
658
|
-
const origFetch = window.fetch;
|
|
659
|
-
window.fetch = async function (...args) {
|
|
660
|
-
const [input, init = {}] = args;
|
|
661
|
-
const url = typeof input === 'string' ? input : input.url;
|
|
662
|
-
const method = init.method || (typeof input !== 'string' && input.method) || 'GET';
|
|
663
|
-
const start = performance.now();
|
|
664
|
-
try {
|
|
665
|
-
const resp = await origFetch.apply(this, args);
|
|
666
|
-
send({
|
|
667
|
-
kind: 'network',
|
|
668
|
-
type: 'fetch',
|
|
669
|
-
method,
|
|
670
|
-
requestUrl: url,
|
|
671
|
-
status: resp.status,
|
|
672
|
-
durationMs: Math.round(performance.now() - start),
|
|
673
|
-
});
|
|
674
|
-
return resp;
|
|
675
|
-
} catch (err) {
|
|
676
|
-
send({
|
|
677
|
-
kind: 'network',
|
|
678
|
-
type: 'fetch',
|
|
679
|
-
method,
|
|
680
|
-
requestUrl: url,
|
|
681
|
-
error: err.message,
|
|
682
|
-
durationMs: Math.round(performance.now() - start),
|
|
683
|
-
});
|
|
684
|
-
throw err;
|
|
685
|
-
}
|
|
686
|
-
};
|
|
687
|
-
|
|
688
|
-
const OrigXHR = window.XMLHttpRequest;
|
|
689
|
-
window.XMLHttpRequest = function () {
|
|
690
|
-
const xhr = new OrigXHR();
|
|
691
|
-
let _method, _url, _start;
|
|
692
|
-
const origOpen = xhr.open;
|
|
693
|
-
xhr.open = function (method, url, ...rest) {
|
|
694
|
-
_method = method;
|
|
695
|
-
_url = url;
|
|
696
|
-
return origOpen.call(this, method, url, ...rest);
|
|
697
|
-
};
|
|
698
|
-
const origSend = xhr.send;
|
|
699
|
-
xhr.send = function (...args) {
|
|
700
|
-
_start = performance.now();
|
|
701
|
-
xhr.addEventListener('loadend', () => {
|
|
702
|
-
send({
|
|
703
|
-
kind: 'network',
|
|
704
|
-
type: 'xhr',
|
|
705
|
-
method: _method,
|
|
706
|
-
requestUrl: _url,
|
|
707
|
-
status: xhr.status,
|
|
708
|
-
durationMs: Math.round(performance.now() - _start),
|
|
709
|
-
});
|
|
710
|
-
});
|
|
711
|
-
return origSend.apply(this, args);
|
|
712
|
-
};
|
|
713
|
-
return xhr;
|
|
714
|
-
};
|
|
715
|
-
|
|
716
722
|
// ============ SPA 路由 ============
|
|
717
723
|
|
|
718
724
|
['pushState', 'replaceState'].forEach(type => {
|