Skip to main content

nieta-topic-page

Scaffold, develop, self-check, and publish a dev-draft of a custom embedded 话题页 (topic page) for nieta-app using the @talesofai/topic-sdk runtime. Use this when a creator (or an agent acting for them) wants to build a custom topic page, render read-only /v1/embed data, handle the app / web-embedded / guest three-state degradation, wire navigation (host carries app launch), and publish a draft for in-app debugging. The creator delivers the finished and debugged project to the internal team for prod publishing. Triggers: "自定义话题页", "内嵌话题页", "topic-sdk", "embed topic page", "搭话题页".

설치로 이동

소스 정보

저장소
talesofai/topic-sdk
최근 소스 활동
2026년 8월 17일 00:23
감지된 SKILL.md 언어
중국어
스타
0
포크
0

설치 방법

기본적으로 소스를 먼저 확인하는 Prompt가 선택됩니다. 직접 명령으로 전환하거나 로컬 사본을 다운로드할 수도 있습니다.

소스 파일 검토

설치 여부를 결정하기 전에 SKILL.md와 SkillsMP에 표시된 보조 파일을 읽어 보세요.

파일 탐색기
17 개 파일

SKILL.md 표시 중

SKILL.md
소스 지침 · 읽기 전용 미리보기
name
nieta-topic-page
description
Scaffold, develop, self-check, and publish a dev-draft of a custom embedded 话题页 (topic page) for nieta-app using the @talesofai/topic-sdk runtime. Use this when a creator (or an agent acting for them) wants to build a custom topic page, render read-only /v1/embed data, handle the app / web-embedded / guest three-state degradation, wire navigation (host carries app launch), and publish a draft for in-app debugging. The creator delivers the finished and debugged project to the internal team for prod publishing. Triggers: "自定义话题页", "内嵌话题页", "topic-sdk", "embed topic page", "搭话题页".
# nieta-app 自定义内嵌话题页 — 搭建、开发与 dev 发布 skill ## ⚠ 开工前必做:自我更新(不允许跳过) 每次执行本 skill、开始任何实际工作之前,先确认手上这份 `skill/`(以及消费方项目里 `@talesofai/topic-sdk` 依赖)不是过期副本: > **例外**:如果你正在对 `topic-sdk` 仓库本身开发/提交代码(比如在某个 feature 分支上改 SDK 源码或本文档),你就是版本源头,跳过下面这一步——`git pull --ff-only origin main` 在领先 main 的分支上不是 fast-forward,硬跑只会把自己卡死。 - 本目录是 git 仓库、且当前在 `main` 分支(`git branch --show-current` 确认):`git pull --ff-only origin main`。拉不动(本地有未提交改动导致冲突等)→ 停下来向用户说明情况,不要在不确定的版本上继续。当前不在 `main`(比如在别人留下的 feature 分支上):`git fetch origin main`,对照 `git log HEAD..origin/main` 看看落后多少,不要不问青红皂白就切分支/强制同步。 - 本目录是从别处复制来的纯拷贝(没有 `.git`,比如放进了 cohub 的 skills 目录):去 `https://github.com/talesofai/topic-sdk` 重新拉取 `skill/` 目录的最新内容,只覆盖 `SKILL.md`/`references/`/`assets/` 这些 skill 自身文件,不要动创作者项目自己的 `.env`/`dist/`/未提交改动。仓库如需鉴权(私有仓库),用你环境里已有的 GitHub 凭据;实在拿不到就如实告诉用户"无法确认新鲜度",不要假装拉取成功了。 - 若正在给创作者项目跑 `pnpm install`:确认锁定解析到的 `@talesofai/topic-sdk` 确实是最新 `dist/`(git 依赖有本地缓存,必要时 `pnpm update @talesofai/topic-sdk` 或清缓存重装)。 - **没有"看起来最近更新过就跳过"这种例外**——每次执行本 skill 都要重新做一次这一步。SDK 契约、红线、脚手架随时可能已经改过,用过期版本干活出的问题算没做这一步的责任。 --- 你(agent)正代创作者开发一个**内嵌话题页**:一个独立的 Web 单页应用,由 nieta-app 在 `/tag?hashtag=X` 路由内以 **iframe** 内嵌。页面只**读**产品内数据(`/v1/embed/*`),所有写动作由宿主固定浮层承载,页面既不绘制也不调用。 按下面的工作流走,每步带**校验门**,过了再进下一步。详细契约见 `references/api-cheatsheet.md`,红线见 `references/compliance.md`(**交付前必须逐项过**)。 ## 入门前必看(非技术用户先看这里) 做内嵌话题页,你(创作者)只要说清楚**业务内容**(话题要展示什么、按钮点了跳哪里),技术活全交给 agent。 > **前置条件(开工前先确认)**:内嵌页**绑定 activity**(发布/上线都以 `activity_uuid` 为锚)。**纯 hashtag 话题(无对应 activity)暂不支持内嵌页**(owner TODO)。所以开工前先确认你的话题有 `activity_uuid`;没有的话目前做不了内嵌页,先停下。 > **发布模型(权限分级,后端强制)**: > - **创作者 → 只能 dev**:用绑定该话题活动的 dev 令牌 + `pnpm deploy:dev` 发**草稿**(不上线),在 app 内用开发者菜单挑这个版本调试;满意后**请内部团队**上线。**创作者永不能 prod。** > - **内部用户(`is_internal`)→ 可 prod + dev**:用完整登录态 `pnpm deploy:prod` 上线(激活绑定),也可发 dev 草稿。 > - 后端对 `target=prod` / `activate` / `unbind` 只接受 `is_internal` 完整登录态;scoped dev 令牌请求这些动作直接被拒(403)。 > **已有现成 HTML**:把 HTML(或链接)给 agent,说"我有个现成页面想做成话题页",agent 走 §2 的现成-HTML 入口。 > **SDK 安装**:`@talesofai/topic-sdk` 是**公开仓库**(`github.com/talesofai/topic-sdk`),`pnpm install` 时直接通过 git 源安装,任何人免认证可装,无需特殊权限。 ## 总原则(本 skill 的用户多为**非技术的运营/产品同事**,务必遵守) - **你(agent)代劳一切技术活**:脚手架、写代码、接数据、开发、自测、排错,全部你做。用户不写代码、不碰命令行细节。 - **绝不把技术决策抛给用户**:CSP、AllowedRoute、缓存策略、构建/路由配置、依赖、字段名、token 机制等**一律由你按本 skill / `references/compliance.md` 默认决定**,不要问用户。若标准不支持用户现成页里的某个技术做法,按 `references/migrate-existing-html.md` 的默认处置,**不要回头问技术问题**。 - **只用大白话问"业务"信息,且尽量一次问全**:话题的 `activity_uuid`(用于 dev 发布和本地 dev proxy 自测)、某按钮点了希望跳到哪个站内页、这个话题页要展示哪些内容(榜单/作品/角色)。例:不要问"AllowedRoute 白名单要加哪些 route",要问"这个'去看榜单'按钮,点了你希望跳到哪个页面?"。 - **错误你来读、你来修**:报错先自己对照校验门与 `references/compliance.md` 诊断修复;只有确实需要用户提供外部信息(业务意图)时,才用大白话说明"需要你做什么"。 - **两条入口**:① 从零做新页 → 走 §2 起脚手架;② **用户已有现成 HTML** → 先按 `references/migrate-existing-html.md` 把它改造进 scaffold,再回到 §3 继续。 ## 0. 先读死规矩(贯穿全程,违反即返工) 1. **只读 + 不持写权**:页面只调 `/v1/embed/*` 只读接口,全程只持 `embed token`(`token_type='embed'`)。**绝不**持有/存储/传递用户完整登录态。 2. **宿主顶栏 + 写动作全在宿主**(D9):宿主顶栏/固定浮层**已提供**「**返回 / 分享 / 主页(一键回内嵌页首页) / 开发者菜单(仅授权用户)**」+ 登录/举报——**页面绝不自绘这些**(返回、分享、主页、登录、举报等),自绘会与宿主重复/冲突。SDK **不暴露** `overlay.*` 写方法。点赞/关注/收藏这类 per-item 写 → `sdk.nav.internal('/collection/interaction', {uuid})` 点卡跳原生页去做。**页面骨架 = 只渲染「可滚动内容区」**:标题块、卡片/列表/瀑布流——**不画顶栏、不画固定头、不加安全区内边距**(看代码形状对照,LLM 容易顺着模板长出顶栏,务必照 ✅ 那行长): ``` ✅ 正确(只内容,可滚动): <div>{title}</div><section>…卡片列表…</section> ❌ 错误(自绘顶栏=与宿主重复): <div style="position:fixed;top:0">← 返回 分享🔗 🏠主页</div> ❌ 错误(自加安全区): padding-top: env(safe-area-inset-top) // safeTop 宿主已占,恒为 0 ``` 3. **URL 统一**:对外唯一身份永远是 `app.nieta.art/tag?hashtag=X`(短链 `t.nieta.art`)。**OSS URL 是内部实现细节,永不写进分享链/`<a href>`/规范链。** 4. **token 只在内存**:不写 `localStorage`/`sessionStorage`/`cookie`。 5. **禁 `history.pushState`**:用 hash 路由或内存路由(否则 Android 返回键会先消费 iframe 内 history)。 6. **viewport `safeTop` 由宿主处理**:页面顶部**不要**再加安全区/Navbar 内边距;只处理 `safeBottom` + 键盘 inset。 7. **三上下文都要兼容**:`app`(iOS/Android 壳)、`web-embedded`(网页版 `/webview`)、`guest`(浏览器裸链,无宿主、token 为 null、写动作唤起 App)。 ## 2. 起脚手架(从 `assets/scaffold/` 复制) > **若用户已有现成 HTML**:先走 `references/migrate-existing-html.md`(把现成页改造进 scaffold:取数改 SDK 只读、写按钮改宿主浮层/nav、外站显示资源打包、去 pushState),改造完再继续本节其余步骤。 把本 skill 的 `assets/scaffold/` 目录复制成创作者项目,然后: - **不再有 `__TOPIC_UUID__` 占位符**:OSS base / prefix 由内部团队发布时实时从后端取回并注入,创作者侧无需任何全局替换。 - `@talesofai/topic-sdk` 在 `package.json` 里是 **git 依赖**(`git+https://github.com/talesofai/topic-sdk.git`)。仓库是**公开仓库**,`pnpm install` 时 clone 仓库并直接使用其中已提交的预构建 `dist/`,**无构建脚本、零摩擦**,任何人免认证可装,无需 GitHub 特殊权限。**不发 npm。** - `pnpm install`(同时安装 `ali-oss` 等发布依赖)。 **校验门**:`pnpm install` 成功;Node >= 18、pnpm >= 8。 ## 3. 初始化 SDK + 三态降级(见 `assets/scaffold/src/sdk.ts`) `createTopicSDK()` 内部按序:端探测 → hello 握手(超时→guest)→ 鉴权(embedded 走 bridge `getEmbedToken`;guest 匿名)→ 能力协商 → 事件订阅。 关键约定: - `sdk.env.context` ∈ `'app'|'web-embedded'|'guest'`;`sdk.env.embedded` 区分有无宿主。 - `sdk.auth.getToken()` 可能为 `null`(匿名/游客)——这是**正常**情况,降级匿名展示,**不要报错**。 - `onAuthLost` 回调里只做匿名降级 + 日志,不抛错。 - `tokenTimeout` 默认 3000ms,**不要**改成 500(那是 v1 bridge 的历史坏值)。 **校验门**:在 `web-embedded` 下 `sdk.auth.getToken()` 3s 内返回非 null;在 `guest` 下 `sdk.env.embedded===false` 且 token 为 null 而页面不崩。 ## 4. 渲染只读数据(见 `assets/scaffold/src/App.tsx` + `references/api-cheatsheet.md`) 全部走 `sdk.topic.*` / `sdk.activity.*` / `sdk.rank.*`,均为只读。**契约要点(已与已部署后端对齐)**: - 大量字段可空:`StoryCard.aspect`、`StoryCard.author.uuid`、`CharacterCard.author.uuid`、`CreatorCard.uuid`、`Leaderboard.startTime/endTime`、`TopicDetail.startTime/endTime` 等都可能为 `null` —— 渲染前**必须判空**(尤其用 `author.uuid` 拼跳转、用 `startTime*1000` 做时间)。 - `viewer` 只有 `{ subscribed, canEdit }`,匿名时整个 `viewer` 为 `null`。**没有 `hasReviewPermission`**。 - `listCharacters` 的 `parentType` 是 `string[]`(省略时后端默认 `['oc','elementum']`)。 - `sdk.rank.get(entity, window, at)`:`oc`/`elementum` **只支持 `at='latest'`**,传时间戳会抛错。 - 分页用 `page.hasNext` 判断是否还有下一页(不要自己用 total 推算)。 - **`coverUrl`/`avatarUrl` 一律用 `ossImage(url, { width })` 包一层再塞进 `<img src>`**(`width` 传该卡片实际渲染宽度,不是原图宽度),不要把原图直出。详见 cheatsheet「图片」一节的推荐宽度参考值。 **校验门**:`getDetail` + `listStories` 能渲染;对所有可空字段已判空(grep 一遍 `.uuid`/`.aspect`/`.startTime` 的使用点);所有图片字段都经过 `ossImage` 处理(grep 一遍 `coverUrl`/`avatarUrl` 的使用点,确认没有裸传进 `<img src>`)。 ## 5. 导航(唯一漏斗:nav.internal) - `sdk.nav.internal(route, query?)`:跳产品内页,`route` 必须在 AllowedRoute 白名单内(见 cheatsheet)。**这是唯一的跳转/写意图漏斗**——没有 `sdk.guest.openApp`,唤起 App 由宿主承载(手机浏览器宿主自动唤起 App;原生 App 内站内跳;桌面站内跳)。 - **参数契约**:自指路由(`/topic` `/tag` `/activity`)省略参数时 SDK 自动从当前页 URL 填;per-item 路由(`/oc` `/user` `/collection/interaction`)必须传 `uuid`(来自被点卡片)。**漏传/传错会被 SDK 拦下(构建期类型 + 运行期 throw),不会静默白屏**——详见 cheatsheet 参数表。 > **caveat**:「拦下不静默白屏」**仅对你显式调用 `sdk.nav.internal(...)` 成立**。如果你写的是原生 `<a href="...">`,SDK 的链接拦截器会接管它并 per-item 调 `nav.internal`,但调用挂了 `.catch(() => {})`——缺 `uuid` 抛的错会被**静默吞掉**(不抛、无提示)。所以 per-item 跳转(作品/角色/用户详情)**务必显式写 `sdk.nav.internal('/oc', { uuid })`**,不要依赖原生 `<a>` 自动拦截来传 per-item 参数。 - `sdk.nav.external(url)`:外跳(embedded 走 bridge;guest 走 `window.open`)。 - **跳到另一个话题活动空间**:`/tag` `/topic` 是自指路由,显式传 `hashtag` 即覆盖当前话题,直接 `sdk.nav.internal('/tag', { hashtag: 'xxx' })` 跳别的话题——目标 hashtag 从哪来(写死 / 页面自己的数据源)由你决定,SDK 不提供"相关话题列表"。 - `sdk.nav.applyHost()`:「申请创建话题活动空间」跳转,跳运营配置的申请表单(飞书多维表单)。**不接受参数**——用户昵称/UID 的 prefill(不含话题名)全由宿主本地态拼,页面拿不到这些字段。运营未配置申请表单时抛 `BridgeError`,**用 try/catch 包一层,未配置就不渲染这个入口**。 **校验门**:所有 `nav.internal` 的 route 都在白名单内;**per-item** 写意图(点赞/关注/收藏/看详情)一律走 `nav.internal` 跳原生页/由宿主唤起 App,**绝不在页面内尝试本地写**。注意区分:**页面级**的 返回/分享/主页/登录/举报 **一概不画**(宿主顶栏已提供,D9)——别把"承载 per-item 写意图"误推广成"画一排导航/分享按钮"。`applyHost()` 这类内容区自己的 CTA 按钮(如"申请创建你自己的话题空间")不算 D9 违规——D9 禁的是仿冒/重复宿主顶栏,不是内容区本该有的功能入口。 ## 6. 自测 ### 6.1 本地嵌入态自测(mock 宿主 harness,无需真 App) `pnpm dev:host` 打开**本地 mock 宿主**(`dev-host/`):它用与真宿主同款 sandbox iframe 嵌你的页面、并扮演 frame-bridge v2 宿主(回 hello、发/拒 token、收 `nav.internal`/`nav.external`、推 `tokenChanged`/`viewport`/`back`),右侧面板看所有桥消息。 → 不依赖真 App、不依赖真人即可验:握手是否成功、`getEmbedToken` 流程、按钮点了走哪个 `nav.internal` route、`ui.toast`、事件响应、原生 `<a>` 跳转是否被 SDK 拦回(不逃逸 OSS)。 面板「embedToken」留空 = 游客态(数据匿名);填一个真 pre/prod embed token = 看带 `viewer` 的数据。 > 局限:真机 iOS/Android WebView 三端差异、真实 token 与真 `/v1/embed` 鉴权数据,mock 宿主替代不了——那部分仍需 §7 在真 App 内验。 ### 6.2 普通本地预览 `pnpm dev` 直开页面(无宿主 → guest 降级,看渲染 + 匿名数据)。重点:token 流程、可空字段不崩、返回键正常(禁 pushState)、游客写动作唤起 App。 **校验门**:mock 宿主下核心路径(握手 / 数据 / 导航 / 事件)通过;`compliance.md` 功能自测段逐项确认。 ## 7. dev 发布(发草稿 + 在 app 真实上下文调试) 本地 dev 预览无法模拟真实 embed 上下文(真 token、桥接、数据)。要做真机调试,需把页面发成**草稿**(不上线),然后在 app 内用开发者菜单挑版本挂载。 ### 7.1 获取 dev 令牌(一次性,7 天有效) 1. 用**创作者自己的账号**登录 nieta-app。 2. 进入**该话题页**,点右上角**「⋯」→「开发者菜单」→「生成开发令牌」**(令牌入口在话题页顶栏的开发者菜单里,不在账号设置)。 3. 平台调用 `POST /v1/topic-embed/dev-publish-token`(正常登录态),返回 `{ token, expires_at }`(TTL 7 天)。 4. 复制令牌。 > **令牌绑定具体话题活动**:dev 令牌签发时就**绑定你正在操作的这个话题活动**——只有该活动的创作者(或内部用户)能签发,且签出的令牌**只能对这一个活动发草稿**,对别的话题无效。所以要给哪个话题发草稿,就进哪个话题页生成令牌;换话题要重新生成。 > **令牌安全说明**:这是平台签发的 **scoped dev 令牌**(`token_type='topic_dev_publish'`,鉴权头 `x-dev-publish-token`),与用户完整登录态、embed token 三向隔离。泄露影响:只能对该话题发草稿,不能上线(prod)、不能做其它用户操作。令牌过期后重新生成。 ### 7.2 配置 .env 把脚手架根目录的 `.env.example` 复制成 `.env`,填入: ```bash NIETA_DEV_PUBLISH_TOKEN=<上一步复制的 dev 令牌> NIETA_ACTIVITY_UUID=<话题的 activity_uuid> NIETA_API_BASE=https://api.talesofai.cn ``` `.env` 不要提交 git。 ### 7.3 发草稿(dev) ```bash pnpm deploy:dev # 等价于:node scripts/deploy.mjs --target dev ``` 脚本流程: 1. 用 dev 令牌(`x-dev-publish-token` 头)调 `upload-grant` 拿临时 STS。 2. `vite build`(注入 `VITE_OSS_BASE`)。 3. 上传 dist/ 到 OSS。 4. `POST .../embed-page/publish` body `{ version, target: "dev" }` — **不激活 activeVersion,草稿不上线**。 5. 打印版本号,提示在 app 内开发者菜单选版本。 **校验门**:脚本完成并打印草稿版本号(如 `version=3`);话题页公众侧无变化(草稿不对外挂载)。 ### 7.4 在 app 内调试(真实 embed 上下文) 打开 app → 进入该话题页 → 点右上角**「⋯」→「开发者菜单」**: - 菜单由宿主 gating:调 `GET .../embed-page/versions` 时返回 200(创作者有权)才显示,公众用户完全不可见。 - 列出所有版本(草稿/active 标注),选择你刚发的版本 → 宿主把 iframe 重挂到该草稿 URL,仍走真实 embed 上下文(真 token、真桥接、真 `/v1/embed` 数据)。 - 也可直链调试:`/tag?hashtag=<X>&embedPreview=<版本号>`(有授权才生效)。 ### 7.5 反复迭代 修改代码后重跑 `pnpm deploy:dev` → app 开发者菜单选新版本 → 验证 → 循环,直到满意。 ### 7.6 请内部上线 调试满意后,将项目源码(不含 `node_modules/`、`dist/`)交给内部团队。内部团队用 `skill-internal-publish/`(位于 **topic-sdk 仓库根**,不随创作者项目交付)里的流程(`pnpm deploy:prod`)完成上线。 > **创作者只能 dev(发草稿),永不能 prod(上线)。** > 后端 `target=prod` / `activate` / `unbind` 仅接受 `is_internal` 完整登录态,scoped dev 令牌请求时直接被拒(403)。 ## 8. 合规门(上线前必须) **多数红线已机器兜底**:`pnpm deploy:dev/prod` 会先跑 `tsc --noEmit`(类型门:可空字段裸用 / 不存在字段 / strict 降级)+ **源码红线扫描**(localStorage/sessionStorage/cookie、`history.pushState`、ServiceWorker、写方法 fetch、`EventSource`、`window.parent.postMessage`、自设 CSP `<meta>`、OSS 可见引用、OAuth 残留)+ 单 HTML 入口校验;命中直接 fail 打回。SDK 运行期还会拦原生 `<a>` 跳转(转 bridge)、守卫 `history.pushState`(embedded 即 throw)。**上面这些违反会在发布/运行时当场报错,你不必死记。**(确系合法的同源用途,可在该行加注释 `sdk-compliance-ok` 豁免,需内部 review。) > > **⚠ 唯一的例外是 D9「自绘宿主顶栏/固定浮层/安全区」**:deploy 对它只有 **best-effort 启发式 warn**(提示而**不 fail**,且会漏报——动态拼的样式、图标按钮等扫不到)。**这一条机器兜底不住,必须你自觉遵守 + 发布后真机核对**:页面只渲染可滚动内容区,返回/分享/主页/登录/举报/安全区**全在宿主**,页面一概不画(见 §0.2 D9 的正/反例)。别因为"deploy 没报错"就以为合规。 > > **定位说明(重要)**:源码红线扫描是**防手滑的 lint / 纵深防御,不是安全边界**——它逐行正则匹配,可被动态构造(`window["local"+"Storage"]`)、第三方依赖夹带、`sdk-compliance-ok` 注释绕过,且只扫 `src/` 不扫 `node_modules`。**真正的安全边界在后端**:embed token 只读、`/v1/embed/*` 无写接口、token 三向隔离(embed / dev-publish / 用户完整登录态),即便页面违规也拿不到写能力或用户完整登录态。所以**别把"扫描通过"等同于"安全"**,也别据此放松后端只读约束。 仍需人工逐项过 `references/compliance.md` 的:三端真机自测(§F)、可空字段语义判空是否合理(类型门只保证不裸用)、文案 / 视觉。**任一不过不交付,停下来报告用户。** ### 8.1 agent 自检(纯对话行为,机器拦不了,发布前自我 attest) - [ ] 全程**没向用户问技术决策**(CSP / 路由 / 缓存 / 字段名 / token / 依赖版本 / AllowedRoute 等):这些要么已结构固化、要么由运营在线配,问了也改不了。标准不支持用户现成做法时,按 `references/migrate-existing-html.md` 默认处置,不回头问。 - [ ] 向用户的提问都是**业务信息**(话题内容 / 跳转目的地 / activity_uuid),尽量一次问全、用大白话。 --- ## 交付 创作者产出:**一个能本地 `pnpm dev` 预览 + `pnpm deploy:dev` 已发草稿 + 通过合规自测的项目**。 交付方式:将项目源码(不含 `node_modules/`、不含 `dist/`)打包或推送 git 仓库,**交给内部团队**。内部团队会用 `skill-internal-publish/`(位于 **topic-sdk 仓库根**,不随创作者项目交付)里的发布流程完成最终上线。 --- **给 agent 的元规则**:本 skill 描述的 SDK/后端契约以 `references/api-cheatsheet.md` 为准(已与已部署后端对齐)。若你发现 SDK 实际 API 与 cheatsheet 不符,**停下来报告差异**,不要擅自猜测或改写契约。
GitHub에서 보기