mirror of
https://github.com/jackwener/OpenCLI.git
synced 2026-09-14 18:25:42 +08:00
12176de1a5
* refactor: simplify core modules — remove root shims, consolidate error classification, streamline cascade/interceptor, clean up synthesize 1. Remove root-level shim files (errors.ts, logger.ts, registry.ts, types.ts, utils.ts, launcher.ts) — update all ~840 adapter imports to reference src/ directly 2. Consolidate interceptor: reuse shared DISGUISE_FN in tap interceptor instead of reimplementing 3. Unify error classification: single ClassifiedError type with icon/exitCode/hint lookup table, eliminating duplicated pattern matching between resolveExitCode and renderError 4. Simplify cascade probe: replace repetitive switch cases with PROBE_OPTIONS lookup map 12. Clean up synthesize.ts: remove deprecated snake_case field aliases (recommended_args, recommended_columns, recommendedColumnsLegacy) and unnecessary constant aliases Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * docs: update import paths in contributor docs and skill templates Update all documentation and skill files to reference src/ directly, matching the shim removal in the previous commit. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: update new Gemini adapter imports to use src/ paths Fix imports in newly added deep-research adapter files that were still referencing the deleted root shim files. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: update xiaohongshu tests for /search_result/ URL change Tests now expect /search_result/<id> for bare note IDs (matching the note-helpers.ts change from PR #774) and updated empty-shell hint assertion. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: update LessWrong and hupu adapter imports to use src/ paths Fix imports in newly merged LessWrong and hupu/mentions adapter files that were still referencing the deleted root shim files. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * test(xueqiu): mock logger via src path --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
175 lines
5.3 KiB
TypeScript
175 lines
5.3 KiB
TypeScript
/**
|
|
* Strategy Cascade: automatic strategy downgrade chain.
|
|
*
|
|
* Probes an API endpoint starting from the simplest strategy (PUBLIC)
|
|
* and automatically downgrades through the strategy tiers until one works:
|
|
*
|
|
* PUBLIC → COOKIE → HEADER → INTERCEPT → UI
|
|
*
|
|
* This eliminates the need for manual strategy selection — the system
|
|
* automatically finds the minimum-privilege strategy that works.
|
|
*/
|
|
|
|
import { Strategy } from './registry.js';
|
|
import type { IPage } from './types.js';
|
|
import { getErrorMessage } from './errors.js';
|
|
|
|
/** Strategy cascade order (simplest → most complex) */
|
|
const CASCADE_ORDER: Strategy[] = [
|
|
Strategy.PUBLIC,
|
|
Strategy.COOKIE,
|
|
Strategy.HEADER,
|
|
Strategy.INTERCEPT,
|
|
Strategy.UI,
|
|
];
|
|
|
|
interface ProbeResult {
|
|
strategy: Strategy;
|
|
success: boolean;
|
|
statusCode?: number;
|
|
hasData?: boolean;
|
|
error?: string;
|
|
responsePreview?: string;
|
|
}
|
|
|
|
interface CascadeResult {
|
|
bestStrategy: Strategy;
|
|
probes: ProbeResult[];
|
|
confidence: number;
|
|
}
|
|
|
|
/**
|
|
* Build the JavaScript source for a fetch probe.
|
|
* Shared logic for PUBLIC, COOKIE, and HEADER strategies.
|
|
*/
|
|
function buildFetchProbeJs(url: string, opts: {
|
|
credentials?: boolean;
|
|
extractCsrf?: boolean;
|
|
}): string {
|
|
const credentialsLine = opts.credentials ? `credentials: 'include',` : '';
|
|
const headerSetup = opts.extractCsrf
|
|
? `
|
|
const cookies = document.cookie.split(';').map(c => c.trim());
|
|
const csrf = cookies.find(c => c.startsWith('ct0=') || c.startsWith('csrf_token=') || c.startsWith('_csrf='))?.split('=').slice(1).join('=');
|
|
const headers = {};
|
|
if (csrf) { headers['X-Csrf-Token'] = csrf; headers['X-XSRF-Token'] = csrf; }
|
|
`
|
|
: 'const headers = {};';
|
|
|
|
return `
|
|
async () => {
|
|
try {
|
|
${headerSetup}
|
|
const resp = await fetch(${JSON.stringify(url)}, {
|
|
${credentialsLine}
|
|
headers
|
|
});
|
|
const status = resp.status;
|
|
if (!resp.ok) return { status, ok: false };
|
|
const text = await resp.text();
|
|
let hasData = false;
|
|
try {
|
|
const json = JSON.parse(text);
|
|
hasData = !!json && (Array.isArray(json) ? json.length > 0 :
|
|
typeof json === 'object' && Object.keys(json).length > 0);
|
|
// Check for API-level error codes (common in Chinese sites)
|
|
if (json.code !== undefined && json.code !== 0) hasData = false;
|
|
} catch {}
|
|
return { status, ok: true, hasData, preview: text.slice(0, 200) };
|
|
} catch (e) { return { ok: false, error: e.message }; }
|
|
}
|
|
`;
|
|
}
|
|
|
|
/** Strategy → fetch probe options mapping for strategies that support probing. */
|
|
const PROBE_OPTIONS: Partial<Record<Strategy, { credentials?: boolean; extractCsrf?: boolean }>> = {
|
|
[Strategy.PUBLIC]: {},
|
|
[Strategy.COOKIE]: { credentials: true },
|
|
[Strategy.HEADER]: { credentials: true, extractCsrf: true },
|
|
};
|
|
|
|
/**
|
|
* Probe an endpoint with a specific strategy.
|
|
* Returns whether the probe succeeded and basic response info.
|
|
*/
|
|
export async function probeEndpoint(
|
|
page: IPage,
|
|
url: string,
|
|
strategy: Strategy,
|
|
_opts: { timeout?: number } = {},
|
|
): Promise<ProbeResult> {
|
|
const result: ProbeResult = { strategy, success: false };
|
|
|
|
try {
|
|
const opts = PROBE_OPTIONS[strategy];
|
|
if (opts) {
|
|
const resp = await page.evaluate(buildFetchProbeJs(url, opts));
|
|
result.statusCode = resp?.status;
|
|
result.success = resp?.ok && resp?.hasData;
|
|
result.hasData = resp?.hasData;
|
|
result.responsePreview = resp?.preview;
|
|
} else {
|
|
result.error = `Strategy ${strategy} requires site-specific implementation`;
|
|
}
|
|
} catch (err) {
|
|
result.success = false;
|
|
result.error = getErrorMessage(err);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Run the cascade: try each strategy in order until one works.
|
|
* Returns the simplest working strategy.
|
|
*/
|
|
export async function cascadeProbe(
|
|
page: IPage,
|
|
url: string,
|
|
opts: { maxStrategy?: Strategy; timeout?: number } = {},
|
|
): Promise<CascadeResult> {
|
|
const rawIdx = opts.maxStrategy
|
|
? CASCADE_ORDER.indexOf(opts.maxStrategy)
|
|
: CASCADE_ORDER.indexOf(Strategy.HEADER); // Don't auto-try INTERCEPT/UI
|
|
const maxIdx = rawIdx === -1 ? CASCADE_ORDER.indexOf(Strategy.HEADER) : rawIdx;
|
|
|
|
const probes: ProbeResult[] = [];
|
|
|
|
for (let i = 0; i <= Math.min(maxIdx, CASCADE_ORDER.length - 1); i++) {
|
|
const strategy = CASCADE_ORDER[i];
|
|
const probe = await probeEndpoint(page, url, strategy, opts);
|
|
probes.push(probe);
|
|
|
|
if (probe.success) {
|
|
return {
|
|
bestStrategy: strategy,
|
|
probes,
|
|
confidence: 1.0 - (i * 0.1), // Higher confidence for simpler strategies
|
|
};
|
|
}
|
|
}
|
|
|
|
// None worked — default to COOKIE (most common for logged-in sites)
|
|
return {
|
|
bestStrategy: Strategy.COOKIE,
|
|
probes,
|
|
confidence: 0.3,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Render cascade results for display.
|
|
*/
|
|
export function renderCascadeResult(result: CascadeResult): string {
|
|
const lines = [
|
|
`Strategy Cascade: ${result.bestStrategy} (${(result.confidence * 100).toFixed(0)}% confidence)`,
|
|
];
|
|
for (const probe of result.probes) {
|
|
const icon = probe.success ? '✅' : '❌';
|
|
const status = probe.statusCode ? ` [${probe.statusCode}]` : '';
|
|
const err = probe.error ? ` — ${probe.error}` : '';
|
|
lines.push(` ${icon} ${probe.strategy}${status}${err}`);
|
|
}
|
|
return lines.join('\n');
|
|
}
|