webtape 1.7.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 (48) hide show
  1. package/README.md +99 -0
  2. package/dist/analyzer.d.ts +27 -0
  3. package/dist/analyzer.d.ts.map +1 -0
  4. package/dist/analyzer.js +128 -0
  5. package/dist/analyzer.js.map +1 -0
  6. package/dist/config.d.ts +36 -0
  7. package/dist/config.d.ts.map +1 -0
  8. package/dist/config.js +158 -0
  9. package/dist/config.js.map +1 -0
  10. package/dist/context.d.ts +4 -0
  11. package/dist/context.d.ts.map +1 -0
  12. package/dist/context.js +276 -0
  13. package/dist/context.js.map +1 -0
  14. package/dist/index.d.ts +3 -0
  15. package/dist/index.d.ts.map +1 -0
  16. package/dist/index.js +326 -0
  17. package/dist/index.js.map +1 -0
  18. package/dist/install.d.ts +17 -0
  19. package/dist/install.d.ts.map +1 -0
  20. package/dist/install.js +235 -0
  21. package/dist/install.js.map +1 -0
  22. package/dist/native-host.d.ts +15 -0
  23. package/dist/native-host.d.ts.map +1 -0
  24. package/dist/native-host.js +129 -0
  25. package/dist/native-host.js.map +1 -0
  26. package/dist/rules.d.ts +59 -0
  27. package/dist/rules.d.ts.map +1 -0
  28. package/dist/rules.js +115 -0
  29. package/dist/rules.js.map +1 -0
  30. package/dist/server.d.ts +32 -0
  31. package/dist/server.d.ts.map +1 -0
  32. package/dist/server.js +122 -0
  33. package/dist/server.js.map +1 -0
  34. package/dist/storage.d.ts +48 -0
  35. package/dist/storage.d.ts.map +1 -0
  36. package/dist/storage.js +271 -0
  37. package/dist/storage.js.map +1 -0
  38. package/dist/templates/context.md.ejs +79 -0
  39. package/dist/types.d.ts +103 -0
  40. package/dist/types.d.ts.map +1 -0
  41. package/dist/types.js +2 -0
  42. package/dist/types.js.map +1 -0
  43. package/dist/workspace.d.ts +15 -0
  44. package/dist/workspace.d.ts.map +1 -0
  45. package/dist/workspace.js +128 -0
  46. package/dist/workspace.js.map +1 -0
  47. package/dist/workspace.zip +0 -0
  48. package/package.json +52 -0
package/dist/server.js ADDED
@@ -0,0 +1,122 @@
1
+ import { createServer } from 'node:http';
2
+ import { performance } from 'node:perf_hooks';
3
+ import { join } from 'node:path';
4
+ import { saveRecording } from './storage.js';
5
+ import { analyzeRecording } from './analyzer.js';
6
+ function readBody(req) {
7
+ return new Promise((resolve, reject) => {
8
+ const chunks = [];
9
+ req.on('data', (chunk) => chunks.push(chunk));
10
+ req.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')));
11
+ req.on('error', reject);
12
+ });
13
+ }
14
+ function json(res, status, body) {
15
+ res.writeHead(status, {
16
+ 'Content-Type': 'application/json',
17
+ 'Access-Control-Allow-Origin': '*',
18
+ });
19
+ res.end(JSON.stringify(body));
20
+ }
21
+ function handleCors(req, res) {
22
+ if (req.method === 'OPTIONS') {
23
+ res.writeHead(204, {
24
+ 'Access-Control-Allow-Origin': '*',
25
+ 'Access-Control-Allow-Methods': 'POST, GET, OPTIONS',
26
+ 'Access-Control-Allow-Headers': 'Content-Type',
27
+ 'Access-Control-Max-Age': '86400',
28
+ });
29
+ res.end();
30
+ return true;
31
+ }
32
+ return false;
33
+ }
34
+ export function createWebhookServer(opts) {
35
+ const { port, workspace, autoAnalyze, analyzerBackend, analyzerModel, onReceive, onAnalyzeLog, onAnalyzeDone, onError } = opts;
36
+ const server = createServer(async (req, res) => {
37
+ if (handleCors(req, res))
38
+ return;
39
+ // Health check
40
+ if (req.method === 'GET' && (req.url === '/' || req.url === '/health')) {
41
+ json(res, 200, {
42
+ status: 'ok',
43
+ service: 'webtape-receiver',
44
+ workspace: workspace.root,
45
+ });
46
+ return;
47
+ }
48
+ // Webhook endpoint
49
+ if (req.method === 'POST' && (req.url === '/' || req.url === '/webhook')) {
50
+ try {
51
+ const t0 = performance.now();
52
+ const body = await readBody(req);
53
+ const t1 = performance.now();
54
+ const payload = JSON.parse(body);
55
+ if (!payload.meta || !payload.content) {
56
+ json(res, 400, { error: 'Invalid WebTape payload: missing meta or content' });
57
+ return;
58
+ }
59
+ const t2 = performance.now();
60
+ const sessionDir = saveRecording(workspace, payload);
61
+ const t3 = performance.now();
62
+ const metrics = {
63
+ readBodyMs: t1 - t0,
64
+ parseValidateMs: t2 - t1,
65
+ persistMs: t3 - t2,
66
+ totalNonAiMs: t3 - t0,
67
+ };
68
+ onReceive?.(sessionDir, payload, metrics);
69
+ if (autoAnalyze) {
70
+ runAnalysis(workspace, sessionDir, analyzerBackend, analyzerModel, onAnalyzeLog, onAnalyzeDone, onError);
71
+ }
72
+ json(res, 200, {
73
+ status: 'received',
74
+ session: sessionDir,
75
+ autoAnalyze,
76
+ timingMs: {
77
+ readBody: Math.round(metrics.readBodyMs * 10) / 10,
78
+ parseValidate: Math.round(metrics.parseValidateMs * 10) / 10,
79
+ persist: Math.round(metrics.persistMs * 10) / 10,
80
+ totalNonAi: Math.round(metrics.totalNonAiMs * 10) / 10,
81
+ },
82
+ });
83
+ }
84
+ catch (err) {
85
+ const message = err instanceof Error ? err.message : String(err);
86
+ onError?.(err instanceof Error ? err : new Error(message));
87
+ json(res, 500, { error: message });
88
+ }
89
+ return;
90
+ }
91
+ // Trigger analysis for a specific session
92
+ if (req.method === 'POST' && req.url?.startsWith('/analyze/')) {
93
+ const sessionName = req.url.slice('/analyze/'.length);
94
+ const sessionDir = join(workspace.recordings, sessionName);
95
+ try {
96
+ runAnalysis(workspace, sessionDir, analyzerBackend, analyzerModel, onAnalyzeLog, onAnalyzeDone, onError);
97
+ json(res, 200, { status: 'analysis_started', session: sessionDir });
98
+ }
99
+ catch (err) {
100
+ const message = err instanceof Error ? err.message : String(err);
101
+ json(res, 500, { error: message });
102
+ }
103
+ return;
104
+ }
105
+ json(res, 404, { error: 'Not found' });
106
+ });
107
+ return {
108
+ start: () => new Promise((resolve) => {
109
+ server.listen(port, () => resolve());
110
+ }),
111
+ stop: () => new Promise((resolve, reject) => {
112
+ server.close((err) => (err ? reject(err) : resolve()));
113
+ }),
114
+ server,
115
+ };
116
+ }
117
+ function runAnalysis(workspace, sessionDir, backend, model, onLog, onDone, onError) {
118
+ analyzeRecording({ backend, workspace, sessionDir, model, onLog })
119
+ .then((result) => onDone?.(result))
120
+ .catch((err) => onError?.(err instanceof Error ? err : new Error(String(err))));
121
+ }
122
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,48 @@
1
+ import type { WorkspacePaths } from './workspace.js';
2
+ import type { WebTapePayload } from './types.js';
3
+ /**
4
+ * Extract the full hostname from a URL string.
5
+ * e.g. "https://www.github.com/page" → "www.github.com"
6
+ */
7
+ export declare function extractHostname(url: string): string;
8
+ /**
9
+ * Parse a session directory name back into its components.
10
+ * New format: ${hostname}/${MMDD}-${HHmmss}
11
+ * Legacy format: ${domain}-${MMDD}-${HHmmss}
12
+ */
13
+ export declare function parseSessionName(name: string): {
14
+ domain: string;
15
+ date: string;
16
+ time: string;
17
+ };
18
+ /**
19
+ * Format a time string "HHmmss" → "HH:mm:ss".
20
+ */
21
+ export declare function formatTime(time: string): string;
22
+ /**
23
+ * Persist a webhook payload to the workspace as structured files:
24
+ *
25
+ * recordings/<session>/
26
+ * index.json
27
+ * requests/
28
+ * req_0001.json
29
+ * responses/
30
+ * res_0001.json
31
+ *
32
+ * Returns the absolute path of the session directory.
33
+ */
34
+ export declare function saveRecording(workspace: WorkspacePaths, payload: WebTapePayload): string;
35
+ /**
36
+ * List existing recording sessions sorted newest-first.
37
+ * Supports both new format (hostname/MMDD-HHmmss) and legacy flat format.
38
+ */
39
+ export declare function listRecordings(workspace: WorkspacePaths): string[];
40
+ /**
41
+ * Check whether a recording session has an analysis report.
42
+ */
43
+ export declare function hasAnalysisReport(workspace: WorkspacePaths, sessionName: string): boolean;
44
+ /**
45
+ * List recordings that do NOT have an analysis report.
46
+ */
47
+ export declare function listUnanalyzedRecordings(workspace: WorkspacePaths): string[];
48
+ //# sourceMappingURL=storage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"storage.d.ts","sourceRoot":"","sources":["../src/storage.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AACrD,OAAO,KAAK,EAAE,cAAc,EAAqC,MAAM,YAAY,CAAC;AAwBpF;;;GAGG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAMnD;AAmBD;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAoB7F;AAED;;GAEG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAG/C;AAoBD;;;;;;;;;;;GAWG;AACH,wBAAgB,aAAa,CAC3B,SAAS,EAAE,cAAc,EACzB,OAAO,EAAE,cAAc,GACtB,MAAM,CA4IR;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,SAAS,EAAE,cAAc,GAAG,MAAM,EAAE,CAuClE;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAEzF;AAED;;GAEG;AACH,wBAAgB,wBAAwB,CAAC,SAAS,EAAE,cAAc,GAAG,MAAM,EAAE,CAE5E"}
@@ -0,0 +1,271 @@
1
+ import { mkdirSync, writeFileSync, readdirSync, existsSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { extractSiteUrl, renderAnalysisContext } from './context.js';
4
+ /**
5
+ * Format unix-ms as Asia/Shanghai wall time with milliseconds (+08:00).
6
+ */
7
+ function formatTimestampCST(ts) {
8
+ const d = new Date(ts);
9
+ const parts = new Intl.DateTimeFormat('en-CA', {
10
+ timeZone: 'Asia/Shanghai',
11
+ year: 'numeric',
12
+ month: '2-digit',
13
+ day: '2-digit',
14
+ hour: '2-digit',
15
+ minute: '2-digit',
16
+ second: '2-digit',
17
+ hour12: false,
18
+ fractionalSecondDigits: 3,
19
+ }).formatToParts(d);
20
+ const g = (type) => parts.find((p) => p.type === type)?.value ?? '';
21
+ return `${g('year')}-${g('month')}-${g('day')}T${g('hour')}:${g('minute')}:${g('second')}+08:00`;
22
+ }
23
+ /**
24
+ * Extract the full hostname from a URL string.
25
+ * e.g. "https://www.github.com/page" → "www.github.com"
26
+ */
27
+ export function extractHostname(url) {
28
+ try {
29
+ return new URL(url).hostname;
30
+ }
31
+ catch {
32
+ return 'unknown';
33
+ }
34
+ }
35
+ /**
36
+ * Build the session directory name from the payload.
37
+ * Format: ${hostname}/${MMDD}-${HHmmss}
38
+ * e.g. "www.github.com/0305-123000"
39
+ */
40
+ function sessionDirName(payload) {
41
+ const firstTs = payload.content['index.json'][0]?.timestamp ?? Date.now();
42
+ const d = new Date(firstTs);
43
+ const pad = (n, len = 2) => String(n).padStart(len, '0');
44
+ const hostname = extractHostname(extractSiteUrl(payload)) || 'unknown';
45
+ const datePart = `${pad(d.getMonth() + 1)}${pad(d.getDate())}`;
46
+ const timePart = `${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}`;
47
+ return `${hostname}/${datePart}-${timePart}`;
48
+ }
49
+ /**
50
+ * Parse a session directory name back into its components.
51
+ * New format: ${hostname}/${MMDD}-${HHmmss}
52
+ * Legacy format: ${domain}-${MMDD}-${HHmmss}
53
+ */
54
+ export function parseSessionName(name) {
55
+ const slashIdx = name.lastIndexOf('/');
56
+ if (slashIdx !== -1) {
57
+ // New format: hostname/MMDD-HHmmss
58
+ const domain = name.slice(0, slashIdx);
59
+ const timePart = name.slice(slashIdx + 1);
60
+ const dashIdx = timePart.indexOf('-');
61
+ if (dashIdx !== -1) {
62
+ return { domain, date: timePart.slice(0, dashIdx), time: timePart.slice(dashIdx + 1) };
63
+ }
64
+ return { domain, date: timePart, time: '' };
65
+ }
66
+ // Legacy format: domain-MMDD-HHmmss
67
+ const lastDash = name.lastIndexOf('-');
68
+ const time = name.slice(lastDash + 1);
69
+ const rest = name.slice(0, lastDash);
70
+ const secondLastDash = rest.lastIndexOf('-');
71
+ const date = rest.slice(secondLastDash + 1);
72
+ const domain = rest.slice(0, secondLastDash);
73
+ return { domain, date, time };
74
+ }
75
+ /**
76
+ * Format a time string "HHmmss" → "HH:mm:ss".
77
+ */
78
+ export function formatTime(time) {
79
+ if (time.length !== 6)
80
+ return time;
81
+ return `${time.slice(0, 2)}:${time.slice(2, 4)}:${time.slice(4, 6)}`;
82
+ }
83
+ /**
84
+ * If `body` is a JSON string, parse it into an object so it is saved as
85
+ * nested JSON rather than an escaped string.
86
+ */
87
+ function withParsedJsonBody(entry) {
88
+ if (typeof entry.body === 'string') {
89
+ try {
90
+ const parsed = JSON.parse(entry.body);
91
+ if (typeof parsed === 'object' && parsed !== null) {
92
+ return { ...entry, body: parsed };
93
+ }
94
+ }
95
+ catch {
96
+ // not valid JSON – keep the original string
97
+ }
98
+ }
99
+ return entry;
100
+ }
101
+ /**
102
+ * Persist a webhook payload to the workspace as structured files:
103
+ *
104
+ * recordings/<session>/
105
+ * index.json
106
+ * requests/
107
+ * req_0001.json
108
+ * responses/
109
+ * res_0001.json
110
+ *
111
+ * Returns the absolute path of the session directory.
112
+ */
113
+ export function saveRecording(workspace, payload) {
114
+ const dirName = sessionDirName(payload);
115
+ const sessionDir = join(workspace.recordings, dirName);
116
+ const reqDir = join(sessionDir, 'requests');
117
+ const resDir = join(sessionDir, 'responses');
118
+ const snapshotDir = join(sessionDir, 'snapshots');
119
+ mkdirSync(reqDir, { recursive: true });
120
+ mkdirSync(resDir, { recursive: true });
121
+ mkdirSync(snapshotDir, { recursive: true });
122
+ // Create a mapping of original req_id → sequential suffix 0001, 0002, …
123
+ const allReqIds = Object.keys(payload.content.requests)
124
+ .map((filename) => filename.replace(/\.json$/, ''))
125
+ .sort((a, b) => {
126
+ const nA = parseInt(a.replace(/^req_/, ''), 10);
127
+ const nB = parseInt(b.replace(/^req_/, ''), 10);
128
+ if (!Number.isNaN(nA) && !Number.isNaN(nB))
129
+ return nA - nB;
130
+ return a.localeCompare(b);
131
+ });
132
+ const idMap = new Map();
133
+ allReqIds.forEach((oldId, index) => {
134
+ idMap.set(oldId, String(index + 1).padStart(4, '0'));
135
+ });
136
+ function seqFromReqId(oldReqId) {
137
+ const mapped = idMap.get(oldReqId);
138
+ if (mapped)
139
+ return mapped;
140
+ const m = /^req_(\d+)$/.exec(oldReqId);
141
+ return m ? m[1] : oldReqId.replace(/^req_/, '');
142
+ }
143
+ // Update index.json: remap req_ids and keep detail_path in sync
144
+ const updatedIndex = payload.content['index.json'].map((block) => ({
145
+ ...block,
146
+ triggered_network: block.triggered_network?.map((net) => {
147
+ const seq = seqFromReqId(net.req_id);
148
+ const newReqId = `req_${seq}`;
149
+ return {
150
+ ...net,
151
+ req_id: newReqId,
152
+ detail_path: {
153
+ request: `requests/${newReqId}.json`,
154
+ response: `responses/res_${seq}.json`,
155
+ },
156
+ };
157
+ }),
158
+ }));
159
+ const savedTimeline = updatedIndex.map((block) => {
160
+ const { timestamp, timestamp_cst, ...rest } = block;
161
+ return {
162
+ ...rest,
163
+ timestamp: timestamp_cst ?? formatTimestampCST(timestamp),
164
+ };
165
+ });
166
+ const savedIndex = {
167
+ meta: {
168
+ version: payload.meta.version,
169
+ recording_started_at_cst: payload.meta.recording_started_at_cst,
170
+ recording_started_at_epoch_ms: payload.meta.recording_started_at_epoch_ms,
171
+ recording_ended_at_cst: payload.meta.recording_ended_at_cst,
172
+ recording_ended_at_epoch_ms: payload.meta.recording_ended_at_epoch_ms,
173
+ },
174
+ timeline: savedTimeline,
175
+ };
176
+ writeFileSync(join(sessionDir, 'index.json'), JSON.stringify(savedIndex, null, 2), 'utf-8');
177
+ const remappedRequests = {};
178
+ const remappedResponses = {};
179
+ for (const [filename, data] of Object.entries(payload.content.requests)) {
180
+ const oldStem = filename.replace(/\.json$/, '');
181
+ const seq = idMap.get(oldStem) ?? seqFromReqId(oldStem);
182
+ const newReqId = `req_${seq}`;
183
+ const newFilename = `${newReqId}.json`;
184
+ const parsedReq = withParsedJsonBody(data);
185
+ const enrichedData = { ...parsedReq, req_id: newReqId, _original_id: oldStem };
186
+ remappedRequests[newFilename] = enrichedData;
187
+ writeFileSync(join(reqDir, newFilename), JSON.stringify(enrichedData, null, 2), 'utf-8');
188
+ }
189
+ for (const [filename, data] of Object.entries(payload.content.responses)) {
190
+ const oldResStem = filename.replace(/\.json$/, '');
191
+ const oldReqStem = oldResStem.startsWith('res_')
192
+ ? oldResStem.replace(/^res_/, 'req_')
193
+ : oldResStem;
194
+ const seq = idMap.get(oldReqStem) ?? seqFromReqId(oldReqStem);
195
+ const newFilename = `res_${seq}.json`;
196
+ const pairedReqId = `req_${seq}`;
197
+ const parsedRes = withParsedJsonBody(data);
198
+ const resPayload = { ...parsedRes, req_id: pairedReqId };
199
+ remappedResponses[newFilename] = resPayload;
200
+ writeFileSync(join(resDir, newFilename), JSON.stringify(resPayload, null, 2), 'utf-8');
201
+ }
202
+ if (payload.content.snapshots) {
203
+ for (const [contextId, snapshotText] of Object.entries(payload.content.snapshots)) {
204
+ writeFileSync(join(snapshotDir, `snapshot_${contextId}.md`), snapshotText, 'utf-8');
205
+ }
206
+ }
207
+ const siteUrl = extractSiteUrl(payload);
208
+ const contextPayload = {
209
+ ...payload,
210
+ content: {
211
+ ...payload.content,
212
+ 'index.json': updatedIndex,
213
+ requests: remappedRequests,
214
+ responses: remappedResponses,
215
+ },
216
+ };
217
+ writeFileSync(join(sessionDir, '_context.md'), renderAnalysisContext(contextPayload, siteUrl), 'utf-8');
218
+ return sessionDir;
219
+ }
220
+ /**
221
+ * List existing recording sessions sorted newest-first.
222
+ * Supports both new format (hostname/MMDD-HHmmss) and legacy flat format.
223
+ */
224
+ export function listRecordings(workspace) {
225
+ try {
226
+ const results = [];
227
+ const topDirs = readdirSync(workspace.recordings, { withFileTypes: true })
228
+ .filter((d) => d.isDirectory());
229
+ for (const dir of topDirs) {
230
+ const dirPath = join(workspace.recordings, dir.name);
231
+ // Legacy flat format: session dir contains index.json directly
232
+ if (existsSync(join(dirPath, 'index.json'))) {
233
+ results.push(dir.name);
234
+ continue;
235
+ }
236
+ // New format: hostname dir contains session subdirectories
237
+ const subDirs = readdirSync(dirPath, { withFileTypes: true })
238
+ .filter((d) => d.isDirectory());
239
+ for (const sub of subDirs) {
240
+ if (existsSync(join(dirPath, sub.name, 'index.json'))) {
241
+ results.push(`${dir.name}/${sub.name}`);
242
+ }
243
+ }
244
+ }
245
+ // Sort by date-time newest-first
246
+ results.sort((a, b) => {
247
+ const aParsed = parseSessionName(a);
248
+ const bParsed = parseSessionName(b);
249
+ const aKey = `${aParsed.date}-${aParsed.time}`;
250
+ const bKey = `${bParsed.date}-${bParsed.time}`;
251
+ return bKey.localeCompare(aKey);
252
+ });
253
+ return results;
254
+ }
255
+ catch {
256
+ return [];
257
+ }
258
+ }
259
+ /**
260
+ * Check whether a recording session has an analysis report.
261
+ */
262
+ export function hasAnalysisReport(workspace, sessionName) {
263
+ return existsSync(join(workspace.recordings, sessionName, 'analysis_report.md'));
264
+ }
265
+ /**
266
+ * List recordings that do NOT have an analysis report.
267
+ */
268
+ export function listUnanalyzedRecordings(workspace) {
269
+ return listRecordings(workspace).filter((name) => !hasAnalysisReport(workspace, name));
270
+ }
271
+ //# sourceMappingURL=storage.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"storage.js","sourceRoot":"","sources":["../src/storage.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,79 @@
1
+ # 录制数据分析上下文
2
+
3
+ > 此文件由 webtape-receiver 自动生成,将录制的时间线、请求和响应整合为单一文档。
4
+ > 已过滤无关请求头(缓存、安全策略、编码等);综述中不展示响应头。超过长度阈值的请求/响应体不在此展开,并附带按体积分级的提示;完整内容请查阅 requests/ 与 responses/ 目录。
5
+
6
+ ## 元数据
7
+
8
+ - 导出时间: <%- meta.timestamp %>(epoch_ms: <%- meta.epoch %>)
9
+ <% if (meta.recording_started_at_cst) { -%>
10
+ - 录制开始: <%- meta.recording_started_at_cst %><% if (meta.recording_started_at_epoch_ms != null) { %>(epoch_ms: <%- meta.recording_started_at_epoch_ms %>)<% } %>
11
+ <% } -%>
12
+ <% if (meta.recording_ended_at_cst) { -%>
13
+ - 录制结束: <%- meta.recording_ended_at_cst %><% if (meta.recording_ended_at_epoch_ms != null) { %>(epoch_ms: <%- meta.recording_ended_at_epoch_ms %>)<% } %>
14
+ <% } -%>
15
+ <% if (meta.hostname) { -%>
16
+ - 网站: <%- meta.hostname %>
17
+ <% } -%>
18
+ <% if (siteUrl) { -%>
19
+ - 入口 URL: <%- siteUrl %>
20
+ <% } -%>
21
+ - 时间线条目: <%- timeline.length %>
22
+ - 请求总数: <%- requestCount %>
23
+
24
+ ## 操作时间线与接口详情
25
+
26
+ <% timeline.forEach((block, i) => { -%>
27
+ ### [<%- i + 1 %>] <%- describeBlock(block) %>
28
+
29
+ <% if (block.state) { -%>
30
+ <% if (block.state.type === 'URL_CHANGE') { -%>
31
+ - 变更前 → 当前 URL: <%- block.state.url %>
32
+ <% if (block.state.title) { -%>
33
+ - 页面标题: <%- block.state.title %>
34
+ <% } -%>
35
+ <% } else { -%>
36
+ - URL: <%- block.state.url %>
37
+ - 标题: <%- block.state.title %>
38
+ <% } -%>
39
+
40
+ <% } -%>
41
+ <% if (block.action) { -%>
42
+ - 操作类型: <%- block.action.type %>
43
+ - 目标元素: `<%- block.action.tag %><%- block.action.id ? '#' + block.action.id : '' %>`<%- block.action.target_element ? ' — ' + block.action.target_element : '' %>
44
+ <% if (block.action.aria_label) { -%>
45
+ - aria-label: <%- block.action.aria_label %>
46
+ <% } -%>
47
+
48
+ <% } -%>
49
+ <% if (block.triggered_network && block.triggered_network.length > 0) { -%>
50
+ **触发的网络请求 (<%- block.triggered_network.length %> 个):**
51
+
52
+ <% block.triggered_network.forEach((net) => { -%>
53
+ <%- renderNetworkEntry(net) %>
54
+ <% }); -%>
55
+ <% } -%>
56
+ <% if (block.snapshot_id) { -%>
57
+ <% const _snapshotText = snapshots[block.snapshot_id]; -%>
58
+ <details>
59
+ <summary>操作后页面状态 (a11y tree 快照 → <code>snapshots/snapshot_<%- block.snapshot_id %>.md</code><%= _snapshotText ? ', ' + _snapshotText.length + ' chars' : '' %>)</summary>
60
+
61
+ <% if (_snapshotText) { -%>
62
+ ```
63
+ <%- _snapshotText %>
64
+ ```
65
+ <% } -%>
66
+
67
+ </details>
68
+
69
+ <% } -%>
70
+ ---
71
+
72
+ <% }); -%>
73
+ <% if (orphanRequests.length > 0) { -%>
74
+ ## 其他网络请求(未关联到操作时间线)
75
+
76
+ <% orphanRequests.forEach((req) => { -%>
77
+ - **[<%- req.id %>] <%- req.method %> <%- req.url %>** → <%- req.status %><%- req.sizeHint %>
78
+ <% }); -%>
79
+ <% } -%>
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Webhook payload sent by the WebTape Chrome extension (background.js).
3
+ */
4
+ export interface WebTapePayload {
5
+ meta: {
6
+ timestamp: string;
7
+ epoch: number;
8
+ version: string;
9
+ source: string;
10
+ hostname?: string;
11
+ /** 插件开始录制(INITIAL 块就绪)的 wall time */
12
+ recording_started_at_cst?: string;
13
+ recording_started_at_epoch_ms?: number;
14
+ /** 插件结束录制(进入导出)的 wall time */
15
+ recording_ended_at_cst?: string;
16
+ recording_ended_at_epoch_ms?: number;
17
+ };
18
+ content: {
19
+ 'index.json': ContextBlock[];
20
+ /** context_id → a11y tree text for each post-action snapshot */
21
+ snapshots?: Record<string, string>;
22
+ requests: Record<string, RequestEntry>;
23
+ responses: Record<string, ResponseEntry>;
24
+ };
25
+ }
26
+ /**
27
+ * ContextBlock as written to disk: timestamp is converted from unix ms to a
28
+ * local-timezone ISO string (e.g. "2026-03-20T14:30:45.123+08:00").
29
+ */
30
+ export type SavedContextBlock = Omit<ContextBlock, 'timestamp'> & {
31
+ timestamp: string;
32
+ };
33
+ /**
34
+ * Shape of the index.json file written to disk for each recording session.
35
+ * meta.json is not written separately; only version (unique to meta) is kept here.
36
+ * All other meta fields (epoch, timestamp, source, hostname) are derivable from timeline.
37
+ */
38
+ export interface SavedIndexFile {
39
+ meta: {
40
+ version: string;
41
+ recording_started_at_cst?: string;
42
+ recording_started_at_epoch_ms?: number;
43
+ recording_ended_at_cst?: string;
44
+ recording_ended_at_epoch_ms?: number;
45
+ };
46
+ timeline: SavedContextBlock[];
47
+ }
48
+ export interface ContextBlock {
49
+ context_id: string;
50
+ timestamp: number;
51
+ /** Asia/Shanghai (+08:00) instant from the extension, e.g. 2026-03-28T15:30:45.123+08:00 */
52
+ timestamp_cst?: string;
53
+ state?: {
54
+ type: string;
55
+ url: string;
56
+ title: string;
57
+ fav_icon_url: string;
58
+ a11y_tree_summary: string;
59
+ };
60
+ action?: {
61
+ type: string;
62
+ target_element: string;
63
+ tag: string;
64
+ id: string;
65
+ aria_label: string;
66
+ };
67
+ triggered_network?: NetworkSummary[] | null;
68
+ /** ID pointing to the post-action a11y snapshot file: snapshots/snapshot_${snapshot_id}.md */
69
+ snapshot_id?: string | null;
70
+ }
71
+ export interface NetworkSummary {
72
+ req_id: string;
73
+ method: string;
74
+ url: string;
75
+ status: number | null;
76
+ type: 'http' | 'sse' | 'websocket';
77
+ detail_path: {
78
+ request: string;
79
+ response: string;
80
+ };
81
+ response_body_bytes?: number;
82
+ }
83
+ export interface RequestEntry {
84
+ req_id: string;
85
+ /** Asia/Shanghai (+08:00), request start wall time */
86
+ timestamp_cst?: string;
87
+ type: 'http' | 'sse' | 'websocket';
88
+ method: string;
89
+ url: string;
90
+ headers: Record<string, string>;
91
+ body: string | null;
92
+ }
93
+ export interface ResponseEntry {
94
+ req_id: string;
95
+ /** Asia/Shanghai (+08:00), response completion wall time when available */
96
+ timestamp_cst?: string;
97
+ type: 'http' | 'sse' | 'websocket';
98
+ status: number | null;
99
+ headers: Record<string, string> | null;
100
+ mime_type?: string;
101
+ body: string | object | null;
102
+ }
103
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map