- sites/spiders: Haidan/HDDolby/RousiPro/SunnyPT/TNode/TorrentLeech/Yema (+mTorrent wrapper), specialized SiteAdapter, indirect download resolved at download time - sites/user-parsers: SiteParserBase + all 20 MP SiteSchema parsers (lxml XPath via xpath+xmldom), refreshSiteUserData with schema fallback - sites/definitions: zod SiteDefinition schemas, 23 MovieClaw v0.27.0 (MIT) definitions via scripts/mp-sync/movieclaw-import.mjs, registry getSiteDefinition/listSiteDefinitions, NOTICE - scripts/mp-sync: ledger.json (upstream sha256), check.mjs, build-ledger.mjs; docs/porting/mp-sync-ledger.md - license-and-provenance: add copied-mit kind, re-anchor MovieClaw rows to v0.27.0 Co-Authored-By: Claude Opus 5.5 <[email protected]>
98 lines
3.4 KiB
TypeScript
98 lines
3.4 KiB
TypeScript
/**
|
|
* 路由表式 fetch 替身(站点用户信息解析器 / API spider 测试用)。
|
|
* 路由键:`METHOD path?query`(query 可省略,省略时匹配任意 query;METHOD 省略为任意方法);
|
|
* 值为响应描述或函数。未命中返回 404。记录全部请求(含头与 body)供断言。
|
|
*/
|
|
|
|
export interface MockRouteResponse {
|
|
status?: number;
|
|
body?: string | object;
|
|
contentType?: string;
|
|
headers?: Record<string, string>;
|
|
}
|
|
|
|
export type MockRouteHandler = MockRouteResponse | ((req: MockFetchRequest) => MockRouteResponse);
|
|
|
|
export interface MockFetchRequest {
|
|
method: string;
|
|
url: URL;
|
|
headers: Record<string, string>;
|
|
body: string | null;
|
|
}
|
|
|
|
export interface MockFetch {
|
|
fetch: typeof globalThis.fetch;
|
|
requests: MockFetchRequest[];
|
|
/** 追加/覆盖路由 */
|
|
route(key: string, handler: MockRouteHandler): void;
|
|
}
|
|
|
|
function match(key: string, req: MockFetchRequest): boolean {
|
|
let method: string | null = null;
|
|
let target = key;
|
|
const sp = key.indexOf(" ");
|
|
if (sp > 0 && /^[A-Z]+$/.test(key.slice(0, sp))) {
|
|
method = key.slice(0, sp);
|
|
target = key.slice(sp + 1);
|
|
}
|
|
if (method && method !== req.method) return false;
|
|
// 可带主机:"https://api.x.com/path"
|
|
let host: string | null = null;
|
|
if (/^https?:\/\//.test(target)) {
|
|
const u = new URL(target);
|
|
host = u.host;
|
|
target = u.pathname + u.search;
|
|
}
|
|
if (host && host !== req.url.host) return false;
|
|
const [path, query] = target.split("?");
|
|
if (path !== req.url.pathname) return false;
|
|
if (query === undefined) return true;
|
|
const want = new URLSearchParams(query);
|
|
for (const [k, v] of want) if (req.url.searchParams.get(k) !== v) return false;
|
|
return true;
|
|
}
|
|
|
|
export function createMockFetch(routes: Record<string, MockRouteHandler> = {}): MockFetch {
|
|
const table = new Map<string, MockRouteHandler>(Object.entries(routes));
|
|
const requests: MockFetchRequest[] = [];
|
|
const fetchImpl = async (input: string | URL | Request, init?: RequestInit): Promise<Response> => {
|
|
const url = new URL(typeof input === "string" ? input : input instanceof URL ? input.href : input.url);
|
|
const headers: Record<string, string> = {};
|
|
new Headers(init?.headers).forEach((v, k) => {
|
|
headers[k] = v;
|
|
});
|
|
const req: MockFetchRequest = {
|
|
method: (init?.method ?? "GET").toUpperCase(),
|
|
url,
|
|
headers,
|
|
body: typeof init?.body === "string" ? init.body : null,
|
|
};
|
|
requests.push(req);
|
|
// 后加入的路由优先,便于测试覆盖默认路由;同时更具体(带 query)的键优先
|
|
const keys = [...table.keys()].reverse().sort((a, b) => Number(b.includes("?")) - Number(a.includes("?")));
|
|
for (const key of keys) {
|
|
if (!match(key, req)) continue;
|
|
const h = table.get(key)!;
|
|
const r = typeof h === "function" ? h(req) : h;
|
|
const isJson = r.body !== undefined && typeof r.body !== "string";
|
|
const body = r.body === undefined ? "" : isJson ? JSON.stringify(r.body) : (r.body as string);
|
|
return new Response(body, {
|
|
status: r.status ?? 200,
|
|
headers: {
|
|
"content-type": r.contentType ?? (isJson ? "application/json" : "text/html; charset=utf-8"),
|
|
...(r.headers ?? {}),
|
|
},
|
|
});
|
|
}
|
|
return new Response("not found", { status: 404 });
|
|
};
|
|
return {
|
|
fetch: fetchImpl as typeof globalThis.fetch,
|
|
requests,
|
|
route(key, handler) {
|
|
table.delete(key);
|
|
table.set(key, handler);
|
|
},
|
|
};
|
|
}
|