Enhance architecture documentation and update project dependencies. Introduce new features for browser recording, page callables, and transform capabilities. Improve build scripts and permissions for better functionality.

This commit is contained in:
go0p
2026-08-06 15:11:35 +08:00
committed by go0p
parent 0371a8b802
commit af5a4db694
125 changed files with 25018 additions and 1675 deletions
@@ -0,0 +1,101 @@
import type { BrowserBusinessFrameHint, BrowserRecordingEvent } from '@/types/models';
const MAX_STACK_EVENTS = 8;
const MAX_STACK_LINES = 16;
const MAX_HINTS = 8;
const RECORDER_FUNCTION = /^recorded[A-Z]|^pauseForDeepCapture$|^stackInfo$/;
interface ParsedStackFrame {
functionName: string;
url?: string;
depth: number;
}
function normalizeFunctionName(value: string): string {
const withoutAlias = value.replace(/\s+\[as\s+[^\]]+\]$/, '').replace(/^(?:async\s+|new\s+)/, '').trim();
const segments = withoutAlias.split('.');
return (segments.at(-1) || withoutAlias || '(anonymous)').replace(/^Object\./, '');
}
function normalizeScriptUrl(value?: string): string | undefined {
if (!value) return undefined;
try {
const url = new URL(value);
url.search = '';
url.hash = '';
return url.toString();
} catch {
return value.split(/[?#]/, 1)[0].slice(0, 2_048) || undefined;
}
}
function dependencyFrame(functionName: string, url?: string): boolean {
const value = `${functionName}\n${url || ''}`.toLowerCase();
return RECORDER_FUNCTION.test(functionName)
|| value.includes('chrome-extension://')
|| value.includes('page-recorder-main-world')
|| value.includes('/node_modules/')
|| value.includes('crypto-js')
|| value.includes('jsencrypt')
|| value.includes('node-forge')
|| value.includes('sm-crypto')
|| value.includes('webpack/runtime');
}
export function parseRecordingStack(stack?: string, fallbackUrl?: string): ParsedStackFrame[] {
if (!stack) return [];
const output: ParsedStackFrame[] = [];
for (const rawLine of stack.split('\n').slice(0, MAX_STACK_LINES)) {
const line = rawLine.trim();
if (!line) continue;
const location = line.match(/((?:(?:https?|file|blob|webpack|chrome-extension):\/\/|\/)[^\s)]+):(\d+):(\d+)\)?$/);
if (!location) continue;
const url = normalizeScriptUrl(location[1] || fallbackUrl);
let prefix = location ? line.slice(0, location.index).trim() : line;
prefix = prefix.replace(/^at\s+/, '').replace(/\($/, '').replace(/@$/, '').trim();
const functionName = normalizeFunctionName(prefix || '(anonymous)');
if (dependencyFrame(functionName, url)) continue;
output.push({ functionName, url, depth: output.length });
}
return output;
}
export function inferBusinessFrameHints(
events: Array<Pick<BrowserRecordingEvent, 'stack' | 'scriptUrl'>>,
): BrowserBusinessFrameHint[] {
const bounded = events.slice(0, MAX_STACK_EVENTS);
if (!bounded.length) return [];
const parsed = bounded.map((event) => parseRecordingStack(event.stack, event.scriptUrl));
if (parsed.some((frames) => !frames.length)) return [];
const evidence = new Map<string, { functionName: string; url?: string; depths: number[]; events: Set<number> }>();
parsed.forEach((frames, eventIndex) => {
const seen = new Set<string>();
for (const frame of frames) {
const key = `${frame.functionName}\n${frame.url || ''}`;
if (seen.has(key)) continue;
seen.add(key);
const current = evidence.get(key) || {
functionName: frame.functionName,
url: frame.url,
depths: [],
events: new Set<number>(),
};
current.depths.push(frame.depth);
current.events.add(eventIndex);
evidence.set(key, current);
}
});
const requiredSupport = bounded.length;
return [...evidence.values()]
.filter((item) => item.events.size === requiredSupport)
.map((item) => ({
functionName: item.functionName.slice(0, 240),
url: item.url?.slice(0, 4_096),
support: item.events.size,
averageDepth: item.depths.reduce((sum, depth) => sum + depth, 0) / Math.max(1, item.depths.length),
}))
.sort((left, right) => left.averageDepth - right.averageDepth
|| left.functionName.localeCompare(right.functionName)
|| (left.url || '').localeCompare(right.url || ''))
.slice(0, MAX_HINTS);
}