diff --git a/.agents/skills/anon-tweet/SKILL.md b/.agents/skills/anon-tweet/SKILL.md index ac180645..8d185263 100644 --- a/.agents/skills/anon-tweet/SKILL.md +++ b/.agents/skills/anon-tweet/SKILL.md @@ -4,16 +4,17 @@ description: >- 匿名浏览 Twitter/X 推文与 Instagram 帖子的 API 使用指南(BFF 聚合接口,读取无需 API Key)。 用 /api/tweet/search 按关键词或 X 高级语法搜索推文、/api/tweet/get/{id} 与 /api/tweet/replies/{id} 读取单条推文及回复、/api/user/get/{username} 查询用户资料、 - /api/ig/get/{id} 拉取 IG 帖子(caption/媒体),另有 AI 翻译与图片代理接口。 - 当需要搜索/抓取推文、查看 X 用户资料或 IG 帖子内容时使用。 + /api/ig/get/{id} 拉取 IG 帖子(caption/媒体),长文推文还会带 X Article(标题 + 块文档正文), + 另有 AI 翻译(推文 / IG / 长文按块)与图片代理接口。 + 当需要搜索/抓取推文、读取 X 长文、查看 X 用户资料或 IG 帖子内容时使用。 license: MIT compatibility: 需要 PowerShell 7+(pwsh)与网络访问部署实例;纯 PowerShell 封装,不依赖 curl/python3 metadata: author: Chilfish - version: "1.2.0" + version: "1.3.0" base-url: https://anon-tweet.chilfish.top repository: https://github.com/Chilfish/anonTweet - updated: "2026-09-12" + updated: "2026-09-27" --- # Anon Tweet API @@ -29,6 +30,7 @@ metadata: - 搜索推文(关键词 / hashtag / 用户 / 高级语法过滤) - 读取单条推文、其回复列表或 List 时间线 +- 读取 X 长文(Article):`/api/tweet/get/{id}` 对文章推文返回 `article`(标题 + 块文档正文) - 查询 X 用户资料 - 拉取 Instagram 帖子(caption / 媒体 / 标签)或翻译 caption - 在页面上展示 IG 图片时走图片代理绕过 CDN CORS @@ -54,7 +56,7 @@ pwsh -NoProfile -File $A help | 方法 | 路径 | 用途 | | --- | --- | --- | | GET | `/api/tweet/search?q=...` | 推文搜索(核心,支持 X 高级语法) | -| GET | `/api/tweet/get/{id}` | 单条推文(三层缓存 → Twitter 原文;返回 EnrichedTweet 数组,可能为空) | +| GET | `/api/tweet/get/{id}` | 单条推文(三层缓存 → Twitter 原文;文章推文另含 `article` 块文档) | | GET | `/api/tweet/replies/{id}?cursor=...` | 推文回复(cursor 分页) | | GET | `/api/tweet/list/{id}` | List 时间线(EnrichedTweet 数组) | | GET | `/api/user/get/{username}` | 用户资料(DB 缓存,无记录返回 null) | @@ -62,7 +64,7 @@ pwsh -NoProfile -File $A help | GET | `/api/ig/get/{id}` | IG 帖子(id 为 shortcode;未配 INS_COOKIES 返回空数组) | | POST | `/api/ig/translate/{id}` | IG caption 翻译(manualTranslation 传入则跳过 AI) | | GET | `/api/proxy/image?url=...` | 图片代理(url 白名单校验,返回二进制) | -| POST | `/api/ai-translation` | 通用 AI 翻译(type + tweet/igPost) | +| POST | `/api/ai-translation` | 通用 AI 翻译(`type`: twitter / ins / article) | | POST | `/api/ai-vision` | 图片/视频 AI 描述 | | POST | `/api/bili-post` | B 站动态抓取 | @@ -116,7 +118,9 @@ Invoke-RestMethod "https://anon-tweet.chilfish.top/api/ig/translate/DWlrun0AVbE" ## 数据结构要点 -- **EnrichedTweet**: `id_str`、`text`、`url`、`lang`(如 en/ja/zxx)、`created_at`(ISO 8601)、`user`(TweetUser)、`entities`(Entity[]),可选 `quoted_tweet_id` / `card` / `mediaDetails` / `visionInfo` +- **EnrichedTweet**: `id_str`、`text`、`url`、`lang`(如 en/ja/zxx)、`created_at`(ISO 8601)、`user`(TweetUser)、`entities`(Entity[]),可选 `quoted_tweet_id` / `card` / `mediaDetails` / `visionInfo` / `article` +- **TweetArticle**(文章推文才有): `id`、`url`、`title`、`previewText`、`plainText`、`format`(`rich` 带 `blocks`,`plain` 仅纯文本)、`coverImage`、`publishedAt`(epoch ms)、`blocks`(ArticleBlock[]) +- **ArticleBlock**: `type` ∈ paragraph / heading / list-item / quote / markdown / divider / image / embed-tweet / link / unknown;`key` 为稳定块 ID;段落类带 `runs`(文本/链接/mention/hashtag);`text`(markdown)、`media`(image)、`tweetId`(embed-tweet) - **TweetUser**: `id_str`、`name`、`screen_name`、`profile_image_url_https`、`verified`、`is_blue_verified`、`verified_type`(Business/Government)、`profile_image_shape`(Circle/Square/Hexagon) - **Entity**: `type` ∈ text / hashtag / mention / url / media / symbol / media_alt / separator;`index` 为文本偏移;hashtag/mention/url/media/symbol 带 `href`;`aiTranslation` / `translation` 存翻译 - **SearchResponse / RepliesResponse**: `{ tweets: EnrichedTweet[], nextCursor: string | null }` diff --git a/.agents/skills/anon-tweet/references/anon-tweet-openapi.json b/.agents/skills/anon-tweet/references/anon-tweet-openapi.json index 77178fe2..6bed6efd 100644 --- a/.agents/skills/anon-tweet/references/anon-tweet-openapi.json +++ b/.agents/skills/anon-tweet/references/anon-tweet-openapi.json @@ -401,7 +401,7 @@ "Tweet" ], "operationId": "tweetSet", - "summary": "保存推文实体编辑", + "summary": "保存推文实体编辑或文章按块译文", "requestBody": { "required": true, "content": { @@ -415,16 +415,18 @@ "properties": { "intent": { "type": "string", - "const": "updateEntities" + "enum": [ + "updateEntities", + "updateArticleTranslations" + ] }, "data": { "type": "array", - "description": "每项包含 tweetId 与 entities(TranslationEntity[])", + "description": "updateEntities:每项 { tweetId, entities };updateArticleTranslations:每项 { tweetId, translation }", "items": { "type": "object", "required": [ - "tweetId", - "entities" + "tweetId" ], "properties": { "tweetId": { @@ -434,7 +436,12 @@ "type": "array", "items": { "$ref": "#/components/schemas/TranslationEntity" - } + }, + "description": "intent=updateEntities 时必填" + }, + "translation": { + "$ref": "#/components/schemas/ArticleTranslation", + "description": "intent=updateArticleTranslations 时必填" } } } @@ -952,7 +959,7 @@ ], "operationId": "aiTranslation", "summary": "统一 AI 翻译端点", - "description": "type=twitter(默认)翻译推文实体;type=ins 翻译 IG caption。baseUrl 走 ENABLE_AI_BASE_URL_WHITELIST 可选白名单(AC-SEC-001)。", + "description": "type=twitter(默认)翻译推文实体;type=ins 翻译 IG caption;type=article 按块翻译 X 长文。baseUrl 走 ENABLE_AI_BASE_URL_WHITELIST 可选白名单(AC-SEC-001)。", "requestBody": { "required": true, "content": { @@ -964,7 +971,8 @@ "type": "string", "enum": [ "twitter", - "ins" + "ins", + "article" ], "default": "twitter" }, @@ -976,6 +984,10 @@ "$ref": "#/components/schemas/IGPost", "description": "type=ins 时必填" }, + "article": { + "$ref": "#/components/schemas/TweetArticle", + "description": "type=article 时必填" + }, "enableAITranslation": { "type": "boolean", "description": "twitter 分支守卫" @@ -1049,6 +1061,14 @@ "captionTranslation": { "type": "string", "description": "ins 分支" + }, + "articleId": { + "type": "string", + "description": "article 分支" + }, + "translation": { + "$ref": "#/components/schemas/ArticleTranslation", + "description": "article 分支:按块 key 的译文" } } } @@ -1068,7 +1088,7 @@ } }, "404": { - "description": "tweet/igPost 缺失", + "description": "tweet / igPost / article 缺失", "content": { "application/json": { "schema": { @@ -1696,6 +1716,10 @@ "type": "object", "description": "X Space 卡片(SpaceDetails,见 app/types/space.ts;仅 Space 推文有)" }, + "article": { + "$ref": "#/components/schemas/TweetArticle", + "description": "X 长文卡片与块文档(app/types/article.ts;仅文章推文有)" + }, "comments": { "type": "array", "items": { @@ -1719,6 +1743,206 @@ }, "additionalProperties": true }, + "TweetArticle": { + "type": "object", + "description": "X 长文(Article)数据(app/types/article.ts TweetArticle),挂在 EnrichedTweet.article", + "required": [ + "id", + "url", + "title", + "format" + ], + "properties": { + "id": { + "type": "string", + "description": "文章 rest_id(与推文 id 不同)" + }, + "url": { + "type": "string", + "format": "uri", + "description": "文章永久链接" + }, + "title": { + "type": "string" + }, + "previewText": { + "type": "string", + "description": "摘要" + }, + "plainText": { + "type": "string", + "description": "纯文本全文(content_state 缺失时的兜底)" + }, + "format": { + "type": "string", + "enum": [ + "rich", + "plain" + ], + "description": "rich 表示 blocks 可用;plain 仅纯文本" + }, + "coverImage": { + "$ref": "#/components/schemas/ArticleMedia" + }, + "publishedAt": { + "type": "integer", + "description": "首次发布时间(epoch ms)" + }, + "blocks": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ArticleBlock" + }, + "description": "块文档(format=rich 时存在)" + } + }, + "additionalProperties": true + }, + "ArticleMedia": { + "type": "object", + "description": "文章内嵌媒体(图片)", + "required": [ + "id", + "url" + ], + "properties": { + "id": { + "type": "string" + }, + "url": { + "type": "string", + "format": "uri" + }, + "width": { + "type": "integer" + }, + "height": { + "type": "integer" + }, + "alt": { + "type": "string" + } + } + }, + "ArticleBlock": { + "type": "object", + "description": "文章块(Draft.js block 语义化,app/types/article.ts ArticleBlock)。type 判别:段落类带 runs;markdown 带 text;image 带 media;embed-tweet 带 tweetId。", + "required": [ + "key", + "type" + ], + "properties": { + "key": { + "type": "string", + "description": "稳定块 ID(按块翻译与渲染对齐用)" + }, + "type": { + "type": "string", + "enum": [ + "paragraph", + "heading", + "list-item", + "quote", + "markdown", + "divider", + "image", + "embed-tweet", + "link", + "unknown" + ] + }, + "runs": { + "type": "array", + "description": "段落的行内片段(文本 / link / mention / hashtag,带样式)", + "items": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "text", + "link", + "mention", + "hashtag" + ] + }, + "text": { + "type": "string" + }, + "url": { + "type": "string", + "description": "type=link" + }, + "href": { + "type": "string", + "description": "type=mention / hashtag" + }, + "styles": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "bold", + "italic", + "strikethrough" + ] + } + } + } + } + }, + "level": { + "type": "integer", + "enum": [ + 1, + 2, + 3 + ], + "description": "heading 级别" + }, + "ordered": { + "type": "boolean", + "description": "list-item 是否有序" + }, + "text": { + "type": "string", + "description": "markdown 块的原始 GFM" + }, + "media": { + "$ref": "#/components/schemas/ArticleMedia", + "description": "image 块" + }, + "caption": { + "type": "string", + "description": "image 块说明" + }, + "tweetId": { + "type": "string", + "description": "embed-tweet 块的推文 id" + }, + "url": { + "type": "string", + "description": "link 块 URL" + } + }, + "additionalProperties": true + }, + "ArticleTranslation": { + "type": "object", + "description": "文章按块译文(app/lib/article/translate.ts ArticleTranslation):title + 块 key → 译文", + "properties": { + "title": { + "type": "string" + }, + "blocks": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "块 key → 译文(link / mention / hashtag 以 <<__LINK_n__>> 占位符锚定)" + } + } + }, "IGAudio": { "type": "object", "description": "IG 帖子附带音频/音乐信息(app/types/ins.ts IGAudio)", @@ -2042,4 +2266,4 @@ } } } -} +} \ No newline at end of file diff --git a/.commandcode/taste/taste/taste.md b/.commandcode/taste/taste/taste.md index 53191df8..46a84e4e 100644 --- a/.commandcode/taste/taste/taste.md +++ b/.commandcode/taste/taste/taste.md @@ -26,7 +26,7 @@ - Wants active planning docs (e.g. `docs/planning/backlog.md`) scoped to only the current phase's open items — completed work must be archived (into an `docs/archive/` file with an index entry) instead of accumulating in the live file; rejects the "keep stacking everything into one list" pattern. Confidence: 0.7 - Wants a single source of truth for version numbers: dislikes the same version hardcoded in multiple files (code, docs, scripts) where the copies can drift out of sync, and expects versioning to derive from one canonical place (e.g. read `package.json` instead of hand-writing literals). Confidence: 0.85 - Expects commit messages to match the repo's existing convention: English Conventional Commits subjects with a scope naming the touched area (e.g. `docs(review): ...`, `docs(readme): ...`, `docs(community): ...`, `chore(taste): ...`) plus a descriptive body enumerating the included changes, rather than free-form subjects. Confidence: 0.75 -- "Commit your changes" (e.g. "commit你的更改") means commit only — do not push unless explicitly asked; report that the branch was left unpushed. Confidence: 0.65 +- "Commit your changes" (e.g. "commit你的更改", "先commit所有的更改") means commit only — do not push unless explicitly asked; report that the branch was left unpushed. Confidence: 0.7 - Fixing a bug is not done until recurrence is prevented: expects the fix to include regression guards (behavioral tests reproducing the failure mode), the relevant check wired into pre-push/CI gates, and inline documentation of the constraint at the point it's used — not just correcting the immediate symptom. Confidence: 0.75 - Wants non-obvious dependency/version constraints pinned and made visible at the point of use (e.g. using `~`/exact pins and an explanatory comment when a version is bounded by another package's peer range) so a future routine upgrade can't silently drift into an incompatible major. Confidence: 0.7 - Prefers minimal, non-noisy UI copy: dislikes long/verbose disclosure text placed directly on the page (calls it "太吵" / too noisy), and values restraint over richness ("有时候丰富不代表好") — favor trimming or dropping informative-but-cluttering content instead of adding more. Confidence: 0.8 @@ -49,6 +49,34 @@ - Treats a changelog as a historical record: reformatting it into a standard structure (Keep a Changelog sections, its original grouping demoted to sub-headings) is fine, but the recorded entries' content must not be rewritten. Confidence: 0.55 rouping demoted to sub-headings) is fine, but the recorded entries' content must not be rewritten. Confidence: 0.55 - For a missing/unmapped data field, expects the fix to run the concrete pipeline end to end — temp probe against the real upstream, add the value into the corresponding field/type, then render it — instead of broad codebase exploration or an upfront design discussion; points at the closest existing repo file (e.g. `app/lib/react-tweet/utils/get-tweet.ts`) to copy as the pattern. Confidence: 0.65 +- Prefers native/first-party framework capabilities over adding a plugin dependency: when a build tool's own config can do the job (e.g. Vite 8's built-in `resolve.tsconfigPaths` instead of the `vite-tsconfig-paths` plugin), wants the built-in used and the extra package not introduced. Confidence: 0.6 - Expects assumptions about external services/URL schemes to be validated against the live endpoint per variant rather than over-generalized from one working example — reports back after real-world testing (e.g. twimg `/media/` works as both `?format=jpg` and `/slug.jpg`, but `/card_img//` only resolves in query form and 404s as `/slug.jpg`, so a blanket normalization broke link-card images). Confidence: 0.65 - Expects degraded/edge states of external data to be surfaced in the UI rather than the element silently disappearing: after verifying the happy path himself, flags that deleted/unavailable resources or replay-disabled cases have "没有对应的UI提示" and wants a visible fallback/tombstone (or an explicit non-actionable state such as "不可回放") instead of a bare or missing element. Confidence: 0.7 -- When reporting a gap or reviewing a feature, supplies a concrete real-world sample (a live URL / specific resource id) to reproduce against rather than describing the case abstractly. Confidence: 0.55 +- When reporting a gap or reviewing a feature, supplies a concrete real-world sample (a live URL / specific resource id) to reproduce against rather than describing the case abstractly — e.g. pastes the full error stack trace plus the exact tweet id when a request fails. Confidence: 0.6 +- Does not accept "it's an upstream problem" as a resolution: when a third-party dependency/library breaks because the external service changed, wants the project made working again via a workaround inside our own code ("虽然这是上游x的问题,你看看我们该怎么解决?") rather than waiting on upstream or merely explaining the cause. Confidence: 0.6 + +- Before acting outside our own repo (opening an upstream PR/issue), wants read-only reconnaissance first and a draft he can review — explicitly says "你先别急着真的去提,只是拿gh cli看看情况"; publishing is a separate, explicitly-approved step, while local/routine actions stay autonomous. Confidence: 0.75 +- When drafting a contribution to a third-party project, expects the agent to mine that project's own conventions first — CONTRIBUTING if present, the PR template filled in verbatim, the real target branch (e.g. `dev`, not `main`), the CI gates, and their formatter/linter config (tabs, quote style, naming rules, member ordering) — instead of applying our repo's conventions or generic open-source etiquette. Confidence: 0.7 +- Wants a proposed fix to fit the upstream project's actual infrastructure: review prior attempts at the same bug, confirm a dependency bump is really impossible before proposing a code workaround, and don't ship test suites or patterns the project doesn't have (e.g. no Vitest tests when the repo only manual-verifies via a playground). Confidence: 0.6 +- For an upstream contribution, expects the fork-based workflow once the draft is approved: fork the project, base the change on our own copy, and keep the fix as a single commit dedicated to that issue ("在我们的版本针对该问题的单条commit"), then open the PR from there — rather than a multi-commit branch or pushing into the upstream repo directly. Confidence: 0.6 +- Wants lengthy deliverables (PR bodies, draft write-ups) put into a temporary markdown file (in the repo's gitignored `tmp/`) rather than pasted into chat, and kept concise — the long, section-by-section PR draft drew "先写在临时的md里,应该不用那么啰嗦" ("write the content into a temp md first, it doesn't need to be so verbose"). Confidence: 0.7 +- Rejects a fix that only works by bypassing a transient external condition: when the workaround succeeds merely because an in-progress upstream rollout hasn't reached that code path yet ("只是当前还没轮到这个页面所以我们才能绕过实现"), he wants the mechanism decomposed first — which specific inputs are actually broken vs. still supplied — and a plan that addresses the dying dependency itself, not the timing exploit shipped as if it were the fix. Confidence: 0.8 +- Before implementing a fix whose approach is still unsettled, wants the research and plan written into a repo doc first ("先把你的调研和计划写到文档里"): root-cause breakdown, the evidence gathered, and a comparison of the candidate solutions with their tradeoffs (multiple options, not just one recommendation), laid out in short/medium/long-term tiers. Confidence: 0.7 +- Scopes a fix to the deployment's actual runtime constraints instead of covering hypothetical ones: when told the project always supplies credentials ("cookie 是必传的,就不存在 guest 的场景了"), expects the agent to verify that assumption against the repo (env/config/auth code) and then simplify — drop the guest/anonymous fallback paths and target the real path, rather than keeping defensive branches for scenarios the project never hits. Confidence: 0.6 +- Strongly rejects over-engineering: expects the change to match the scale of the reference/prior implementation, and pushes back when the agent builds far more machinery than the problem needs ("真的有这么复杂吗?... 的实现没那么多吧?") — a ~70-line probe loop holding new module consts, helper functions, a new enum member and an extracted method, where the project's own precedent for the same bug was a one-line URL swap, is wrong-sized. Prefer the smallest diff that solves the problem, and check the project's existing precedent for how big the fix should be. Confidence: 0.8 +- Once a fix's approach is settled, wants it implemented in the project's own repository and taken through the full PR flow ("先在本项目实现该改进,按pr流程走") — create a `fix/...` branch, split the work into commits by concern (code / tests / docs), run the repo gates, push, open the PR with a concise body, and verify CI — rather than only preparing an upstream patch or editing files in place. Confidence: 0.75 +- For an implementation he didn't write (e.g. reviewing someone else's merged/pending PR), wants a blunt critical assessment ("锐评这个实现,它真的做好了吗?") rather than validation: concrete file/line evidence, findings ranked by severity, an explicit split of what the change genuinely got right vs. what is still unfinished, and a forward-looking answer for when the upstream changes again — not a neutral summary. Confidence: 0.6 +- Treats "the PR is mergeable" as the trigger for the whole close-out to run autonomously — "你就能走后续的合并和本地同步流程": execute the merge by the repo's convention (`gh pr merge --merge --delete-branch`), then sync the local checkout (checkout main, fast-forward to `origin/main`, delete the merged local branch, prune stale remote branches) and confirm the post-merge CI run on main is green. For our own repo a merge is routine/mechanical work, not something to stop and ask permission for (unlike publishing to a third-party upstream repo). Confidence: 0.7 +- Defines "done" for a PR as _genuinely mergeable_, and expects the agent to drive it there autonomously by the repo's own conventions — "按规范来实施吧,直到该pr真的能被合并了". A passing CI/gate is not the bar on its own: when the new behaviour lacks its acceptance criterion / regression test, closing that verification gap counts as part of the merge work (runtime semantics kept unchanged if possible), and the work ends only once checks are green and the PR reports MERGEABLE/CLEAN. Confidence: 0.7 +- Treats code quality as a first-class deliverable, not a nicety: reads through the touched code and calls out "难看" / weird / non-idiomatic writing ("不合规范的写法") that makes it hard to understand and maintain, expecting the implementation task to include cleaning it up (extract cohesive modules, drop non-null assertions/dead branches, type-narrow instead of ad-hoc control flow) rather than only adding the new feature. Confidence: 0.8 +- Expects UI design to get explicit attention as part of any implementation, not just functional correctness — flags it in the task brief ("注意UI方面的设计"), so visual polish/design of the changed surfaces should be actively addressed and reported. Confidence: 0.65 +- Dislikes redundant data in payloads/caches: when one field already covers a second field's content (e.g. a rich block document already contains the full text, making `plainText` a duplicate that can be tens of KB), wants the redundant field dropped rather than carrying both — "感觉有些浪费空间,毕竟都有blocks了". Confidence: 0.7 +- Expects caching to be wired at _every_ applicable layer, not just the persistent store: if a value is cached to the DB it must also be cached in the local (client/FS) cache, so it survives DB-off / local-dev and a reload doesn't recompute it ("翻译的内容即便是local cache也得缓存到位吧"). Confidence: 0.65 +- Expects feature/UI parity across analogous surfaces: when a new page mirrors an existing one (article reader vs tweet page), it should carry the same toolbar/actions (settings/config entry, one-click screenshot, copy, share, download) rather than shipping a stripped-down version ("tweet 页的那些工具栏缺了"). Confidence: 0.7 +- Wants a feature reasoned about from the real end-user's need/job, not just made functionally complete — explicitly asks the agent to step back and rethink the true user-need UX for the surface ("再想想这一块真实用户需求的UX"). Confidence: 0.6 +- Prefers reusing the project's existing components over placeholder/stub UI: when content of a type the app already renders (an embedded tweet) shows as a dumb placeholder card, expects the existing component to be wired in — "感觉其实是能用我们的推文组件去渲染" — rather than accepting the placeholder. Confidence: 0.65 +- Expects independent upstream fetches to be issued in parallel (batch/Promise.all through the existing cache chain) instead of serially, and assumes parallelism is the natural design for such cases ("这应该是能并行获取的吧"). Confidence: 0.6 +- Drops UI affordances that duplicate what the content already provides: once the embedded element itself links to the canonical destination (a rendered tweet's timestamp → the X permalink), the extra "在 X 查看" button is redundant and should go (keeping a fallback link only for the unavailable case). Confidence: 0.6 +- Treats Storybook stories as a required part of shipping a component, not an optional extra: when a change adds new components without their stories he flags it ("还没写完整的 Storybook 样例") and expects complete, per-component stories added (and wired into the UI coverage spec) before the work is done. Confidence: 0.8 +- Wants Storybook stories to reuse the project's existing fixtures (the committed `test/fixtures/...` files) instead of hand-writing new inline sample data ("可以复用已有的 fixtures") — keep one source of truth for sample data across stories and acceptance tests. Confidence: 0.75 +- Expects client/Storybook module graphs to stay free of server-only code: rejects browser-runtime failures such as `process is not defined` caused by a story/fixture transitively importing server-only modules (e.g. `ai-timeout`), and expects the pure logic split into its own dependency-free module with the barrel no longer re-exporting the server-only entry (`~/lib/article` stopped re-exporting `./translate`, with a comment explaining why). Confidence: 0.6 diff --git a/.storybook/vite.config.ts b/.storybook/vite.config.ts index d1822146..e6748ec1 100644 --- a/.storybook/vite.config.ts +++ b/.storybook/vite.config.ts @@ -1,10 +1,11 @@ import tailwindcss from '@tailwindcss/vite' import { defineConfig } from 'vite' -import tsconfigPaths from 'vite-tsconfig-paths' export default defineConfig(() => ({ plugins: [ tailwindcss(), - tsconfigPaths(), ], + resolve: { + tsconfigPaths: true, + }, })) diff --git a/app/components/article/ArticleBody.tsx b/app/components/article/ArticleBody.tsx new file mode 100644 index 00000000..af77b68a --- /dev/null +++ b/app/components/article/ArticleBody.tsx @@ -0,0 +1,178 @@ +import type { ReactNode } from 'react' +import type { ArticleBlock, ArticleRun } from '~/types' +import { Separator } from '~/components/ui/separator' +import { TweetLink } from '~/lib/react-tweet/twitter-theme/tweet-link' +import { cn } from '~/lib/utils' +import { ArticleEmbedTweet } from './ArticleEmbedTweet' +import { ArticleImage } from './ArticleImage' +import { ArticleMarkdown } from './ArticleMarkdown' +import { ArticleRuns } from './ArticleRuns' +import { ArticleTranslatedText } from './ArticleTranslatedText' + +export type ArticleViewMode = 'original' | 'translation' | 'bilingual' + +/** 非列表块(列表项由外层归组为