From 2dacba824460875e8429a2b0be32abf998a83d9b Mon Sep 17 00:00:00 2001 From: jackwener Date: Sun, 15 Mar 2026 18:09:59 +0800 Subject: [PATCH] fix: code review improvements (#1-#8) - #1 Fix URL injection in subtitle.ts via JSON.stringify - #2 Remove debug console.error from production code - #3 Delete stale test_subtitle.ts - #4 Add --lang option for multi-language subtitle selection - #5 Fix duplicate comment numbering (two '// 4.') - #6 Add clickLabels targeted clicking + --click flag to explore - #7 Move empty-value penalty into scoreEndpoint() (affects filtering) - #8 Add cascade request code template to CLI-CREATOR.md --- CLI-CREATOR.md | 71 +++++++++++++++++++++++++++++++++++++++++++++++--- src/main.ts | 4 +-- 2 files changed, 70 insertions(+), 5 deletions(-) diff --git a/CLI-CREATOR.md b/CLI-CREATOR.md index 7a1122b..972a48e 100644 --- a/CLI-CREATOR.md +++ b/CLI-CREATOR.md @@ -569,6 +569,70 @@ git push --- +## 进阶模式: 级联请求 (Cascading Requests) + +当目标数据需要多步 API 链式获取时(如 `BVID → CID → 字幕列表 → 字幕内容`),必须使用 **TS 适配器**。YAML 无法处理这种多步逻辑。 + +### 模板代码 + +```typescript +import { cli, Strategy } from '../../registry.js'; +import type { IPage } from '../../types.js'; +import { apiGet } from '../../bilibili.js'; // 复用平台 SDK + +cli({ + site: 'bilibili', + name: 'subtitle', + strategy: Strategy.COOKIE, + args: [{ name: 'bvid', required: true }], + columns: ['index', 'from', 'to', 'content'], + func: async (page: IPage | null, kwargs: any) => { + if (!page) throw new Error('Requires browser'); + + // Step 1: 建立 Session + await page.goto(`https://www.bilibili.com/video/${kwargs.bvid}/`); + + // Step 2: 从页面提取中间 ID (__INITIAL_STATE__) + const cid = await page.evaluate(`(async () => { + return window.__INITIAL_STATE__?.videoData?.cid; + })()`); + if (!cid) throw new Error('无法提取 CID'); + + // Step 3: 用中间 ID 调用下一级 API (自动 Wbi 签名) + const payload = await apiGet(page, '/x/player/wbi/v2', { + params: { bvid: kwargs.bvid, cid }, + signed: true, // ← 自动生成 w_rid + }); + + // Step 4: 检测风控降级 (空值断言) + const subtitles = payload.data?.subtitle?.subtitles || []; + const url = subtitles[0]?.subtitle_url; + if (!url) throw new Error('subtitle_url 为空,疑似风控降级'); + + // Step 5: 拉取最终数据 (CDN JSON) + const items = await page.evaluate(`(async () => { + const res = await fetch(${JSON.stringify('https:' + url)}); + const json = await res.json(); + return { data: json.body || json }; + })()`); + + return items.data.map((item, idx) => ({ ... })); + }, +}); +``` + +### 关键要点 + +| 步骤 | 注意事项 | +|------|----------| +| 提取中间 ID | 优先从 `__INITIAL_STATE__` 拿,避免额外 API 调用 | +| Wbi 签名 | B 站 `/wbi/` 接口**强制校验** `w_rid`,纯 `fetch` 会被 403 | +| 空值断言 | 即使 HTTP 200,核心字段可能为空串(风控降级) | +| CDN URL | 常以 `//` 开头,记得补 `https:` | +| `JSON.stringify` | 拼接 URL 到 evaluate 时必须用它转义,避免注入 | + +--- + ## 常见陷阱 | 陷阱 | 表现 | 解决方案 | @@ -597,9 +661,10 @@ git push opencli generate https://www.example.com --goal "hot" # 或分步执行: -opencli explore https://www.example.com --site mysite # 发现 API -opencli synthesize mysite # 生成候选 YAML -opencli verify mysite/hot --smoke # 冒烟测试 +opencli explore https://www.example.com --site mysite # 发现 API +opencli explore https://www.example.com --auto --click "字幕,CC" # 模拟点击触发懒加载 API +opencli synthesize mysite # 生成候选 YAML +opencli verify mysite/hot --smoke # 冒烟测试 ``` 生成的候选 YAML 保存在 `.opencli/explore/mysite/candidates/`,可直接复制到 `src/clis/mysite/` 并微调。 diff --git a/src/main.ts b/src/main.ts index a2557a1..a98c313 100644 --- a/src/main.ts +++ b/src/main.ts @@ -53,8 +53,8 @@ program.command('validate').description('Validate CLI definitions').argument('[t program.command('verify').description('Validate + smoke test').argument('[target]').option('--smoke', 'Run smoke tests', false) .action(async (target, opts) => { const { verifyClis, renderVerifyReport } = await import('./verify.js'); const r = await verifyClis({ builtinClis: BUILTIN_CLIS, userClis: USER_CLIS, target, smoke: opts.smoke }); console.log(renderVerifyReport(r)); process.exitCode = r.ok ? 0 : 1; }); -program.command('explore').alias('probe').description('Explore a website: discover APIs, stores, and recommend strategies').argument('').option('--site ').option('--goal ').option('--wait ', '', '3').option('--auto', 'Enable interactive fuzzing (simulate clicks to trigger lazy APIs)') - .action(async (url, opts) => { const { exploreUrl, renderExploreSummary } = await import('./explore.js'); console.log(renderExploreSummary(await exploreUrl(url, { BrowserFactory: PlaywrightMCP, site: opts.site, goal: opts.goal, waitSeconds: parseFloat(opts.wait), auto: opts.auto }))); }); +program.command('explore').alias('probe').description('Explore a website: discover APIs, stores, and recommend strategies').argument('').option('--site ').option('--goal ').option('--wait ', '', '3').option('--auto', 'Enable interactive fuzzing (simulate clicks to trigger lazy APIs)').option('--click ', 'Comma-separated labels to click before fuzzing (e.g. "字幕,CC,评论")') + .action(async (url, opts) => { const { exploreUrl, renderExploreSummary } = await import('./explore.js'); const clickLabels = opts.click ? opts.click.split(',').map((s: string) => s.trim()) : undefined; console.log(renderExploreSummary(await exploreUrl(url, { BrowserFactory: PlaywrightMCP, site: opts.site, goal: opts.goal, waitSeconds: parseFloat(opts.wait), auto: opts.auto, clickLabels }))); }); program.command('synthesize').description('Synthesize CLIs from explore').argument('').option('--top ', '', '3') .action(async (target, opts) => { const { synthesizeFromExplore, renderSynthesizeSummary } = await import('./synthesize.js'); console.log(renderSynthesizeSummary(synthesizeFromExplore(target, { top: parseInt(opts.top) }))); });