From 3ecfc46022183562888426b23b4e9764cf8c2247 Mon Sep 17 00:00:00 2001 From: Chilfish Date: Fri, 25 Sep 2026 23:35:21 +0800 Subject: [PATCH 1/5] fix(tweet): send cookies when fetching the X shell for the transaction ID x-client-transaction-id resolves the ondemand.s chunk URL from the webpack chunk map, which X now only serves on the authenticated shell: anonymous requests get the new Rolldown/Vite build, so ClientTransaction.create() throws OnDemandFileUrlResolutionError before the request is sent. The shell fetch only sent config.headers, so it was always anonymous even with an API key configured (a regression from the same fix in Rettiwt #885). Add buildXShellHeaders() to attach the decoded cookie, and keep the candidate routes (/home first) as a guest fallback. Co-authored-by: CommandCodeBot --- .../services/public/FetcherService.ts | 60 ++++++++++++++----- 1 file changed, 44 insertions(+), 16 deletions(-) diff --git a/app/lib/rettiwt-api/services/public/FetcherService.ts b/app/lib/rettiwt-api/services/public/FetcherService.ts index adeb195..49cac73 100644 --- a/app/lib/rettiwt-api/services/public/FetcherService.ts +++ b/app/lib/rettiwt-api/services/public/FetcherService.ts @@ -24,24 +24,52 @@ import { ErrorService } from '../internal/ErrorService' import { LogService } from '../internal/LogService' /** - * X routes that still serve the legacy (webpack) client shell. + * Candidate URLs for the X shell document that drives `ClientTransaction`. * - * X is migrating its frontend to a Rolldown/Vite build ("x-web") that no longer - * embeds the `ondemand.s` webpack chunk map required by `x-client-transaction-id` - * to derive the key byte indices. The landing pages (`/`, `/home`) already serve - * the new build, but the routes below still return the legacy shell, so we probe - * them in order until one exposes the runtime the library needs. + * The `ondemand.s` webpack chunk map required by `x-client-transaction-id` only + * remains in the legacy shell, and X serves that legacy shell only to an + * authenticated session (see `buildXShellHeaders`). The canonical homepage comes + * first; the remaining routes still return the legacy shell anonymously, as a + * fallback for deployments without an API key. * * @internal */ -export const X_LEGACY_HOME_URLS = [ +export const X_SHELL_URLS = [ + 'https://x.com/home', 'https://x.com/i/topics', 'https://x.com/i/display', 'https://x.com/i/timeline', 'https://x.com/i/communitynotes', - 'https://x.com/home', ] +/** + * Builds the headers used to fetch the X shell document. + * + * X only serves the shell containing the `ondemand.s` chunk map to an + * authenticated session, so the decoded cookie must be attached whenever an API + * key is configured. An anonymous request returns the new Rolldown/Vite build + * (which dropped the chunk map), making `ClientTransaction` uninitializable. + * + * @param headers - The base request headers. + * @param apiKey - The API key whose cookies are to be attached, if any. + * + * @returns A copy of the given headers, with the cookie attached when applicable. + * + * @internal + */ +export function buildXShellHeaders( + headers: { [key: string]: string }, + apiKey?: string, +): { [key: string]: string } { + const shellHeaders = { ...headers } + + if (apiKey) { + shellHeaders.cookie = AuthService.decodeCookie(apiKey) + } + + return shellHeaders +} + /** * Whether the given document is a usable legacy X shell for client transaction * ID generation: it must expose both the site verification key and the @@ -162,11 +190,11 @@ export class FetcherService { /** * Resolves a DOM document that can drive `ClientTransaction` initialization. * - * X's new Rolldown/Vite shell dropped the `ondemand.s` webpack chunk map, so - * the previous fixed `/home` fetch fails with `OnDemandFileUrlResolutionError`. - * We probe the legacy routes in `X_LEGACY_HOME_URLS` and return the first - * usable shell, falling back to the last parsed document so the library can - * raise its own specific initialization error. + * X's new Rolldown/Vite shell dropped the `ondemand.s` webpack chunk map, which + * is why the shell must be fetched with the session cookie (see + * `buildXShellHeaders`). We probe the candidates in `X_SHELL_URLS` and return the + * first usable shell, falling back to the last parsed document so the library + * can raise its own specific initialization error. * * @returns The parsed X document. */ @@ -174,7 +202,7 @@ export class FetcherService { let lastDocument: Document | undefined let lastError: unknown - for (const url of X_LEGACY_HOME_URLS) { + for (const url of X_SHELL_URLS) { try { const document = await this._fetchXHomePage(url) @@ -207,9 +235,9 @@ export class FetcherService { * @returns The parsed X document. */ private async _fetchXHomePage(url: string): Promise { - // Fetch the X page shell + // Fetch the X page shell (with the session cookie, when available) const homePageResponse = await axios.get(url, { - headers: this.config.headers, + headers: buildXShellHeaders(this.config.headers, this.config.apiKey), httpAgent: this.config.httpsAgent, httpsAgent: this.config.httpsAgent, }) From 94b4102fa20fc2d90c92b3ab62180db90d0d755d Mon Sep 17 00:00:00 2001 From: Chilfish Date: Fri, 25 Sep 2026 23:35:26 +0800 Subject: [PATCH 2/5] test(tweet): cover X shell cookie headers and candidate list Co-authored-by: CommandCodeBot --- .../unit/rettiwt-transaction-document.spec.ts | 40 +++++++++++++++---- 1 file changed, 32 insertions(+), 8 deletions(-) diff --git a/test/unit/rettiwt-transaction-document.spec.ts b/test/unit/rettiwt-transaction-document.spec.ts index 5ec6001..f602d8e 100644 --- a/test/unit/rettiwt-transaction-document.spec.ts +++ b/test/unit/rettiwt-transaction-document.spec.ts @@ -1,14 +1,14 @@ import { parseHTML } from 'linkedom' import { describe, expect, it } from 'vitest' -import { isUsableXDocument, X_LEGACY_HOME_URLS } from '~/lib/rettiwt-api/services/public/FetcherService' +import { buildXShellHeaders, isUsableXDocument, X_SHELL_URLS } from '~/lib/rettiwt-api/services/public/FetcherService' /** * test/unit/rettiwt-transaction-document.spec.ts * - * X 迁移到 Rolldown/Vite 后,`/` ` /home` 不再内嵌 `ondemand.s` webpack chunk map, - * 导致 x-client-transaction-id 抛 OnDemandFileUrlResolutionError。FetcherService - * 改为在 X_LEGACY_HOME_URLS 中探测仍提供旧版外壳的页面。这里固化「可用文档」判定: - * 必须同时具备 site verification meta 与 ondemand.s chunk 引用。 + * X 只对**登录态**返回仍内嵌 `ondemand.s` webpack chunk map 的旧版外壳,匿名请求拿到 + * 新的 Rolldown/Vite 外壳 → x-client-transaction-id 抛 OnDemandFileUrlResolutionError。 + * 这里固化两处纯逻辑:外壳请求头必须附带登录 cookie(buildXShellHeaders),以及候选 + * 外壳的可用性判定(isUsableXDocument:需同时具备 site verification meta 与 ondemand.s)。 */ function doc(body: string): Document { @@ -17,6 +17,29 @@ function doc(body: string): Document { const VERIFICATION_META = '' +describe('buildXShellHeaders', () => { + it('attaches the decoded cookie when an API key is present', () => { + const cookies = 'auth_token=abc;ct0=def;twid=u%3D1;' + const apiKey = Buffer.from(cookies).toString('base64') + + const headers = buildXShellHeaders({ 'User-Agent': 'ua' }, apiKey) + + expect(headers.cookie).toBe(cookies) + expect(headers['User-Agent']).toBe('ua') + }) + + it('does not attach a cookie without an API key', () => { + expect(buildXShellHeaders({ 'User-Agent': 'ua' })).not.toHaveProperty('cookie') + }) + + it('does not mutate the given headers', () => { + const base = { 'User-Agent': 'ua' } + buildXShellHeaders(base, Buffer.from('auth_token=abc;').toString('base64')) + + expect(base).toEqual({ 'User-Agent': 'ua' }) + }) +}) + describe('isUsableXDocument', () => { it('accepts a legacy shell exposing both the verification key and the ondemand chunk', () => { const d = doc(`${VERIFICATION_META}`) @@ -37,8 +60,9 @@ describe('isUsableXDocument', () => { expect(isUsableXDocument(doc(''))).toBe(false) }) - it('keeps a non-empty probe list with /home as the last-resort fallback', () => { - expect(X_LEGACY_HOME_URLS.length).toBeGreaterThan(1) - expect(X_LEGACY_HOME_URLS.at(-1)).toBe('https://x.com/home') + it('probes the canonical homepage first, with legacy routes as a guest fallback', () => { + expect(X_SHELL_URLS.length).toBeGreaterThan(1) + expect(X_SHELL_URLS[0]).toBe('https://x.com/home') + expect(X_SHELL_URLS).toContain('https://x.com/i/topics') }) }) From d32eed073f2cb63032921845bc0486ca97687cdd Mon Sep 17 00:00:00 2001 From: Chilfish Date: Fri, 25 Sep 2026 23:35:29 +0800 Subject: [PATCH 3/5] docs: document the transaction-ID frontend drift and cookie root cause Add docs/features/tweet/transaction-id.md with the measurements (cookie vs anonymous shell, indices rotating per build), update postmortem 013 with the corrected root cause, and refresh the dev log and CHANGELOG. Co-authored-by: CommandCodeBot --- CHANGELOG.md | 5 +- docs/development-log/2026-09-25.md | 29 +++++ docs/development-log/README.md | 30 ++--- docs/features/tweet/transaction-id.md | 105 ++++++++++++++++++ .../postmortem/013-upstream-frontend-drift.md | 30 ++++- 5 files changed, 178 insertions(+), 21 deletions(-) create mode 100644 docs/features/tweet/transaction-id.md diff --git a/CHANGELOG.md b/CHANGELOG.md index dea4f18..e3b3d3b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -72,8 +72,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/lang/zh-CN/ #### X 前端迁移修复(2026-09-25) -- X 登出首页迁移到 Rolldown/Vite 后不再内嵌 `ondemand.s` webpack chunk map,`x-client-transaction-id` 因此抛 `OnDemandFileUrlResolutionError`,导致所有走 rettiwt-api 的 X 数据接口失败。`FetcherService` 改为在仍提供旧版 webpack 外壳的路由中探测取文档(`X_LEGACY_HOME_URLS` + `isUsableXDocument` 双条件校验),不再固定依赖 `/home` -- 新增 `test/unit/rettiwt-transaction-document.spec.ts`;沉淀 postmortem 013 +- X 迁移到 Rolldown/Vite 后不再内嵌 `ondemand.s` webpack chunk map,`x-client-transaction-id` 因此抛 `OnDemandFileUrlResolutionError`,所有走 rettiwt-api 的 X 数据接口失败。**真因是外壳请求漏带登录 cookie**(X 仅对登录态返回仍含该 chunk map 的旧外壳):新增 `buildXShellHeaders`,配置 API Key 时附带解码后的 cookie,`_fetchXHomePage` 改为使用它 +- 外壳候选列表改为 `X_SHELL_URLS`(`/home` 优先,legacy 路由作无 Key 时的 guest 兜底)+ `isUsableXDocument` 双条件校验;新增/扩充 `test/unit/rettiwt-transaction-document.spec.ts` +- 沉淀 postmortem 013 与 `docs/features/tweet/transaction-id.md`(含 `ondemand.s` indices 每构建随机、不可 pin 的实测) #### 验证诚信修复(2026-09-12) diff --git a/docs/development-log/2026-09-25.md b/docs/development-log/2026-09-25.md index 1e8397a..2039cfd 100644 --- a/docs/development-log/2026-09-25.md +++ b/docs/development-log/2026-09-25.md @@ -45,3 +45,32 @@ **文档**:[postmortem 013](../postmortem/013-upstream-frontend-drift.md) +「高频雷区」第 11 条 + `CHANGELOG.md`。 + +## 真正根因:外壳请求缺 cookie(✅ 已完成,同日追加) + +**背景**:owner 质疑「X 的重构是逐步的,`/i/topics` 只是还没轮到,库依赖的东西才是核心」—— +顺着这个质疑复测,发现根因不在「换哪个页面」。 + +**决定性实测**(同一 URL `x.com/home`,仅切换 `Cookie`): + +| 请求 | 外壳 | `ondemand.s` | +| ----------------- | -------------- | ------------ | +| 匿名 `/home` | 16.8 KB 新外壳 | ❌ | +| 带 cookie `/home` | 305 KB 旧外壳 | ✅ | + +- 上游/此前的 `_handleXMigration()` 只发 `config.headers`,cookie 只在 `request()` 组装 API 请求时 + 注入 → **外壳请求永远匿名**,即便配置了 API Key。 +- 这是**回归**:Rettiwt #885 曾以「抓外壳时附 cookie」修好同一错误,被 #888 合并时丢失; + 上游 Lqm1 issue #20 亦归因为「需要有效登录 session」。 +- 端到端:`TweetDetail` 与 `TweetLikers`(#908 的复现接口)均成功返回。 + +**实现**:新增纯函数 `buildXShellHeaders(headers, apiKey)`(存在 key 时附 +`AuthService.decodeCookie(apiKey)`),`_fetchXHomePage` 改为使用它;候选列表改名 `X_SHELL_URLS` +并把 `/home` 提到首位(有 cookie 时首个即命中,legacy 路由降级为无 Key 时的 guest 兜底)。 + +**另**:实测 `ondemand.s` 的 key byte indices **每次构建随机**(30 个日更版本 30/30 不同), +故只可缓存 URL(内容哈希、不可变),不可 pin / 缓存 indices。 + +**验证**:`test/unit/rettiwt-transaction-document.spec.ts` 扩到 8 用例(含 cookie 头三项断言); +真实上游 E2E 复核通过。详见 [postmortem 013](../postmortem/013-upstream-frontend-drift.md) +「后续修正」与 [transaction-id.md](../features/tweet/transaction-id.md)。 diff --git a/docs/development-log/README.md b/docs/development-log/README.md index 406bafc..a41dbb0 100644 --- a/docs/development-log/README.md +++ b/docs/development-log/README.md @@ -4,21 +4,21 @@ ## 时间线(新 → 旧) -| 日期 | 主题 | -| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| [2026-09-25](2026-09-25.md) | 修复 X 前端迁移(webpack→Rolldown/Vite)击穿 client transaction id 导致 get tweet 全站失败:改从仍提供旧外壳的路由探测取文档(`X_LEGACY_HOME_URLS`)+ `isUsableXDocument` 双条件校验;沉淀 postmortem 013 | -| [2026-09-16](2026-09-16.md) | X Space 卡片接入(audiospace 空壳卡片 → `AudioSpaceById` 补全标题/主播/人数/时长;范围裁定「卡片 + 跳转 X 播放」;AC-SPACE-001~008;不改高危解析器) | -| [2026-09-12](2026-09-12.md) | 验证诚信修复落地(F1 dev server `NODE_ENV` 泄漏 / F2 零匹配守卫 + 模块别名 / F3 死断言 / F4 AC↔测试名元测试 / F5 workflow 步骤解析 / F8 去 fixture 自证 / F9 AC-TRANS 命名 / F10 断言 lint / F12 README / F13 deprecated 参数)+ postmortem 011 | -| [2026-09-11](2026-09-11.md) | anon-tweet Agent Skill 升级 v1.2.0(随附 PowerShell 封装 + OpenAPI 快照;随附 spec 与 llms.ts 14/14 奇偶一致)+ 版本单源治理(应用版本归 package.json / skill 版本归 SKILL.md,去手写副本)+ 验证套件诚信审查(AC 空跑报绿 / 静态扫描冒充行为断言;修复 backlog F1~F13) | -| [2026-09-06](2026-09-06.md) | 可安装 PWA + Web Share Target(系统分享直达推文/IG;极简网络透传 SW + 首页 GET 接收复用 TweetInputForm) | -| [2026-09-04](2026-09-04.md) | 媒体图片链接统一为 `/slug.jpg`(去掉 `?name=&format=` 查询参数,normalizeMediaUrl 收口) | -| [2026-08-31](2026-08-31.md) | 推文搜索接入(/search 页面 + /api/tweet/search + rettiwt-api guest 授权补缺)+ llms.txt / openapi.json 接入 + tweet GET 全部入口统一走缓存链(getLocalTweet) | -| [2026-08-19](2026-08-19.md) | 主线推送 34 提交 + baseUrl 白名单改可选加固(默认关闭) | -| [2026-08-18](2026-08-18.md) | 修复 unified_card 解析只认 `details_1` 布局(Trending/topic 卡片丢失 og 信息) | -| [2026-08-17](2026-08-17.md) | 修复「句首补充」显示与持久化链路(index 对齐合并丢实体 + AI 流丢片段 + 保存即同步) | -| [2026-08-14](2026-08-14.md) | 测试基线红灯修复(vitest 冷加载超时,独立任务) | -| [2026-08-13](2026-08-13.md) | AI 视觉描述子系统规划(OpenRouter + MiMo-V2.5,文档先行 Phase 0) | -| [2026-08-09](2026-08-09.md) | 工程规范对标 Float + verify 二期 S5/S6/S8(CI / Screenshot Verifier / --server 自动管理) | +| 日期 | 主题 | +| --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [2026-09-25](2026-09-25.md) | 修复 X 前端迁移(webpack→Rolldown/Vite)击穿 client transaction id 导致 get tweet 全站失败:改从仍提供旧外壳的路由探测取文档 → **真因 = 外壳请求漏带 cookie**(`buildXShellHeaders`,仅登录态返回含 `ondemand.s` 的旧外壳)+ `X_SHELL_URLS` 候选校验作 guest 兜底;沉淀 postmortem 013 | +| [2026-09-16](2026-09-16.md) | X Space 卡片接入(audiospace 空壳卡片 → `AudioSpaceById` 补全标题/主播/人数/时长;范围裁定「卡片 + 跳转 X 播放」;AC-SPACE-001~008;不改高危解析器) | +| [2026-09-12](2026-09-12.md) | 验证诚信修复落地(F1 dev server `NODE_ENV` 泄漏 / F2 零匹配守卫 + 模块别名 / F3 死断言 / F4 AC↔测试名元测试 / F5 workflow 步骤解析 / F8 去 fixture 自证 / F9 AC-TRANS 命名 / F10 断言 lint / F12 README / F13 deprecated 参数)+ postmortem 011 | +| [2026-09-11](2026-09-11.md) | anon-tweet Agent Skill 升级 v1.2.0(随附 PowerShell 封装 + OpenAPI 快照;随附 spec 与 llms.ts 14/14 奇偶一致)+ 版本单源治理(应用版本归 package.json / skill 版本归 SKILL.md,去手写副本)+ 验证套件诚信审查(AC 空跑报绿 / 静态扫描冒充行为断言;修复 backlog F1~F13) | +| [2026-09-06](2026-09-06.md) | 可安装 PWA + Web Share Target(系统分享直达推文/IG;极简网络透传 SW + 首页 GET 接收复用 TweetInputForm) | +| [2026-09-04](2026-09-04.md) | 媒体图片链接统一为 `/slug.jpg`(去掉 `?name=&format=` 查询参数,normalizeMediaUrl 收口) | +| [2026-08-31](2026-08-31.md) | 推文搜索接入(/search 页面 + /api/tweet/search + rettiwt-api guest 授权补缺)+ llms.txt / openapi.json 接入 + tweet GET 全部入口统一走缓存链(getLocalTweet) | +| [2026-08-19](2026-08-19.md) | 主线推送 34 提交 + baseUrl 白名单改可选加固(默认关闭) | +| [2026-08-18](2026-08-18.md) | 修复 unified_card 解析只认 `details_1` 布局(Trending/topic 卡片丢失 og 信息) | +| [2026-08-17](2026-08-17.md) | 修复「句首补充」显示与持久化链路(index 对齐合并丢实体 + AI 流丢片段 + 保存即同步) | +| [2026-08-14](2026-08-14.md) | 测试基线红灯修复(vitest 冷加载超时,独立任务) | +| [2026-08-13](2026-08-13.md) | AI 视觉描述子系统规划(OpenRouter + MiMo-V2.5,文档先行 Phase 0) | +| [2026-08-09](2026-08-09.md) | 工程规范对标 Float + verify 二期 S5/S6/S8(CI / Screenshot Verifier / --server 自动管理) | ## 记录约定 diff --git a/docs/features/tweet/transaction-id.md b/docs/features/tweet/transaction-id.md new file mode 100644 index 0000000..671a3d8 --- /dev/null +++ b/docs/features/tweet/transaction-id.md @@ -0,0 +1,105 @@ +# X client transaction ID:前端迁移(webpack → Rolldown/Vite)的根因与应对 + +> 状态:调研完成,结论已实测 | 日期:2026-09-25 +> 关联:[postmortem 013](../../postmortem/013-upstream-frontend-drift.md) · Rettiwt-API #908 · Lqm1/x-client-transaction-id #17/#20 · tweety #298 + +## 0. 结论先行 + +- **根因不在「换哪个页面」,而在「外壳请求没带登录 cookie」。** + 同一 URL `x.com/home`:匿名 → 新外壳(无 `ondemand.s`);**带 cookie → 旧外壳(有 `ondemand.s`)**。 +- 上游 `_handleXMigration()` 只发 `config.headers`,**从不发 cookie**,所以即使用户配了 API Key, + 外壳请求仍是匿名的 → 必然拿到新外壳 → 抛 `OnDemandFileUrlResolutionError`。 +- 这是**回归**:Rettiwt #885 曾用「带上解密 cookie」修好过,被 #888 合并时回退掉了。 +- 我们的项目 `TWEET_KEYS` 是必配的(生产无 guest 场景),所以修好 = 外壳请求带 cookie,一行级别的改动。 + +## 1. 机制拆解:库需要什么 + +| 输入 | 来源 | 旧外壳 | 新外壳 | +| --------------------- | ------------------------------------------ | ------ | ------ | +| site verification key | `` | ✅ | ✅ | +| anim frames | `#loading-x-anim-*` 的 SVG path | ✅ | ✅ | +| **key byte indices** | `ondemand.s` chunk 内的 `(\w[n], 16)` 正则 | ✅ | ❌ | + +只有 indices 绑定 webpack chunk map;另两项输入新外壳也提供。所以问题收敛为「如何拿到旧外壳」。 + +## 2. 关键证据 + +### 2.1 决定性:登录态决定拿到哪个外壳(本机实测) + +同一 URL,仅切换 `Cookie` 头(cookie 来自本机 `.env` 的 `TWEET_KEYS`,`Buffer.from(key,'base64')`): + +| 模式 | URL | 响应大小 | `ondemand.s` | meta | anim frames | +| --------- | ----------- | -------- | ------------ | ---- | ----------- | +| 匿名 | `/home` | 16.8 KB | **0** | 1 | 4 | +| 带 cookie | `/home` | 305 KB | **1** | 1 | 4 | +| 带 cookie | `/` | 305 KB | **1** | 1 | 4 | +| 带 cookie | `/i/topics` | 305 KB | **1** | 1 | 4 | + +**端到端**:用 cookie 版 `/home` 文档喂给真实 `FetcherService`(真实 `apiKey`), +`TWEET_DETAILS`(guest 允许)与 **`TWEET_LIKERS`(仅登录可用,即 #908 的复现接口)均成功返回**。 +`ClientTransaction.create()` 初始化正常、生成 94 位 transaction id。 + +### 2.2 上游缺陷所在 + +`_handleXMigration()`(`src/services/public/FetcherService.ts`)只用 `this.config.headers` 取外壳, +cookie 只在 `request()` 组装 API 请求时才通过 `cred.toHeader()` 注入 → 外壳请求永远匿名。 +#885 正是加了 `migrationHeaders['cookie'] = AuthService.decodeCookie(apiKey)` 修好的;#888 合并后丢失。 +上游 Lqm1 侧亦把同一现象归因为「需要有效登录 session」(issue #20)。 + +### 2.3 indices 每次构建都变,不能 pin / 长期缓存 + +取 fa0311 维护的 legacy chunk map 历史,逐个下载当日 `ondemand.s` 并用库自带正则提取 —— **30/30 全部不同**: + +| 日期 | indices | 日期 | indices | +| ----- | ----------- | ----- | ----------- | +| 09-24 | 19,19,21,4 | 09-09 | 1,28,3,46 | +| 09-22 | 20,37,47,5 | 09-05 | 20,45,22,32 | +| 09-19 | 47,41,13,12 | 09-01 | 11,2,33,34 | +| 09-16 | 18,15,11,13 | 08-20 | 2,1,1,30 | +| 09-12 | 9,10,21,24 | 08-14 | 42,2,4,4 | + +⇒ **可缓存 URL(内容哈希、不可变),不可缓存 indices**(会算出无效 id)。必须运行时解析当前 chunk。 + +### 2.4 「换页面」只是时间赛跑(非本方案依赖) + +匿名下 `/home`、`/`、`/explore`、`/i/grok` 恒为新外壳;`/i/topics`、`/i/display`、`/i/timeline`、 +`/i/communitynotes`、`/jobs` 恒为旧外壳 —— 仅因尚未迁移。带 cookie 后所有页面都是旧外壳,无需依赖这些。 + +### 2.5 上游库现状 + +Lqm1:issue #17/#20 同一错误;历史修复为「改正则」(PR #18/#19) + 「带登录 session」(Rettiwt #885 的思路) + +- 文档改指 `/home`(PR #21)。`main` 此后未改,npm 最新 `0.3.1` 即当前实现。 + +## 3. 方案与推荐 + +| 方案 | 做法 | 评价 | +| ------------------------------- | --------------------------------------------------------- | --------------------------------------------------------- | +| **B 外壳请求带 cookie**(推荐) | `_handleXMigration` 附 `AuthService.decodeCookie(apiKey)` | 命中我们「cookie 必配」的场景;上游已有先例;真正修复根因 | +| A 换页面 URL | `/home` → `/i/topics` | 仅绕过;不解决根因,且匿名下仍是时间赛跑 | +| C 多页面探测 + 校验 | 候选列表 + 校验 | 匿名 fallback 才有意义 | +| E 缓存 URL(短 TTL) | 只缓存 ondemand URL | 降载,可叠加 | +| F 上游库可插拔 | `create(doc, { onDemandFileUrl })` / 多页面 / 回退 | 长期方向,需 Lqm1 合并 | +| G 跟踪新栈 | `x-tfe-transaction-id` 等新头 | 战略观察 | + +**推荐**:B 为主(本项目 + 上游 PR),E 可选叠加。guest(无 key)残留问题单列(见 §4)。 + +## 4. 残留问题 / 后续 + +1. **guest 模式**(无 `apiKey`,如自部署未配 `TWEET_KEYS`)仍然匿名 → 仍会失败。上游支持 guest, + 若要覆盖,需要「候选页面探测」兜底(方案 C)或上游可插拔(F)。我们项目不涉及。 +2. 向上游提 PR:以「#885 回归修复」的定位提交 B(最小、有先例、易被接受)。 +3. 监控:加每日探针(匿名/登录外壳是否含 `ondemand.s`),失效告警。 +4. 是否 X 会把需要 transaction id 的 legacy GraphQL 端点也迁到新栈?若迁,机制整体改变。 + +## 5. 复现方式 + +```bash +# cookie 来自 TWEET_KEYS[0](base64) +KEY=$(grep -E '^\s*TWEET_KEYS\s*=' .env | cut -d= -f2- | cut -d, -f1 | tr -d ' ') +COOKIE=$(node -e "process.stdout.write(Buffer.from(process.argv[1],'base64').toString())" "$KEY") + +UA='Mozilla/5.0 (X11; Linux x86_64; rv:144.0) Gecko/20100101 Firefox/144.0' +curl -sL --compressed -A "$UA" https://x.com/home | grep -c ondemand.s # 0 = 新外壳 +curl -sL --compressed -A "$UA" -H "Cookie: $COOKIE" https://x.com/home | grep -c ondemand.s # 1 = 旧外壳 +``` diff --git a/docs/postmortem/013-upstream-frontend-drift.md b/docs/postmortem/013-upstream-frontend-drift.md index 672a6ee..a8f99e4 100644 --- a/docs/postmortem/013-upstream-frontend-drift.md +++ b/docs/postmortem/013-upstream-frontend-drift.md @@ -58,9 +58,9 @@ X 把登出首页从 webpack 迁到 **Rolldown/Vite**(新 "x-web"):`/` 与 ### 缓解(针对已发生的具体缺口) -- [x] `_handleXMigration` → `_fetchTransactionDocument`:遍历 `X_LEGACY_HOME_URLS` - (`/i/topics` → `/i/display` → `/i/timeline` → `/i/communitynotes` → `/home`), - 返回第一个 `isUsableXDocument` 为真的文档;无命中则回退最后一个文档,让库抛出更具体的错误(2026-09-25) +- [x] `_handleXMigration` → `_fetchTransactionDocument`:遍历候选外壳 `X_SHELL_URLS` + (`/home` 优先,`/i/topics` 等为 guest 兜底),返回第一个 `isUsableXDocument` 为真的文档; + 无命中则回退最后一个文档,让库抛出更具体的错误(2026-09-25) - [x] 新增 `isUsableXDocument(document)`(`twitter-site-verification` meta + `ondemand.s` 双条件)(2026-09-25) - [x] 新增错误 `ApiErrors.HOMEPAGE_FETCH_FAILED`(所有候选均网络失败时使用)(2026-09-25) - [x] 单测 `test/unit/rettiwt-transaction-document.spec.ts`(旧/新外壳判定 + 列表非空)(2026-09-25) @@ -71,12 +71,33 @@ X 把登出首页从 webpack 迁到 **Rolldown/Vite**(新 "x-web"):`/` 与 或加一层「外壳健康探针」定时校验并在异常时告警),避免再次全站硬失败 - [ ] 评估将 transaction 文档做**短 TTL 缓存**,顺带降低每请求 300KB 首页拉取的开销 +## 后续修正(2026-09-25):真正根因是外壳请求缺 cookie + +初次修复(探测候选页面)之后进一步实测,定位到**真正根因**并修正: + +- 同一 URL `x.com/home`:**匿名** → 新 Rolldown/Vite 外壳(16.8 KB,无 `ondemand.s`); + **带 cookie** → 旧外壳(305 KB,有 `ondemand.s`)。 +- 上游 `_handleXMigration()` 只发 `config.headers`,cookie 只在 `request()` 组装 API 请求时注入 + → **外壳请求永远匿名**,即便配置了 API Key。 +- 这是**回归**:Rettiwt #885 曾以「抓外壳时附 cookie」修好同一错误,被 #888 合并时丢失; + 上游 Lqm1 issue #20 亦归因为「需要有效登录 session」。 + +修法:`buildXShellHeaders(headers, apiKey)` 在存在 API Key 时附带 +`AuthService.decodeCookie(apiKey)`;候选页面列表(`X_SHELL_URLS`,`/home` 优先)降级为 +**无 Key(guest)时的兜底**。实测:真实 `TweetDetail` 与 `TweetLikers`(#908 复现接口)均成功。 + +另:实测 `ondemand.s` 的 key byte indices **每次构建随机**(30 个日更版本 30/30 不同), +故只能缓存 **URL**(内容哈希、不可变),不能 pin / 缓存 indices。 + ## 教训 +- **别只盯「上游库坏了」,先查「我们发出去的请求对不对」**:本次真因是我们自己的请求缺 cookie + (且是历史修复被合并丢失的回归),换页面只是绕开症状。诊断顺序应为「我们的请求 → 上游库 → 上游站点」。 +- 「同一 URL 因身份不同返回不同内容」的登录态依赖,必须**同时验证匿名与登录两条路径**。 - **依赖上游页面/接口结构的功能,必须多候选 + 可用性校验 + 明确降级**,不能把某个 URL 当成稳定单点;「上游库最新版」不等于「上游已适配」。 - 上游格式漂移是**运行时**风险,typecheck/lint/test 覆盖不到——关键外部依赖需要有 - 「真实调用冒烟」类验证(本次用真实 guest 请求兜底)。 + 「真实调用冒烟」类验证(本次用真实登录请求兜底)。 ## Changed Files @@ -84,6 +105,7 @@ X 把登出首页从 webpack 迁到 **Rolldown/Vite**(新 "x-web"):`/` 与 app/lib/rettiwt-api/services/public/FetcherService.ts app/lib/rettiwt-api/enums/Api.ts test/unit/rettiwt-transaction-document.spec.ts +docs/features/tweet/transaction-id.md docs/postmortem/013-upstream-frontend-drift.md docs/postmortem/README.md docs/development-log/2026-09-25.md From da99296ea4ba81e7b4dd227392881a7bdbf4513b Mon Sep 17 00:00:00 2001 From: Chilfish Date: Sat, 26 Sep 2026 00:04:45 +0800 Subject: [PATCH 4/5] test(tweet): guard the X shell cookie header against regression Add AC-TWEET-011 plus a behavioral test that mocks axios.get and runs the real `_fetchTransactionDocument`, asserting the shell request actually carries the decoded cookie (and stays anonymous without an API key). The pure-function unit tests cannot catch the regression shape -- the caller dropping the cookie -- which is exactly how upstream #885's fix was lost in #888. Falsifiability checked: reverting `_fetchXHomePage` to `this.config.headers` turns the case red (`expected undefined to be 'auth_token=...'`). - test/acceptance/ac-tweet.spec.ts: AC-TWEET-011 (2 cases) - verify/acceptance-criteria/AC-tweet.md: v1.2, AC-TWEET-011 + summary row - verify/README.md: AC-TWEET-001~011, suite 0.3.2 Co-authored-by: CommandCodeBot --- test/acceptance/ac-tweet.spec.ts | 54 +++++++++++++++++++++++++- verify/README.md | 4 +- verify/acceptance-criteria/AC-tweet.md | 48 ++++++++++++++++------- 3 files changed, 88 insertions(+), 18 deletions(-) diff --git a/test/acceptance/ac-tweet.spec.ts b/test/acceptance/ac-tweet.spec.ts index d883edc..f744e1f 100644 --- a/test/acceptance/ac-tweet.spec.ts +++ b/test/acceptance/ac-tweet.spec.ts @@ -1,3 +1,4 @@ +import type { AxiosResponse } from 'axios' import type { ITweetSearchResponse } from '~/lib/rettiwt-api/types/raw/tweet/Search' import type { EnrichedTweet, Entity } from '~/types' /** @@ -7,10 +8,14 @@ import type { EnrichedTweet, Entity } from '~/types' * - AC-TWEET-001~004 / 007:**fixture 作输入 → 调真实纯函数 → 断言产出**(F8 去「fixture 自证」, * review-2026-09-11 P2-1:原实现直接断言 fixture JSON 自身字段,解析回归不会红) * - AC-TWEET-009:真实 `parseSearchTimeline` + * - AC-TWEET-011:mock `axios.get` 后跑真实 `_fetchTransactionDocument`,断言外壳请求带 cookie * - AC-TWEET-005/006/008/010 为集成测试,见 test/integration/api.tweet.spec.ts */ -import { describe, expect, it } from 'vitest' +import axios from 'axios' +import { describe, expect, it, vi } from 'vitest' import { parseSearchTimeline } from '~/lib/react-tweet/utils/get-tweet' +import { RettiwtConfig } from '~/lib/rettiwt-api/models/RettiwtConfig' +import { FetcherService } from '~/lib/rettiwt-api/services/public/FetcherService' import { stripTranslationsFromTweets } from '~/lib/stores/logic' import { mergeEntityTranslationsByIndex } from '~/lib/translation/resolveEntities' import { loadFixture } from '../helpers/load-fixture' @@ -115,3 +120,50 @@ describe('AC-TWEET tweet parsing (fixture → real pipeline)', () => { expect(nextCursor).toBe('dGhlX2JvdHRvbV9jdXJzb3Jfb2Zfc2VhcmNo') }) }) + +/** + * AC-TWEET-011 回归防护:外壳请求必须带 cookie。 + * + * 上游 #885 曾以「抓外壳时附 cookie」修好 `OnDemandFileUrlResolutionError`,被 #888 合并时丢失; + * 纯函数 `buildXShellHeaders` 的单测无法发现「调用点不再使用它」这一类回归,故这里 mock + * `axios.get` 后跑真实 `_fetchTransactionDocument`,断言**实际发出的请求头**。 + */ +const SHELL_COOKIES = 'auth_token=abc;ct0=def;twid=u%3D12345;' +const LEGACY_SHELL = [ + '', + '', + '', + '', +].join('') + +interface ShellProbe { _fetchTransactionDocument: () => Promise } +interface ShellRequest { headers: Record } + +function mockShellGet() { + return vi.spyOn(axios, 'get').mockResolvedValue({ data: LEGACY_SHELL } as unknown as AxiosResponse) +} + +describe('AC-TWEET-011: X shell request carries the session cookie', () => { + it('attaches the decoded cookie when an API key is configured', async () => { + const apiKey = Buffer.from(SHELL_COOKIES).toString('base64') + const service = new FetcherService(new RettiwtConfig({ apiKey })) + const get = mockShellGet() + + await (service as unknown as ShellProbe)._fetchTransactionDocument() + + expect(get).toHaveBeenCalledTimes(1) + const [url, config] = get.mock.calls[0]! + expect(url).toBe('https://x.com/home') + expect((config as unknown as ShellRequest).headers.cookie).toBe(SHELL_COOKIES) + }) + + it('stays anonymous when no API key is configured', async () => { + const service = new FetcherService(new RettiwtConfig({})) + const get = mockShellGet() + + await (service as unknown as ShellProbe)._fetchTransactionDocument() + + const [, config] = get.mock.calls[0]! + expect((config as unknown as ShellRequest).headers).not.toHaveProperty('cookie') + }) +}) diff --git a/verify/README.md b/verify/README.md index f81b3dd..fbebf38 100644 --- a/verify/README.md +++ b/verify/README.md @@ -2,7 +2,7 @@ > AI 自验证体系 — AC 验收标准 + Vitest 三层测试架构 > 项目:[AnonTweet](..) -> 版本:0.3.1(2026-09-12 IG Story 接入 AC-IG-STORY-001~003;0.3.0 为验证诚信修复 F1~~F13:见 [backlog](../docs/planning/backlog.md) / +> 版本:0.3.2(2026-09-25 新增 AC-TWEET-011 外壳请求 cookie 回归防护;0.3.1 为 IG Story 接入 AC-IG-STORY-001~003;0.3.0 为验证诚信修复 F1~~F13:见 [backlog](../docs/planning/backlog.md) / > [review](../docs/reviews/review-2026-09-11-test-suite-honesty.md)) > 历史建设日志:[log.md](log.md)(2026-07~~08,勿当现行用法) @@ -73,7 +73,7 @@ AC-DEV-001(dev server 健康)、AC-IG-009(缺 cookies → 500)、AC-MEDI | 文档 | 覆盖 | | ---------------------------------------------------------- | ----------------------------------------------------- | -| [AC-tweet.md](acceptance-criteria/AC-tweet.md) | AC-TWEET-001~010(解析 + API + 搜索) | +| [AC-tweet.md](acceptance-criteria/AC-tweet.md) | AC-TWEET-001~011(解析 + API + 搜索 + 外壳鉴权) | | [AC-translation.md](acceptance-criteria/AC-translation.md) | AC-TRANS-001~007(占位符管线 + 视图/Provider) | | [AC-ig.md](acceptance-criteria/AC-ig.md) | AC-IG-001~009(IG 集成) | | [AC-ig-story.md](acceptance-criteria/AC-ig-story.md) | AC-IG-STORY-001~005(Story 提取/列表/下载/渲染/缓存) | diff --git a/verify/acceptance-criteria/AC-tweet.md b/verify/acceptance-criteria/AC-tweet.md index ade1382..6568f80 100644 --- a/verify/acceptance-criteria/AC-tweet.md +++ b/verify/acceptance-criteria/AC-tweet.md @@ -1,6 +1,6 @@ # Tweet API 验收标准 -> 版本:1.1 | 日期:2026-08-31(新增 AC-TWEET-009/010 搜索验收) +> 版本:1.2 | 日期:2026-09-25(新增 AC-TWEET-011 外壳请求 cookie 回归防护;1.1 为 AC-TWEET-009/010 搜索验收) > 对应 Postmortem:001 (Tweet Parsing), 005 (Media) > 关联 Verifier:`verify/modules/tweet.verifier.ts` > 执行命令:`bun verify --module tweet [--ac AC-TWEET-NNN]` @@ -133,19 +133,37 @@ --- -## 总计:10 条 AC - -| AC | 分类 | 依赖外部 API | 依赖 AI | -| ------------ | ----------- | ------------ | ------- | -| AC-TWEET-001 | 纯函数/离线 | 否 | 否 | -| AC-TWEET-002 | 纯函数/离线 | 否 | 否 | -| AC-TWEET-003 | 纯函数/离线 | 否 | 否 | -| AC-TWEET-004 | 纯函数/离线 | 否 | 否 | -| AC-TWEET-005 | 集成 | 是 (Twitter) | 否 | -| AC-TWEET-006 | 集成 | 是 (Twitter) | 否 | -| AC-TWEET-007 | 纯函数/离线 | 否 | 否 | -| AC-TWEET-008 | 集成 | 是 (Twitter) | 否 | -| AC-TWEET-009 | 纯函数/离线 | 否 | 否 | -| AC-TWEET-010 | 集成 | 是 (Twitter) | 否 | +## AC-TWEET-011:外壳请求携带登录 cookie(回归防护) + +- **输入**:`FetcherService`(`apiKey` = base64(`auth_token=…;ct0=…;twid=u%3D…;`));`axios.get` 被 spy, + 返回可用的 legacy 外壳 HTML(含 site verification meta + `ondemand.s`) +- **预期输出**:解析 transaction 文档时,对 X 外壳 URL 发出的请求头附带**解码后的 cookie**; + 未配置 `apiKey` 时不带 cookie +- **验证方法**:`bun run verify/index.ts --ac AC-TWEET-011` +- **前置条件**:无(mock,离线,不触网) +- **Pass 条件**: + - 首个请求 URL 为 `https://x.com/home`(`X_SHELL_URLS[0]`) + - `config.headers.cookie` 等于解码后的 cookie 字符串 + - 未配置 `apiKey` 时请求头不含 `cookie` 键 +- **回归来源**:上游 Rettiwt #885 曾以「抓外壳时附 cookie」修好同一错误,被 #888 合并时丢失。 + 本 AC 锁住「外壳请求必须带 cookie」这一不变量(见 [postmortem 013](../../docs/postmortem/013-upstream-frontend-drift.md)) + +--- + +## 总计:11 条 AC + +| AC | 分类 | 依赖外部 API | 依赖 AI | +| ------------ | --------------------- | ------------ | ------- | +| AC-TWEET-001 | 纯函数/离线 | 否 | 否 | +| AC-TWEET-002 | 纯函数/离线 | 否 | 否 | +| AC-TWEET-003 | 纯函数/离线 | 否 | 否 | +| AC-TWEET-004 | 纯函数/离线 | 否 | 否 | +| AC-TWEET-005 | 集成 | 是 (Twitter) | 否 | +| AC-TWEET-006 | 集成 | 是 (Twitter) | 否 | +| AC-TWEET-007 | 纯函数/离线 | 否 | 否 | +| AC-TWEET-008 | 集成 | 是 (Twitter) | 否 | +| AC-TWEET-009 | 纯函数/离线 | 否 | 否 | +| AC-TWEET-010 | 集成 | 是 (Twitter) | 否 | +| AC-TWEET-011 | 行为断言(mock/离线) | 否 | 否 | > 离线 AC 可通过 fixture 直接验证,无需网络;集成 AC 需要 `TWEET_KEYS` 环境变量。 From 0810ab3f8ff86a0f1b8bf79c0c4687bfa7f44c1d Mon Sep 17 00:00:00 2001 From: Chilfish Date: Sat, 26 Sep 2026 00:04:59 +0800 Subject: [PATCH 5/5] docs(tweet): record the AC-TWEET-011 close-out - docs/development-log/2026-09-25.md: AC close-out section -- motivation (the pure-function tests cannot see the caller dropping the cookie), placement, falsifiability check and scope (verification only, runtime semantics unchanged) - docs/development-log/README.md: add AC-TWEET-011 to the 2026-09-25 timeline row - CHANGELOG.md: AC-TWEET-011 regression guard entry Co-authored-by: CommandCodeBot --- CHANGELOG.md | 1 + docs/development-log/2026-09-25.md | 16 ++++++++++++++++ docs/development-log/README.md | 30 +++++++++++++++--------------- 3 files changed, 32 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e3b3d3b..868b3cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -75,6 +75,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/lang/zh-CN/ - X 迁移到 Rolldown/Vite 后不再内嵌 `ondemand.s` webpack chunk map,`x-client-transaction-id` 因此抛 `OnDemandFileUrlResolutionError`,所有走 rettiwt-api 的 X 数据接口失败。**真因是外壳请求漏带登录 cookie**(X 仅对登录态返回仍含该 chunk map 的旧外壳):新增 `buildXShellHeaders`,配置 API Key 时附带解码后的 cookie,`_fetchXHomePage` 改为使用它 - 外壳候选列表改为 `X_SHELL_URLS`(`/home` 优先,legacy 路由作无 Key 时的 guest 兜底)+ `isUsableXDocument` 双条件校验;新增/扩充 `test/unit/rettiwt-transaction-document.spec.ts` - 沉淀 postmortem 013 与 `docs/features/tweet/transaction-id.md`(含 `ondemand.s` indices 每构建随机、不可 pin 的实测) +- 补 AC-TWEET-011 回归防护(行为断言:mock `axios.get` 跑真实 `_fetchTransactionDocument`,锁定外壳请求必带 cookie、无 Key 时保持匿名),与 AC 文档 1:1 绑定 #### 验证诚信修复(2026-09-12) diff --git a/docs/development-log/2026-09-25.md b/docs/development-log/2026-09-25.md index 2039cfd..d728fff 100644 --- a/docs/development-log/2026-09-25.md +++ b/docs/development-log/2026-09-25.md @@ -74,3 +74,19 @@ **验证**:`test/unit/rettiwt-transaction-document.spec.ts` 扩到 8 用例(含 cookie 头三项断言); 真实上游 E2E 复核通过。详见 [postmortem 013](../postmortem/013-upstream-frontend-drift.md) 「后续修正」与 [transaction-id.md](../features/tweet/transaction-id.md)。 + +## AC 收口:外壳 cookie 回归防护(AC-TWEET-011,✅ 已完成) + +**动机**:纯函数 `buildXShellHeaders` 的单测只能证明「这个函数对」,**发现不了「调用点不再用它」**—— +而这正是本次 bug 的形态(上游 #885 修好、#888 合并时丢失的回归)。所以补一条**行为断言**: +mock `axios.get` 后跑真实 `_fetchTransactionDocument`,断言**实际发出的**外壳请求头 `cookie` +等于解码后的 cookie;无 `apiKey` 时保持匿名。 + +**落位**:`test/acceptance/ac-tweet.spec.ts`(`describe('AC-TWEET-011: …')`),与 +`verify/acceptance-criteria/AC-tweet.md` v1.2 的 AC-TWEET-011 按 ac-contract 契约 1:1 绑定。 + +**Falsifiability 复核**:临时把 `_fetchXHomePage` 回退为 `this.config.headers` 后,该用例报 +`expected undefined to be 'auth_token=…'`——确认它真能捕获本类回归,而非恒真(对齐 postmortem 011 +「门禁先问『改坏它会不会红』」)。 + +**范围**:仅补验证与文档,运行时语义不变——PR #21 的 cookie 修复即最终实现。 diff --git a/docs/development-log/README.md b/docs/development-log/README.md index a41dbb0..2f09d58 100644 --- a/docs/development-log/README.md +++ b/docs/development-log/README.md @@ -4,21 +4,21 @@ ## 时间线(新 → 旧) -| 日期 | 主题 | -| --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| [2026-09-25](2026-09-25.md) | 修复 X 前端迁移(webpack→Rolldown/Vite)击穿 client transaction id 导致 get tweet 全站失败:改从仍提供旧外壳的路由探测取文档 → **真因 = 外壳请求漏带 cookie**(`buildXShellHeaders`,仅登录态返回含 `ondemand.s` 的旧外壳)+ `X_SHELL_URLS` 候选校验作 guest 兜底;沉淀 postmortem 013 | -| [2026-09-16](2026-09-16.md) | X Space 卡片接入(audiospace 空壳卡片 → `AudioSpaceById` 补全标题/主播/人数/时长;范围裁定「卡片 + 跳转 X 播放」;AC-SPACE-001~008;不改高危解析器) | -| [2026-09-12](2026-09-12.md) | 验证诚信修复落地(F1 dev server `NODE_ENV` 泄漏 / F2 零匹配守卫 + 模块别名 / F3 死断言 / F4 AC↔测试名元测试 / F5 workflow 步骤解析 / F8 去 fixture 自证 / F9 AC-TRANS 命名 / F10 断言 lint / F12 README / F13 deprecated 参数)+ postmortem 011 | -| [2026-09-11](2026-09-11.md) | anon-tweet Agent Skill 升级 v1.2.0(随附 PowerShell 封装 + OpenAPI 快照;随附 spec 与 llms.ts 14/14 奇偶一致)+ 版本单源治理(应用版本归 package.json / skill 版本归 SKILL.md,去手写副本)+ 验证套件诚信审查(AC 空跑报绿 / 静态扫描冒充行为断言;修复 backlog F1~F13) | -| [2026-09-06](2026-09-06.md) | 可安装 PWA + Web Share Target(系统分享直达推文/IG;极简网络透传 SW + 首页 GET 接收复用 TweetInputForm) | -| [2026-09-04](2026-09-04.md) | 媒体图片链接统一为 `/slug.jpg`(去掉 `?name=&format=` 查询参数,normalizeMediaUrl 收口) | -| [2026-08-31](2026-08-31.md) | 推文搜索接入(/search 页面 + /api/tweet/search + rettiwt-api guest 授权补缺)+ llms.txt / openapi.json 接入 + tweet GET 全部入口统一走缓存链(getLocalTweet) | -| [2026-08-19](2026-08-19.md) | 主线推送 34 提交 + baseUrl 白名单改可选加固(默认关闭) | -| [2026-08-18](2026-08-18.md) | 修复 unified_card 解析只认 `details_1` 布局(Trending/topic 卡片丢失 og 信息) | -| [2026-08-17](2026-08-17.md) | 修复「句首补充」显示与持久化链路(index 对齐合并丢实体 + AI 流丢片段 + 保存即同步) | -| [2026-08-14](2026-08-14.md) | 测试基线红灯修复(vitest 冷加载超时,独立任务) | -| [2026-08-13](2026-08-13.md) | AI 视觉描述子系统规划(OpenRouter + MiMo-V2.5,文档先行 Phase 0) | -| [2026-08-09](2026-08-09.md) | 工程规范对标 Float + verify 二期 S5/S6/S8(CI / Screenshot Verifier / --server 自动管理) | +| 日期 | 主题 | +| --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [2026-09-25](2026-09-25.md) | 修复 X 前端迁移(webpack→Rolldown/Vite)击穿 client transaction id 导致 get tweet 全站失败:改从仍提供旧外壳的路由探测取文档 → **真因 = 外壳请求漏带 cookie**(`buildXShellHeaders`,仅登录态返回含 `ondemand.s` 的旧外壳)+ `X_SHELL_URLS` 候选校验作 guest 兜底;沉淀 postmortem 013 + AC-TWEET-011 外壳 cookie 回归防护 | +| [2026-09-16](2026-09-16.md) | X Space 卡片接入(audiospace 空壳卡片 → `AudioSpaceById` 补全标题/主播/人数/时长;范围裁定「卡片 + 跳转 X 播放」;AC-SPACE-001~008;不改高危解析器) | +| [2026-09-12](2026-09-12.md) | 验证诚信修复落地(F1 dev server `NODE_ENV` 泄漏 / F2 零匹配守卫 + 模块别名 / F3 死断言 / F4 AC↔测试名元测试 / F5 workflow 步骤解析 / F8 去 fixture 自证 / F9 AC-TRANS 命名 / F10 断言 lint / F12 README / F13 deprecated 参数)+ postmortem 011 | +| [2026-09-11](2026-09-11.md) | anon-tweet Agent Skill 升级 v1.2.0(随附 PowerShell 封装 + OpenAPI 快照;随附 spec 与 llms.ts 14/14 奇偶一致)+ 版本单源治理(应用版本归 package.json / skill 版本归 SKILL.md,去手写副本)+ 验证套件诚信审查(AC 空跑报绿 / 静态扫描冒充行为断言;修复 backlog F1~F13) | +| [2026-09-06](2026-09-06.md) | 可安装 PWA + Web Share Target(系统分享直达推文/IG;极简网络透传 SW + 首页 GET 接收复用 TweetInputForm) | +| [2026-09-04](2026-09-04.md) | 媒体图片链接统一为 `/slug.jpg`(去掉 `?name=&format=` 查询参数,normalizeMediaUrl 收口) | +| [2026-08-31](2026-08-31.md) | 推文搜索接入(/search 页面 + /api/tweet/search + rettiwt-api guest 授权补缺)+ llms.txt / openapi.json 接入 + tweet GET 全部入口统一走缓存链(getLocalTweet) | +| [2026-08-19](2026-08-19.md) | 主线推送 34 提交 + baseUrl 白名单改可选加固(默认关闭) | +| [2026-08-18](2026-08-18.md) | 修复 unified_card 解析只认 `details_1` 布局(Trending/topic 卡片丢失 og 信息) | +| [2026-08-17](2026-08-17.md) | 修复「句首补充」显示与持久化链路(index 对齐合并丢实体 + AI 流丢片段 + 保存即同步) | +| [2026-08-14](2026-08-14.md) | 测试基线红灯修复(vitest 冷加载超时,独立任务) | +| [2026-08-13](2026-08-13.md) | AI 视觉描述子系统规划(OpenRouter + MiMo-V2.5,文档先行 Phase 0) | +| [2026-08-09](2026-08-09.md) | 工程规范对标 Float + verify 二期 S5/S6/S8(CI / Screenshot Verifier / --server 自动管理) | ## 记录约定