diff --git a/CHANGELOG.md b/CHANGELOG.md index acc4c00..8cf33a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # @talesofai/topic-sdk +## Unreleased + +### Minor Changes + +- 新增 `ossImage(url, { width, dpr?, quality?, webp? })` / `ossImageSrcSet(url, width)`:按渲染宽度 + 设备像素比拼接 OSS `x-oss-process`(resize/format/quality),与 talesofai 其余前端(weapp/event/bff 的 `getImageLink`)同一套图片处理约定。非 http(s) URL(如 `data:` 内联图)或已带 `x-oss-process` 的 URL 原样返回,不强行拼参数破坏。scaffold `App.tsx` 示例与 skill 文档已同步改用它包 `coverUrl`/`avatarUrl`,不再原图直出。 +- `skill/`、`skill-internal-publish/` 两处 SKILL.md 顶部新增强制自我更新步骤:每次执行前先确认本地不是过期副本(git 仓库则 `git pull`,纯拷贝则重新从源仓库拉取覆盖,且明确排除"正在给 topic-sdk 本身开发"的场景),不允许跳过。(`topic-embed-review` 仓库单独做了同款改动,见该仓库自己的 commit,不在本仓库范围内。) + ## 0.1.0-dev.0 ### Minor Changes diff --git a/README.md b/README.md index d3dd875..55cd768 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,7 @@ const page = await sdk.topic.listStories(hashtag, { pageIndex: 0, pageSize: 20, - **自指路由** `/topic` `/tag` `/activity`:参数可省,SDK 从当前页 `?hashtag=`/`?activity_uuid=` 自动填(可覆盖)。 - **per-item 路由** `/oc` `/user` `/collection/interaction`:必须传 `uuid`(来自被点卡片),漏传**构建期类型 + 运行期都会报错打回**。 - **可空字段**:`detail.startTime/endTime/title`、`StoryCard.aspect`、`*.author.uuid`、`Leaderboard.startTime/endTime` 等均可能为 `null`,渲染前判空(详见 cheatsheet)。 +- **图片按尺寸取**:`coverUrl`/`avatarUrl` 都是 OSS 直出图,渲染前用 `ossImage(url, { width })` 包一层(按卡片渲染宽度 + 设备像素比拼 OSS resize/format 参数),别把原图尺寸直出——详见 cheatsheet「图片」一节。 完整契约、AllowedRoute 白名单与参数表、错误模型、三态降级见配套 skill 的 `references/api-cheatsheet.md`。 diff --git a/dist/index.cjs b/dist/index.cjs index 3eba112..588a14f 100644 --- a/dist/index.cjs +++ b/dist/index.cjs @@ -26,7 +26,9 @@ __export(src_exports, { PageCursor: () => PageCursor, TopicApiError: () => TopicApiError, UnsupportedError: () => UnsupportedError, - createTopicSDK: () => createTopicSDK + createTopicSDK: () => createTopicSDK, + ossImage: () => ossImage, + ossImageSrcSet: () => ossImageSrcSet }); module.exports = __toCommonJS(src_exports); @@ -743,6 +745,49 @@ var SDKUiImpl = class { } }; +// src/image.ts +var DEFAULT_QUALITY = 80; +var MAX_DPR = 3; +function resolveDpr(explicit) { + if (typeof explicit === "number" && explicit > 0) + return Math.min(explicit, MAX_DPR); + const detected = typeof window !== "undefined" && typeof window.devicePixelRatio === "number" ? window.devicePixelRatio : 1; + return Math.min(Math.max(detected, 1), MAX_DPR); +} +function buildOssProcess(pixelWidth, quality, webp) { + const styles = ["image", "auto-orient,1"]; + if (pixelWidth && pixelWidth > 0) + styles.push(`resize,m_lfit,w_${Math.round(pixelWidth)}`); + if (webp) + styles.push("format,webp"); + styles.push(`quality,q_${quality}`); + return styles.join("/"); +} +function ossImage(source, options) { + if (!source) + return null; + let url; + try { + url = new URL(source); + } catch { + return source; + } + if (url.protocol !== "http:" && url.protocol !== "https:") + return source; + if (url.searchParams.has("x-oss-process")) + return source; + const { width, quality = DEFAULT_QUALITY, webp = true, dpr } = options ?? {}; + const pixelWidth = width && width > 0 ? width * resolveDpr(dpr) : void 0; + url.searchParams.set("x-oss-process", buildOssProcess(pixelWidth, quality, webp)); + return url.toString(); +} +function ossImageSrcSet(source, width, options) { + if (!source) + return null; + const scales = [1, 2, 3]; + return scales.map((scale) => `${ossImage(source, { ...options, width, dpr: scale })} ${scale}x`).join(", "); +} + // src/index.ts var SDK_VERSION = "0.1.0"; function installLinkInterceptor(nav) { @@ -880,6 +925,8 @@ async function createTopicSDK(options = {}) { PageCursor, TopicApiError, UnsupportedError, - createTopicSDK + createTopicSDK, + ossImage, + ossImageSrcSet }); //# sourceMappingURL=index.cjs.map \ No newline at end of file diff --git a/dist/index.cjs.map b/dist/index.cjs.map index b33e57a..2e85c3e 100644 --- a/dist/index.cjs.map +++ b/dist/index.cjs.map @@ -1 +1 @@ -{"version":3,"sources":["../src/index.ts","../src/auth.ts","../src/errors.ts","../src/bridge.ts","../src/data.ts","../src/types.ts","../src/env.ts","../src/events.ts","../src/nav.ts","../src/ui.ts"],"sourcesContent":["import { SDKAuthImpl } from \"./auth.js\";\nimport { BridgeClient } from \"./bridge.js\";\nimport { SDKActivityImpl, SDKRankImpl, SDKTopicImpl } from \"./data.js\";\nimport { buildCapabilities, detectEnv } from \"./env.js\";\nimport { SDKEventsImpl } from \"./events.js\";\nimport { SDKNavImpl } from \"./nav.js\";\nimport type { AllowedRoute, Capability, SDKAuth, TopicSDK, TopicSDKOptions } from \"./types.js\";\nimport { SDKUiImpl } from \"./ui.js\";\n\nexport type {\n AllowedRoute,\n BridgeClient as BridgeClientType,\n // 活动\n CampaignCard,\n // 角色\n CharacterCard,\n // 环境/能力\n ClientContext,\n CreatorCard,\n HelloResult,\n HighlightPage,\n Leaderboard,\n // 世界观\n LoreEvent,\n MyStoryKind,\n // 通用\n Page,\n RankEntity,\n RankEntry,\n // 榜单\n RankWindow,\n RichText,\n SDKActivity,\n // 接口\n SDKAuth,\n SDKEvents,\n SDKNav,\n SDKRank,\n SDKTopic,\n SDKUi,\n // 作品\n StoryCard,\n TopicDetail,\n TopicSDK,\n TopicSDKOptions,\n // 话题\n TopicTab,\n ViewportInfo,\n} from \"./types.js\";\n\nexport { BridgeClient } from \"./bridge.js\";\nexport { PageCursor } from \"./data.js\";\nexport { BridgeError, TopicApiError, UnsupportedError } from \"./errors.js\";\nexport { Capability } from \"./types.js\";\n\nconst SDK_VERSION = \"0.1.0\";\n\n/**\n * 全局拦截 点击。内嵌页跑在 sandbox iframe(无 allow-top-navigation)里,原生 /相对路径跳转会逃逸到\n * iframe 自身源(OSS)而非宿主 App。故在捕获阶段接管:\n * - 同源路径跳转 → nav.internal(命中 AllowedRoute 由宿主导航/游客转深链;未命中被宿主拦截、不逃逸);\n * - 跨站外链 → nav.external(宿主用系统浏览器打开);\n * - 同 path 仅改 hash(页面内视图切换)、target=_blank、download、非 http(s) scheme → 放行。\n * 这样创作者即便写了原生链接,也不会把 iframe 跳出内嵌页。返回卸载函数。\n */\nfunction installLinkInterceptor(nav: SDKNavImpl): () => void {\n const onClick = (e: MouseEvent) => {\n if (e.defaultPrevented || e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;\n const targetEl = e.target instanceof Element ? e.target : ((e.target as Node | null)?.parentElement ?? null);\n const a = targetEl?.closest(\"a\") as HTMLAnchorElement | null;\n if (!a) return;\n const href = a.getAttribute(\"href\");\n if (!href) return;\n const target = a.getAttribute(\"target\");\n if (target && target !== \"_self\") return;\n if (a.hasAttribute(\"download\")) return;\n let url: URL;\n try {\n url = new URL(href, location.href);\n } catch {\n return;\n }\n if (url.protocol !== \"http:\" && url.protocol !== \"https:\") return;\n if (url.origin === location.origin) {\n // 同 path 仅改 hash(页面内视图切换)→ 放行,交给页面自己处理\n if (url.pathname === location.pathname && url.search === location.search && url.hash) return;\n const query: Record = {};\n url.searchParams.forEach((v, k) => {\n query[k] = v;\n });\n e.preventDefault();\n nav.internal(url.pathname as AllowedRoute, query).catch(() => {});\n return;\n }\n e.preventDefault();\n nav.external(url.href).catch(() => {});\n };\n document.addEventListener(\"click\", onClick, true);\n return () => document.removeEventListener(\"click\", onClick, true);\n}\n\n/**\n * 守卫 history.pushState/replaceState。内嵌页禁用(会污染 Android WebView 返回栈,返回键先消费 iframe history),\n * 应改用 hash 路由 / 内存路由。embedded 下安装,违例当场 throw(dev 期即暴露);destroy 恢复原函数。\n */\nfunction installPushStateGuard(): () => void {\n if (typeof history === \"undefined\") return () => {};\n const origPush = history.pushState.bind(history);\n const origReplace = history.replaceState.bind(history);\n const guard = (method: string) => () => {\n const msg =\n `[topic-sdk] 内嵌页禁止 history.${method}(会污染 App 返回栈);` +\n `请改用 hash 路由(location.hash)或内存路由做页面内视图切换。`;\n console.error(msg);\n throw new Error(msg);\n };\n history.pushState = guard(\"pushState\") as typeof history.pushState;\n history.replaceState = guard(\"replaceState\") as typeof history.replaceState;\n return () => {\n history.pushState = origPush;\n history.replaceState = origReplace;\n };\n}\n\n/**\n * 初始化并返回 TopicSDK 实例。\n *\n * 初始化序列:\n * ① 端探测(UA 嗅探预判)\n * ② hello 握手(超时→ guest)\n * ③ 鉴权(embedded:bridge getEmbedToken;guest:匿名)\n * ④ 能力协商(填充 Capability 位图)\n * ⑤ 事件订阅(tokenChanged / viewport / back)\n */\nexport async function createTopicSDK(options: TopicSDKOptions = {}): Promise {\n const {\n apiBaseUrl = \"https://pre.api.talesofai.cn\",\n helloTimeout = 1500,\n tokenTimeout = 3000,\n tokenRefreshEarlyMs = 5 * 60 * 1000,\n onAuthLost,\n } = options;\n\n // tokenTimeout 下界 clamp:500ms 是 v1 bridge 已知坏值;低于 1000ms 上调并告警(SKILL 固定 3000)。\n let effectiveTokenTimeout = tokenTimeout;\n if (effectiveTokenTimeout < 1000) {\n console.warn(\n `[topic-sdk] tokenTimeout ${effectiveTokenTimeout}ms 低于下限 1000ms(500ms 是 v1 bridge 已知坏值),已上调到 1000ms。`,\n );\n effectiveTokenTimeout = 1000;\n }\n\n // ① + ② 端探测 + hello 握手\n // BridgeClient は常に作成するが、guest の場合はすぐ破棄する\n const bridge = new BridgeClient(effectiveTokenTimeout);\n const env = await detectEnv(bridge, SDK_VERSION, helloTimeout);\n\n // embedded 上下文:守卫 history.pushState/replaceState(内嵌页禁用,会污染 App 返回栈);违例当场 throw,dev 期即暴露。\n const removePushStateGuard = env.embedded ? installPushStateGuard() : () => {};\n\n // guest の場合は bridge を使わない\n const activeBridge = env.context === \"guest\" ? null : bridge;\n if (env.context === \"guest\") {\n // guest 模式:bridge 不再需要,但保留 message listener 无害(destroy 可选)\n // 为节约资源,立即销毁\n bridge.destroy();\n }\n\n // ③ 鉴权\n const auth = new SDKAuthImpl(activeBridge, effectiveTokenTimeout, tokenRefreshEarlyMs, onAuthLost);\n await auth.init();\n\n // ④ 能力协商\n const capabilities = buildCapabilities(env);\n\n // ⑤ 事件订阅:tokenChanged → 自动重取 token\n const eventsImpl = new SDKEventsImpl(activeBridge);\n\n if (activeBridge) {\n activeBridge.onEvent(\"tokenChanged\", (_data: unknown) => {\n // 宿主事件不携带新 token(恒 null),SDK 内部 re-exchange 后再把真实 token 下发给消费方\n auth\n .handleTokenChanged()\n .then(() => eventsImpl.notifyTokenChanged(auth.getToken()))\n .catch(() => eventsImpl.notifyTokenChanged(null));\n });\n }\n\n // ————— 构造子模块 —————\n const topicImpl = new SDKTopicImpl(apiBaseUrl, auth as SDKAuth);\n const activityImpl = new SDKActivityImpl(apiBaseUrl, auth as SDKAuth);\n const rankImpl = new SDKRankImpl(apiBaseUrl, auth as SDKAuth);\n const navImpl = new SDKNavImpl(activeBridge, env.context);\n const uiImpl = new SDKUiImpl(activeBridge, env.context);\n\n // 根治\"原生 /相对跳转在 sandbox iframe 内逃逸到 OSS 源\"的问题:全局接管 点击,改走 bridge 导航。\n const removeLinkInterceptor = installLinkInterceptor(navImpl);\n\n // ————— TopicSDK 对象 —————\n const sdk: TopicSDK = {\n env: {\n context: env.context,\n embedded: env.embedded,\n client: env.client,\n appVersion: env.appVersion,\n features: env.features,\n },\n\n auth: auth as SDKAuth,\n topic: topicImpl,\n activity: activityImpl,\n rank: rankImpl,\n nav: navImpl,\n ui: uiImpl,\n events: eventsImpl,\n\n can(cap: Capability): boolean {\n return capabilities.has(cap);\n },\n\n destroy(): void {\n removeLinkInterceptor();\n removePushStateGuard();\n auth.destroy();\n eventsImpl.destroy();\n activeBridge?.destroy();\n },\n };\n\n return sdk;\n}\n","import type { BridgeClient } from \"./bridge.js\";\nimport type { SDKAuth } from \"./types.js\";\n\ninterface EmbedTokenResult {\n // 宿主回包字段名为 embedToken(对齐后端 EmbedTokenResponse.embedToken)\n embedToken: string;\n expiresAt: number | null; // UTC ms;宿主可能回 null\n}\n\nexport class SDKAuthImpl implements SDKAuth {\n private _token: string | null = null;\n private _expiresAt: number | null = null;\n private _refreshTimer: ReturnType | null = null;\n\n public constructor(\n private readonly _bridge: BridgeClient | null,\n private readonly _tokenTimeout: number,\n private readonly _tokenRefreshEarlyMs: number,\n private readonly _onAuthLost: ((reason: string) => void) | undefined,\n ) {}\n\n // ————— SDKAuth 公开 API —————\n\n public getToken(): string | null {\n return this._token;\n }\n\n public getExpiresAt(): number | null {\n return this._expiresAt;\n }\n\n public isAuthenticated(): boolean {\n return !!this._token && !this._isExpired();\n }\n\n // ————— 内部 API —————\n\n /**\n * 初始化鉴权:向 bridge 请求 embed token。\n * guest 模式(无 bridge):直接以匿名模式初始化,不抛错。\n */\n public async init(): Promise {\n if (!this._bridge) {\n // guest 模式:匿名\n return;\n }\n await this._fetchToken();\n }\n\n /**\n * 宿主推送 tokenChanged 事件时调用(清除旧 token 并重新请求)。\n */\n public async handleTokenChanged(): Promise {\n this._clearToken();\n if (!this._bridge) return;\n await this._fetchToken();\n }\n\n public destroy(): void {\n if (this._refreshTimer !== null) {\n clearTimeout(this._refreshTimer);\n this._refreshTimer = null;\n }\n }\n\n // ————— 私有方法 —————\n\n private async _fetchToken(retryCount = 0): Promise {\n if (!this._bridge) return;\n\n try {\n const result = await this._bridge.send(\"getEmbedToken\", undefined, this._tokenTimeout);\n if (!result || !result.embedToken) {\n throw new Error(\"getEmbedToken returned no embedToken\");\n }\n this._token = result.embedToken;\n this._expiresAt = result.expiresAt ?? null;\n this._scheduleRefresh();\n } catch (err) {\n if (retryCount < 1) {\n // exponential backoff:第一次重试等 500ms\n await delay(500 * Math.pow(2, retryCount));\n return this._fetchToken(retryCount + 1);\n }\n // 达到最大重试次数\n this._onAuthLost?.(`Failed to get embed token: ${String(err)}`);\n }\n }\n\n private _scheduleRefresh(): void {\n if (this._refreshTimer !== null) {\n clearTimeout(this._refreshTimer);\n this._refreshTimer = null;\n }\n if (this._expiresAt === null) return;\n\n const now = Date.now();\n const delay = this._expiresAt - now - this._tokenRefreshEarlyMs;\n if (delay <= 0) {\n // 已过期或即将过期,立即重取\n this._fetchToken().catch(() => {});\n return;\n }\n\n this._refreshTimer = setTimeout(() => {\n this._refreshTimer = null;\n this._fetchToken().catch(() => {});\n }, delay);\n }\n\n private _clearToken(): void {\n this._token = null;\n this._expiresAt = null;\n if (this._refreshTimer !== null) {\n clearTimeout(this._refreshTimer);\n this._refreshTimer = null;\n }\n }\n\n private _isExpired(): boolean {\n if (this._expiresAt === null) return false;\n return Date.now() > this._expiresAt;\n }\n}\n\nfunction delay(ms: number): Promise {\n return new Promise((resolve) => {\n setTimeout(resolve, ms);\n });\n}\n","import type { Capability, ClientContext } from \"./types.js\";\n\n/**\n * HTTP API 错误\n * statusCode -1 = 网络错误/解析失败\n */\nexport class TopicApiError extends Error {\n public readonly cause?: unknown;\n\n public constructor(\n public readonly statusCode: number,\n message: string,\n public readonly endpoint: string,\n cause?: unknown,\n ) {\n super(message);\n this.name = \"TopicApiError\";\n if (cause !== undefined) {\n this.cause = cause;\n }\n }\n}\n\n/**\n * frame-bridge 通信错误\n */\nexport class BridgeError extends Error {\n public readonly cause?: unknown;\n\n public constructor(\n public readonly code: \"timeout\" | \"rejected\" | \"origin-mismatch\" | \"method-not-allowed\" | \"parse-error\",\n public readonly method: string,\n public readonly requestId: string,\n cause?: unknown,\n ) {\n super(`Bridge error [${code}] on method ${method}`);\n this.name = \"BridgeError\";\n if (cause !== undefined) {\n this.cause = cause;\n }\n }\n}\n\n/**\n * 能力不支持错误(不静默 no-op,统一抛出)\n */\nexport class UnsupportedError extends Error {\n public constructor(\n public readonly capability: Capability | string,\n public readonly context: ClientContext,\n ) {\n super(`Capability '${capability}' not supported in context '${context}'`);\n this.name = \"UnsupportedError\";\n }\n}\n","import { BridgeError } from \"./errors.js\";\nimport type { HelloResult } from \"./types.js\";\n\n// ————— wire 协议类型 —————\n\ninterface BridgeRequest {\n v: 2;\n id: string;\n method: string;\n params?: unknown;\n}\n\ninterface BridgeResponse {\n v: 2;\n id: string;\n ok: true;\n result: unknown;\n}\n\ninterface BridgeErrorResponse {\n v: 2;\n id: string;\n ok: false;\n error: { code: string; message: string };\n}\n\ninterface BridgeEventMessage {\n v: 2;\n event: \"tokenChanged\" | \"viewport\" | \"back\";\n data: unknown;\n}\n\ntype IncomingMessage = BridgeResponse | BridgeErrorResponse | BridgeEventMessage;\n\n// ————— ID 生成 —————\n\nlet _counter = 0;\nfunction generateId(): string {\n if (typeof crypto !== \"undefined\" && typeof crypto.randomUUID === \"function\") {\n return crypto.randomUUID();\n }\n // counter fallback(极少数旧环境)\n _counter += 1;\n return `sdk-req-${Date.now()}-${_counter}`;\n}\n\n// ————— pending request 类型 —————\n\ninterface PendingRequest {\n resolve: (value: unknown) => void;\n reject: (reason: unknown) => void;\n timer: ReturnType;\n method: string;\n}\n\ntype EventHandler = (data: unknown) => void;\n\n// ————— BridgeClient —————\n\n/**\n * frame-bridge v2 客户端。\n * 负责与宿主(window.parent)通过 postMessage 通信。\n */\nexport class BridgeClient {\n private readonly _pending = new Map();\n private readonly _eventHandlers = new Map>();\n private readonly _defaultTimeout: number;\n private _destroyed = false;\n\n public constructor(defaultTimeout = 3000) {\n this._defaultTimeout = defaultTimeout;\n window.addEventListener(\"message\", this._onMessage);\n }\n\n /**\n * 向宿主发送请求,等待回包。\n */\n public send(method: string, params?: unknown, timeout?: number): Promise {\n return new Promise((resolve, reject) => {\n const id = generateId();\n const effectiveTimeout = timeout ?? this._defaultTimeout;\n\n const timer = setTimeout(() => {\n this._pending.delete(id);\n reject(new BridgeError(\"timeout\", method, id));\n }, effectiveTimeout);\n\n this._pending.set(id, {\n resolve: resolve as (v: unknown) => void,\n reject,\n timer,\n method,\n });\n\n const request: BridgeRequest = { v: 2, id, method, params };\n window.parent.postMessage(request, \"*\");\n });\n }\n\n /**\n * 订阅宿主主动推送的事件。\n */\n public onEvent(event: string, handler: EventHandler): void {\n if (!this._eventHandlers.has(event)) {\n this._eventHandlers.set(event, new Set());\n }\n this._eventHandlers.get(event)!.add(handler);\n }\n\n /**\n * 取消订阅。\n */\n public offEvent(event: string, handler: EventHandler): void {\n this._eventHandlers.get(event)?.delete(handler);\n }\n\n /**\n * hello 握手。\n * 超时时返回 null(表示 guest 模式)。\n */\n public async hello(sdkVersion: string, timeout?: number): Promise {\n try {\n const result = await this.send(\"hello\", { sdkVersion }, timeout);\n return result;\n } catch {\n return null;\n }\n }\n\n /**\n * 销毁,清理所有 listener 和 pending。\n */\n public destroy(): void {\n this._destroyed = true;\n window.removeEventListener(\"message\", this._onMessage);\n\n // 清除所有 pending 请求\n for (const [id, pending] of this._pending) {\n clearTimeout(pending.timer);\n pending.reject(new BridgeError(\"timeout\", pending.method, id));\n }\n this._pending.clear();\n this._eventHandlers.clear();\n }\n\n private _onMessage = (ev: MessageEvent): void => {\n if (this._destroyed) return;\n\n // 只接受来自宿主父窗口的消息,丢弃任意 iframe/opener 伪造的回包/事件\n // (否则可伪造 hello 把 guest 骗成 embedded、或伪造 getEmbedToken 注入假 token)\n if (ev.source !== window.parent) return;\n\n let msg: IncomingMessage;\n try {\n // data 可能已经是对象(同 origin),或是序列化字符串\n msg = typeof ev.data === \"string\" ? (JSON.parse(ev.data) as IncomingMessage) : (ev.data as IncomingMessage);\n } catch {\n return;\n }\n\n if (!msg || msg.v !== 2) return;\n\n // 事件推送(无 id 字段)\n if (\"event\" in msg) {\n const handlers = this._eventHandlers.get(msg.event);\n if (handlers) {\n handlers.forEach((h) => h(msg.data));\n }\n return;\n }\n\n // 请求回包(有 id 字段)\n if (\"id\" in msg) {\n const pending = this._pending.get(msg.id);\n if (!pending) return;\n clearTimeout(pending.timer);\n this._pending.delete(msg.id);\n\n if (msg.ok) {\n pending.resolve(msg.result);\n } else {\n pending.reject(new BridgeError(\"rejected\", pending.method, msg.id));\n }\n }\n };\n}\n","import { TopicApiError } from \"./errors.js\";\nimport type {\n CampaignCard,\n CharacterCard,\n CreatorCard,\n HighlightPage,\n HotStoryCard,\n Leaderboard,\n LoreEvent,\n MyStoryKind,\n Page,\n RankEntity,\n RankWindow,\n SDKActivity,\n SDKAuth,\n SDKRank,\n SDKTopic,\n StoryCard,\n TopicDetail,\n TopicTab,\n WeeklyHottest,\n} from \"./types.js\";\n\n// ————— 内部 fetch 工具 —————\n\nasync function apiFetch(\n baseUrl: string,\n path: string,\n auth: SDKAuth,\n query?: Record,\n): Promise {\n // 构造 URL\n const url = new URL(path, baseUrl);\n if (query) {\n for (const [k, v] of Object.entries(query)) {\n if (v === undefined) continue;\n if (Array.isArray(v)) {\n // 多值参数(如 parentType=oc&parentType=elementum),对齐后端 List[str]\n for (const item of v) {\n url.searchParams.append(k, String(item));\n }\n } else {\n url.searchParams.set(k, String(v));\n }\n }\n }\n\n // 构造 headers\n const headers: Record = {\n \"Content-Type\": \"application/json\",\n };\n const token = auth.getToken();\n if (token) {\n headers[\"x-embed-token\"] = token;\n }\n\n let response: Response;\n try {\n response = await fetch(url.toString(), { method: \"GET\", headers });\n } catch (err) {\n throw new TopicApiError(-1, \"Network error\", path, err);\n }\n\n if (!response.ok) {\n let message = `HTTP ${response.status}`;\n try {\n const body = (await response.json()) as { message?: string };\n if (body.message) message = body.message;\n } catch {\n // ignore JSON parse errors\n }\n throw new TopicApiError(response.status, message, path);\n }\n\n try {\n return (await response.json()) as T;\n } catch (err) {\n throw new TopicApiError(-1, \"JSON parse error\", path, err);\n }\n}\n\n// ————— SDKTopicImpl —————\n\nexport class SDKTopicImpl implements SDKTopic {\n public constructor(\n private readonly _baseUrl: string,\n private readonly _auth: SDKAuth,\n ) {}\n\n public async getDetail(name: string): Promise {\n return apiFetch(this._baseUrl, `/v1/embed/topic/${encodeURIComponent(name)}`, this._auth);\n }\n\n public async listStories(\n name: string,\n query: {\n pageIndex: number;\n pageSize: number;\n sort: \"hot\" | \"like_count\" | \"highlight_mark_time\";\n startTime?: number;\n endTime?: number;\n authorUuid?: string;\n },\n ): Promise> {\n return apiFetch>(this._baseUrl, `/v1/embed/topic/${encodeURIComponent(name)}/stories`, this._auth, {\n pageIndex: query.pageIndex,\n pageSize: query.pageSize,\n sort: query.sort,\n // 可选参数:undefined 时 apiFetch 会跳过,不写入 query。\n startTime: query.startTime,\n endTime: query.endTime,\n authorUuid: query.authorUuid,\n });\n }\n\n public async listMyStories(\n name: string,\n query: {\n kind: MyStoryKind;\n pageIndex?: number;\n pageSize?: number;\n },\n ): Promise> {\n return apiFetch>(\n this._baseUrl,\n `/v1/embed/topic/${encodeURIComponent(name)}/my-stories`,\n this._auth,\n {\n kind: query.kind,\n pageIndex: query.pageIndex,\n pageSize: query.pageSize,\n },\n );\n }\n\n public async listCharacters(\n name: string,\n query: {\n parentType?: string[];\n pageIndex: number;\n pageSize: number;\n sort?: string;\n },\n ): Promise> {\n return apiFetch>(\n this._baseUrl,\n `/v1/embed/topic/${encodeURIComponent(name)}/characters`,\n this._auth,\n {\n parentType: query.parentType,\n pageIndex: query.pageIndex,\n pageSize: query.pageSize,\n sort: query.sort,\n },\n );\n }\n\n public async listCampaigns(\n name: string,\n query: { pageIndex: number; pageSize: number },\n ): Promise> {\n return apiFetch>(\n this._baseUrl,\n `/v1/embed/topic/${encodeURIComponent(name)}/campaigns`,\n this._auth,\n {\n pageIndex: query.pageIndex,\n pageSize: query.pageSize,\n },\n );\n }\n\n public async listLoreEvents(name: string): Promise {\n return apiFetch(this._baseUrl, `/v1/embed/topic/${encodeURIComponent(name)}/lore-events`, this._auth);\n }\n\n public async listHot(name: string): Promise {\n // 后端响应模型为 { stories: HotStoryCard[] }(无分页 bounded 热门列表),此处解包为数组。\n const resp = await apiFetch<{ stories: HotStoryCard[] }>(\n this._baseUrl,\n `/v1/embed/topic/${encodeURIComponent(name)}/hot-stories`,\n this._auth,\n );\n return resp.stories;\n }\n\n public async getWeeklyHottest(name: string): Promise {\n // 后端响应模型为 WeeklyHottest | null(无近 7 天同款 UV 最高作品时返回 null)。\n return apiFetch(\n this._baseUrl,\n `/v1/embed/topic/${encodeURIComponent(name)}/weekly-hottest`,\n this._auth,\n );\n }\n}\n\n// ————— SDKActivityImpl —————\n\nexport class SDKActivityImpl implements SDKActivity {\n public constructor(\n private readonly _baseUrl: string,\n private readonly _auth: SDKAuth,\n ) {}\n\n public async listTabs(uuid: string): Promise {\n return apiFetch(this._baseUrl, `/v1/embed/activity/${encodeURIComponent(uuid)}/tabs`, this._auth);\n }\n\n public async listSelectedStories(\n uuid: string,\n tabKey: string,\n query: {\n pageIndex: number;\n pageSize: number;\n sort?: string;\n },\n ): Promise {\n return apiFetch(\n this._baseUrl,\n `/v1/embed/activity/${encodeURIComponent(uuid)}/tab/${encodeURIComponent(tabKey)}`,\n this._auth,\n {\n pageIndex: query.pageIndex,\n pageSize: query.pageSize,\n sort: query.sort,\n },\n );\n }\n}\n\n// ————— SDKRankImpl —————\n\nexport class SDKRankImpl implements SDKRank {\n public constructor(\n private readonly _baseUrl: string,\n private readonly _auth: SDKAuth,\n ) {}\n\n public async get(\n entity: RankEntity,\n window: RankWindow,\n at: number | \"latest\",\n ): Promise> {\n // oc / elementum 仅支持 latest(后端非 latest 返回 400),提前抛出更友好的错误\n if ((entity === \"oc\" || entity === \"elementum\") && at !== \"latest\") {\n throw new TopicApiError(\n 400,\n `rank entity '${entity}' only supports at='latest'`,\n `/v1/embed/rank/${entity}/${window}/${String(at)}`,\n );\n }\n const atStr = at === \"latest\" ? \"latest\" : String(at);\n return apiFetch>(\n this._baseUrl,\n `/v1/embed/rank/${encodeURIComponent(entity)}/${encodeURIComponent(window)}/${encodeURIComponent(atStr)}`,\n this._auth,\n );\n }\n}\n\n// ————— PageCursor —————\n\nexport class PageCursor {\n private _pageIndex = 0;\n private _hasNext = true;\n private _total: number | null = null;\n\n public constructor(\n private readonly _fetcher: (pageIndex: number, pageSize: number) => Promise>,\n private readonly _pageSize = 20,\n ) {}\n\n public hasNext(): boolean {\n return this._hasNext;\n }\n\n public async next(): Promise {\n if (!this._hasNext) return [];\n const page = await this._fetcher(this._pageIndex, this._pageSize);\n this._total = page.total;\n this._hasNext = page.hasNext;\n this._pageIndex += 1;\n return page.list;\n }\n\n public reset(): void {\n this._pageIndex = 0;\n this._hasNext = true;\n this._total = null;\n }\n\n public currentPage(): number {\n return this._pageIndex;\n }\n\n public totalItems(): number | null {\n return this._total;\n }\n}\n","// ————— 通用 —————\n\nexport interface Page {\n total: number;\n pageIndex: number;\n pageSize: number;\n list: T[];\n /** facade 重算(修复 api_hashtag.py:693 bug) */\n hasNext: boolean;\n}\n\nexport interface RichText {\n contentType: \"text\" | \"html\" | \"markdown\";\n /** html 类型必须服务端消毒(sanitize)后下发 */\n content: string;\n}\n\n// ————— 话题 —————\n\nexport interface TopicTab {\n key: string;\n label: string;\n visible: boolean;\n}\n\nexport interface TopicDetail {\n hashtagName: string;\n kind: \"hashtag\" | \"activity\";\n activityUuid: string | null;\n title: string | null;\n description: RichText | null;\n bannerPic: string | null;\n smallBannerPic: string | null;\n headerPic: string | null;\n phase: \"draft\" | \"preparing\" | \"running\" | \"ended\" | null;\n startTime: number | null; // UTC ms\n endTime: number | null; // UTC ms\n /** 合并 popularity / hashtag_heat 为单字段单语义 */\n heat: number;\n participantsCount: number;\n subscribeCount: number;\n action: {\n visible: boolean;\n label: string | null;\n url: string | null;\n } | null;\n tabs: TopicTab[];\n ruleStoryUuid: string | null;\n /** 登录态填充;匿名/游客(无 embed token)为 null */\n viewer: {\n subscribed: boolean;\n canEdit: boolean;\n } | null;\n}\n\n// ————— 作品卡片 —————\n\nexport interface StoryCard {\n storyId: string;\n title: string | null;\n coverUrl: string | null;\n shareUrl: string | null;\n aspect: string | null;\n author: {\n uuid: string | null;\n nickName: string | null;\n avatarUrl: string | null;\n };\n metrics: {\n likeCount: number;\n sameStyleCount: number;\n picCount: number;\n };\n flags: {\n hasVideo: boolean;\n hasBgm: boolean;\n isInteractive: boolean;\n isPinned: boolean;\n };\n hashtagNames: string[];\n /** 匿名/游客 null */\n viewer: {\n liked: boolean;\n favored: boolean;\n } | null;\n}\n\n// ————— 话题热门流 / 本周最热 —————\n\n/** 话题算法热门流的作品卡片:现有 StoryCard 全字段 + 热度分(已乘 1000 取整)。 */\nexport interface HotStoryCard extends StoryCard {\n /** 热度分(后端 doc.sort[0]*1000 取整),int。 */\n hotScore: number;\n}\n\n/** 本周最热单品(近 7 天同款 UV 最高的一条),无则 null。全 camelCase。 */\nexport interface WeeklyHottest {\n collectionUuid: string;\n title: string;\n coverUrl: string | null;\n sameStyleUv: number;\n creatorName: string;\n creatorUuid: string;\n creatorAvatar: string | null;\n isInteractive: boolean;\n}\n\n// ————— 活动精选 —————\n\nexport interface HighlightPage extends Page {\n /** topList 仅 pageIndex=0 时非空,其余页为 [] */\n topList: StoryCard[];\n}\n\n// ————— 角色卡片 —————\n\nexport interface CharacterCard {\n uuid: string;\n name: string | null;\n coverUrl: string | null;\n type: \"oc\" | \"elementum\" | string;\n author: {\n uuid: string | null;\n nickName: string | null;\n avatarUrl: string | null;\n };\n}\n\n// ————— 活动卡片 —————\n\nexport interface CampaignCard {\n uuid: string;\n title: string | null;\n coverUrl: string | null;\n pv: number;\n}\n\n// ————— 世界观事件 —————\n\nexport interface LoreEvent {\n uuid: string;\n name: string;\n category: string | null;\n description: string | null;\n sortIndex: number;\n collectionCount: number;\n boundTopic: {\n name: string;\n startTime: number | null; // UTC ms\n endTime: number | null; // UTC ms\n } | null;\n stories: StoryCard[];\n}\n\n// ————— 榜单 —————\n\nexport type RankWindow = \"daily\" | \"weekly\" | \"monthly\";\nexport type RankEntity = \"stories\" | \"creators\" | \"oc\" | \"elementum\";\n\nexport interface Leaderboard {\n window: RankWindow;\n entity: RankEntity;\n startTime: number | null; // UTC ms;后端 Optional,可能下发 null\n endTime: number | null; // UTC ms;后端 Optional,可能下发 null\n lastUpdatedAt: number | null;\n list: RankEntry[];\n}\n\nexport interface RankEntry {\n rank: number;\n /** 经 score_multiplier 缩放后的整数 */\n score: number;\n item: T;\n}\n\nexport interface CreatorCard {\n uuid: string | null;\n nickName: string | null;\n avatarUrl: string | null;\n subscriberCount: number;\n storyCount: number;\n topStories: StoryCard[];\n}\n\n// ————— 客户端上下文 —————\n\nexport type ClientContext = \"app\" | \"web-embedded\" | \"guest\";\n\n/** hello 响应中的 client 字段(宿主回传具体平台) */\nexport type BridgeClient = \"ios\" | \"android\" | \"web\";\n\nexport interface HelloResult {\n /** 宿主平台 */\n client: BridgeClient;\n /** App 版本号,Web 宿主为 null */\n appVersion: string | null;\n /** 宿主支持的 bridge method 列表 */\n features: string[];\n}\n\n// ————— 能力位图 —————\n\nexport enum Capability {\n // 数据能力(所有上下文均支持)\n ReadTopic = \"read.topic\",\n ReadStories = \"read.stories\",\n ReadCharacters = \"read.characters\",\n ReadCampaigns = \"read.campaigns\",\n ReadLoreEvents = \"read.loreEvents\",\n ReadActivity = \"read.activity\",\n ReadRank = \"read.rank\",\n\n // 桥接能力(仅 App/Web-embedded)\n Bridge = \"bridge\",\n NavInternal = \"nav.internal\",\n NavExternal = \"nav.external\",\n NavApplyHost = \"nav.applyHost\",\n Toast = \"ui.toast\",\n Viewport = \"ui.viewport\",\n EventBack = \"event.back\",\n EventTokenChanged = \"event.tokenChanged\",\n EventViewport = \"event.viewport\",\n}\n\n// ————— AllowedRoute 白名单 —————\n\n/** AllowedRoute v1 硬编码白名单 */\nexport type AllowedRoute =\n | \"/tag\"\n | \"/topic\"\n | \"/activity\"\n | \"/ranking\"\n | \"/collection/interaction\"\n | \"/oc\"\n | \"/user\"\n | \"/generate\";\n\n// ————— viewport —————\n\nexport interface ViewportInfo {\n /** 嵌入态永远为 0(防双叠加) */\n safeTop: 0;\n safeBottom: number;\n keyboardInset: number;\n width: number;\n height: number;\n}\n\n// ————— SDK 选项 —————\n\nexport interface TopicSDKOptions {\n /** 后端 API 基址,默认 'https://pre.api.talesofai.cn' */\n apiBaseUrl?: string;\n /** hello 握手超时 ms,默认 1500 */\n helloTimeout?: number;\n /** getEmbedToken bridge 调用超时 ms,默认 3000 */\n tokenTimeout?: number;\n /** embed token 过期前多久触发 re-exchange,ms,默认 5 * 60 * 1000 */\n tokenRefreshEarlyMs?: number;\n /** token 无法恢复时的回调 */\n onAuthLost?: (reason: string) => void;\n}\n\n// ————— 公开 API 接口 —————\n\nexport interface SDKAuth {\n /** 当前 embed token,匿名时为 null */\n getToken(): string | null;\n /** token 过期时间(UTC ms),匿名时为 null */\n getExpiresAt(): number | null;\n /** 是否已登录(有有效 token) */\n isAuthenticated(): boolean;\n}\n\n/** listMyStories 的 kind:favored=当前 user 收藏的本话题作品;created=当前 user 在本话题投稿的作品;liked=当前 user 点赞的本话题作品。 */\nexport type MyStoryKind = \"favored\" | \"created\" | \"liked\";\n\nexport interface SDKTopic {\n /** GET /v1/embed/topic/{name} */\n getDetail(name: string): Promise;\n\n /** GET /v1/embed/topic/{name}/stories */\n listStories(\n name: string,\n query: {\n pageIndex: number;\n pageSize: number;\n sort: \"hot\" | \"like_count\" | \"highlight_mark_time\";\n /** 可选时间窗(UTC ms),透传后端 start_time/end_time */\n startTime?: number;\n /** 可选时间窗(UTC ms),透传后端 start_time/end_time */\n endTime?: number;\n /** 可选作者过滤,透传后端 authorUuid */\n authorUuid?: string;\n },\n ): Promise>;\n\n /**\n * GET /v1/embed/topic/{name}/my-stories\n * 当前 embed user 在本话题下的作品:kind=favored(收藏)/ created(投稿)/ liked(点赞)。\n * 匿名(无 embed token)返回空 Page(list=[]、hasNext=false),不抛错、不 401。\n */\n listMyStories(\n name: string,\n query: {\n kind: MyStoryKind;\n pageIndex?: number;\n pageSize?: number;\n },\n ): Promise>;\n\n /** GET /v1/embed/topic/{name}/characters */\n listCharacters(\n name: string,\n query: {\n /** 后端为 List[str](alias parentType),多值。默认 [\"oc\",\"elementum\"] */\n parentType?: string[];\n pageIndex: number;\n pageSize: number;\n sort?: string;\n },\n ): Promise>;\n\n /** GET /v1/embed/topic/{name}/campaigns */\n listCampaigns(name: string, query: { pageIndex: number; pageSize: number }): Promise>;\n\n /** GET /v1/embed/topic/{name}/lore-events */\n listLoreEvents(name: string): Promise;\n\n /**\n * GET /v1/embed/topic/{name}/hot-stories\n * 话题算法热门流(bounded 热门列表,无分页),每条带 hotScore。\n */\n listHot(name: string): Promise;\n\n /**\n * GET /v1/embed/topic/{name}/weekly-hottest\n * 本周最热单品(近 7 天同款 UV 最高的一条),无则 null。\n */\n getWeeklyHottest(name: string): Promise;\n}\n\nexport interface SDKActivity {\n /** GET /v1/embed/activity/{uuid}/tabs */\n listTabs(uuid: string): Promise;\n\n /**\n * GET /v1/embed/activity/{uuid}/tab/{tabKey}\n * HighlightPage.topList 仅 pageIndex=0 时非空\n */\n listSelectedStories(\n uuid: string,\n tabKey: string,\n query: {\n pageIndex: number;\n pageSize: number;\n sort?: string;\n },\n ): Promise;\n}\n\nexport interface SDKRank {\n /**\n * GET /v1/embed/rank/{entity}/{window}/{at}\n * entity 直接作为路径段传给后端(facade 内部映射到 collection/user/tcp-oc/tcp-elementum,对外保持 stories/creators/oc/elementum)。\n * at: UTC ms 或字符串 'latest'。\n * 注意:oc / elementum 仅支持 at='latest',传时间戳后端返回 400(SDK 侧亦会提前抛错)。\n */\n get(\n entity: RankEntity,\n window: RankWindow,\n at: number | \"latest\",\n ): Promise>;\n}\n\nexport interface SDKNav {\n /**\n * 跳进产品内页面(AllowedRoute v1 白名单)。\n *\n * 参数契约按路由性质分两类(SDK 运行期兜底 + 此处类型收窄,缺参构建期/运行期都会被拦):\n * - **自指路由** `/topic` `/tag` `/activity`:参数=「当前这个」,可省略,SDK 自动从当前页\n * `?hashtag=` / `?activity_uuid=` 填入(创作者可显式覆盖)。`/ranking` `/generate` 无需参数。\n * - **per-item 路由** `/oc` `/user` `/collection/interaction`:参数指向「具体某个」实体,\n * **必须传 `uuid`**(来自被点卡片的数据,SDK 无从代填)。\n *\n * guest 上下文(仅本地 dev 无宿主时)本地 dev 无宿主仅 console 提示,不跳转;生产入口恒为宿主内嵌,唤起 App 由宿主承载。\n */\n internal(\n route: \"/topic\" | \"/tag\" | \"/activity\" | \"/ranking\" | \"/generate\",\n query?: Record,\n ): Promise;\n internal(\n route: \"/oc\" | \"/user\" | \"/collection/interaction\",\n query: { uuid: string | number } & Record,\n ): Promise;\n\n /**\n * 跳外部 URL。\n * embedded: bridge nav.external\n * guest: window.open(url, '_blank')\n */\n external(url: string): Promise;\n\n /**\n * 申请创建/主持一个新话题活动空间:跳转到运营配置的申请表单(飞书多维表单),宿主自动带上\n * 当前登录用户昵称/UID 的 prefill 参数(不含话题名)。**不接受参数**——SDK/页面拿不到、也不经手\n * 这些用户数据,全由宿主本地态直接拼 URL(与 `getEmbedToken` 不下发宿主 token 同一不变量)。\n *\n * 运营未配置申请表单时抛 `BridgeError`(宿主报 not-configured)。\n * guest 上下文(仅本地 dev 无宿主)本地不跳转,仅 console 提示;生产入口恒为宿主内嵌。\n */\n applyHost(): Promise;\n}\n\nexport interface SDKUi {\n /**\n * 宿主 Toast(embedded)。\n * guest / unsupported:抛 UnsupportedError。\n */\n toast(text: string, options?: { duration?: number; level?: \"info\" | \"warn\" }): Promise;\n\n /**\n * 当前 viewport 安全区信息。\n * guest:抛 UnsupportedError。\n */\n viewport(): Promise;\n}\n\nexport interface SDKEvents {\n on(event: \"tokenChanged\", handler: (newToken: string | null) => void): () => void;\n on(event: \"viewport\", handler: (info: ViewportInfo) => void): () => void;\n on(event: \"back\", handler: (ev: { preventDefault(): void }) => void): () => void;\n off(event: string, handler: (...args: unknown[]) => void): void;\n}\n\n// ————— SDK 顶层 —————\n\nexport interface TopicSDK {\n env: {\n context: ClientContext;\n embedded: boolean;\n client: BridgeClient | \"unknown\";\n appVersion: string | null;\n features: string[];\n };\n auth: SDKAuth;\n topic: SDKTopic;\n activity: SDKActivity;\n rank: SDKRank;\n nav: SDKNav;\n ui: SDKUi;\n events: SDKEvents;\n can(cap: Capability): boolean;\n destroy(): void;\n}\n","import type { BridgeClient } from \"./bridge.js\";\nimport { UnsupportedError } from \"./errors.js\";\nimport type { ClientContext, HelloResult } from \"./types.js\";\nimport { Capability } from \"./types.js\";\n\nexport interface EnvResult {\n context: ClientContext;\n embedded: boolean;\n client: \"ios\" | \"android\" | \"web\" | \"unknown\";\n appVersion: string | null;\n features: string[];\n hello: HelloResult | null;\n /** 宿主注入的 ?activity_uuid=(无则 null)。决定 ReadActivity 能力是否授予(纯 hashtag 话题无活动数据)。 */\n activityUuid: string | null;\n}\n\nexport async function detectEnv(bridge: BridgeClient, sdkVersion: string, helloTimeout: number): Promise {\n const ua = navigator.userAgent;\n // 宿主把绑定的 activity_uuid 作为 query 注入 iframe(无则纯 hashtag 话题)。\n const activityUuid =\n typeof window !== \"undefined\" ? new URLSearchParams(window.location.search).get(\"activity_uuid\") || null : null;\n\n // UA 预判:小程序直接出局\n if (/miniProgram/i.test(ua)) {\n throw new UnsupportedError(\"weapp-not-supported\", \"guest\" as ClientContext);\n }\n\n // hello 握手(超时→ null = guest)\n const hello = await bridge.hello(sdkVersion, helloTimeout);\n\n if (!hello) {\n // 握手超时 → guest\n return {\n context: \"guest\",\n embedded: false,\n client: \"unknown\",\n appVersion: null,\n features: [],\n hello: null,\n activityUuid,\n };\n }\n\n // 宿主回包确认客户端类型(最终以握手结果为准,UA 仅用于 miniProgram 预判出局)\n const context: ClientContext = hello.client === \"ios\" || hello.client === \"android\" ? \"app\" : \"web-embedded\";\n\n return {\n context,\n embedded: true,\n client: hello.client,\n appVersion: hello.appVersion,\n features: hello.features,\n hello,\n activityUuid,\n };\n}\n\n/**\n * 根据 EnvResult 计算支持的 Capability 集合。\n */\nexport function buildCapabilities(env: EnvResult): Set {\n const caps = new Set();\n\n // 数据能力:所有上下文均支持\n caps.add(Capability.ReadTopic);\n caps.add(Capability.ReadStories);\n caps.add(Capability.ReadCharacters);\n caps.add(Capability.ReadCampaigns);\n caps.add(Capability.ReadLoreEvents);\n caps.add(Capability.ReadRank);\n // ReadActivity 仅当话题绑定了 activity(宿主注入 ?activity_uuid=)才授予;纯 hashtag 话题无活动数据,\n // 不报 ReadActivity,避免 can(ReadActivity) 恒 true 误导创作者渲染空活动区块。\n if (env.activityUuid) {\n caps.add(Capability.ReadActivity);\n }\n\n if (env.context !== \"guest\") {\n // 桥接能力(仅 App/Web-embedded)\n caps.add(Capability.Bridge);\n caps.add(Capability.NavInternal);\n caps.add(Capability.NavExternal);\n caps.add(Capability.NavApplyHost);\n caps.add(Capability.Toast);\n caps.add(Capability.Viewport);\n caps.add(Capability.EventBack);\n caps.add(Capability.EventTokenChanged);\n caps.add(Capability.EventViewport);\n }\n // guest(仅本地 dev 无宿主可达):仅只读数据能力;唤起 App 统一靠宿主 nav.internal,不再暴露独立 OpenApp 能力。\n\n return caps;\n}\n","import type { BridgeClient } from \"./bridge.js\";\nimport type { SDKEvents, ViewportInfo } from \"./types.js\";\n\ntype TokenChangedHandler = (newToken: string | null) => void;\ntype ViewportHandler = (info: ViewportInfo) => void;\ntype BackHandler = (ev: { preventDefault(): void }) => void;\ntype AnyHandler = TokenChangedHandler | ViewportHandler | BackHandler;\n\ninterface HandlerEntry {\n event: string;\n handler: AnyHandler;\n bridgeHandler: (data: unknown) => void;\n}\n\nexport class SDKEventsImpl implements SDKEvents {\n private readonly _entries: HandlerEntry[] = [];\n // tokenChanged 不直接订阅 bridge:宿主事件里 token 恒为 null,\n // 真正的新 token 由 SDK 内部 re-exchange 完成后经 notifyTokenChanged 下发。\n private readonly _tokenChangedHandlers = new Set();\n\n public constructor(private readonly _bridge: BridgeClient | null) {}\n\n public on(event: \"tokenChanged\", handler: TokenChangedHandler): () => void;\n public on(event: \"viewport\", handler: ViewportHandler): () => void;\n public on(event: \"back\", handler: BackHandler): () => void;\n public on(event: string, handler: AnyHandler): () => void {\n // tokenChanged:与 bridge 事件解耦,由 SDK 内部 re-exchange 后回调新 token\n if (event === \"tokenChanged\") {\n const h = handler as TokenChangedHandler;\n this._tokenChangedHandlers.add(h);\n return () => {\n this._tokenChangedHandlers.delete(h);\n };\n }\n\n if (!this._bridge) {\n // guest 模式:无宿主事件,返回空 unsubscribe\n return () => {};\n }\n\n let bridgeHandler: (data: unknown) => void;\n\n switch (event) {\n case \"viewport\": {\n const h = handler as ViewportHandler;\n bridgeHandler = (data: unknown) => {\n const raw = data as {\n safeTop?: number;\n safeBottom?: number;\n keyboardInset?: number;\n width?: number;\n height?: number;\n };\n h({\n safeTop: 0, // 固定 0,防双叠加\n safeBottom: raw.safeBottom ?? 0,\n keyboardInset: raw.keyboardInset ?? 0,\n width: raw.width ?? window.innerWidth,\n height: raw.height ?? window.innerHeight,\n });\n };\n break;\n }\n case \"back\": {\n const h = handler as BackHandler;\n bridgeHandler = (_data: unknown) => {\n let _defaultPrevented = false;\n h({\n preventDefault() {\n _defaultPrevented = true;\n },\n });\n // SDK 不需要把 defaultPrevented 回传给宿主;\n // 宿主侧设计为\"等待一帧确认\"(H8),此处不需要额外处理\n };\n break;\n }\n default:\n return () => {};\n }\n\n this._bridge.onEvent(event, bridgeHandler);\n const entry: HandlerEntry = { event, handler, bridgeHandler };\n this._entries.push(entry);\n\n return () => {\n this.off(event, handler as (...args: unknown[]) => void);\n };\n }\n\n /**\n * 由 SDK 内部在 re-exchange 完成后调用,向消费方下发最新 token。\n * 宿主 tokenChanged 事件本身不携带 token(恒 null),故新值取自 auth 当前 token。\n */\n public notifyTokenChanged(token: string | null): void {\n this._tokenChangedHandlers.forEach((h) => h(token));\n }\n\n public off(event: string, handler: (...args: unknown[]) => void): void {\n if (event === \"tokenChanged\") {\n this._tokenChangedHandlers.delete(handler as unknown as TokenChangedHandler);\n return;\n }\n const idx = this._entries.findIndex((e) => e.event === event && e.handler === handler);\n if (idx === -1) return;\n const entry = this._entries[idx]!;\n this._bridge?.offEvent(event, entry.bridgeHandler);\n this._entries.splice(idx, 1);\n }\n\n public destroy(): void {\n for (const entry of this._entries) {\n this._bridge?.offEvent(entry.event, entry.bridgeHandler);\n }\n this._entries.length = 0;\n this._tokenChangedHandlers.clear();\n }\n}\n","import type { BridgeClient } from \"./bridge.js\";\nimport { UnsupportedError } from \"./errors.js\";\nimport type { AllowedRoute, ClientContext, SDKNav } from \"./types.js\";\n\n/**\n * 自指路由:参数=「当前这个」,缺省时 SDK 从当前页 URL 自动填(创作者可显式覆盖)。\n * 宿主把 hashtag / activity_uuid 作为 query 注入 iframe src,故内嵌页 `location.search` 必有这些值。\n */\nconst SELF_PARAM_FROM_URL: Partial> = {\n \"/topic\": { param: \"hashtag\", urlKey: \"hashtag\" },\n \"/tag\": { param: \"hashtag\", urlKey: \"hashtag\" },\n \"/activity\": { param: \"uuid\", urlKey: \"activity_uuid\" },\n};\n\n/** per-item 路由:参数指向「具体某个」实体(来自被点卡片),SDK 无从代填 → 缺则抛错打回。 */\nconst REQUIRED_PARAMS: Partial> = {\n \"/oc\": [\"uuid\"],\n \"/user\": [\"uuid\"],\n \"/collection/interaction\": [\"uuid\"],\n};\n\nconst isBlank = (v: unknown): boolean => v === undefined || v === null || v === \"\";\n\nexport class SDKNavImpl implements SDKNav {\n public constructor(\n private readonly _bridge: BridgeClient | null,\n private readonly _context: ClientContext,\n ) {}\n\n /**\n * 解析最终 query:自指路由缺参从 URL 自动填;per-item 路由缺必需参数则抛错(开发期就被打回,而非线上白屏)。\n */\n private _resolveQuery(route: AllowedRoute, query?: Record): Record {\n const q: Record = { ...(query ?? {}) };\n\n // 1) 自指路由:缺参从当前页 URL 自动填\n const selfRef = SELF_PARAM_FROM_URL[route];\n if (selfRef && isBlank(q[selfRef.param]) && typeof window !== \"undefined\") {\n const fromUrl = new URLSearchParams(window.location.search).get(selfRef.urlKey);\n if (fromUrl) q[selfRef.param] = fromUrl;\n }\n\n // 2) 必需参数校验(自指填完后再查)\n const required = REQUIRED_PARAMS[route] ?? (selfRef ? [selfRef.param] : []);\n for (const p of required) {\n if (isBlank(q[p])) {\n const hint = selfRef\n ? `该自指路由通常由 SDK 从当前页 ?${selfRef.urlKey}= 自动填,但当前 URL 没有该值`\n : `该路由指向具体实体,请从被点卡片数据传入 ${p}(如 ${p}: story.uuid)`;\n throw new Error(`[topic-sdk] nav.internal('${route}') 缺少必需参数 '${p}'。${hint}。`);\n }\n }\n return q;\n }\n\n public async internal(route: AllowedRoute, query?: Record): Promise {\n const effectiveQuery = this._resolveQuery(route, query);\n if (this._context === \"guest\") {\n // guest(仅本地 dev 无宿主):_resolveQuery 已先跑(保留缺参 throw);无宿主不跳转,生产由宿主唤起 / 站内跳。\n console.info(\"[topic-sdk] nav.internal(route) 本地 dev 无宿主不跳转;生产由宿主唤起/站内跳\");\n return;\n }\n if (!this._bridge) {\n throw new UnsupportedError(\"nav.internal\", this._context);\n }\n await this._bridge.send(\"nav.internal\", { route, query: effectiveQuery });\n }\n\n public async external(url: string): Promise {\n if (this._context === \"guest\") {\n window.open(url, \"_blank\");\n return;\n }\n if (!this._bridge) {\n throw new UnsupportedError(\"nav.external\", this._context);\n }\n await this._bridge.send(\"nav.external\", { url });\n }\n\n public async applyHost(): Promise {\n if (this._context === \"guest\") {\n // guest(仅本地 dev 无宿主):拼 URL 需要的用户态/运营配置只有宿主本地有,dev 无宿主不跳转;生产由宿主承载。\n console.info(\"[topic-sdk] nav.applyHost() 本地 dev 无宿主不跳转;生产由宿主用本地用户态拼 URL 后跳转\");\n return;\n }\n if (!this._bridge) {\n throw new UnsupportedError(\"nav.applyHost\", this._context);\n }\n await this._bridge.send(\"nav.applyHost\");\n }\n}\n","import type { BridgeClient } from \"./bridge.js\";\nimport { UnsupportedError } from \"./errors.js\";\nimport type { ClientContext, SDKUi, ViewportInfo } from \"./types.js\";\n\nexport class SDKUiImpl implements SDKUi {\n public constructor(\n private readonly _bridge: BridgeClient | null,\n private readonly _context: ClientContext,\n ) {}\n\n public async toast(text: string, options?: { duration?: number; level?: \"info\" | \"warn\" }): Promise {\n if (this._context === \"guest\" || !this._bridge) {\n throw new UnsupportedError(\"ui.toast\", this._context);\n }\n await this._bridge.send(\"ui.toast\", {\n text,\n duration: options?.duration,\n level: options?.level ?? \"info\",\n });\n }\n\n public async viewport(): Promise {\n if (this._context === \"guest\" || !this._bridge) {\n throw new UnsupportedError(\"ui.viewport\", this._context);\n }\n const raw = await this._bridge.send<{\n safeTop?: number;\n safeBottom?: number;\n keyboardInset?: number;\n width?: number;\n height?: number;\n }>(\"ui.viewport\");\n\n // safeTop 固定返回 0(防双叠加,宿主已 safeTop:true/page.tsx:39)\n // 其余字段对宿主漏传做兜底,与 events.ts viewport 分支保持一致\n return {\n safeTop: 0,\n safeBottom: raw.safeBottom ?? 0,\n keyboardInset: raw.keyboardInset ?? 0,\n width: raw.width ?? window.innerWidth,\n height: raw.height ?? window.innerHeight,\n };\n }\n}\n"],"mappings":"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","names":["delay","window","Capability"]} \ No newline at end of file +{"version":3,"sources":["../src/index.ts","../src/auth.ts","../src/errors.ts","../src/bridge.ts","../src/data.ts","../src/types.ts","../src/env.ts","../src/events.ts","../src/nav.ts","../src/ui.ts","../src/image.ts"],"sourcesContent":["import { SDKAuthImpl } from \"./auth.js\";\nimport { BridgeClient } from \"./bridge.js\";\nimport { SDKActivityImpl, SDKRankImpl, SDKTopicImpl } from \"./data.js\";\nimport { buildCapabilities, detectEnv } from \"./env.js\";\nimport { SDKEventsImpl } from \"./events.js\";\nimport { SDKNavImpl } from \"./nav.js\";\nimport type { AllowedRoute, Capability, SDKAuth, TopicSDK, TopicSDKOptions } from \"./types.js\";\nimport { SDKUiImpl } from \"./ui.js\";\n\nexport type {\n AllowedRoute,\n BridgeClient as BridgeClientType,\n // 活动\n CampaignCard,\n // 角色\n CharacterCard,\n // 环境/能力\n ClientContext,\n CreatorCard,\n HelloResult,\n HighlightPage,\n Leaderboard,\n // 世界观\n LoreEvent,\n MyStoryKind,\n // 通用\n Page,\n RankEntity,\n RankEntry,\n // 榜单\n RankWindow,\n RichText,\n SDKActivity,\n // 接口\n SDKAuth,\n SDKEvents,\n SDKNav,\n SDKRank,\n SDKTopic,\n SDKUi,\n // 作品\n StoryCard,\n TopicDetail,\n TopicSDK,\n TopicSDKOptions,\n // 话题\n TopicTab,\n ViewportInfo,\n} from \"./types.js\";\n\nexport { BridgeClient } from \"./bridge.js\";\nexport { PageCursor } from \"./data.js\";\nexport { BridgeError, TopicApiError, UnsupportedError } from \"./errors.js\";\nexport { ossImage, ossImageSrcSet } from \"./image.js\";\nexport type { OssImageOptions } from \"./image.js\";\nexport { Capability } from \"./types.js\";\n\nconst SDK_VERSION = \"0.1.0\";\n\n/**\n * 全局拦截 点击。内嵌页跑在 sandbox iframe(无 allow-top-navigation)里,原生 /相对路径跳转会逃逸到\n * iframe 自身源(OSS)而非宿主 App。故在捕获阶段接管:\n * - 同源路径跳转 → nav.internal(命中 AllowedRoute 由宿主导航/游客转深链;未命中被宿主拦截、不逃逸);\n * - 跨站外链 → nav.external(宿主用系统浏览器打开);\n * - 同 path 仅改 hash(页面内视图切换)、target=_blank、download、非 http(s) scheme → 放行。\n * 这样创作者即便写了原生链接,也不会把 iframe 跳出内嵌页。返回卸载函数。\n */\nfunction installLinkInterceptor(nav: SDKNavImpl): () => void {\n const onClick = (e: MouseEvent) => {\n if (e.defaultPrevented || e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;\n const targetEl = e.target instanceof Element ? e.target : ((e.target as Node | null)?.parentElement ?? null);\n const a = targetEl?.closest(\"a\") as HTMLAnchorElement | null;\n if (!a) return;\n const href = a.getAttribute(\"href\");\n if (!href) return;\n const target = a.getAttribute(\"target\");\n if (target && target !== \"_self\") return;\n if (a.hasAttribute(\"download\")) return;\n let url: URL;\n try {\n url = new URL(href, location.href);\n } catch {\n return;\n }\n if (url.protocol !== \"http:\" && url.protocol !== \"https:\") return;\n if (url.origin === location.origin) {\n // 同 path 仅改 hash(页面内视图切换)→ 放行,交给页面自己处理\n if (url.pathname === location.pathname && url.search === location.search && url.hash) return;\n const query: Record = {};\n url.searchParams.forEach((v, k) => {\n query[k] = v;\n });\n e.preventDefault();\n nav.internal(url.pathname as AllowedRoute, query).catch(() => {});\n return;\n }\n e.preventDefault();\n nav.external(url.href).catch(() => {});\n };\n document.addEventListener(\"click\", onClick, true);\n return () => document.removeEventListener(\"click\", onClick, true);\n}\n\n/**\n * 守卫 history.pushState/replaceState。内嵌页禁用(会污染 Android WebView 返回栈,返回键先消费 iframe history),\n * 应改用 hash 路由 / 内存路由。embedded 下安装,违例当场 throw(dev 期即暴露);destroy 恢复原函数。\n */\nfunction installPushStateGuard(): () => void {\n if (typeof history === \"undefined\") return () => {};\n const origPush = history.pushState.bind(history);\n const origReplace = history.replaceState.bind(history);\n const guard = (method: string) => () => {\n const msg =\n `[topic-sdk] 内嵌页禁止 history.${method}(会污染 App 返回栈);` +\n `请改用 hash 路由(location.hash)或内存路由做页面内视图切换。`;\n console.error(msg);\n throw new Error(msg);\n };\n history.pushState = guard(\"pushState\") as typeof history.pushState;\n history.replaceState = guard(\"replaceState\") as typeof history.replaceState;\n return () => {\n history.pushState = origPush;\n history.replaceState = origReplace;\n };\n}\n\n/**\n * 初始化并返回 TopicSDK 实例。\n *\n * 初始化序列:\n * ① 端探测(UA 嗅探预判)\n * ② hello 握手(超时→ guest)\n * ③ 鉴权(embedded:bridge getEmbedToken;guest:匿名)\n * ④ 能力协商(填充 Capability 位图)\n * ⑤ 事件订阅(tokenChanged / viewport / back)\n */\nexport async function createTopicSDK(options: TopicSDKOptions = {}): Promise {\n const {\n apiBaseUrl = \"https://pre.api.talesofai.cn\",\n helloTimeout = 1500,\n tokenTimeout = 3000,\n tokenRefreshEarlyMs = 5 * 60 * 1000,\n onAuthLost,\n } = options;\n\n // tokenTimeout 下界 clamp:500ms 是 v1 bridge 已知坏值;低于 1000ms 上调并告警(SKILL 固定 3000)。\n let effectiveTokenTimeout = tokenTimeout;\n if (effectiveTokenTimeout < 1000) {\n console.warn(\n `[topic-sdk] tokenTimeout ${effectiveTokenTimeout}ms 低于下限 1000ms(500ms 是 v1 bridge 已知坏值),已上调到 1000ms。`,\n );\n effectiveTokenTimeout = 1000;\n }\n\n // ① + ② 端探测 + hello 握手\n // BridgeClient は常に作成するが、guest の場合はすぐ破棄する\n const bridge = new BridgeClient(effectiveTokenTimeout);\n const env = await detectEnv(bridge, SDK_VERSION, helloTimeout);\n\n // embedded 上下文:守卫 history.pushState/replaceState(内嵌页禁用,会污染 App 返回栈);违例当场 throw,dev 期即暴露。\n const removePushStateGuard = env.embedded ? installPushStateGuard() : () => {};\n\n // guest の場合は bridge を使わない\n const activeBridge = env.context === \"guest\" ? null : bridge;\n if (env.context === \"guest\") {\n // guest 模式:bridge 不再需要,但保留 message listener 无害(destroy 可选)\n // 为节约资源,立即销毁\n bridge.destroy();\n }\n\n // ③ 鉴权\n const auth = new SDKAuthImpl(activeBridge, effectiveTokenTimeout, tokenRefreshEarlyMs, onAuthLost);\n await auth.init();\n\n // ④ 能力协商\n const capabilities = buildCapabilities(env);\n\n // ⑤ 事件订阅:tokenChanged → 自动重取 token\n const eventsImpl = new SDKEventsImpl(activeBridge);\n\n if (activeBridge) {\n activeBridge.onEvent(\"tokenChanged\", (_data: unknown) => {\n // 宿主事件不携带新 token(恒 null),SDK 内部 re-exchange 后再把真实 token 下发给消费方\n auth\n .handleTokenChanged()\n .then(() => eventsImpl.notifyTokenChanged(auth.getToken()))\n .catch(() => eventsImpl.notifyTokenChanged(null));\n });\n }\n\n // ————— 构造子模块 —————\n const topicImpl = new SDKTopicImpl(apiBaseUrl, auth as SDKAuth);\n const activityImpl = new SDKActivityImpl(apiBaseUrl, auth as SDKAuth);\n const rankImpl = new SDKRankImpl(apiBaseUrl, auth as SDKAuth);\n const navImpl = new SDKNavImpl(activeBridge, env.context);\n const uiImpl = new SDKUiImpl(activeBridge, env.context);\n\n // 根治\"原生 /相对跳转在 sandbox iframe 内逃逸到 OSS 源\"的问题:全局接管 点击,改走 bridge 导航。\n const removeLinkInterceptor = installLinkInterceptor(navImpl);\n\n // ————— TopicSDK 对象 —————\n const sdk: TopicSDK = {\n env: {\n context: env.context,\n embedded: env.embedded,\n client: env.client,\n appVersion: env.appVersion,\n features: env.features,\n },\n\n auth: auth as SDKAuth,\n topic: topicImpl,\n activity: activityImpl,\n rank: rankImpl,\n nav: navImpl,\n ui: uiImpl,\n events: eventsImpl,\n\n can(cap: Capability): boolean {\n return capabilities.has(cap);\n },\n\n destroy(): void {\n removeLinkInterceptor();\n removePushStateGuard();\n auth.destroy();\n eventsImpl.destroy();\n activeBridge?.destroy();\n },\n };\n\n return sdk;\n}\n","import type { BridgeClient } from \"./bridge.js\";\nimport type { SDKAuth } from \"./types.js\";\n\ninterface EmbedTokenResult {\n // 宿主回包字段名为 embedToken(对齐后端 EmbedTokenResponse.embedToken)\n embedToken: string;\n expiresAt: number | null; // UTC ms;宿主可能回 null\n}\n\nexport class SDKAuthImpl implements SDKAuth {\n private _token: string | null = null;\n private _expiresAt: number | null = null;\n private _refreshTimer: ReturnType | null = null;\n\n public constructor(\n private readonly _bridge: BridgeClient | null,\n private readonly _tokenTimeout: number,\n private readonly _tokenRefreshEarlyMs: number,\n private readonly _onAuthLost: ((reason: string) => void) | undefined,\n ) {}\n\n // ————— SDKAuth 公开 API —————\n\n public getToken(): string | null {\n return this._token;\n }\n\n public getExpiresAt(): number | null {\n return this._expiresAt;\n }\n\n public isAuthenticated(): boolean {\n return !!this._token && !this._isExpired();\n }\n\n // ————— 内部 API —————\n\n /**\n * 初始化鉴权:向 bridge 请求 embed token。\n * guest 模式(无 bridge):直接以匿名模式初始化,不抛错。\n */\n public async init(): Promise {\n if (!this._bridge) {\n // guest 模式:匿名\n return;\n }\n await this._fetchToken();\n }\n\n /**\n * 宿主推送 tokenChanged 事件时调用(清除旧 token 并重新请求)。\n */\n public async handleTokenChanged(): Promise {\n this._clearToken();\n if (!this._bridge) return;\n await this._fetchToken();\n }\n\n public destroy(): void {\n if (this._refreshTimer !== null) {\n clearTimeout(this._refreshTimer);\n this._refreshTimer = null;\n }\n }\n\n // ————— 私有方法 —————\n\n private async _fetchToken(retryCount = 0): Promise {\n if (!this._bridge) return;\n\n try {\n const result = await this._bridge.send(\"getEmbedToken\", undefined, this._tokenTimeout);\n if (!result || !result.embedToken) {\n throw new Error(\"getEmbedToken returned no embedToken\");\n }\n this._token = result.embedToken;\n this._expiresAt = result.expiresAt ?? null;\n this._scheduleRefresh();\n } catch (err) {\n if (retryCount < 1) {\n // exponential backoff:第一次重试等 500ms\n await delay(500 * Math.pow(2, retryCount));\n return this._fetchToken(retryCount + 1);\n }\n // 达到最大重试次数\n this._onAuthLost?.(`Failed to get embed token: ${String(err)}`);\n }\n }\n\n private _scheduleRefresh(): void {\n if (this._refreshTimer !== null) {\n clearTimeout(this._refreshTimer);\n this._refreshTimer = null;\n }\n if (this._expiresAt === null) return;\n\n const now = Date.now();\n const delay = this._expiresAt - now - this._tokenRefreshEarlyMs;\n if (delay <= 0) {\n // 已过期或即将过期,立即重取\n this._fetchToken().catch(() => {});\n return;\n }\n\n this._refreshTimer = setTimeout(() => {\n this._refreshTimer = null;\n this._fetchToken().catch(() => {});\n }, delay);\n }\n\n private _clearToken(): void {\n this._token = null;\n this._expiresAt = null;\n if (this._refreshTimer !== null) {\n clearTimeout(this._refreshTimer);\n this._refreshTimer = null;\n }\n }\n\n private _isExpired(): boolean {\n if (this._expiresAt === null) return false;\n return Date.now() > this._expiresAt;\n }\n}\n\nfunction delay(ms: number): Promise {\n return new Promise((resolve) => {\n setTimeout(resolve, ms);\n });\n}\n","import type { Capability, ClientContext } from \"./types.js\";\n\n/**\n * HTTP API 错误\n * statusCode -1 = 网络错误/解析失败\n */\nexport class TopicApiError extends Error {\n public readonly cause?: unknown;\n\n public constructor(\n public readonly statusCode: number,\n message: string,\n public readonly endpoint: string,\n cause?: unknown,\n ) {\n super(message);\n this.name = \"TopicApiError\";\n if (cause !== undefined) {\n this.cause = cause;\n }\n }\n}\n\n/**\n * frame-bridge 通信错误\n */\nexport class BridgeError extends Error {\n public readonly cause?: unknown;\n\n public constructor(\n public readonly code: \"timeout\" | \"rejected\" | \"origin-mismatch\" | \"method-not-allowed\" | \"parse-error\",\n public readonly method: string,\n public readonly requestId: string,\n cause?: unknown,\n ) {\n super(`Bridge error [${code}] on method ${method}`);\n this.name = \"BridgeError\";\n if (cause !== undefined) {\n this.cause = cause;\n }\n }\n}\n\n/**\n * 能力不支持错误(不静默 no-op,统一抛出)\n */\nexport class UnsupportedError extends Error {\n public constructor(\n public readonly capability: Capability | string,\n public readonly context: ClientContext,\n ) {\n super(`Capability '${capability}' not supported in context '${context}'`);\n this.name = \"UnsupportedError\";\n }\n}\n","import { BridgeError } from \"./errors.js\";\nimport type { HelloResult } from \"./types.js\";\n\n// ————— wire 协议类型 —————\n\ninterface BridgeRequest {\n v: 2;\n id: string;\n method: string;\n params?: unknown;\n}\n\ninterface BridgeResponse {\n v: 2;\n id: string;\n ok: true;\n result: unknown;\n}\n\ninterface BridgeErrorResponse {\n v: 2;\n id: string;\n ok: false;\n error: { code: string; message: string };\n}\n\ninterface BridgeEventMessage {\n v: 2;\n event: \"tokenChanged\" | \"viewport\" | \"back\";\n data: unknown;\n}\n\ntype IncomingMessage = BridgeResponse | BridgeErrorResponse | BridgeEventMessage;\n\n// ————— ID 生成 —————\n\nlet _counter = 0;\nfunction generateId(): string {\n if (typeof crypto !== \"undefined\" && typeof crypto.randomUUID === \"function\") {\n return crypto.randomUUID();\n }\n // counter fallback(极少数旧环境)\n _counter += 1;\n return `sdk-req-${Date.now()}-${_counter}`;\n}\n\n// ————— pending request 类型 —————\n\ninterface PendingRequest {\n resolve: (value: unknown) => void;\n reject: (reason: unknown) => void;\n timer: ReturnType;\n method: string;\n}\n\ntype EventHandler = (data: unknown) => void;\n\n// ————— BridgeClient —————\n\n/**\n * frame-bridge v2 客户端。\n * 负责与宿主(window.parent)通过 postMessage 通信。\n */\nexport class BridgeClient {\n private readonly _pending = new Map();\n private readonly _eventHandlers = new Map>();\n private readonly _defaultTimeout: number;\n private _destroyed = false;\n\n public constructor(defaultTimeout = 3000) {\n this._defaultTimeout = defaultTimeout;\n window.addEventListener(\"message\", this._onMessage);\n }\n\n /**\n * 向宿主发送请求,等待回包。\n */\n public send(method: string, params?: unknown, timeout?: number): Promise {\n return new Promise((resolve, reject) => {\n const id = generateId();\n const effectiveTimeout = timeout ?? this._defaultTimeout;\n\n const timer = setTimeout(() => {\n this._pending.delete(id);\n reject(new BridgeError(\"timeout\", method, id));\n }, effectiveTimeout);\n\n this._pending.set(id, {\n resolve: resolve as (v: unknown) => void,\n reject,\n timer,\n method,\n });\n\n const request: BridgeRequest = { v: 2, id, method, params };\n window.parent.postMessage(request, \"*\");\n });\n }\n\n /**\n * 订阅宿主主动推送的事件。\n */\n public onEvent(event: string, handler: EventHandler): void {\n if (!this._eventHandlers.has(event)) {\n this._eventHandlers.set(event, new Set());\n }\n this._eventHandlers.get(event)!.add(handler);\n }\n\n /**\n * 取消订阅。\n */\n public offEvent(event: string, handler: EventHandler): void {\n this._eventHandlers.get(event)?.delete(handler);\n }\n\n /**\n * hello 握手。\n * 超时时返回 null(表示 guest 模式)。\n */\n public async hello(sdkVersion: string, timeout?: number): Promise {\n try {\n const result = await this.send(\"hello\", { sdkVersion }, timeout);\n return result;\n } catch {\n return null;\n }\n }\n\n /**\n * 销毁,清理所有 listener 和 pending。\n */\n public destroy(): void {\n this._destroyed = true;\n window.removeEventListener(\"message\", this._onMessage);\n\n // 清除所有 pending 请求\n for (const [id, pending] of this._pending) {\n clearTimeout(pending.timer);\n pending.reject(new BridgeError(\"timeout\", pending.method, id));\n }\n this._pending.clear();\n this._eventHandlers.clear();\n }\n\n private _onMessage = (ev: MessageEvent): void => {\n if (this._destroyed) return;\n\n // 只接受来自宿主父窗口的消息,丢弃任意 iframe/opener 伪造的回包/事件\n // (否则可伪造 hello 把 guest 骗成 embedded、或伪造 getEmbedToken 注入假 token)\n if (ev.source !== window.parent) return;\n\n let msg: IncomingMessage;\n try {\n // data 可能已经是对象(同 origin),或是序列化字符串\n msg = typeof ev.data === \"string\" ? (JSON.parse(ev.data) as IncomingMessage) : (ev.data as IncomingMessage);\n } catch {\n return;\n }\n\n if (!msg || msg.v !== 2) return;\n\n // 事件推送(无 id 字段)\n if (\"event\" in msg) {\n const handlers = this._eventHandlers.get(msg.event);\n if (handlers) {\n handlers.forEach((h) => h(msg.data));\n }\n return;\n }\n\n // 请求回包(有 id 字段)\n if (\"id\" in msg) {\n const pending = this._pending.get(msg.id);\n if (!pending) return;\n clearTimeout(pending.timer);\n this._pending.delete(msg.id);\n\n if (msg.ok) {\n pending.resolve(msg.result);\n } else {\n pending.reject(new BridgeError(\"rejected\", pending.method, msg.id));\n }\n }\n };\n}\n","import { TopicApiError } from \"./errors.js\";\nimport type {\n CampaignCard,\n CharacterCard,\n CreatorCard,\n HighlightPage,\n HotStoryCard,\n Leaderboard,\n LoreEvent,\n MyStoryKind,\n Page,\n RankEntity,\n RankWindow,\n SDKActivity,\n SDKAuth,\n SDKRank,\n SDKTopic,\n StoryCard,\n TopicDetail,\n TopicTab,\n WeeklyHottest,\n} from \"./types.js\";\n\n// ————— 内部 fetch 工具 —————\n\nasync function apiFetch(\n baseUrl: string,\n path: string,\n auth: SDKAuth,\n query?: Record,\n): Promise {\n // 构造 URL\n const url = new URL(path, baseUrl);\n if (query) {\n for (const [k, v] of Object.entries(query)) {\n if (v === undefined) continue;\n if (Array.isArray(v)) {\n // 多值参数(如 parentType=oc&parentType=elementum),对齐后端 List[str]\n for (const item of v) {\n url.searchParams.append(k, String(item));\n }\n } else {\n url.searchParams.set(k, String(v));\n }\n }\n }\n\n // 构造 headers\n const headers: Record = {\n \"Content-Type\": \"application/json\",\n };\n const token = auth.getToken();\n if (token) {\n headers[\"x-embed-token\"] = token;\n }\n\n let response: Response;\n try {\n response = await fetch(url.toString(), { method: \"GET\", headers });\n } catch (err) {\n throw new TopicApiError(-1, \"Network error\", path, err);\n }\n\n if (!response.ok) {\n let message = `HTTP ${response.status}`;\n try {\n const body = (await response.json()) as { message?: string };\n if (body.message) message = body.message;\n } catch {\n // ignore JSON parse errors\n }\n throw new TopicApiError(response.status, message, path);\n }\n\n try {\n return (await response.json()) as T;\n } catch (err) {\n throw new TopicApiError(-1, \"JSON parse error\", path, err);\n }\n}\n\n// ————— SDKTopicImpl —————\n\nexport class SDKTopicImpl implements SDKTopic {\n public constructor(\n private readonly _baseUrl: string,\n private readonly _auth: SDKAuth,\n ) {}\n\n public async getDetail(name: string): Promise {\n return apiFetch(this._baseUrl, `/v1/embed/topic/${encodeURIComponent(name)}`, this._auth);\n }\n\n public async listStories(\n name: string,\n query: {\n pageIndex: number;\n pageSize: number;\n sort: \"hot\" | \"like_count\" | \"highlight_mark_time\";\n startTime?: number;\n endTime?: number;\n authorUuid?: string;\n },\n ): Promise> {\n return apiFetch>(this._baseUrl, `/v1/embed/topic/${encodeURIComponent(name)}/stories`, this._auth, {\n pageIndex: query.pageIndex,\n pageSize: query.pageSize,\n sort: query.sort,\n // 可选参数:undefined 时 apiFetch 会跳过,不写入 query。\n startTime: query.startTime,\n endTime: query.endTime,\n authorUuid: query.authorUuid,\n });\n }\n\n public async listMyStories(\n name: string,\n query: {\n kind: MyStoryKind;\n pageIndex?: number;\n pageSize?: number;\n },\n ): Promise> {\n return apiFetch>(\n this._baseUrl,\n `/v1/embed/topic/${encodeURIComponent(name)}/my-stories`,\n this._auth,\n {\n kind: query.kind,\n pageIndex: query.pageIndex,\n pageSize: query.pageSize,\n },\n );\n }\n\n public async listCharacters(\n name: string,\n query: {\n parentType?: string[];\n pageIndex: number;\n pageSize: number;\n sort?: string;\n },\n ): Promise> {\n return apiFetch>(\n this._baseUrl,\n `/v1/embed/topic/${encodeURIComponent(name)}/characters`,\n this._auth,\n {\n parentType: query.parentType,\n pageIndex: query.pageIndex,\n pageSize: query.pageSize,\n sort: query.sort,\n },\n );\n }\n\n public async listCampaigns(\n name: string,\n query: { pageIndex: number; pageSize: number },\n ): Promise> {\n return apiFetch>(\n this._baseUrl,\n `/v1/embed/topic/${encodeURIComponent(name)}/campaigns`,\n this._auth,\n {\n pageIndex: query.pageIndex,\n pageSize: query.pageSize,\n },\n );\n }\n\n public async listLoreEvents(name: string): Promise {\n return apiFetch(this._baseUrl, `/v1/embed/topic/${encodeURIComponent(name)}/lore-events`, this._auth);\n }\n\n public async listHot(name: string): Promise {\n // 后端响应模型为 { stories: HotStoryCard[] }(无分页 bounded 热门列表),此处解包为数组。\n const resp = await apiFetch<{ stories: HotStoryCard[] }>(\n this._baseUrl,\n `/v1/embed/topic/${encodeURIComponent(name)}/hot-stories`,\n this._auth,\n );\n return resp.stories;\n }\n\n public async getWeeklyHottest(name: string): Promise {\n // 后端响应模型为 WeeklyHottest | null(无近 7 天同款 UV 最高作品时返回 null)。\n return apiFetch(\n this._baseUrl,\n `/v1/embed/topic/${encodeURIComponent(name)}/weekly-hottest`,\n this._auth,\n );\n }\n}\n\n// ————— SDKActivityImpl —————\n\nexport class SDKActivityImpl implements SDKActivity {\n public constructor(\n private readonly _baseUrl: string,\n private readonly _auth: SDKAuth,\n ) {}\n\n public async listTabs(uuid: string): Promise {\n return apiFetch(this._baseUrl, `/v1/embed/activity/${encodeURIComponent(uuid)}/tabs`, this._auth);\n }\n\n public async listSelectedStories(\n uuid: string,\n tabKey: string,\n query: {\n pageIndex: number;\n pageSize: number;\n sort?: string;\n },\n ): Promise {\n return apiFetch(\n this._baseUrl,\n `/v1/embed/activity/${encodeURIComponent(uuid)}/tab/${encodeURIComponent(tabKey)}`,\n this._auth,\n {\n pageIndex: query.pageIndex,\n pageSize: query.pageSize,\n sort: query.sort,\n },\n );\n }\n}\n\n// ————— SDKRankImpl —————\n\nexport class SDKRankImpl implements SDKRank {\n public constructor(\n private readonly _baseUrl: string,\n private readonly _auth: SDKAuth,\n ) {}\n\n public async get(\n entity: RankEntity,\n window: RankWindow,\n at: number | \"latest\",\n ): Promise> {\n // oc / elementum 仅支持 latest(后端非 latest 返回 400),提前抛出更友好的错误\n if ((entity === \"oc\" || entity === \"elementum\") && at !== \"latest\") {\n throw new TopicApiError(\n 400,\n `rank entity '${entity}' only supports at='latest'`,\n `/v1/embed/rank/${entity}/${window}/${String(at)}`,\n );\n }\n const atStr = at === \"latest\" ? \"latest\" : String(at);\n return apiFetch>(\n this._baseUrl,\n `/v1/embed/rank/${encodeURIComponent(entity)}/${encodeURIComponent(window)}/${encodeURIComponent(atStr)}`,\n this._auth,\n );\n }\n}\n\n// ————— PageCursor —————\n\nexport class PageCursor {\n private _pageIndex = 0;\n private _hasNext = true;\n private _total: number | null = null;\n\n public constructor(\n private readonly _fetcher: (pageIndex: number, pageSize: number) => Promise>,\n private readonly _pageSize = 20,\n ) {}\n\n public hasNext(): boolean {\n return this._hasNext;\n }\n\n public async next(): Promise {\n if (!this._hasNext) return [];\n const page = await this._fetcher(this._pageIndex, this._pageSize);\n this._total = page.total;\n this._hasNext = page.hasNext;\n this._pageIndex += 1;\n return page.list;\n }\n\n public reset(): void {\n this._pageIndex = 0;\n this._hasNext = true;\n this._total = null;\n }\n\n public currentPage(): number {\n return this._pageIndex;\n }\n\n public totalItems(): number | null {\n return this._total;\n }\n}\n","// ————— 通用 —————\n\nexport interface Page {\n total: number;\n pageIndex: number;\n pageSize: number;\n list: T[];\n /** facade 重算(修复 api_hashtag.py:693 bug) */\n hasNext: boolean;\n}\n\nexport interface RichText {\n contentType: \"text\" | \"html\" | \"markdown\";\n /** html 类型必须服务端消毒(sanitize)后下发 */\n content: string;\n}\n\n// ————— 话题 —————\n\nexport interface TopicTab {\n key: string;\n label: string;\n visible: boolean;\n}\n\nexport interface TopicDetail {\n hashtagName: string;\n kind: \"hashtag\" | \"activity\";\n activityUuid: string | null;\n title: string | null;\n description: RichText | null;\n bannerPic: string | null;\n smallBannerPic: string | null;\n headerPic: string | null;\n phase: \"draft\" | \"preparing\" | \"running\" | \"ended\" | null;\n startTime: number | null; // UTC ms\n endTime: number | null; // UTC ms\n /** 合并 popularity / hashtag_heat 为单字段单语义 */\n heat: number;\n participantsCount: number;\n subscribeCount: number;\n action: {\n visible: boolean;\n label: string | null;\n url: string | null;\n } | null;\n tabs: TopicTab[];\n ruleStoryUuid: string | null;\n /** 登录态填充;匿名/游客(无 embed token)为 null */\n viewer: {\n subscribed: boolean;\n canEdit: boolean;\n } | null;\n}\n\n// ————— 作品卡片 —————\n\nexport interface StoryCard {\n storyId: string;\n title: string | null;\n coverUrl: string | null;\n shareUrl: string | null;\n aspect: string | null;\n author: {\n uuid: string | null;\n nickName: string | null;\n avatarUrl: string | null;\n };\n metrics: {\n likeCount: number;\n sameStyleCount: number;\n picCount: number;\n };\n flags: {\n hasVideo: boolean;\n hasBgm: boolean;\n isInteractive: boolean;\n isPinned: boolean;\n };\n hashtagNames: string[];\n /** 匿名/游客 null */\n viewer: {\n liked: boolean;\n favored: boolean;\n } | null;\n}\n\n// ————— 话题热门流 / 本周最热 —————\n\n/** 话题算法热门流的作品卡片:现有 StoryCard 全字段 + 热度分(已乘 1000 取整)。 */\nexport interface HotStoryCard extends StoryCard {\n /** 热度分(后端 doc.sort[0]*1000 取整),int。 */\n hotScore: number;\n}\n\n/** 本周最热单品(近 7 天同款 UV 最高的一条),无则 null。全 camelCase。 */\nexport interface WeeklyHottest {\n collectionUuid: string;\n title: string;\n coverUrl: string | null;\n sameStyleUv: number;\n creatorName: string;\n creatorUuid: string;\n creatorAvatar: string | null;\n isInteractive: boolean;\n}\n\n// ————— 活动精选 —————\n\nexport interface HighlightPage extends Page {\n /** topList 仅 pageIndex=0 时非空,其余页为 [] */\n topList: StoryCard[];\n}\n\n// ————— 角色卡片 —————\n\nexport interface CharacterCard {\n uuid: string;\n name: string | null;\n coverUrl: string | null;\n type: \"oc\" | \"elementum\" | string;\n author: {\n uuid: string | null;\n nickName: string | null;\n avatarUrl: string | null;\n };\n}\n\n// ————— 活动卡片 —————\n\nexport interface CampaignCard {\n uuid: string;\n title: string | null;\n coverUrl: string | null;\n pv: number;\n}\n\n// ————— 世界观事件 —————\n\nexport interface LoreEvent {\n uuid: string;\n name: string;\n category: string | null;\n description: string | null;\n sortIndex: number;\n collectionCount: number;\n boundTopic: {\n name: string;\n startTime: number | null; // UTC ms\n endTime: number | null; // UTC ms\n } | null;\n stories: StoryCard[];\n}\n\n// ————— 榜单 —————\n\nexport type RankWindow = \"daily\" | \"weekly\" | \"monthly\";\nexport type RankEntity = \"stories\" | \"creators\" | \"oc\" | \"elementum\";\n\nexport interface Leaderboard {\n window: RankWindow;\n entity: RankEntity;\n startTime: number | null; // UTC ms;后端 Optional,可能下发 null\n endTime: number | null; // UTC ms;后端 Optional,可能下发 null\n lastUpdatedAt: number | null;\n list: RankEntry[];\n}\n\nexport interface RankEntry {\n rank: number;\n /** 经 score_multiplier 缩放后的整数 */\n score: number;\n item: T;\n}\n\nexport interface CreatorCard {\n uuid: string | null;\n nickName: string | null;\n avatarUrl: string | null;\n subscriberCount: number;\n storyCount: number;\n topStories: StoryCard[];\n}\n\n// ————— 客户端上下文 —————\n\nexport type ClientContext = \"app\" | \"web-embedded\" | \"guest\";\n\n/** hello 响应中的 client 字段(宿主回传具体平台) */\nexport type BridgeClient = \"ios\" | \"android\" | \"web\";\n\nexport interface HelloResult {\n /** 宿主平台 */\n client: BridgeClient;\n /** App 版本号,Web 宿主为 null */\n appVersion: string | null;\n /** 宿主支持的 bridge method 列表 */\n features: string[];\n}\n\n// ————— 能力位图 —————\n\nexport enum Capability {\n // 数据能力(所有上下文均支持)\n ReadTopic = \"read.topic\",\n ReadStories = \"read.stories\",\n ReadCharacters = \"read.characters\",\n ReadCampaigns = \"read.campaigns\",\n ReadLoreEvents = \"read.loreEvents\",\n ReadActivity = \"read.activity\",\n ReadRank = \"read.rank\",\n\n // 桥接能力(仅 App/Web-embedded)\n Bridge = \"bridge\",\n NavInternal = \"nav.internal\",\n NavExternal = \"nav.external\",\n NavApplyHost = \"nav.applyHost\",\n Toast = \"ui.toast\",\n Viewport = \"ui.viewport\",\n EventBack = \"event.back\",\n EventTokenChanged = \"event.tokenChanged\",\n EventViewport = \"event.viewport\",\n}\n\n// ————— AllowedRoute 白名单 —————\n\n/** AllowedRoute v1 硬编码白名单 */\nexport type AllowedRoute =\n | \"/tag\"\n | \"/topic\"\n | \"/activity\"\n | \"/ranking\"\n | \"/collection/interaction\"\n | \"/oc\"\n | \"/user\"\n | \"/generate\";\n\n// ————— viewport —————\n\nexport interface ViewportInfo {\n /** 嵌入态永远为 0(防双叠加) */\n safeTop: 0;\n safeBottom: number;\n keyboardInset: number;\n width: number;\n height: number;\n}\n\n// ————— SDK 选项 —————\n\nexport interface TopicSDKOptions {\n /** 后端 API 基址,默认 'https://pre.api.talesofai.cn' */\n apiBaseUrl?: string;\n /** hello 握手超时 ms,默认 1500 */\n helloTimeout?: number;\n /** getEmbedToken bridge 调用超时 ms,默认 3000 */\n tokenTimeout?: number;\n /** embed token 过期前多久触发 re-exchange,ms,默认 5 * 60 * 1000 */\n tokenRefreshEarlyMs?: number;\n /** token 无法恢复时的回调 */\n onAuthLost?: (reason: string) => void;\n}\n\n// ————— 公开 API 接口 —————\n\nexport interface SDKAuth {\n /** 当前 embed token,匿名时为 null */\n getToken(): string | null;\n /** token 过期时间(UTC ms),匿名时为 null */\n getExpiresAt(): number | null;\n /** 是否已登录(有有效 token) */\n isAuthenticated(): boolean;\n}\n\n/** listMyStories 的 kind:favored=当前 user 收藏的本话题作品;created=当前 user 在本话题投稿的作品;liked=当前 user 点赞的本话题作品。 */\nexport type MyStoryKind = \"favored\" | \"created\" | \"liked\";\n\nexport interface SDKTopic {\n /** GET /v1/embed/topic/{name} */\n getDetail(name: string): Promise;\n\n /** GET /v1/embed/topic/{name}/stories */\n listStories(\n name: string,\n query: {\n pageIndex: number;\n pageSize: number;\n sort: \"hot\" | \"like_count\" | \"highlight_mark_time\";\n /** 可选时间窗(UTC ms),透传后端 start_time/end_time */\n startTime?: number;\n /** 可选时间窗(UTC ms),透传后端 start_time/end_time */\n endTime?: number;\n /** 可选作者过滤,透传后端 authorUuid */\n authorUuid?: string;\n },\n ): Promise>;\n\n /**\n * GET /v1/embed/topic/{name}/my-stories\n * 当前 embed user 在本话题下的作品:kind=favored(收藏)/ created(投稿)/ liked(点赞)。\n * 匿名(无 embed token)返回空 Page(list=[]、hasNext=false),不抛错、不 401。\n */\n listMyStories(\n name: string,\n query: {\n kind: MyStoryKind;\n pageIndex?: number;\n pageSize?: number;\n },\n ): Promise>;\n\n /** GET /v1/embed/topic/{name}/characters */\n listCharacters(\n name: string,\n query: {\n /** 后端为 List[str](alias parentType),多值。默认 [\"oc\",\"elementum\"] */\n parentType?: string[];\n pageIndex: number;\n pageSize: number;\n sort?: string;\n },\n ): Promise>;\n\n /** GET /v1/embed/topic/{name}/campaigns */\n listCampaigns(name: string, query: { pageIndex: number; pageSize: number }): Promise>;\n\n /** GET /v1/embed/topic/{name}/lore-events */\n listLoreEvents(name: string): Promise;\n\n /**\n * GET /v1/embed/topic/{name}/hot-stories\n * 话题算法热门流(bounded 热门列表,无分页),每条带 hotScore。\n */\n listHot(name: string): Promise;\n\n /**\n * GET /v1/embed/topic/{name}/weekly-hottest\n * 本周最热单品(近 7 天同款 UV 最高的一条),无则 null。\n */\n getWeeklyHottest(name: string): Promise;\n}\n\nexport interface SDKActivity {\n /** GET /v1/embed/activity/{uuid}/tabs */\n listTabs(uuid: string): Promise;\n\n /**\n * GET /v1/embed/activity/{uuid}/tab/{tabKey}\n * HighlightPage.topList 仅 pageIndex=0 时非空\n */\n listSelectedStories(\n uuid: string,\n tabKey: string,\n query: {\n pageIndex: number;\n pageSize: number;\n sort?: string;\n },\n ): Promise;\n}\n\nexport interface SDKRank {\n /**\n * GET /v1/embed/rank/{entity}/{window}/{at}\n * entity 直接作为路径段传给后端(facade 内部映射到 collection/user/tcp-oc/tcp-elementum,对外保持 stories/creators/oc/elementum)。\n * at: UTC ms 或字符串 'latest'。\n * 注意:oc / elementum 仅支持 at='latest',传时间戳后端返回 400(SDK 侧亦会提前抛错)。\n */\n get(\n entity: RankEntity,\n window: RankWindow,\n at: number | \"latest\",\n ): Promise>;\n}\n\nexport interface SDKNav {\n /**\n * 跳进产品内页面(AllowedRoute v1 白名单)。\n *\n * 参数契约按路由性质分两类(SDK 运行期兜底 + 此处类型收窄,缺参构建期/运行期都会被拦):\n * - **自指路由** `/topic` `/tag` `/activity`:参数=「当前这个」,可省略,SDK 自动从当前页\n * `?hashtag=` / `?activity_uuid=` 填入(创作者可显式覆盖)。`/ranking` `/generate` 无需参数。\n * - **per-item 路由** `/oc` `/user` `/collection/interaction`:参数指向「具体某个」实体,\n * **必须传 `uuid`**(来自被点卡片的数据,SDK 无从代填)。\n *\n * guest 上下文(仅本地 dev 无宿主时)本地 dev 无宿主仅 console 提示,不跳转;生产入口恒为宿主内嵌,唤起 App 由宿主承载。\n */\n internal(\n route: \"/topic\" | \"/tag\" | \"/activity\" | \"/ranking\" | \"/generate\",\n query?: Record,\n ): Promise;\n internal(\n route: \"/oc\" | \"/user\" | \"/collection/interaction\",\n query: { uuid: string | number } & Record,\n ): Promise;\n\n /**\n * 跳外部 URL。\n * embedded: bridge nav.external\n * guest: window.open(url, '_blank')\n */\n external(url: string): Promise;\n\n /**\n * 申请创建/主持一个新话题活动空间:跳转到运营配置的申请表单(飞书多维表单),宿主自动带上\n * 当前登录用户昵称/UID 的 prefill 参数(不含话题名)。**不接受参数**——SDK/页面拿不到、也不经手\n * 这些用户数据,全由宿主本地态直接拼 URL(与 `getEmbedToken` 不下发宿主 token 同一不变量)。\n *\n * 运营未配置申请表单时抛 `BridgeError`(宿主报 not-configured)。\n * guest 上下文(仅本地 dev 无宿主)本地不跳转,仅 console 提示;生产入口恒为宿主内嵌。\n */\n applyHost(): Promise;\n}\n\nexport interface SDKUi {\n /**\n * 宿主 Toast(embedded)。\n * guest / unsupported:抛 UnsupportedError。\n */\n toast(text: string, options?: { duration?: number; level?: \"info\" | \"warn\" }): Promise;\n\n /**\n * 当前 viewport 安全区信息。\n * guest:抛 UnsupportedError。\n */\n viewport(): Promise;\n}\n\nexport interface SDKEvents {\n on(event: \"tokenChanged\", handler: (newToken: string | null) => void): () => void;\n on(event: \"viewport\", handler: (info: ViewportInfo) => void): () => void;\n on(event: \"back\", handler: (ev: { preventDefault(): void }) => void): () => void;\n off(event: string, handler: (...args: unknown[]) => void): void;\n}\n\n// ————— SDK 顶层 —————\n\nexport interface TopicSDK {\n env: {\n context: ClientContext;\n embedded: boolean;\n client: BridgeClient | \"unknown\";\n appVersion: string | null;\n features: string[];\n };\n auth: SDKAuth;\n topic: SDKTopic;\n activity: SDKActivity;\n rank: SDKRank;\n nav: SDKNav;\n ui: SDKUi;\n events: SDKEvents;\n can(cap: Capability): boolean;\n destroy(): void;\n}\n","import type { BridgeClient } from \"./bridge.js\";\nimport { UnsupportedError } from \"./errors.js\";\nimport type { ClientContext, HelloResult } from \"./types.js\";\nimport { Capability } from \"./types.js\";\n\nexport interface EnvResult {\n context: ClientContext;\n embedded: boolean;\n client: \"ios\" | \"android\" | \"web\" | \"unknown\";\n appVersion: string | null;\n features: string[];\n hello: HelloResult | null;\n /** 宿主注入的 ?activity_uuid=(无则 null)。决定 ReadActivity 能力是否授予(纯 hashtag 话题无活动数据)。 */\n activityUuid: string | null;\n}\n\nexport async function detectEnv(bridge: BridgeClient, sdkVersion: string, helloTimeout: number): Promise {\n const ua = navigator.userAgent;\n // 宿主把绑定的 activity_uuid 作为 query 注入 iframe(无则纯 hashtag 话题)。\n const activityUuid =\n typeof window !== \"undefined\" ? new URLSearchParams(window.location.search).get(\"activity_uuid\") || null : null;\n\n // UA 预判:小程序直接出局\n if (/miniProgram/i.test(ua)) {\n throw new UnsupportedError(\"weapp-not-supported\", \"guest\" as ClientContext);\n }\n\n // hello 握手(超时→ null = guest)\n const hello = await bridge.hello(sdkVersion, helloTimeout);\n\n if (!hello) {\n // 握手超时 → guest\n return {\n context: \"guest\",\n embedded: false,\n client: \"unknown\",\n appVersion: null,\n features: [],\n hello: null,\n activityUuid,\n };\n }\n\n // 宿主回包确认客户端类型(最终以握手结果为准,UA 仅用于 miniProgram 预判出局)\n const context: ClientContext = hello.client === \"ios\" || hello.client === \"android\" ? \"app\" : \"web-embedded\";\n\n return {\n context,\n embedded: true,\n client: hello.client,\n appVersion: hello.appVersion,\n features: hello.features,\n hello,\n activityUuid,\n };\n}\n\n/**\n * 根据 EnvResult 计算支持的 Capability 集合。\n */\nexport function buildCapabilities(env: EnvResult): Set {\n const caps = new Set();\n\n // 数据能力:所有上下文均支持\n caps.add(Capability.ReadTopic);\n caps.add(Capability.ReadStories);\n caps.add(Capability.ReadCharacters);\n caps.add(Capability.ReadCampaigns);\n caps.add(Capability.ReadLoreEvents);\n caps.add(Capability.ReadRank);\n // ReadActivity 仅当话题绑定了 activity(宿主注入 ?activity_uuid=)才授予;纯 hashtag 话题无活动数据,\n // 不报 ReadActivity,避免 can(ReadActivity) 恒 true 误导创作者渲染空活动区块。\n if (env.activityUuid) {\n caps.add(Capability.ReadActivity);\n }\n\n if (env.context !== \"guest\") {\n // 桥接能力(仅 App/Web-embedded)\n caps.add(Capability.Bridge);\n caps.add(Capability.NavInternal);\n caps.add(Capability.NavExternal);\n caps.add(Capability.NavApplyHost);\n caps.add(Capability.Toast);\n caps.add(Capability.Viewport);\n caps.add(Capability.EventBack);\n caps.add(Capability.EventTokenChanged);\n caps.add(Capability.EventViewport);\n }\n // guest(仅本地 dev 无宿主可达):仅只读数据能力;唤起 App 统一靠宿主 nav.internal,不再暴露独立 OpenApp 能力。\n\n return caps;\n}\n","import type { BridgeClient } from \"./bridge.js\";\nimport type { SDKEvents, ViewportInfo } from \"./types.js\";\n\ntype TokenChangedHandler = (newToken: string | null) => void;\ntype ViewportHandler = (info: ViewportInfo) => void;\ntype BackHandler = (ev: { preventDefault(): void }) => void;\ntype AnyHandler = TokenChangedHandler | ViewportHandler | BackHandler;\n\ninterface HandlerEntry {\n event: string;\n handler: AnyHandler;\n bridgeHandler: (data: unknown) => void;\n}\n\nexport class SDKEventsImpl implements SDKEvents {\n private readonly _entries: HandlerEntry[] = [];\n // tokenChanged 不直接订阅 bridge:宿主事件里 token 恒为 null,\n // 真正的新 token 由 SDK 内部 re-exchange 完成后经 notifyTokenChanged 下发。\n private readonly _tokenChangedHandlers = new Set();\n\n public constructor(private readonly _bridge: BridgeClient | null) {}\n\n public on(event: \"tokenChanged\", handler: TokenChangedHandler): () => void;\n public on(event: \"viewport\", handler: ViewportHandler): () => void;\n public on(event: \"back\", handler: BackHandler): () => void;\n public on(event: string, handler: AnyHandler): () => void {\n // tokenChanged:与 bridge 事件解耦,由 SDK 内部 re-exchange 后回调新 token\n if (event === \"tokenChanged\") {\n const h = handler as TokenChangedHandler;\n this._tokenChangedHandlers.add(h);\n return () => {\n this._tokenChangedHandlers.delete(h);\n };\n }\n\n if (!this._bridge) {\n // guest 模式:无宿主事件,返回空 unsubscribe\n return () => {};\n }\n\n let bridgeHandler: (data: unknown) => void;\n\n switch (event) {\n case \"viewport\": {\n const h = handler as ViewportHandler;\n bridgeHandler = (data: unknown) => {\n const raw = data as {\n safeTop?: number;\n safeBottom?: number;\n keyboardInset?: number;\n width?: number;\n height?: number;\n };\n h({\n safeTop: 0, // 固定 0,防双叠加\n safeBottom: raw.safeBottom ?? 0,\n keyboardInset: raw.keyboardInset ?? 0,\n width: raw.width ?? window.innerWidth,\n height: raw.height ?? window.innerHeight,\n });\n };\n break;\n }\n case \"back\": {\n const h = handler as BackHandler;\n bridgeHandler = (_data: unknown) => {\n let _defaultPrevented = false;\n h({\n preventDefault() {\n _defaultPrevented = true;\n },\n });\n // SDK 不需要把 defaultPrevented 回传给宿主;\n // 宿主侧设计为\"等待一帧确认\"(H8),此处不需要额外处理\n };\n break;\n }\n default:\n return () => {};\n }\n\n this._bridge.onEvent(event, bridgeHandler);\n const entry: HandlerEntry = { event, handler, bridgeHandler };\n this._entries.push(entry);\n\n return () => {\n this.off(event, handler as (...args: unknown[]) => void);\n };\n }\n\n /**\n * 由 SDK 内部在 re-exchange 完成后调用,向消费方下发最新 token。\n * 宿主 tokenChanged 事件本身不携带 token(恒 null),故新值取自 auth 当前 token。\n */\n public notifyTokenChanged(token: string | null): void {\n this._tokenChangedHandlers.forEach((h) => h(token));\n }\n\n public off(event: string, handler: (...args: unknown[]) => void): void {\n if (event === \"tokenChanged\") {\n this._tokenChangedHandlers.delete(handler as unknown as TokenChangedHandler);\n return;\n }\n const idx = this._entries.findIndex((e) => e.event === event && e.handler === handler);\n if (idx === -1) return;\n const entry = this._entries[idx]!;\n this._bridge?.offEvent(event, entry.bridgeHandler);\n this._entries.splice(idx, 1);\n }\n\n public destroy(): void {\n for (const entry of this._entries) {\n this._bridge?.offEvent(entry.event, entry.bridgeHandler);\n }\n this._entries.length = 0;\n this._tokenChangedHandlers.clear();\n }\n}\n","import type { BridgeClient } from \"./bridge.js\";\nimport { UnsupportedError } from \"./errors.js\";\nimport type { AllowedRoute, ClientContext, SDKNav } from \"./types.js\";\n\n/**\n * 自指路由:参数=「当前这个」,缺省时 SDK 从当前页 URL 自动填(创作者可显式覆盖)。\n * 宿主把 hashtag / activity_uuid 作为 query 注入 iframe src,故内嵌页 `location.search` 必有这些值。\n */\nconst SELF_PARAM_FROM_URL: Partial> = {\n \"/topic\": { param: \"hashtag\", urlKey: \"hashtag\" },\n \"/tag\": { param: \"hashtag\", urlKey: \"hashtag\" },\n \"/activity\": { param: \"uuid\", urlKey: \"activity_uuid\" },\n};\n\n/** per-item 路由:参数指向「具体某个」实体(来自被点卡片),SDK 无从代填 → 缺则抛错打回。 */\nconst REQUIRED_PARAMS: Partial> = {\n \"/oc\": [\"uuid\"],\n \"/user\": [\"uuid\"],\n \"/collection/interaction\": [\"uuid\"],\n};\n\nconst isBlank = (v: unknown): boolean => v === undefined || v === null || v === \"\";\n\nexport class SDKNavImpl implements SDKNav {\n public constructor(\n private readonly _bridge: BridgeClient | null,\n private readonly _context: ClientContext,\n ) {}\n\n /**\n * 解析最终 query:自指路由缺参从 URL 自动填;per-item 路由缺必需参数则抛错(开发期就被打回,而非线上白屏)。\n */\n private _resolveQuery(route: AllowedRoute, query?: Record): Record {\n const q: Record = { ...(query ?? {}) };\n\n // 1) 自指路由:缺参从当前页 URL 自动填\n const selfRef = SELF_PARAM_FROM_URL[route];\n if (selfRef && isBlank(q[selfRef.param]) && typeof window !== \"undefined\") {\n const fromUrl = new URLSearchParams(window.location.search).get(selfRef.urlKey);\n if (fromUrl) q[selfRef.param] = fromUrl;\n }\n\n // 2) 必需参数校验(自指填完后再查)\n const required = REQUIRED_PARAMS[route] ?? (selfRef ? [selfRef.param] : []);\n for (const p of required) {\n if (isBlank(q[p])) {\n const hint = selfRef\n ? `该自指路由通常由 SDK 从当前页 ?${selfRef.urlKey}= 自动填,但当前 URL 没有该值`\n : `该路由指向具体实体,请从被点卡片数据传入 ${p}(如 ${p}: story.uuid)`;\n throw new Error(`[topic-sdk] nav.internal('${route}') 缺少必需参数 '${p}'。${hint}。`);\n }\n }\n return q;\n }\n\n public async internal(route: AllowedRoute, query?: Record): Promise {\n const effectiveQuery = this._resolveQuery(route, query);\n if (this._context === \"guest\") {\n // guest(仅本地 dev 无宿主):_resolveQuery 已先跑(保留缺参 throw);无宿主不跳转,生产由宿主唤起 / 站内跳。\n console.info(\"[topic-sdk] nav.internal(route) 本地 dev 无宿主不跳转;生产由宿主唤起/站内跳\");\n return;\n }\n if (!this._bridge) {\n throw new UnsupportedError(\"nav.internal\", this._context);\n }\n await this._bridge.send(\"nav.internal\", { route, query: effectiveQuery });\n }\n\n public async external(url: string): Promise {\n if (this._context === \"guest\") {\n window.open(url, \"_blank\");\n return;\n }\n if (!this._bridge) {\n throw new UnsupportedError(\"nav.external\", this._context);\n }\n await this._bridge.send(\"nav.external\", { url });\n }\n\n public async applyHost(): Promise {\n if (this._context === \"guest\") {\n // guest(仅本地 dev 无宿主):拼 URL 需要的用户态/运营配置只有宿主本地有,dev 无宿主不跳转;生产由宿主承载。\n console.info(\"[topic-sdk] nav.applyHost() 本地 dev 无宿主不跳转;生产由宿主用本地用户态拼 URL 后跳转\");\n return;\n }\n if (!this._bridge) {\n throw new UnsupportedError(\"nav.applyHost\", this._context);\n }\n await this._bridge.send(\"nav.applyHost\");\n }\n}\n","import type { BridgeClient } from \"./bridge.js\";\nimport { UnsupportedError } from \"./errors.js\";\nimport type { ClientContext, SDKUi, ViewportInfo } from \"./types.js\";\n\nexport class SDKUiImpl implements SDKUi {\n public constructor(\n private readonly _bridge: BridgeClient | null,\n private readonly _context: ClientContext,\n ) {}\n\n public async toast(text: string, options?: { duration?: number; level?: \"info\" | \"warn\" }): Promise {\n if (this._context === \"guest\" || !this._bridge) {\n throw new UnsupportedError(\"ui.toast\", this._context);\n }\n await this._bridge.send(\"ui.toast\", {\n text,\n duration: options?.duration,\n level: options?.level ?? \"info\",\n });\n }\n\n public async viewport(): Promise {\n if (this._context === \"guest\" || !this._bridge) {\n throw new UnsupportedError(\"ui.viewport\", this._context);\n }\n const raw = await this._bridge.send<{\n safeTop?: number;\n safeBottom?: number;\n keyboardInset?: number;\n width?: number;\n height?: number;\n }>(\"ui.viewport\");\n\n // safeTop 固定返回 0(防双叠加,宿主已 safeTop:true/page.tsx:39)\n // 其余字段对宿主漏传做兜底,与 events.ts viewport 分支保持一致\n return {\n safeTop: 0,\n safeBottom: raw.safeBottom ?? 0,\n keyboardInset: raw.keyboardInset ?? 0,\n width: raw.width ?? window.innerWidth,\n height: raw.height ?? window.innerHeight,\n };\n }\n}\n","/**\n * OSS 图片处理参数化:拼接 `x-oss-process`(resize/format/quality)。\n * 与 talesofai 其余前端(weapp/event/bff 的 getImageLink)同一套 OSS 图片处理约定,\n * 移植到这里是为了让内嵌页也别再把原图尺寸直出——不同卡片位置渲染宽度不同,\n * 高分屏(devicePixelRatio > 1)不按比例多请求像素会糊,全量按比例请求又会浪费流量。\n */\n\nconst DEFAULT_QUALITY = 80;\n/** 设备像素比 clamp 上限:4x/5x 屏再按比例放大意义不大,只会白白增加流量。 */\nconst MAX_DPR = 3;\n\nexport interface OssImageOptions {\n /** 目标 CSS 展示宽度(px)。省略则不做 resize,只处理 format/quality。 */\n width?: number;\n /** 设备像素比倍率;省略则取 `window.devicePixelRatio`(非浏览器环境兜底 1),并 clamp 到 [1, MAX_DPR]。 */\n dpr?: number;\n /** 输出质量 1-100,默认 80(与其余前端一致)。 */\n quality?: number;\n /** 是否转 webp,默认 true。 */\n webp?: boolean;\n}\n\nfunction resolveDpr(explicit?: number): number {\n if (typeof explicit === \"number\" && explicit > 0) return Math.min(explicit, MAX_DPR);\n const detected = typeof window !== \"undefined\" && typeof window.devicePixelRatio === \"number\" ? window.devicePixelRatio : 1;\n return Math.min(Math.max(detected, 1), MAX_DPR);\n}\n\nfunction buildOssProcess(pixelWidth: number | undefined, quality: number, webp: boolean): string {\n const styles: string[] = [\"image\", \"auto-orient,1\"];\n if (pixelWidth && pixelWidth > 0) styles.push(`resize,m_lfit,w_${Math.round(pixelWidth)}`);\n if (webp) styles.push(\"format,webp\");\n styles.push(`quality,q_${quality}`);\n return styles.join(\"/\");\n}\n\n/**\n * 按目标展示宽度 + 设备像素比拼接 OSS 图片处理参数。\n * 卡片封面/头像等按实际渲染宽度传 `width`(如缩略图 200、大图 750),SDK 按屏幕 dpr 换算成实取像素宽。\n * 非 http(s)(如 `data:` 内联图)解析失败时原样返回,不强行拼参数——`data:` URL 拼 `?x-oss-process=`\n * 会把 base64 payload 直接拼坏。已经带 `x-oss-process` 的 URL(后端预处理过 / 重复调用)也原样返回,\n * 不再叠加第二个同名 query key(OSS 对重复 key 的解析行为未定义)。\n */\nexport function ossImage(source: string | null | undefined, options?: OssImageOptions): string | null {\n if (!source) return null;\n let url: URL;\n try {\n url = new URL(source);\n } catch {\n return source;\n }\n if (url.protocol !== \"http:\" && url.protocol !== \"https:\") return source;\n if (url.searchParams.has(\"x-oss-process\")) return source;\n const { width, quality = DEFAULT_QUALITY, webp = true, dpr } = options ?? {};\n const pixelWidth = width && width > 0 ? width * resolveDpr(dpr) : undefined;\n url.searchParams.set(\"x-oss-process\", buildOssProcess(pixelWidth, quality, webp));\n return url.toString();\n}\n\n/**\n * 生成 1x/2x/3x 三档 `srcset`(配合 `` 用),让浏览器按实际设备像素比自己选图。\n * `width` 是 1x(CSS px)基准宽度;2x/3x 档在此基础上按比例放大取图。\n */\nexport function ossImageSrcSet(\n source: string | null | undefined,\n width: number,\n options?: Omit,\n): string | null {\n if (!source) return null;\n const scales = [1, 2, 3];\n return scales.map((scale) => `${ossImage(source, { ...options, width, dpr: scale })} ${scale}x`).join(\", \");\n}\n"],"mappings":"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","names":["delay","window","Capability"]} \ No newline at end of file diff --git a/dist/index.d.cts b/dist/index.d.cts index 2754eb7..5adc8bc 100644 --- a/dist/index.d.cts +++ b/dist/index.d.cts @@ -437,6 +437,36 @@ declare class UnsupportedError extends Error { constructor(capability: Capability | string, context: ClientContext); } +/** + * OSS 图片处理参数化:拼接 `x-oss-process`(resize/format/quality)。 + * 与 talesofai 其余前端(weapp/event/bff 的 getImageLink)同一套 OSS 图片处理约定, + * 移植到这里是为了让内嵌页也别再把原图尺寸直出——不同卡片位置渲染宽度不同, + * 高分屏(devicePixelRatio > 1)不按比例多请求像素会糊,全量按比例请求又会浪费流量。 + */ +interface OssImageOptions { + /** 目标 CSS 展示宽度(px)。省略则不做 resize,只处理 format/quality。 */ + width?: number; + /** 设备像素比倍率;省略则取 `window.devicePixelRatio`(非浏览器环境兜底 1),并 clamp 到 [1, MAX_DPR]。 */ + dpr?: number; + /** 输出质量 1-100,默认 80(与其余前端一致)。 */ + quality?: number; + /** 是否转 webp,默认 true。 */ + webp?: boolean; +} +/** + * 按目标展示宽度 + 设备像素比拼接 OSS 图片处理参数。 + * 卡片封面/头像等按实际渲染宽度传 `width`(如缩略图 200、大图 750),SDK 按屏幕 dpr 换算成实取像素宽。 + * 非 http(s)(如 `data:` 内联图)解析失败时原样返回,不强行拼参数——`data:` URL 拼 `?x-oss-process=` + * 会把 base64 payload 直接拼坏。已经带 `x-oss-process` 的 URL(后端预处理过 / 重复调用)也原样返回, + * 不再叠加第二个同名 query key(OSS 对重复 key 的解析行为未定义)。 + */ +declare function ossImage(source: string | null | undefined, options?: OssImageOptions): string | null; +/** + * 生成 1x/2x/3x 三档 `srcset`(配合 `` 用),让浏览器按实际设备像素比自己选图。 + * `width` 是 1x(CSS px)基准宽度;2x/3x 档在此基础上按比例放大取图。 + */ +declare function ossImageSrcSet(source: string | null | undefined, width: number, options?: Omit): string | null; + /** * 初始化并返回 TopicSDK 实例。 * @@ -449,4 +479,4 @@ declare class UnsupportedError extends Error { */ declare function createTopicSDK(options?: TopicSDKOptions): Promise; -export { type AllowedRoute, BridgeClient, type BridgeClient$1 as BridgeClientType, BridgeError, type CampaignCard, Capability, type CharacterCard, type ClientContext, type CreatorCard, type HelloResult, type HighlightPage, type Leaderboard, type LoreEvent, type MyStoryKind, type Page, PageCursor, type RankEntity, type RankEntry, type RankWindow, type RichText, type SDKActivity, type SDKAuth, type SDKEvents, type SDKNav, type SDKRank, type SDKTopic, type SDKUi, type StoryCard, TopicApiError, type TopicDetail, type TopicSDK, type TopicSDKOptions, type TopicTab, UnsupportedError, type ViewportInfo, createTopicSDK }; +export { type AllowedRoute, BridgeClient, type BridgeClient$1 as BridgeClientType, BridgeError, type CampaignCard, Capability, type CharacterCard, type ClientContext, type CreatorCard, type HelloResult, type HighlightPage, type Leaderboard, type LoreEvent, type MyStoryKind, type OssImageOptions, type Page, PageCursor, type RankEntity, type RankEntry, type RankWindow, type RichText, type SDKActivity, type SDKAuth, type SDKEvents, type SDKNav, type SDKRank, type SDKTopic, type SDKUi, type StoryCard, TopicApiError, type TopicDetail, type TopicSDK, type TopicSDKOptions, type TopicTab, UnsupportedError, type ViewportInfo, createTopicSDK, ossImage, ossImageSrcSet }; diff --git a/dist/index.d.ts b/dist/index.d.ts index 2754eb7..5adc8bc 100644 --- a/dist/index.d.ts +++ b/dist/index.d.ts @@ -437,6 +437,36 @@ declare class UnsupportedError extends Error { constructor(capability: Capability | string, context: ClientContext); } +/** + * OSS 图片处理参数化:拼接 `x-oss-process`(resize/format/quality)。 + * 与 talesofai 其余前端(weapp/event/bff 的 getImageLink)同一套 OSS 图片处理约定, + * 移植到这里是为了让内嵌页也别再把原图尺寸直出——不同卡片位置渲染宽度不同, + * 高分屏(devicePixelRatio > 1)不按比例多请求像素会糊,全量按比例请求又会浪费流量。 + */ +interface OssImageOptions { + /** 目标 CSS 展示宽度(px)。省略则不做 resize,只处理 format/quality。 */ + width?: number; + /** 设备像素比倍率;省略则取 `window.devicePixelRatio`(非浏览器环境兜底 1),并 clamp 到 [1, MAX_DPR]。 */ + dpr?: number; + /** 输出质量 1-100,默认 80(与其余前端一致)。 */ + quality?: number; + /** 是否转 webp,默认 true。 */ + webp?: boolean; +} +/** + * 按目标展示宽度 + 设备像素比拼接 OSS 图片处理参数。 + * 卡片封面/头像等按实际渲染宽度传 `width`(如缩略图 200、大图 750),SDK 按屏幕 dpr 换算成实取像素宽。 + * 非 http(s)(如 `data:` 内联图)解析失败时原样返回,不强行拼参数——`data:` URL 拼 `?x-oss-process=` + * 会把 base64 payload 直接拼坏。已经带 `x-oss-process` 的 URL(后端预处理过 / 重复调用)也原样返回, + * 不再叠加第二个同名 query key(OSS 对重复 key 的解析行为未定义)。 + */ +declare function ossImage(source: string | null | undefined, options?: OssImageOptions): string | null; +/** + * 生成 1x/2x/3x 三档 `srcset`(配合 `` 用),让浏览器按实际设备像素比自己选图。 + * `width` 是 1x(CSS px)基准宽度;2x/3x 档在此基础上按比例放大取图。 + */ +declare function ossImageSrcSet(source: string | null | undefined, width: number, options?: Omit): string | null; + /** * 初始化并返回 TopicSDK 实例。 * @@ -449,4 +479,4 @@ declare class UnsupportedError extends Error { */ declare function createTopicSDK(options?: TopicSDKOptions): Promise; -export { type AllowedRoute, BridgeClient, type BridgeClient$1 as BridgeClientType, BridgeError, type CampaignCard, Capability, type CharacterCard, type ClientContext, type CreatorCard, type HelloResult, type HighlightPage, type Leaderboard, type LoreEvent, type MyStoryKind, type Page, PageCursor, type RankEntity, type RankEntry, type RankWindow, type RichText, type SDKActivity, type SDKAuth, type SDKEvents, type SDKNav, type SDKRank, type SDKTopic, type SDKUi, type StoryCard, TopicApiError, type TopicDetail, type TopicSDK, type TopicSDKOptions, type TopicTab, UnsupportedError, type ViewportInfo, createTopicSDK }; +export { type AllowedRoute, BridgeClient, type BridgeClient$1 as BridgeClientType, BridgeError, type CampaignCard, Capability, type CharacterCard, type ClientContext, type CreatorCard, type HelloResult, type HighlightPage, type Leaderboard, type LoreEvent, type MyStoryKind, type OssImageOptions, type Page, PageCursor, type RankEntity, type RankEntry, type RankWindow, type RichText, type SDKActivity, type SDKAuth, type SDKEvents, type SDKNav, type SDKRank, type SDKTopic, type SDKUi, type StoryCard, TopicApiError, type TopicDetail, type TopicSDK, type TopicSDKOptions, type TopicTab, UnsupportedError, type ViewportInfo, createTopicSDK, ossImage, ossImageSrcSet }; diff --git a/dist/index.js b/dist/index.js index b6391ad..d52ae2c 100644 --- a/dist/index.js +++ b/dist/index.js @@ -711,6 +711,49 @@ var SDKUiImpl = class { } }; +// src/image.ts +var DEFAULT_QUALITY = 80; +var MAX_DPR = 3; +function resolveDpr(explicit) { + if (typeof explicit === "number" && explicit > 0) + return Math.min(explicit, MAX_DPR); + const detected = typeof window !== "undefined" && typeof window.devicePixelRatio === "number" ? window.devicePixelRatio : 1; + return Math.min(Math.max(detected, 1), MAX_DPR); +} +function buildOssProcess(pixelWidth, quality, webp) { + const styles = ["image", "auto-orient,1"]; + if (pixelWidth && pixelWidth > 0) + styles.push(`resize,m_lfit,w_${Math.round(pixelWidth)}`); + if (webp) + styles.push("format,webp"); + styles.push(`quality,q_${quality}`); + return styles.join("/"); +} +function ossImage(source, options) { + if (!source) + return null; + let url; + try { + url = new URL(source); + } catch { + return source; + } + if (url.protocol !== "http:" && url.protocol !== "https:") + return source; + if (url.searchParams.has("x-oss-process")) + return source; + const { width, quality = DEFAULT_QUALITY, webp = true, dpr } = options ?? {}; + const pixelWidth = width && width > 0 ? width * resolveDpr(dpr) : void 0; + url.searchParams.set("x-oss-process", buildOssProcess(pixelWidth, quality, webp)); + return url.toString(); +} +function ossImageSrcSet(source, width, options) { + if (!source) + return null; + const scales = [1, 2, 3]; + return scales.map((scale) => `${ossImage(source, { ...options, width, dpr: scale })} ${scale}x`).join(", "); +} + // src/index.ts var SDK_VERSION = "0.1.0"; function installLinkInterceptor(nav) { @@ -847,6 +890,8 @@ export { PageCursor, TopicApiError, UnsupportedError, - createTopicSDK + createTopicSDK, + ossImage, + ossImageSrcSet }; //# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/dist/index.js.map b/dist/index.js.map index e10242f..54a41ba 100644 --- a/dist/index.js.map +++ b/dist/index.js.map @@ -1 +1 @@ -{"version":3,"sources":["../src/auth.ts","../src/errors.ts","../src/bridge.ts","../src/data.ts","../src/types.ts","../src/env.ts","../src/events.ts","../src/nav.ts","../src/ui.ts","../src/index.ts"],"sourcesContent":["import type { BridgeClient } from \"./bridge.js\";\nimport type { SDKAuth } from \"./types.js\";\n\ninterface EmbedTokenResult {\n // 宿主回包字段名为 embedToken(对齐后端 EmbedTokenResponse.embedToken)\n embedToken: string;\n expiresAt: number | null; // UTC ms;宿主可能回 null\n}\n\nexport class SDKAuthImpl implements SDKAuth {\n private _token: string | null = null;\n private _expiresAt: number | null = null;\n private _refreshTimer: ReturnType | null = null;\n\n public constructor(\n private readonly _bridge: BridgeClient | null,\n private readonly _tokenTimeout: number,\n private readonly _tokenRefreshEarlyMs: number,\n private readonly _onAuthLost: ((reason: string) => void) | undefined,\n ) {}\n\n // ————— SDKAuth 公开 API —————\n\n public getToken(): string | null {\n return this._token;\n }\n\n public getExpiresAt(): number | null {\n return this._expiresAt;\n }\n\n public isAuthenticated(): boolean {\n return !!this._token && !this._isExpired();\n }\n\n // ————— 内部 API —————\n\n /**\n * 初始化鉴权:向 bridge 请求 embed token。\n * guest 模式(无 bridge):直接以匿名模式初始化,不抛错。\n */\n public async init(): Promise {\n if (!this._bridge) {\n // guest 模式:匿名\n return;\n }\n await this._fetchToken();\n }\n\n /**\n * 宿主推送 tokenChanged 事件时调用(清除旧 token 并重新请求)。\n */\n public async handleTokenChanged(): Promise {\n this._clearToken();\n if (!this._bridge) return;\n await this._fetchToken();\n }\n\n public destroy(): void {\n if (this._refreshTimer !== null) {\n clearTimeout(this._refreshTimer);\n this._refreshTimer = null;\n }\n }\n\n // ————— 私有方法 —————\n\n private async _fetchToken(retryCount = 0): Promise {\n if (!this._bridge) return;\n\n try {\n const result = await this._bridge.send(\"getEmbedToken\", undefined, this._tokenTimeout);\n if (!result || !result.embedToken) {\n throw new Error(\"getEmbedToken returned no embedToken\");\n }\n this._token = result.embedToken;\n this._expiresAt = result.expiresAt ?? null;\n this._scheduleRefresh();\n } catch (err) {\n if (retryCount < 1) {\n // exponential backoff:第一次重试等 500ms\n await delay(500 * Math.pow(2, retryCount));\n return this._fetchToken(retryCount + 1);\n }\n // 达到最大重试次数\n this._onAuthLost?.(`Failed to get embed token: ${String(err)}`);\n }\n }\n\n private _scheduleRefresh(): void {\n if (this._refreshTimer !== null) {\n clearTimeout(this._refreshTimer);\n this._refreshTimer = null;\n }\n if (this._expiresAt === null) return;\n\n const now = Date.now();\n const delay = this._expiresAt - now - this._tokenRefreshEarlyMs;\n if (delay <= 0) {\n // 已过期或即将过期,立即重取\n this._fetchToken().catch(() => {});\n return;\n }\n\n this._refreshTimer = setTimeout(() => {\n this._refreshTimer = null;\n this._fetchToken().catch(() => {});\n }, delay);\n }\n\n private _clearToken(): void {\n this._token = null;\n this._expiresAt = null;\n if (this._refreshTimer !== null) {\n clearTimeout(this._refreshTimer);\n this._refreshTimer = null;\n }\n }\n\n private _isExpired(): boolean {\n if (this._expiresAt === null) return false;\n return Date.now() > this._expiresAt;\n }\n}\n\nfunction delay(ms: number): Promise {\n return new Promise((resolve) => {\n setTimeout(resolve, ms);\n });\n}\n","import type { Capability, ClientContext } from \"./types.js\";\n\n/**\n * HTTP API 错误\n * statusCode -1 = 网络错误/解析失败\n */\nexport class TopicApiError extends Error {\n public readonly cause?: unknown;\n\n public constructor(\n public readonly statusCode: number,\n message: string,\n public readonly endpoint: string,\n cause?: unknown,\n ) {\n super(message);\n this.name = \"TopicApiError\";\n if (cause !== undefined) {\n this.cause = cause;\n }\n }\n}\n\n/**\n * frame-bridge 通信错误\n */\nexport class BridgeError extends Error {\n public readonly cause?: unknown;\n\n public constructor(\n public readonly code: \"timeout\" | \"rejected\" | \"origin-mismatch\" | \"method-not-allowed\" | \"parse-error\",\n public readonly method: string,\n public readonly requestId: string,\n cause?: unknown,\n ) {\n super(`Bridge error [${code}] on method ${method}`);\n this.name = \"BridgeError\";\n if (cause !== undefined) {\n this.cause = cause;\n }\n }\n}\n\n/**\n * 能力不支持错误(不静默 no-op,统一抛出)\n */\nexport class UnsupportedError extends Error {\n public constructor(\n public readonly capability: Capability | string,\n public readonly context: ClientContext,\n ) {\n super(`Capability '${capability}' not supported in context '${context}'`);\n this.name = \"UnsupportedError\";\n }\n}\n","import { BridgeError } from \"./errors.js\";\nimport type { HelloResult } from \"./types.js\";\n\n// ————— wire 协议类型 —————\n\ninterface BridgeRequest {\n v: 2;\n id: string;\n method: string;\n params?: unknown;\n}\n\ninterface BridgeResponse {\n v: 2;\n id: string;\n ok: true;\n result: unknown;\n}\n\ninterface BridgeErrorResponse {\n v: 2;\n id: string;\n ok: false;\n error: { code: string; message: string };\n}\n\ninterface BridgeEventMessage {\n v: 2;\n event: \"tokenChanged\" | \"viewport\" | \"back\";\n data: unknown;\n}\n\ntype IncomingMessage = BridgeResponse | BridgeErrorResponse | BridgeEventMessage;\n\n// ————— ID 生成 —————\n\nlet _counter = 0;\nfunction generateId(): string {\n if (typeof crypto !== \"undefined\" && typeof crypto.randomUUID === \"function\") {\n return crypto.randomUUID();\n }\n // counter fallback(极少数旧环境)\n _counter += 1;\n return `sdk-req-${Date.now()}-${_counter}`;\n}\n\n// ————— pending request 类型 —————\n\ninterface PendingRequest {\n resolve: (value: unknown) => void;\n reject: (reason: unknown) => void;\n timer: ReturnType;\n method: string;\n}\n\ntype EventHandler = (data: unknown) => void;\n\n// ————— BridgeClient —————\n\n/**\n * frame-bridge v2 客户端。\n * 负责与宿主(window.parent)通过 postMessage 通信。\n */\nexport class BridgeClient {\n private readonly _pending = new Map();\n private readonly _eventHandlers = new Map>();\n private readonly _defaultTimeout: number;\n private _destroyed = false;\n\n public constructor(defaultTimeout = 3000) {\n this._defaultTimeout = defaultTimeout;\n window.addEventListener(\"message\", this._onMessage);\n }\n\n /**\n * 向宿主发送请求,等待回包。\n */\n public send(method: string, params?: unknown, timeout?: number): Promise {\n return new Promise((resolve, reject) => {\n const id = generateId();\n const effectiveTimeout = timeout ?? this._defaultTimeout;\n\n const timer = setTimeout(() => {\n this._pending.delete(id);\n reject(new BridgeError(\"timeout\", method, id));\n }, effectiveTimeout);\n\n this._pending.set(id, {\n resolve: resolve as (v: unknown) => void,\n reject,\n timer,\n method,\n });\n\n const request: BridgeRequest = { v: 2, id, method, params };\n window.parent.postMessage(request, \"*\");\n });\n }\n\n /**\n * 订阅宿主主动推送的事件。\n */\n public onEvent(event: string, handler: EventHandler): void {\n if (!this._eventHandlers.has(event)) {\n this._eventHandlers.set(event, new Set());\n }\n this._eventHandlers.get(event)!.add(handler);\n }\n\n /**\n * 取消订阅。\n */\n public offEvent(event: string, handler: EventHandler): void {\n this._eventHandlers.get(event)?.delete(handler);\n }\n\n /**\n * hello 握手。\n * 超时时返回 null(表示 guest 模式)。\n */\n public async hello(sdkVersion: string, timeout?: number): Promise {\n try {\n const result = await this.send(\"hello\", { sdkVersion }, timeout);\n return result;\n } catch {\n return null;\n }\n }\n\n /**\n * 销毁,清理所有 listener 和 pending。\n */\n public destroy(): void {\n this._destroyed = true;\n window.removeEventListener(\"message\", this._onMessage);\n\n // 清除所有 pending 请求\n for (const [id, pending] of this._pending) {\n clearTimeout(pending.timer);\n pending.reject(new BridgeError(\"timeout\", pending.method, id));\n }\n this._pending.clear();\n this._eventHandlers.clear();\n }\n\n private _onMessage = (ev: MessageEvent): void => {\n if (this._destroyed) return;\n\n // 只接受来自宿主父窗口的消息,丢弃任意 iframe/opener 伪造的回包/事件\n // (否则可伪造 hello 把 guest 骗成 embedded、或伪造 getEmbedToken 注入假 token)\n if (ev.source !== window.parent) return;\n\n let msg: IncomingMessage;\n try {\n // data 可能已经是对象(同 origin),或是序列化字符串\n msg = typeof ev.data === \"string\" ? (JSON.parse(ev.data) as IncomingMessage) : (ev.data as IncomingMessage);\n } catch {\n return;\n }\n\n if (!msg || msg.v !== 2) return;\n\n // 事件推送(无 id 字段)\n if (\"event\" in msg) {\n const handlers = this._eventHandlers.get(msg.event);\n if (handlers) {\n handlers.forEach((h) => h(msg.data));\n }\n return;\n }\n\n // 请求回包(有 id 字段)\n if (\"id\" in msg) {\n const pending = this._pending.get(msg.id);\n if (!pending) return;\n clearTimeout(pending.timer);\n this._pending.delete(msg.id);\n\n if (msg.ok) {\n pending.resolve(msg.result);\n } else {\n pending.reject(new BridgeError(\"rejected\", pending.method, msg.id));\n }\n }\n };\n}\n","import { TopicApiError } from \"./errors.js\";\nimport type {\n CampaignCard,\n CharacterCard,\n CreatorCard,\n HighlightPage,\n HotStoryCard,\n Leaderboard,\n LoreEvent,\n MyStoryKind,\n Page,\n RankEntity,\n RankWindow,\n SDKActivity,\n SDKAuth,\n SDKRank,\n SDKTopic,\n StoryCard,\n TopicDetail,\n TopicTab,\n WeeklyHottest,\n} from \"./types.js\";\n\n// ————— 内部 fetch 工具 —————\n\nasync function apiFetch(\n baseUrl: string,\n path: string,\n auth: SDKAuth,\n query?: Record,\n): Promise {\n // 构造 URL\n const url = new URL(path, baseUrl);\n if (query) {\n for (const [k, v] of Object.entries(query)) {\n if (v === undefined) continue;\n if (Array.isArray(v)) {\n // 多值参数(如 parentType=oc&parentType=elementum),对齐后端 List[str]\n for (const item of v) {\n url.searchParams.append(k, String(item));\n }\n } else {\n url.searchParams.set(k, String(v));\n }\n }\n }\n\n // 构造 headers\n const headers: Record = {\n \"Content-Type\": \"application/json\",\n };\n const token = auth.getToken();\n if (token) {\n headers[\"x-embed-token\"] = token;\n }\n\n let response: Response;\n try {\n response = await fetch(url.toString(), { method: \"GET\", headers });\n } catch (err) {\n throw new TopicApiError(-1, \"Network error\", path, err);\n }\n\n if (!response.ok) {\n let message = `HTTP ${response.status}`;\n try {\n const body = (await response.json()) as { message?: string };\n if (body.message) message = body.message;\n } catch {\n // ignore JSON parse errors\n }\n throw new TopicApiError(response.status, message, path);\n }\n\n try {\n return (await response.json()) as T;\n } catch (err) {\n throw new TopicApiError(-1, \"JSON parse error\", path, err);\n }\n}\n\n// ————— SDKTopicImpl —————\n\nexport class SDKTopicImpl implements SDKTopic {\n public constructor(\n private readonly _baseUrl: string,\n private readonly _auth: SDKAuth,\n ) {}\n\n public async getDetail(name: string): Promise {\n return apiFetch(this._baseUrl, `/v1/embed/topic/${encodeURIComponent(name)}`, this._auth);\n }\n\n public async listStories(\n name: string,\n query: {\n pageIndex: number;\n pageSize: number;\n sort: \"hot\" | \"like_count\" | \"highlight_mark_time\";\n startTime?: number;\n endTime?: number;\n authorUuid?: string;\n },\n ): Promise> {\n return apiFetch>(this._baseUrl, `/v1/embed/topic/${encodeURIComponent(name)}/stories`, this._auth, {\n pageIndex: query.pageIndex,\n pageSize: query.pageSize,\n sort: query.sort,\n // 可选参数:undefined 时 apiFetch 会跳过,不写入 query。\n startTime: query.startTime,\n endTime: query.endTime,\n authorUuid: query.authorUuid,\n });\n }\n\n public async listMyStories(\n name: string,\n query: {\n kind: MyStoryKind;\n pageIndex?: number;\n pageSize?: number;\n },\n ): Promise> {\n return apiFetch>(\n this._baseUrl,\n `/v1/embed/topic/${encodeURIComponent(name)}/my-stories`,\n this._auth,\n {\n kind: query.kind,\n pageIndex: query.pageIndex,\n pageSize: query.pageSize,\n },\n );\n }\n\n public async listCharacters(\n name: string,\n query: {\n parentType?: string[];\n pageIndex: number;\n pageSize: number;\n sort?: string;\n },\n ): Promise> {\n return apiFetch>(\n this._baseUrl,\n `/v1/embed/topic/${encodeURIComponent(name)}/characters`,\n this._auth,\n {\n parentType: query.parentType,\n pageIndex: query.pageIndex,\n pageSize: query.pageSize,\n sort: query.sort,\n },\n );\n }\n\n public async listCampaigns(\n name: string,\n query: { pageIndex: number; pageSize: number },\n ): Promise> {\n return apiFetch>(\n this._baseUrl,\n `/v1/embed/topic/${encodeURIComponent(name)}/campaigns`,\n this._auth,\n {\n pageIndex: query.pageIndex,\n pageSize: query.pageSize,\n },\n );\n }\n\n public async listLoreEvents(name: string): Promise {\n return apiFetch(this._baseUrl, `/v1/embed/topic/${encodeURIComponent(name)}/lore-events`, this._auth);\n }\n\n public async listHot(name: string): Promise {\n // 后端响应模型为 { stories: HotStoryCard[] }(无分页 bounded 热门列表),此处解包为数组。\n const resp = await apiFetch<{ stories: HotStoryCard[] }>(\n this._baseUrl,\n `/v1/embed/topic/${encodeURIComponent(name)}/hot-stories`,\n this._auth,\n );\n return resp.stories;\n }\n\n public async getWeeklyHottest(name: string): Promise {\n // 后端响应模型为 WeeklyHottest | null(无近 7 天同款 UV 最高作品时返回 null)。\n return apiFetch(\n this._baseUrl,\n `/v1/embed/topic/${encodeURIComponent(name)}/weekly-hottest`,\n this._auth,\n );\n }\n}\n\n// ————— SDKActivityImpl —————\n\nexport class SDKActivityImpl implements SDKActivity {\n public constructor(\n private readonly _baseUrl: string,\n private readonly _auth: SDKAuth,\n ) {}\n\n public async listTabs(uuid: string): Promise {\n return apiFetch(this._baseUrl, `/v1/embed/activity/${encodeURIComponent(uuid)}/tabs`, this._auth);\n }\n\n public async listSelectedStories(\n uuid: string,\n tabKey: string,\n query: {\n pageIndex: number;\n pageSize: number;\n sort?: string;\n },\n ): Promise {\n return apiFetch(\n this._baseUrl,\n `/v1/embed/activity/${encodeURIComponent(uuid)}/tab/${encodeURIComponent(tabKey)}`,\n this._auth,\n {\n pageIndex: query.pageIndex,\n pageSize: query.pageSize,\n sort: query.sort,\n },\n );\n }\n}\n\n// ————— SDKRankImpl —————\n\nexport class SDKRankImpl implements SDKRank {\n public constructor(\n private readonly _baseUrl: string,\n private readonly _auth: SDKAuth,\n ) {}\n\n public async get(\n entity: RankEntity,\n window: RankWindow,\n at: number | \"latest\",\n ): Promise> {\n // oc / elementum 仅支持 latest(后端非 latest 返回 400),提前抛出更友好的错误\n if ((entity === \"oc\" || entity === \"elementum\") && at !== \"latest\") {\n throw new TopicApiError(\n 400,\n `rank entity '${entity}' only supports at='latest'`,\n `/v1/embed/rank/${entity}/${window}/${String(at)}`,\n );\n }\n const atStr = at === \"latest\" ? \"latest\" : String(at);\n return apiFetch>(\n this._baseUrl,\n `/v1/embed/rank/${encodeURIComponent(entity)}/${encodeURIComponent(window)}/${encodeURIComponent(atStr)}`,\n this._auth,\n );\n }\n}\n\n// ————— PageCursor —————\n\nexport class PageCursor {\n private _pageIndex = 0;\n private _hasNext = true;\n private _total: number | null = null;\n\n public constructor(\n private readonly _fetcher: (pageIndex: number, pageSize: number) => Promise>,\n private readonly _pageSize = 20,\n ) {}\n\n public hasNext(): boolean {\n return this._hasNext;\n }\n\n public async next(): Promise {\n if (!this._hasNext) return [];\n const page = await this._fetcher(this._pageIndex, this._pageSize);\n this._total = page.total;\n this._hasNext = page.hasNext;\n this._pageIndex += 1;\n return page.list;\n }\n\n public reset(): void {\n this._pageIndex = 0;\n this._hasNext = true;\n this._total = null;\n }\n\n public currentPage(): number {\n return this._pageIndex;\n }\n\n public totalItems(): number | null {\n return this._total;\n }\n}\n","// ————— 通用 —————\n\nexport interface Page {\n total: number;\n pageIndex: number;\n pageSize: number;\n list: T[];\n /** facade 重算(修复 api_hashtag.py:693 bug) */\n hasNext: boolean;\n}\n\nexport interface RichText {\n contentType: \"text\" | \"html\" | \"markdown\";\n /** html 类型必须服务端消毒(sanitize)后下发 */\n content: string;\n}\n\n// ————— 话题 —————\n\nexport interface TopicTab {\n key: string;\n label: string;\n visible: boolean;\n}\n\nexport interface TopicDetail {\n hashtagName: string;\n kind: \"hashtag\" | \"activity\";\n activityUuid: string | null;\n title: string | null;\n description: RichText | null;\n bannerPic: string | null;\n smallBannerPic: string | null;\n headerPic: string | null;\n phase: \"draft\" | \"preparing\" | \"running\" | \"ended\" | null;\n startTime: number | null; // UTC ms\n endTime: number | null; // UTC ms\n /** 合并 popularity / hashtag_heat 为单字段单语义 */\n heat: number;\n participantsCount: number;\n subscribeCount: number;\n action: {\n visible: boolean;\n label: string | null;\n url: string | null;\n } | null;\n tabs: TopicTab[];\n ruleStoryUuid: string | null;\n /** 登录态填充;匿名/游客(无 embed token)为 null */\n viewer: {\n subscribed: boolean;\n canEdit: boolean;\n } | null;\n}\n\n// ————— 作品卡片 —————\n\nexport interface StoryCard {\n storyId: string;\n title: string | null;\n coverUrl: string | null;\n shareUrl: string | null;\n aspect: string | null;\n author: {\n uuid: string | null;\n nickName: string | null;\n avatarUrl: string | null;\n };\n metrics: {\n likeCount: number;\n sameStyleCount: number;\n picCount: number;\n };\n flags: {\n hasVideo: boolean;\n hasBgm: boolean;\n isInteractive: boolean;\n isPinned: boolean;\n };\n hashtagNames: string[];\n /** 匿名/游客 null */\n viewer: {\n liked: boolean;\n favored: boolean;\n } | null;\n}\n\n// ————— 话题热门流 / 本周最热 —————\n\n/** 话题算法热门流的作品卡片:现有 StoryCard 全字段 + 热度分(已乘 1000 取整)。 */\nexport interface HotStoryCard extends StoryCard {\n /** 热度分(后端 doc.sort[0]*1000 取整),int。 */\n hotScore: number;\n}\n\n/** 本周最热单品(近 7 天同款 UV 最高的一条),无则 null。全 camelCase。 */\nexport interface WeeklyHottest {\n collectionUuid: string;\n title: string;\n coverUrl: string | null;\n sameStyleUv: number;\n creatorName: string;\n creatorUuid: string;\n creatorAvatar: string | null;\n isInteractive: boolean;\n}\n\n// ————— 活动精选 —————\n\nexport interface HighlightPage extends Page {\n /** topList 仅 pageIndex=0 时非空,其余页为 [] */\n topList: StoryCard[];\n}\n\n// ————— 角色卡片 —————\n\nexport interface CharacterCard {\n uuid: string;\n name: string | null;\n coverUrl: string | null;\n type: \"oc\" | \"elementum\" | string;\n author: {\n uuid: string | null;\n nickName: string | null;\n avatarUrl: string | null;\n };\n}\n\n// ————— 活动卡片 —————\n\nexport interface CampaignCard {\n uuid: string;\n title: string | null;\n coverUrl: string | null;\n pv: number;\n}\n\n// ————— 世界观事件 —————\n\nexport interface LoreEvent {\n uuid: string;\n name: string;\n category: string | null;\n description: string | null;\n sortIndex: number;\n collectionCount: number;\n boundTopic: {\n name: string;\n startTime: number | null; // UTC ms\n endTime: number | null; // UTC ms\n } | null;\n stories: StoryCard[];\n}\n\n// ————— 榜单 —————\n\nexport type RankWindow = \"daily\" | \"weekly\" | \"monthly\";\nexport type RankEntity = \"stories\" | \"creators\" | \"oc\" | \"elementum\";\n\nexport interface Leaderboard {\n window: RankWindow;\n entity: RankEntity;\n startTime: number | null; // UTC ms;后端 Optional,可能下发 null\n endTime: number | null; // UTC ms;后端 Optional,可能下发 null\n lastUpdatedAt: number | null;\n list: RankEntry[];\n}\n\nexport interface RankEntry {\n rank: number;\n /** 经 score_multiplier 缩放后的整数 */\n score: number;\n item: T;\n}\n\nexport interface CreatorCard {\n uuid: string | null;\n nickName: string | null;\n avatarUrl: string | null;\n subscriberCount: number;\n storyCount: number;\n topStories: StoryCard[];\n}\n\n// ————— 客户端上下文 —————\n\nexport type ClientContext = \"app\" | \"web-embedded\" | \"guest\";\n\n/** hello 响应中的 client 字段(宿主回传具体平台) */\nexport type BridgeClient = \"ios\" | \"android\" | \"web\";\n\nexport interface HelloResult {\n /** 宿主平台 */\n client: BridgeClient;\n /** App 版本号,Web 宿主为 null */\n appVersion: string | null;\n /** 宿主支持的 bridge method 列表 */\n features: string[];\n}\n\n// ————— 能力位图 —————\n\nexport enum Capability {\n // 数据能力(所有上下文均支持)\n ReadTopic = \"read.topic\",\n ReadStories = \"read.stories\",\n ReadCharacters = \"read.characters\",\n ReadCampaigns = \"read.campaigns\",\n ReadLoreEvents = \"read.loreEvents\",\n ReadActivity = \"read.activity\",\n ReadRank = \"read.rank\",\n\n // 桥接能力(仅 App/Web-embedded)\n Bridge = \"bridge\",\n NavInternal = \"nav.internal\",\n NavExternal = \"nav.external\",\n NavApplyHost = \"nav.applyHost\",\n Toast = \"ui.toast\",\n Viewport = \"ui.viewport\",\n EventBack = \"event.back\",\n EventTokenChanged = \"event.tokenChanged\",\n EventViewport = \"event.viewport\",\n}\n\n// ————— AllowedRoute 白名单 —————\n\n/** AllowedRoute v1 硬编码白名单 */\nexport type AllowedRoute =\n | \"/tag\"\n | \"/topic\"\n | \"/activity\"\n | \"/ranking\"\n | \"/collection/interaction\"\n | \"/oc\"\n | \"/user\"\n | \"/generate\";\n\n// ————— viewport —————\n\nexport interface ViewportInfo {\n /** 嵌入态永远为 0(防双叠加) */\n safeTop: 0;\n safeBottom: number;\n keyboardInset: number;\n width: number;\n height: number;\n}\n\n// ————— SDK 选项 —————\n\nexport interface TopicSDKOptions {\n /** 后端 API 基址,默认 'https://pre.api.talesofai.cn' */\n apiBaseUrl?: string;\n /** hello 握手超时 ms,默认 1500 */\n helloTimeout?: number;\n /** getEmbedToken bridge 调用超时 ms,默认 3000 */\n tokenTimeout?: number;\n /** embed token 过期前多久触发 re-exchange,ms,默认 5 * 60 * 1000 */\n tokenRefreshEarlyMs?: number;\n /** token 无法恢复时的回调 */\n onAuthLost?: (reason: string) => void;\n}\n\n// ————— 公开 API 接口 —————\n\nexport interface SDKAuth {\n /** 当前 embed token,匿名时为 null */\n getToken(): string | null;\n /** token 过期时间(UTC ms),匿名时为 null */\n getExpiresAt(): number | null;\n /** 是否已登录(有有效 token) */\n isAuthenticated(): boolean;\n}\n\n/** listMyStories 的 kind:favored=当前 user 收藏的本话题作品;created=当前 user 在本话题投稿的作品;liked=当前 user 点赞的本话题作品。 */\nexport type MyStoryKind = \"favored\" | \"created\" | \"liked\";\n\nexport interface SDKTopic {\n /** GET /v1/embed/topic/{name} */\n getDetail(name: string): Promise;\n\n /** GET /v1/embed/topic/{name}/stories */\n listStories(\n name: string,\n query: {\n pageIndex: number;\n pageSize: number;\n sort: \"hot\" | \"like_count\" | \"highlight_mark_time\";\n /** 可选时间窗(UTC ms),透传后端 start_time/end_time */\n startTime?: number;\n /** 可选时间窗(UTC ms),透传后端 start_time/end_time */\n endTime?: number;\n /** 可选作者过滤,透传后端 authorUuid */\n authorUuid?: string;\n },\n ): Promise>;\n\n /**\n * GET /v1/embed/topic/{name}/my-stories\n * 当前 embed user 在本话题下的作品:kind=favored(收藏)/ created(投稿)/ liked(点赞)。\n * 匿名(无 embed token)返回空 Page(list=[]、hasNext=false),不抛错、不 401。\n */\n listMyStories(\n name: string,\n query: {\n kind: MyStoryKind;\n pageIndex?: number;\n pageSize?: number;\n },\n ): Promise>;\n\n /** GET /v1/embed/topic/{name}/characters */\n listCharacters(\n name: string,\n query: {\n /** 后端为 List[str](alias parentType),多值。默认 [\"oc\",\"elementum\"] */\n parentType?: string[];\n pageIndex: number;\n pageSize: number;\n sort?: string;\n },\n ): Promise>;\n\n /** GET /v1/embed/topic/{name}/campaigns */\n listCampaigns(name: string, query: { pageIndex: number; pageSize: number }): Promise>;\n\n /** GET /v1/embed/topic/{name}/lore-events */\n listLoreEvents(name: string): Promise;\n\n /**\n * GET /v1/embed/topic/{name}/hot-stories\n * 话题算法热门流(bounded 热门列表,无分页),每条带 hotScore。\n */\n listHot(name: string): Promise;\n\n /**\n * GET /v1/embed/topic/{name}/weekly-hottest\n * 本周最热单品(近 7 天同款 UV 最高的一条),无则 null。\n */\n getWeeklyHottest(name: string): Promise;\n}\n\nexport interface SDKActivity {\n /** GET /v1/embed/activity/{uuid}/tabs */\n listTabs(uuid: string): Promise;\n\n /**\n * GET /v1/embed/activity/{uuid}/tab/{tabKey}\n * HighlightPage.topList 仅 pageIndex=0 时非空\n */\n listSelectedStories(\n uuid: string,\n tabKey: string,\n query: {\n pageIndex: number;\n pageSize: number;\n sort?: string;\n },\n ): Promise;\n}\n\nexport interface SDKRank {\n /**\n * GET /v1/embed/rank/{entity}/{window}/{at}\n * entity 直接作为路径段传给后端(facade 内部映射到 collection/user/tcp-oc/tcp-elementum,对外保持 stories/creators/oc/elementum)。\n * at: UTC ms 或字符串 'latest'。\n * 注意:oc / elementum 仅支持 at='latest',传时间戳后端返回 400(SDK 侧亦会提前抛错)。\n */\n get(\n entity: RankEntity,\n window: RankWindow,\n at: number | \"latest\",\n ): Promise>;\n}\n\nexport interface SDKNav {\n /**\n * 跳进产品内页面(AllowedRoute v1 白名单)。\n *\n * 参数契约按路由性质分两类(SDK 运行期兜底 + 此处类型收窄,缺参构建期/运行期都会被拦):\n * - **自指路由** `/topic` `/tag` `/activity`:参数=「当前这个」,可省略,SDK 自动从当前页\n * `?hashtag=` / `?activity_uuid=` 填入(创作者可显式覆盖)。`/ranking` `/generate` 无需参数。\n * - **per-item 路由** `/oc` `/user` `/collection/interaction`:参数指向「具体某个」实体,\n * **必须传 `uuid`**(来自被点卡片的数据,SDK 无从代填)。\n *\n * guest 上下文(仅本地 dev 无宿主时)本地 dev 无宿主仅 console 提示,不跳转;生产入口恒为宿主内嵌,唤起 App 由宿主承载。\n */\n internal(\n route: \"/topic\" | \"/tag\" | \"/activity\" | \"/ranking\" | \"/generate\",\n query?: Record,\n ): Promise;\n internal(\n route: \"/oc\" | \"/user\" | \"/collection/interaction\",\n query: { uuid: string | number } & Record,\n ): Promise;\n\n /**\n * 跳外部 URL。\n * embedded: bridge nav.external\n * guest: window.open(url, '_blank')\n */\n external(url: string): Promise;\n\n /**\n * 申请创建/主持一个新话题活动空间:跳转到运营配置的申请表单(飞书多维表单),宿主自动带上\n * 当前登录用户昵称/UID 的 prefill 参数(不含话题名)。**不接受参数**——SDK/页面拿不到、也不经手\n * 这些用户数据,全由宿主本地态直接拼 URL(与 `getEmbedToken` 不下发宿主 token 同一不变量)。\n *\n * 运营未配置申请表单时抛 `BridgeError`(宿主报 not-configured)。\n * guest 上下文(仅本地 dev 无宿主)本地不跳转,仅 console 提示;生产入口恒为宿主内嵌。\n */\n applyHost(): Promise;\n}\n\nexport interface SDKUi {\n /**\n * 宿主 Toast(embedded)。\n * guest / unsupported:抛 UnsupportedError。\n */\n toast(text: string, options?: { duration?: number; level?: \"info\" | \"warn\" }): Promise;\n\n /**\n * 当前 viewport 安全区信息。\n * guest:抛 UnsupportedError。\n */\n viewport(): Promise;\n}\n\nexport interface SDKEvents {\n on(event: \"tokenChanged\", handler: (newToken: string | null) => void): () => void;\n on(event: \"viewport\", handler: (info: ViewportInfo) => void): () => void;\n on(event: \"back\", handler: (ev: { preventDefault(): void }) => void): () => void;\n off(event: string, handler: (...args: unknown[]) => void): void;\n}\n\n// ————— SDK 顶层 —————\n\nexport interface TopicSDK {\n env: {\n context: ClientContext;\n embedded: boolean;\n client: BridgeClient | \"unknown\";\n appVersion: string | null;\n features: string[];\n };\n auth: SDKAuth;\n topic: SDKTopic;\n activity: SDKActivity;\n rank: SDKRank;\n nav: SDKNav;\n ui: SDKUi;\n events: SDKEvents;\n can(cap: Capability): boolean;\n destroy(): void;\n}\n","import type { BridgeClient } from \"./bridge.js\";\nimport { UnsupportedError } from \"./errors.js\";\nimport type { ClientContext, HelloResult } from \"./types.js\";\nimport { Capability } from \"./types.js\";\n\nexport interface EnvResult {\n context: ClientContext;\n embedded: boolean;\n client: \"ios\" | \"android\" | \"web\" | \"unknown\";\n appVersion: string | null;\n features: string[];\n hello: HelloResult | null;\n /** 宿主注入的 ?activity_uuid=(无则 null)。决定 ReadActivity 能力是否授予(纯 hashtag 话题无活动数据)。 */\n activityUuid: string | null;\n}\n\nexport async function detectEnv(bridge: BridgeClient, sdkVersion: string, helloTimeout: number): Promise {\n const ua = navigator.userAgent;\n // 宿主把绑定的 activity_uuid 作为 query 注入 iframe(无则纯 hashtag 话题)。\n const activityUuid =\n typeof window !== \"undefined\" ? new URLSearchParams(window.location.search).get(\"activity_uuid\") || null : null;\n\n // UA 预判:小程序直接出局\n if (/miniProgram/i.test(ua)) {\n throw new UnsupportedError(\"weapp-not-supported\", \"guest\" as ClientContext);\n }\n\n // hello 握手(超时→ null = guest)\n const hello = await bridge.hello(sdkVersion, helloTimeout);\n\n if (!hello) {\n // 握手超时 → guest\n return {\n context: \"guest\",\n embedded: false,\n client: \"unknown\",\n appVersion: null,\n features: [],\n hello: null,\n activityUuid,\n };\n }\n\n // 宿主回包确认客户端类型(最终以握手结果为准,UA 仅用于 miniProgram 预判出局)\n const context: ClientContext = hello.client === \"ios\" || hello.client === \"android\" ? \"app\" : \"web-embedded\";\n\n return {\n context,\n embedded: true,\n client: hello.client,\n appVersion: hello.appVersion,\n features: hello.features,\n hello,\n activityUuid,\n };\n}\n\n/**\n * 根据 EnvResult 计算支持的 Capability 集合。\n */\nexport function buildCapabilities(env: EnvResult): Set {\n const caps = new Set();\n\n // 数据能力:所有上下文均支持\n caps.add(Capability.ReadTopic);\n caps.add(Capability.ReadStories);\n caps.add(Capability.ReadCharacters);\n caps.add(Capability.ReadCampaigns);\n caps.add(Capability.ReadLoreEvents);\n caps.add(Capability.ReadRank);\n // ReadActivity 仅当话题绑定了 activity(宿主注入 ?activity_uuid=)才授予;纯 hashtag 话题无活动数据,\n // 不报 ReadActivity,避免 can(ReadActivity) 恒 true 误导创作者渲染空活动区块。\n if (env.activityUuid) {\n caps.add(Capability.ReadActivity);\n }\n\n if (env.context !== \"guest\") {\n // 桥接能力(仅 App/Web-embedded)\n caps.add(Capability.Bridge);\n caps.add(Capability.NavInternal);\n caps.add(Capability.NavExternal);\n caps.add(Capability.NavApplyHost);\n caps.add(Capability.Toast);\n caps.add(Capability.Viewport);\n caps.add(Capability.EventBack);\n caps.add(Capability.EventTokenChanged);\n caps.add(Capability.EventViewport);\n }\n // guest(仅本地 dev 无宿主可达):仅只读数据能力;唤起 App 统一靠宿主 nav.internal,不再暴露独立 OpenApp 能力。\n\n return caps;\n}\n","import type { BridgeClient } from \"./bridge.js\";\nimport type { SDKEvents, ViewportInfo } from \"./types.js\";\n\ntype TokenChangedHandler = (newToken: string | null) => void;\ntype ViewportHandler = (info: ViewportInfo) => void;\ntype BackHandler = (ev: { preventDefault(): void }) => void;\ntype AnyHandler = TokenChangedHandler | ViewportHandler | BackHandler;\n\ninterface HandlerEntry {\n event: string;\n handler: AnyHandler;\n bridgeHandler: (data: unknown) => void;\n}\n\nexport class SDKEventsImpl implements SDKEvents {\n private readonly _entries: HandlerEntry[] = [];\n // tokenChanged 不直接订阅 bridge:宿主事件里 token 恒为 null,\n // 真正的新 token 由 SDK 内部 re-exchange 完成后经 notifyTokenChanged 下发。\n private readonly _tokenChangedHandlers = new Set();\n\n public constructor(private readonly _bridge: BridgeClient | null) {}\n\n public on(event: \"tokenChanged\", handler: TokenChangedHandler): () => void;\n public on(event: \"viewport\", handler: ViewportHandler): () => void;\n public on(event: \"back\", handler: BackHandler): () => void;\n public on(event: string, handler: AnyHandler): () => void {\n // tokenChanged:与 bridge 事件解耦,由 SDK 内部 re-exchange 后回调新 token\n if (event === \"tokenChanged\") {\n const h = handler as TokenChangedHandler;\n this._tokenChangedHandlers.add(h);\n return () => {\n this._tokenChangedHandlers.delete(h);\n };\n }\n\n if (!this._bridge) {\n // guest 模式:无宿主事件,返回空 unsubscribe\n return () => {};\n }\n\n let bridgeHandler: (data: unknown) => void;\n\n switch (event) {\n case \"viewport\": {\n const h = handler as ViewportHandler;\n bridgeHandler = (data: unknown) => {\n const raw = data as {\n safeTop?: number;\n safeBottom?: number;\n keyboardInset?: number;\n width?: number;\n height?: number;\n };\n h({\n safeTop: 0, // 固定 0,防双叠加\n safeBottom: raw.safeBottom ?? 0,\n keyboardInset: raw.keyboardInset ?? 0,\n width: raw.width ?? window.innerWidth,\n height: raw.height ?? window.innerHeight,\n });\n };\n break;\n }\n case \"back\": {\n const h = handler as BackHandler;\n bridgeHandler = (_data: unknown) => {\n let _defaultPrevented = false;\n h({\n preventDefault() {\n _defaultPrevented = true;\n },\n });\n // SDK 不需要把 defaultPrevented 回传给宿主;\n // 宿主侧设计为\"等待一帧确认\"(H8),此处不需要额外处理\n };\n break;\n }\n default:\n return () => {};\n }\n\n this._bridge.onEvent(event, bridgeHandler);\n const entry: HandlerEntry = { event, handler, bridgeHandler };\n this._entries.push(entry);\n\n return () => {\n this.off(event, handler as (...args: unknown[]) => void);\n };\n }\n\n /**\n * 由 SDK 内部在 re-exchange 完成后调用,向消费方下发最新 token。\n * 宿主 tokenChanged 事件本身不携带 token(恒 null),故新值取自 auth 当前 token。\n */\n public notifyTokenChanged(token: string | null): void {\n this._tokenChangedHandlers.forEach((h) => h(token));\n }\n\n public off(event: string, handler: (...args: unknown[]) => void): void {\n if (event === \"tokenChanged\") {\n this._tokenChangedHandlers.delete(handler as unknown as TokenChangedHandler);\n return;\n }\n const idx = this._entries.findIndex((e) => e.event === event && e.handler === handler);\n if (idx === -1) return;\n const entry = this._entries[idx]!;\n this._bridge?.offEvent(event, entry.bridgeHandler);\n this._entries.splice(idx, 1);\n }\n\n public destroy(): void {\n for (const entry of this._entries) {\n this._bridge?.offEvent(entry.event, entry.bridgeHandler);\n }\n this._entries.length = 0;\n this._tokenChangedHandlers.clear();\n }\n}\n","import type { BridgeClient } from \"./bridge.js\";\nimport { UnsupportedError } from \"./errors.js\";\nimport type { AllowedRoute, ClientContext, SDKNav } from \"./types.js\";\n\n/**\n * 自指路由:参数=「当前这个」,缺省时 SDK 从当前页 URL 自动填(创作者可显式覆盖)。\n * 宿主把 hashtag / activity_uuid 作为 query 注入 iframe src,故内嵌页 `location.search` 必有这些值。\n */\nconst SELF_PARAM_FROM_URL: Partial> = {\n \"/topic\": { param: \"hashtag\", urlKey: \"hashtag\" },\n \"/tag\": { param: \"hashtag\", urlKey: \"hashtag\" },\n \"/activity\": { param: \"uuid\", urlKey: \"activity_uuid\" },\n};\n\n/** per-item 路由:参数指向「具体某个」实体(来自被点卡片),SDK 无从代填 → 缺则抛错打回。 */\nconst REQUIRED_PARAMS: Partial> = {\n \"/oc\": [\"uuid\"],\n \"/user\": [\"uuid\"],\n \"/collection/interaction\": [\"uuid\"],\n};\n\nconst isBlank = (v: unknown): boolean => v === undefined || v === null || v === \"\";\n\nexport class SDKNavImpl implements SDKNav {\n public constructor(\n private readonly _bridge: BridgeClient | null,\n private readonly _context: ClientContext,\n ) {}\n\n /**\n * 解析最终 query:自指路由缺参从 URL 自动填;per-item 路由缺必需参数则抛错(开发期就被打回,而非线上白屏)。\n */\n private _resolveQuery(route: AllowedRoute, query?: Record): Record {\n const q: Record = { ...(query ?? {}) };\n\n // 1) 自指路由:缺参从当前页 URL 自动填\n const selfRef = SELF_PARAM_FROM_URL[route];\n if (selfRef && isBlank(q[selfRef.param]) && typeof window !== \"undefined\") {\n const fromUrl = new URLSearchParams(window.location.search).get(selfRef.urlKey);\n if (fromUrl) q[selfRef.param] = fromUrl;\n }\n\n // 2) 必需参数校验(自指填完后再查)\n const required = REQUIRED_PARAMS[route] ?? (selfRef ? [selfRef.param] : []);\n for (const p of required) {\n if (isBlank(q[p])) {\n const hint = selfRef\n ? `该自指路由通常由 SDK 从当前页 ?${selfRef.urlKey}= 自动填,但当前 URL 没有该值`\n : `该路由指向具体实体,请从被点卡片数据传入 ${p}(如 ${p}: story.uuid)`;\n throw new Error(`[topic-sdk] nav.internal('${route}') 缺少必需参数 '${p}'。${hint}。`);\n }\n }\n return q;\n }\n\n public async internal(route: AllowedRoute, query?: Record): Promise {\n const effectiveQuery = this._resolveQuery(route, query);\n if (this._context === \"guest\") {\n // guest(仅本地 dev 无宿主):_resolveQuery 已先跑(保留缺参 throw);无宿主不跳转,生产由宿主唤起 / 站内跳。\n console.info(\"[topic-sdk] nav.internal(route) 本地 dev 无宿主不跳转;生产由宿主唤起/站内跳\");\n return;\n }\n if (!this._bridge) {\n throw new UnsupportedError(\"nav.internal\", this._context);\n }\n await this._bridge.send(\"nav.internal\", { route, query: effectiveQuery });\n }\n\n public async external(url: string): Promise {\n if (this._context === \"guest\") {\n window.open(url, \"_blank\");\n return;\n }\n if (!this._bridge) {\n throw new UnsupportedError(\"nav.external\", this._context);\n }\n await this._bridge.send(\"nav.external\", { url });\n }\n\n public async applyHost(): Promise {\n if (this._context === \"guest\") {\n // guest(仅本地 dev 无宿主):拼 URL 需要的用户态/运营配置只有宿主本地有,dev 无宿主不跳转;生产由宿主承载。\n console.info(\"[topic-sdk] nav.applyHost() 本地 dev 无宿主不跳转;生产由宿主用本地用户态拼 URL 后跳转\");\n return;\n }\n if (!this._bridge) {\n throw new UnsupportedError(\"nav.applyHost\", this._context);\n }\n await this._bridge.send(\"nav.applyHost\");\n }\n}\n","import type { BridgeClient } from \"./bridge.js\";\nimport { UnsupportedError } from \"./errors.js\";\nimport type { ClientContext, SDKUi, ViewportInfo } from \"./types.js\";\n\nexport class SDKUiImpl implements SDKUi {\n public constructor(\n private readonly _bridge: BridgeClient | null,\n private readonly _context: ClientContext,\n ) {}\n\n public async toast(text: string, options?: { duration?: number; level?: \"info\" | \"warn\" }): Promise {\n if (this._context === \"guest\" || !this._bridge) {\n throw new UnsupportedError(\"ui.toast\", this._context);\n }\n await this._bridge.send(\"ui.toast\", {\n text,\n duration: options?.duration,\n level: options?.level ?? \"info\",\n });\n }\n\n public async viewport(): Promise {\n if (this._context === \"guest\" || !this._bridge) {\n throw new UnsupportedError(\"ui.viewport\", this._context);\n }\n const raw = await this._bridge.send<{\n safeTop?: number;\n safeBottom?: number;\n keyboardInset?: number;\n width?: number;\n height?: number;\n }>(\"ui.viewport\");\n\n // safeTop 固定返回 0(防双叠加,宿主已 safeTop:true/page.tsx:39)\n // 其余字段对宿主漏传做兜底,与 events.ts viewport 分支保持一致\n return {\n safeTop: 0,\n safeBottom: raw.safeBottom ?? 0,\n keyboardInset: raw.keyboardInset ?? 0,\n width: raw.width ?? window.innerWidth,\n height: raw.height ?? window.innerHeight,\n };\n }\n}\n","import { SDKAuthImpl } from \"./auth.js\";\nimport { BridgeClient } from \"./bridge.js\";\nimport { SDKActivityImpl, SDKRankImpl, SDKTopicImpl } from \"./data.js\";\nimport { buildCapabilities, detectEnv } from \"./env.js\";\nimport { SDKEventsImpl } from \"./events.js\";\nimport { SDKNavImpl } from \"./nav.js\";\nimport type { AllowedRoute, Capability, SDKAuth, TopicSDK, TopicSDKOptions } from \"./types.js\";\nimport { SDKUiImpl } from \"./ui.js\";\n\nexport type {\n AllowedRoute,\n BridgeClient as BridgeClientType,\n // 活动\n CampaignCard,\n // 角色\n CharacterCard,\n // 环境/能力\n ClientContext,\n CreatorCard,\n HelloResult,\n HighlightPage,\n Leaderboard,\n // 世界观\n LoreEvent,\n MyStoryKind,\n // 通用\n Page,\n RankEntity,\n RankEntry,\n // 榜单\n RankWindow,\n RichText,\n SDKActivity,\n // 接口\n SDKAuth,\n SDKEvents,\n SDKNav,\n SDKRank,\n SDKTopic,\n SDKUi,\n // 作品\n StoryCard,\n TopicDetail,\n TopicSDK,\n TopicSDKOptions,\n // 话题\n TopicTab,\n ViewportInfo,\n} from \"./types.js\";\n\nexport { BridgeClient } from \"./bridge.js\";\nexport { PageCursor } from \"./data.js\";\nexport { BridgeError, TopicApiError, UnsupportedError } from \"./errors.js\";\nexport { Capability } from \"./types.js\";\n\nconst SDK_VERSION = \"0.1.0\";\n\n/**\n * 全局拦截 点击。内嵌页跑在 sandbox iframe(无 allow-top-navigation)里,原生 /相对路径跳转会逃逸到\n * iframe 自身源(OSS)而非宿主 App。故在捕获阶段接管:\n * - 同源路径跳转 → nav.internal(命中 AllowedRoute 由宿主导航/游客转深链;未命中被宿主拦截、不逃逸);\n * - 跨站外链 → nav.external(宿主用系统浏览器打开);\n * - 同 path 仅改 hash(页面内视图切换)、target=_blank、download、非 http(s) scheme → 放行。\n * 这样创作者即便写了原生链接,也不会把 iframe 跳出内嵌页。返回卸载函数。\n */\nfunction installLinkInterceptor(nav: SDKNavImpl): () => void {\n const onClick = (e: MouseEvent) => {\n if (e.defaultPrevented || e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;\n const targetEl = e.target instanceof Element ? e.target : ((e.target as Node | null)?.parentElement ?? null);\n const a = targetEl?.closest(\"a\") as HTMLAnchorElement | null;\n if (!a) return;\n const href = a.getAttribute(\"href\");\n if (!href) return;\n const target = a.getAttribute(\"target\");\n if (target && target !== \"_self\") return;\n if (a.hasAttribute(\"download\")) return;\n let url: URL;\n try {\n url = new URL(href, location.href);\n } catch {\n return;\n }\n if (url.protocol !== \"http:\" && url.protocol !== \"https:\") return;\n if (url.origin === location.origin) {\n // 同 path 仅改 hash(页面内视图切换)→ 放行,交给页面自己处理\n if (url.pathname === location.pathname && url.search === location.search && url.hash) return;\n const query: Record = {};\n url.searchParams.forEach((v, k) => {\n query[k] = v;\n });\n e.preventDefault();\n nav.internal(url.pathname as AllowedRoute, query).catch(() => {});\n return;\n }\n e.preventDefault();\n nav.external(url.href).catch(() => {});\n };\n document.addEventListener(\"click\", onClick, true);\n return () => document.removeEventListener(\"click\", onClick, true);\n}\n\n/**\n * 守卫 history.pushState/replaceState。内嵌页禁用(会污染 Android WebView 返回栈,返回键先消费 iframe history),\n * 应改用 hash 路由 / 内存路由。embedded 下安装,违例当场 throw(dev 期即暴露);destroy 恢复原函数。\n */\nfunction installPushStateGuard(): () => void {\n if (typeof history === \"undefined\") return () => {};\n const origPush = history.pushState.bind(history);\n const origReplace = history.replaceState.bind(history);\n const guard = (method: string) => () => {\n const msg =\n `[topic-sdk] 内嵌页禁止 history.${method}(会污染 App 返回栈);` +\n `请改用 hash 路由(location.hash)或内存路由做页面内视图切换。`;\n console.error(msg);\n throw new Error(msg);\n };\n history.pushState = guard(\"pushState\") as typeof history.pushState;\n history.replaceState = guard(\"replaceState\") as typeof history.replaceState;\n return () => {\n history.pushState = origPush;\n history.replaceState = origReplace;\n };\n}\n\n/**\n * 初始化并返回 TopicSDK 实例。\n *\n * 初始化序列:\n * ① 端探测(UA 嗅探预判)\n * ② hello 握手(超时→ guest)\n * ③ 鉴权(embedded:bridge getEmbedToken;guest:匿名)\n * ④ 能力协商(填充 Capability 位图)\n * ⑤ 事件订阅(tokenChanged / viewport / back)\n */\nexport async function createTopicSDK(options: TopicSDKOptions = {}): Promise {\n const {\n apiBaseUrl = \"https://pre.api.talesofai.cn\",\n helloTimeout = 1500,\n tokenTimeout = 3000,\n tokenRefreshEarlyMs = 5 * 60 * 1000,\n onAuthLost,\n } = options;\n\n // tokenTimeout 下界 clamp:500ms 是 v1 bridge 已知坏值;低于 1000ms 上调并告警(SKILL 固定 3000)。\n let effectiveTokenTimeout = tokenTimeout;\n if (effectiveTokenTimeout < 1000) {\n console.warn(\n `[topic-sdk] tokenTimeout ${effectiveTokenTimeout}ms 低于下限 1000ms(500ms 是 v1 bridge 已知坏值),已上调到 1000ms。`,\n );\n effectiveTokenTimeout = 1000;\n }\n\n // ① + ② 端探测 + hello 握手\n // BridgeClient は常に作成するが、guest の場合はすぐ破棄する\n const bridge = new BridgeClient(effectiveTokenTimeout);\n const env = await detectEnv(bridge, SDK_VERSION, helloTimeout);\n\n // embedded 上下文:守卫 history.pushState/replaceState(内嵌页禁用,会污染 App 返回栈);违例当场 throw,dev 期即暴露。\n const removePushStateGuard = env.embedded ? installPushStateGuard() : () => {};\n\n // guest の場合は bridge を使わない\n const activeBridge = env.context === \"guest\" ? null : bridge;\n if (env.context === \"guest\") {\n // guest 模式:bridge 不再需要,但保留 message listener 无害(destroy 可选)\n // 为节约资源,立即销毁\n bridge.destroy();\n }\n\n // ③ 鉴权\n const auth = new SDKAuthImpl(activeBridge, effectiveTokenTimeout, tokenRefreshEarlyMs, onAuthLost);\n await auth.init();\n\n // ④ 能力协商\n const capabilities = buildCapabilities(env);\n\n // ⑤ 事件订阅:tokenChanged → 自动重取 token\n const eventsImpl = new SDKEventsImpl(activeBridge);\n\n if (activeBridge) {\n activeBridge.onEvent(\"tokenChanged\", (_data: unknown) => {\n // 宿主事件不携带新 token(恒 null),SDK 内部 re-exchange 后再把真实 token 下发给消费方\n auth\n .handleTokenChanged()\n .then(() => eventsImpl.notifyTokenChanged(auth.getToken()))\n .catch(() => eventsImpl.notifyTokenChanged(null));\n });\n }\n\n // ————— 构造子模块 —————\n const topicImpl = new SDKTopicImpl(apiBaseUrl, auth as SDKAuth);\n const activityImpl = new SDKActivityImpl(apiBaseUrl, auth as SDKAuth);\n const rankImpl = new SDKRankImpl(apiBaseUrl, auth as SDKAuth);\n const navImpl = new SDKNavImpl(activeBridge, env.context);\n const uiImpl = new SDKUiImpl(activeBridge, env.context);\n\n // 根治\"原生 /相对跳转在 sandbox iframe 内逃逸到 OSS 源\"的问题:全局接管 点击,改走 bridge 导航。\n const removeLinkInterceptor = installLinkInterceptor(navImpl);\n\n // ————— TopicSDK 对象 —————\n const sdk: TopicSDK = {\n env: {\n context: env.context,\n embedded: env.embedded,\n client: env.client,\n appVersion: env.appVersion,\n features: env.features,\n },\n\n auth: auth as SDKAuth,\n topic: topicImpl,\n activity: activityImpl,\n rank: rankImpl,\n nav: navImpl,\n ui: uiImpl,\n events: eventsImpl,\n\n can(cap: Capability): boolean {\n return capabilities.has(cap);\n },\n\n destroy(): void {\n removeLinkInterceptor();\n removePushStateGuard();\n auth.destroy();\n eventsImpl.destroy();\n activeBridge?.destroy();\n },\n };\n\n return sdk;\n}\n"],"mappings":"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","names":["delay","window","Capability"]} \ No newline at end of file +{"version":3,"sources":["../src/auth.ts","../src/errors.ts","../src/bridge.ts","../src/data.ts","../src/types.ts","../src/env.ts","../src/events.ts","../src/nav.ts","../src/ui.ts","../src/image.ts","../src/index.ts"],"sourcesContent":["import type { BridgeClient } from \"./bridge.js\";\nimport type { SDKAuth } from \"./types.js\";\n\ninterface EmbedTokenResult {\n // 宿主回包字段名为 embedToken(对齐后端 EmbedTokenResponse.embedToken)\n embedToken: string;\n expiresAt: number | null; // UTC ms;宿主可能回 null\n}\n\nexport class SDKAuthImpl implements SDKAuth {\n private _token: string | null = null;\n private _expiresAt: number | null = null;\n private _refreshTimer: ReturnType | null = null;\n\n public constructor(\n private readonly _bridge: BridgeClient | null,\n private readonly _tokenTimeout: number,\n private readonly _tokenRefreshEarlyMs: number,\n private readonly _onAuthLost: ((reason: string) => void) | undefined,\n ) {}\n\n // ————— SDKAuth 公开 API —————\n\n public getToken(): string | null {\n return this._token;\n }\n\n public getExpiresAt(): number | null {\n return this._expiresAt;\n }\n\n public isAuthenticated(): boolean {\n return !!this._token && !this._isExpired();\n }\n\n // ————— 内部 API —————\n\n /**\n * 初始化鉴权:向 bridge 请求 embed token。\n * guest 模式(无 bridge):直接以匿名模式初始化,不抛错。\n */\n public async init(): Promise {\n if (!this._bridge) {\n // guest 模式:匿名\n return;\n }\n await this._fetchToken();\n }\n\n /**\n * 宿主推送 tokenChanged 事件时调用(清除旧 token 并重新请求)。\n */\n public async handleTokenChanged(): Promise {\n this._clearToken();\n if (!this._bridge) return;\n await this._fetchToken();\n }\n\n public destroy(): void {\n if (this._refreshTimer !== null) {\n clearTimeout(this._refreshTimer);\n this._refreshTimer = null;\n }\n }\n\n // ————— 私有方法 —————\n\n private async _fetchToken(retryCount = 0): Promise {\n if (!this._bridge) return;\n\n try {\n const result = await this._bridge.send(\"getEmbedToken\", undefined, this._tokenTimeout);\n if (!result || !result.embedToken) {\n throw new Error(\"getEmbedToken returned no embedToken\");\n }\n this._token = result.embedToken;\n this._expiresAt = result.expiresAt ?? null;\n this._scheduleRefresh();\n } catch (err) {\n if (retryCount < 1) {\n // exponential backoff:第一次重试等 500ms\n await delay(500 * Math.pow(2, retryCount));\n return this._fetchToken(retryCount + 1);\n }\n // 达到最大重试次数\n this._onAuthLost?.(`Failed to get embed token: ${String(err)}`);\n }\n }\n\n private _scheduleRefresh(): void {\n if (this._refreshTimer !== null) {\n clearTimeout(this._refreshTimer);\n this._refreshTimer = null;\n }\n if (this._expiresAt === null) return;\n\n const now = Date.now();\n const delay = this._expiresAt - now - this._tokenRefreshEarlyMs;\n if (delay <= 0) {\n // 已过期或即将过期,立即重取\n this._fetchToken().catch(() => {});\n return;\n }\n\n this._refreshTimer = setTimeout(() => {\n this._refreshTimer = null;\n this._fetchToken().catch(() => {});\n }, delay);\n }\n\n private _clearToken(): void {\n this._token = null;\n this._expiresAt = null;\n if (this._refreshTimer !== null) {\n clearTimeout(this._refreshTimer);\n this._refreshTimer = null;\n }\n }\n\n private _isExpired(): boolean {\n if (this._expiresAt === null) return false;\n return Date.now() > this._expiresAt;\n }\n}\n\nfunction delay(ms: number): Promise {\n return new Promise((resolve) => {\n setTimeout(resolve, ms);\n });\n}\n","import type { Capability, ClientContext } from \"./types.js\";\n\n/**\n * HTTP API 错误\n * statusCode -1 = 网络错误/解析失败\n */\nexport class TopicApiError extends Error {\n public readonly cause?: unknown;\n\n public constructor(\n public readonly statusCode: number,\n message: string,\n public readonly endpoint: string,\n cause?: unknown,\n ) {\n super(message);\n this.name = \"TopicApiError\";\n if (cause !== undefined) {\n this.cause = cause;\n }\n }\n}\n\n/**\n * frame-bridge 通信错误\n */\nexport class BridgeError extends Error {\n public readonly cause?: unknown;\n\n public constructor(\n public readonly code: \"timeout\" | \"rejected\" | \"origin-mismatch\" | \"method-not-allowed\" | \"parse-error\",\n public readonly method: string,\n public readonly requestId: string,\n cause?: unknown,\n ) {\n super(`Bridge error [${code}] on method ${method}`);\n this.name = \"BridgeError\";\n if (cause !== undefined) {\n this.cause = cause;\n }\n }\n}\n\n/**\n * 能力不支持错误(不静默 no-op,统一抛出)\n */\nexport class UnsupportedError extends Error {\n public constructor(\n public readonly capability: Capability | string,\n public readonly context: ClientContext,\n ) {\n super(`Capability '${capability}' not supported in context '${context}'`);\n this.name = \"UnsupportedError\";\n }\n}\n","import { BridgeError } from \"./errors.js\";\nimport type { HelloResult } from \"./types.js\";\n\n// ————— wire 协议类型 —————\n\ninterface BridgeRequest {\n v: 2;\n id: string;\n method: string;\n params?: unknown;\n}\n\ninterface BridgeResponse {\n v: 2;\n id: string;\n ok: true;\n result: unknown;\n}\n\ninterface BridgeErrorResponse {\n v: 2;\n id: string;\n ok: false;\n error: { code: string; message: string };\n}\n\ninterface BridgeEventMessage {\n v: 2;\n event: \"tokenChanged\" | \"viewport\" | \"back\";\n data: unknown;\n}\n\ntype IncomingMessage = BridgeResponse | BridgeErrorResponse | BridgeEventMessage;\n\n// ————— ID 生成 —————\n\nlet _counter = 0;\nfunction generateId(): string {\n if (typeof crypto !== \"undefined\" && typeof crypto.randomUUID === \"function\") {\n return crypto.randomUUID();\n }\n // counter fallback(极少数旧环境)\n _counter += 1;\n return `sdk-req-${Date.now()}-${_counter}`;\n}\n\n// ————— pending request 类型 —————\n\ninterface PendingRequest {\n resolve: (value: unknown) => void;\n reject: (reason: unknown) => void;\n timer: ReturnType;\n method: string;\n}\n\ntype EventHandler = (data: unknown) => void;\n\n// ————— BridgeClient —————\n\n/**\n * frame-bridge v2 客户端。\n * 负责与宿主(window.parent)通过 postMessage 通信。\n */\nexport class BridgeClient {\n private readonly _pending = new Map();\n private readonly _eventHandlers = new Map>();\n private readonly _defaultTimeout: number;\n private _destroyed = false;\n\n public constructor(defaultTimeout = 3000) {\n this._defaultTimeout = defaultTimeout;\n window.addEventListener(\"message\", this._onMessage);\n }\n\n /**\n * 向宿主发送请求,等待回包。\n */\n public send(method: string, params?: unknown, timeout?: number): Promise {\n return new Promise((resolve, reject) => {\n const id = generateId();\n const effectiveTimeout = timeout ?? this._defaultTimeout;\n\n const timer = setTimeout(() => {\n this._pending.delete(id);\n reject(new BridgeError(\"timeout\", method, id));\n }, effectiveTimeout);\n\n this._pending.set(id, {\n resolve: resolve as (v: unknown) => void,\n reject,\n timer,\n method,\n });\n\n const request: BridgeRequest = { v: 2, id, method, params };\n window.parent.postMessage(request, \"*\");\n });\n }\n\n /**\n * 订阅宿主主动推送的事件。\n */\n public onEvent(event: string, handler: EventHandler): void {\n if (!this._eventHandlers.has(event)) {\n this._eventHandlers.set(event, new Set());\n }\n this._eventHandlers.get(event)!.add(handler);\n }\n\n /**\n * 取消订阅。\n */\n public offEvent(event: string, handler: EventHandler): void {\n this._eventHandlers.get(event)?.delete(handler);\n }\n\n /**\n * hello 握手。\n * 超时时返回 null(表示 guest 模式)。\n */\n public async hello(sdkVersion: string, timeout?: number): Promise {\n try {\n const result = await this.send(\"hello\", { sdkVersion }, timeout);\n return result;\n } catch {\n return null;\n }\n }\n\n /**\n * 销毁,清理所有 listener 和 pending。\n */\n public destroy(): void {\n this._destroyed = true;\n window.removeEventListener(\"message\", this._onMessage);\n\n // 清除所有 pending 请求\n for (const [id, pending] of this._pending) {\n clearTimeout(pending.timer);\n pending.reject(new BridgeError(\"timeout\", pending.method, id));\n }\n this._pending.clear();\n this._eventHandlers.clear();\n }\n\n private _onMessage = (ev: MessageEvent): void => {\n if (this._destroyed) return;\n\n // 只接受来自宿主父窗口的消息,丢弃任意 iframe/opener 伪造的回包/事件\n // (否则可伪造 hello 把 guest 骗成 embedded、或伪造 getEmbedToken 注入假 token)\n if (ev.source !== window.parent) return;\n\n let msg: IncomingMessage;\n try {\n // data 可能已经是对象(同 origin),或是序列化字符串\n msg = typeof ev.data === \"string\" ? (JSON.parse(ev.data) as IncomingMessage) : (ev.data as IncomingMessage);\n } catch {\n return;\n }\n\n if (!msg || msg.v !== 2) return;\n\n // 事件推送(无 id 字段)\n if (\"event\" in msg) {\n const handlers = this._eventHandlers.get(msg.event);\n if (handlers) {\n handlers.forEach((h) => h(msg.data));\n }\n return;\n }\n\n // 请求回包(有 id 字段)\n if (\"id\" in msg) {\n const pending = this._pending.get(msg.id);\n if (!pending) return;\n clearTimeout(pending.timer);\n this._pending.delete(msg.id);\n\n if (msg.ok) {\n pending.resolve(msg.result);\n } else {\n pending.reject(new BridgeError(\"rejected\", pending.method, msg.id));\n }\n }\n };\n}\n","import { TopicApiError } from \"./errors.js\";\nimport type {\n CampaignCard,\n CharacterCard,\n CreatorCard,\n HighlightPage,\n HotStoryCard,\n Leaderboard,\n LoreEvent,\n MyStoryKind,\n Page,\n RankEntity,\n RankWindow,\n SDKActivity,\n SDKAuth,\n SDKRank,\n SDKTopic,\n StoryCard,\n TopicDetail,\n TopicTab,\n WeeklyHottest,\n} from \"./types.js\";\n\n// ————— 内部 fetch 工具 —————\n\nasync function apiFetch(\n baseUrl: string,\n path: string,\n auth: SDKAuth,\n query?: Record,\n): Promise {\n // 构造 URL\n const url = new URL(path, baseUrl);\n if (query) {\n for (const [k, v] of Object.entries(query)) {\n if (v === undefined) continue;\n if (Array.isArray(v)) {\n // 多值参数(如 parentType=oc&parentType=elementum),对齐后端 List[str]\n for (const item of v) {\n url.searchParams.append(k, String(item));\n }\n } else {\n url.searchParams.set(k, String(v));\n }\n }\n }\n\n // 构造 headers\n const headers: Record = {\n \"Content-Type\": \"application/json\",\n };\n const token = auth.getToken();\n if (token) {\n headers[\"x-embed-token\"] = token;\n }\n\n let response: Response;\n try {\n response = await fetch(url.toString(), { method: \"GET\", headers });\n } catch (err) {\n throw new TopicApiError(-1, \"Network error\", path, err);\n }\n\n if (!response.ok) {\n let message = `HTTP ${response.status}`;\n try {\n const body = (await response.json()) as { message?: string };\n if (body.message) message = body.message;\n } catch {\n // ignore JSON parse errors\n }\n throw new TopicApiError(response.status, message, path);\n }\n\n try {\n return (await response.json()) as T;\n } catch (err) {\n throw new TopicApiError(-1, \"JSON parse error\", path, err);\n }\n}\n\n// ————— SDKTopicImpl —————\n\nexport class SDKTopicImpl implements SDKTopic {\n public constructor(\n private readonly _baseUrl: string,\n private readonly _auth: SDKAuth,\n ) {}\n\n public async getDetail(name: string): Promise {\n return apiFetch(this._baseUrl, `/v1/embed/topic/${encodeURIComponent(name)}`, this._auth);\n }\n\n public async listStories(\n name: string,\n query: {\n pageIndex: number;\n pageSize: number;\n sort: \"hot\" | \"like_count\" | \"highlight_mark_time\";\n startTime?: number;\n endTime?: number;\n authorUuid?: string;\n },\n ): Promise> {\n return apiFetch>(this._baseUrl, `/v1/embed/topic/${encodeURIComponent(name)}/stories`, this._auth, {\n pageIndex: query.pageIndex,\n pageSize: query.pageSize,\n sort: query.sort,\n // 可选参数:undefined 时 apiFetch 会跳过,不写入 query。\n startTime: query.startTime,\n endTime: query.endTime,\n authorUuid: query.authorUuid,\n });\n }\n\n public async listMyStories(\n name: string,\n query: {\n kind: MyStoryKind;\n pageIndex?: number;\n pageSize?: number;\n },\n ): Promise> {\n return apiFetch>(\n this._baseUrl,\n `/v1/embed/topic/${encodeURIComponent(name)}/my-stories`,\n this._auth,\n {\n kind: query.kind,\n pageIndex: query.pageIndex,\n pageSize: query.pageSize,\n },\n );\n }\n\n public async listCharacters(\n name: string,\n query: {\n parentType?: string[];\n pageIndex: number;\n pageSize: number;\n sort?: string;\n },\n ): Promise> {\n return apiFetch>(\n this._baseUrl,\n `/v1/embed/topic/${encodeURIComponent(name)}/characters`,\n this._auth,\n {\n parentType: query.parentType,\n pageIndex: query.pageIndex,\n pageSize: query.pageSize,\n sort: query.sort,\n },\n );\n }\n\n public async listCampaigns(\n name: string,\n query: { pageIndex: number; pageSize: number },\n ): Promise> {\n return apiFetch>(\n this._baseUrl,\n `/v1/embed/topic/${encodeURIComponent(name)}/campaigns`,\n this._auth,\n {\n pageIndex: query.pageIndex,\n pageSize: query.pageSize,\n },\n );\n }\n\n public async listLoreEvents(name: string): Promise {\n return apiFetch(this._baseUrl, `/v1/embed/topic/${encodeURIComponent(name)}/lore-events`, this._auth);\n }\n\n public async listHot(name: string): Promise {\n // 后端响应模型为 { stories: HotStoryCard[] }(无分页 bounded 热门列表),此处解包为数组。\n const resp = await apiFetch<{ stories: HotStoryCard[] }>(\n this._baseUrl,\n `/v1/embed/topic/${encodeURIComponent(name)}/hot-stories`,\n this._auth,\n );\n return resp.stories;\n }\n\n public async getWeeklyHottest(name: string): Promise {\n // 后端响应模型为 WeeklyHottest | null(无近 7 天同款 UV 最高作品时返回 null)。\n return apiFetch(\n this._baseUrl,\n `/v1/embed/topic/${encodeURIComponent(name)}/weekly-hottest`,\n this._auth,\n );\n }\n}\n\n// ————— SDKActivityImpl —————\n\nexport class SDKActivityImpl implements SDKActivity {\n public constructor(\n private readonly _baseUrl: string,\n private readonly _auth: SDKAuth,\n ) {}\n\n public async listTabs(uuid: string): Promise {\n return apiFetch(this._baseUrl, `/v1/embed/activity/${encodeURIComponent(uuid)}/tabs`, this._auth);\n }\n\n public async listSelectedStories(\n uuid: string,\n tabKey: string,\n query: {\n pageIndex: number;\n pageSize: number;\n sort?: string;\n },\n ): Promise {\n return apiFetch(\n this._baseUrl,\n `/v1/embed/activity/${encodeURIComponent(uuid)}/tab/${encodeURIComponent(tabKey)}`,\n this._auth,\n {\n pageIndex: query.pageIndex,\n pageSize: query.pageSize,\n sort: query.sort,\n },\n );\n }\n}\n\n// ————— SDKRankImpl —————\n\nexport class SDKRankImpl implements SDKRank {\n public constructor(\n private readonly _baseUrl: string,\n private readonly _auth: SDKAuth,\n ) {}\n\n public async get(\n entity: RankEntity,\n window: RankWindow,\n at: number | \"latest\",\n ): Promise> {\n // oc / elementum 仅支持 latest(后端非 latest 返回 400),提前抛出更友好的错误\n if ((entity === \"oc\" || entity === \"elementum\") && at !== \"latest\") {\n throw new TopicApiError(\n 400,\n `rank entity '${entity}' only supports at='latest'`,\n `/v1/embed/rank/${entity}/${window}/${String(at)}`,\n );\n }\n const atStr = at === \"latest\" ? \"latest\" : String(at);\n return apiFetch>(\n this._baseUrl,\n `/v1/embed/rank/${encodeURIComponent(entity)}/${encodeURIComponent(window)}/${encodeURIComponent(atStr)}`,\n this._auth,\n );\n }\n}\n\n// ————— PageCursor —————\n\nexport class PageCursor {\n private _pageIndex = 0;\n private _hasNext = true;\n private _total: number | null = null;\n\n public constructor(\n private readonly _fetcher: (pageIndex: number, pageSize: number) => Promise>,\n private readonly _pageSize = 20,\n ) {}\n\n public hasNext(): boolean {\n return this._hasNext;\n }\n\n public async next(): Promise {\n if (!this._hasNext) return [];\n const page = await this._fetcher(this._pageIndex, this._pageSize);\n this._total = page.total;\n this._hasNext = page.hasNext;\n this._pageIndex += 1;\n return page.list;\n }\n\n public reset(): void {\n this._pageIndex = 0;\n this._hasNext = true;\n this._total = null;\n }\n\n public currentPage(): number {\n return this._pageIndex;\n }\n\n public totalItems(): number | null {\n return this._total;\n }\n}\n","// ————— 通用 —————\n\nexport interface Page {\n total: number;\n pageIndex: number;\n pageSize: number;\n list: T[];\n /** facade 重算(修复 api_hashtag.py:693 bug) */\n hasNext: boolean;\n}\n\nexport interface RichText {\n contentType: \"text\" | \"html\" | \"markdown\";\n /** html 类型必须服务端消毒(sanitize)后下发 */\n content: string;\n}\n\n// ————— 话题 —————\n\nexport interface TopicTab {\n key: string;\n label: string;\n visible: boolean;\n}\n\nexport interface TopicDetail {\n hashtagName: string;\n kind: \"hashtag\" | \"activity\";\n activityUuid: string | null;\n title: string | null;\n description: RichText | null;\n bannerPic: string | null;\n smallBannerPic: string | null;\n headerPic: string | null;\n phase: \"draft\" | \"preparing\" | \"running\" | \"ended\" | null;\n startTime: number | null; // UTC ms\n endTime: number | null; // UTC ms\n /** 合并 popularity / hashtag_heat 为单字段单语义 */\n heat: number;\n participantsCount: number;\n subscribeCount: number;\n action: {\n visible: boolean;\n label: string | null;\n url: string | null;\n } | null;\n tabs: TopicTab[];\n ruleStoryUuid: string | null;\n /** 登录态填充;匿名/游客(无 embed token)为 null */\n viewer: {\n subscribed: boolean;\n canEdit: boolean;\n } | null;\n}\n\n// ————— 作品卡片 —————\n\nexport interface StoryCard {\n storyId: string;\n title: string | null;\n coverUrl: string | null;\n shareUrl: string | null;\n aspect: string | null;\n author: {\n uuid: string | null;\n nickName: string | null;\n avatarUrl: string | null;\n };\n metrics: {\n likeCount: number;\n sameStyleCount: number;\n picCount: number;\n };\n flags: {\n hasVideo: boolean;\n hasBgm: boolean;\n isInteractive: boolean;\n isPinned: boolean;\n };\n hashtagNames: string[];\n /** 匿名/游客 null */\n viewer: {\n liked: boolean;\n favored: boolean;\n } | null;\n}\n\n// ————— 话题热门流 / 本周最热 —————\n\n/** 话题算法热门流的作品卡片:现有 StoryCard 全字段 + 热度分(已乘 1000 取整)。 */\nexport interface HotStoryCard extends StoryCard {\n /** 热度分(后端 doc.sort[0]*1000 取整),int。 */\n hotScore: number;\n}\n\n/** 本周最热单品(近 7 天同款 UV 最高的一条),无则 null。全 camelCase。 */\nexport interface WeeklyHottest {\n collectionUuid: string;\n title: string;\n coverUrl: string | null;\n sameStyleUv: number;\n creatorName: string;\n creatorUuid: string;\n creatorAvatar: string | null;\n isInteractive: boolean;\n}\n\n// ————— 活动精选 —————\n\nexport interface HighlightPage extends Page {\n /** topList 仅 pageIndex=0 时非空,其余页为 [] */\n topList: StoryCard[];\n}\n\n// ————— 角色卡片 —————\n\nexport interface CharacterCard {\n uuid: string;\n name: string | null;\n coverUrl: string | null;\n type: \"oc\" | \"elementum\" | string;\n author: {\n uuid: string | null;\n nickName: string | null;\n avatarUrl: string | null;\n };\n}\n\n// ————— 活动卡片 —————\n\nexport interface CampaignCard {\n uuid: string;\n title: string | null;\n coverUrl: string | null;\n pv: number;\n}\n\n// ————— 世界观事件 —————\n\nexport interface LoreEvent {\n uuid: string;\n name: string;\n category: string | null;\n description: string | null;\n sortIndex: number;\n collectionCount: number;\n boundTopic: {\n name: string;\n startTime: number | null; // UTC ms\n endTime: number | null; // UTC ms\n } | null;\n stories: StoryCard[];\n}\n\n// ————— 榜单 —————\n\nexport type RankWindow = \"daily\" | \"weekly\" | \"monthly\";\nexport type RankEntity = \"stories\" | \"creators\" | \"oc\" | \"elementum\";\n\nexport interface Leaderboard {\n window: RankWindow;\n entity: RankEntity;\n startTime: number | null; // UTC ms;后端 Optional,可能下发 null\n endTime: number | null; // UTC ms;后端 Optional,可能下发 null\n lastUpdatedAt: number | null;\n list: RankEntry[];\n}\n\nexport interface RankEntry {\n rank: number;\n /** 经 score_multiplier 缩放后的整数 */\n score: number;\n item: T;\n}\n\nexport interface CreatorCard {\n uuid: string | null;\n nickName: string | null;\n avatarUrl: string | null;\n subscriberCount: number;\n storyCount: number;\n topStories: StoryCard[];\n}\n\n// ————— 客户端上下文 —————\n\nexport type ClientContext = \"app\" | \"web-embedded\" | \"guest\";\n\n/** hello 响应中的 client 字段(宿主回传具体平台) */\nexport type BridgeClient = \"ios\" | \"android\" | \"web\";\n\nexport interface HelloResult {\n /** 宿主平台 */\n client: BridgeClient;\n /** App 版本号,Web 宿主为 null */\n appVersion: string | null;\n /** 宿主支持的 bridge method 列表 */\n features: string[];\n}\n\n// ————— 能力位图 —————\n\nexport enum Capability {\n // 数据能力(所有上下文均支持)\n ReadTopic = \"read.topic\",\n ReadStories = \"read.stories\",\n ReadCharacters = \"read.characters\",\n ReadCampaigns = \"read.campaigns\",\n ReadLoreEvents = \"read.loreEvents\",\n ReadActivity = \"read.activity\",\n ReadRank = \"read.rank\",\n\n // 桥接能力(仅 App/Web-embedded)\n Bridge = \"bridge\",\n NavInternal = \"nav.internal\",\n NavExternal = \"nav.external\",\n NavApplyHost = \"nav.applyHost\",\n Toast = \"ui.toast\",\n Viewport = \"ui.viewport\",\n EventBack = \"event.back\",\n EventTokenChanged = \"event.tokenChanged\",\n EventViewport = \"event.viewport\",\n}\n\n// ————— AllowedRoute 白名单 —————\n\n/** AllowedRoute v1 硬编码白名单 */\nexport type AllowedRoute =\n | \"/tag\"\n | \"/topic\"\n | \"/activity\"\n | \"/ranking\"\n | \"/collection/interaction\"\n | \"/oc\"\n | \"/user\"\n | \"/generate\";\n\n// ————— viewport —————\n\nexport interface ViewportInfo {\n /** 嵌入态永远为 0(防双叠加) */\n safeTop: 0;\n safeBottom: number;\n keyboardInset: number;\n width: number;\n height: number;\n}\n\n// ————— SDK 选项 —————\n\nexport interface TopicSDKOptions {\n /** 后端 API 基址,默认 'https://pre.api.talesofai.cn' */\n apiBaseUrl?: string;\n /** hello 握手超时 ms,默认 1500 */\n helloTimeout?: number;\n /** getEmbedToken bridge 调用超时 ms,默认 3000 */\n tokenTimeout?: number;\n /** embed token 过期前多久触发 re-exchange,ms,默认 5 * 60 * 1000 */\n tokenRefreshEarlyMs?: number;\n /** token 无法恢复时的回调 */\n onAuthLost?: (reason: string) => void;\n}\n\n// ————— 公开 API 接口 —————\n\nexport interface SDKAuth {\n /** 当前 embed token,匿名时为 null */\n getToken(): string | null;\n /** token 过期时间(UTC ms),匿名时为 null */\n getExpiresAt(): number | null;\n /** 是否已登录(有有效 token) */\n isAuthenticated(): boolean;\n}\n\n/** listMyStories 的 kind:favored=当前 user 收藏的本话题作品;created=当前 user 在本话题投稿的作品;liked=当前 user 点赞的本话题作品。 */\nexport type MyStoryKind = \"favored\" | \"created\" | \"liked\";\n\nexport interface SDKTopic {\n /** GET /v1/embed/topic/{name} */\n getDetail(name: string): Promise;\n\n /** GET /v1/embed/topic/{name}/stories */\n listStories(\n name: string,\n query: {\n pageIndex: number;\n pageSize: number;\n sort: \"hot\" | \"like_count\" | \"highlight_mark_time\";\n /** 可选时间窗(UTC ms),透传后端 start_time/end_time */\n startTime?: number;\n /** 可选时间窗(UTC ms),透传后端 start_time/end_time */\n endTime?: number;\n /** 可选作者过滤,透传后端 authorUuid */\n authorUuid?: string;\n },\n ): Promise>;\n\n /**\n * GET /v1/embed/topic/{name}/my-stories\n * 当前 embed user 在本话题下的作品:kind=favored(收藏)/ created(投稿)/ liked(点赞)。\n * 匿名(无 embed token)返回空 Page(list=[]、hasNext=false),不抛错、不 401。\n */\n listMyStories(\n name: string,\n query: {\n kind: MyStoryKind;\n pageIndex?: number;\n pageSize?: number;\n },\n ): Promise>;\n\n /** GET /v1/embed/topic/{name}/characters */\n listCharacters(\n name: string,\n query: {\n /** 后端为 List[str](alias parentType),多值。默认 [\"oc\",\"elementum\"] */\n parentType?: string[];\n pageIndex: number;\n pageSize: number;\n sort?: string;\n },\n ): Promise>;\n\n /** GET /v1/embed/topic/{name}/campaigns */\n listCampaigns(name: string, query: { pageIndex: number; pageSize: number }): Promise>;\n\n /** GET /v1/embed/topic/{name}/lore-events */\n listLoreEvents(name: string): Promise;\n\n /**\n * GET /v1/embed/topic/{name}/hot-stories\n * 话题算法热门流(bounded 热门列表,无分页),每条带 hotScore。\n */\n listHot(name: string): Promise;\n\n /**\n * GET /v1/embed/topic/{name}/weekly-hottest\n * 本周最热单品(近 7 天同款 UV 最高的一条),无则 null。\n */\n getWeeklyHottest(name: string): Promise;\n}\n\nexport interface SDKActivity {\n /** GET /v1/embed/activity/{uuid}/tabs */\n listTabs(uuid: string): Promise;\n\n /**\n * GET /v1/embed/activity/{uuid}/tab/{tabKey}\n * HighlightPage.topList 仅 pageIndex=0 时非空\n */\n listSelectedStories(\n uuid: string,\n tabKey: string,\n query: {\n pageIndex: number;\n pageSize: number;\n sort?: string;\n },\n ): Promise;\n}\n\nexport interface SDKRank {\n /**\n * GET /v1/embed/rank/{entity}/{window}/{at}\n * entity 直接作为路径段传给后端(facade 内部映射到 collection/user/tcp-oc/tcp-elementum,对外保持 stories/creators/oc/elementum)。\n * at: UTC ms 或字符串 'latest'。\n * 注意:oc / elementum 仅支持 at='latest',传时间戳后端返回 400(SDK 侧亦会提前抛错)。\n */\n get(\n entity: RankEntity,\n window: RankWindow,\n at: number | \"latest\",\n ): Promise>;\n}\n\nexport interface SDKNav {\n /**\n * 跳进产品内页面(AllowedRoute v1 白名单)。\n *\n * 参数契约按路由性质分两类(SDK 运行期兜底 + 此处类型收窄,缺参构建期/运行期都会被拦):\n * - **自指路由** `/topic` `/tag` `/activity`:参数=「当前这个」,可省略,SDK 自动从当前页\n * `?hashtag=` / `?activity_uuid=` 填入(创作者可显式覆盖)。`/ranking` `/generate` 无需参数。\n * - **per-item 路由** `/oc` `/user` `/collection/interaction`:参数指向「具体某个」实体,\n * **必须传 `uuid`**(来自被点卡片的数据,SDK 无从代填)。\n *\n * guest 上下文(仅本地 dev 无宿主时)本地 dev 无宿主仅 console 提示,不跳转;生产入口恒为宿主内嵌,唤起 App 由宿主承载。\n */\n internal(\n route: \"/topic\" | \"/tag\" | \"/activity\" | \"/ranking\" | \"/generate\",\n query?: Record,\n ): Promise;\n internal(\n route: \"/oc\" | \"/user\" | \"/collection/interaction\",\n query: { uuid: string | number } & Record,\n ): Promise;\n\n /**\n * 跳外部 URL。\n * embedded: bridge nav.external\n * guest: window.open(url, '_blank')\n */\n external(url: string): Promise;\n\n /**\n * 申请创建/主持一个新话题活动空间:跳转到运营配置的申请表单(飞书多维表单),宿主自动带上\n * 当前登录用户昵称/UID 的 prefill 参数(不含话题名)。**不接受参数**——SDK/页面拿不到、也不经手\n * 这些用户数据,全由宿主本地态直接拼 URL(与 `getEmbedToken` 不下发宿主 token 同一不变量)。\n *\n * 运营未配置申请表单时抛 `BridgeError`(宿主报 not-configured)。\n * guest 上下文(仅本地 dev 无宿主)本地不跳转,仅 console 提示;生产入口恒为宿主内嵌。\n */\n applyHost(): Promise;\n}\n\nexport interface SDKUi {\n /**\n * 宿主 Toast(embedded)。\n * guest / unsupported:抛 UnsupportedError。\n */\n toast(text: string, options?: { duration?: number; level?: \"info\" | \"warn\" }): Promise;\n\n /**\n * 当前 viewport 安全区信息。\n * guest:抛 UnsupportedError。\n */\n viewport(): Promise;\n}\n\nexport interface SDKEvents {\n on(event: \"tokenChanged\", handler: (newToken: string | null) => void): () => void;\n on(event: \"viewport\", handler: (info: ViewportInfo) => void): () => void;\n on(event: \"back\", handler: (ev: { preventDefault(): void }) => void): () => void;\n off(event: string, handler: (...args: unknown[]) => void): void;\n}\n\n// ————— SDK 顶层 —————\n\nexport interface TopicSDK {\n env: {\n context: ClientContext;\n embedded: boolean;\n client: BridgeClient | \"unknown\";\n appVersion: string | null;\n features: string[];\n };\n auth: SDKAuth;\n topic: SDKTopic;\n activity: SDKActivity;\n rank: SDKRank;\n nav: SDKNav;\n ui: SDKUi;\n events: SDKEvents;\n can(cap: Capability): boolean;\n destroy(): void;\n}\n","import type { BridgeClient } from \"./bridge.js\";\nimport { UnsupportedError } from \"./errors.js\";\nimport type { ClientContext, HelloResult } from \"./types.js\";\nimport { Capability } from \"./types.js\";\n\nexport interface EnvResult {\n context: ClientContext;\n embedded: boolean;\n client: \"ios\" | \"android\" | \"web\" | \"unknown\";\n appVersion: string | null;\n features: string[];\n hello: HelloResult | null;\n /** 宿主注入的 ?activity_uuid=(无则 null)。决定 ReadActivity 能力是否授予(纯 hashtag 话题无活动数据)。 */\n activityUuid: string | null;\n}\n\nexport async function detectEnv(bridge: BridgeClient, sdkVersion: string, helloTimeout: number): Promise {\n const ua = navigator.userAgent;\n // 宿主把绑定的 activity_uuid 作为 query 注入 iframe(无则纯 hashtag 话题)。\n const activityUuid =\n typeof window !== \"undefined\" ? new URLSearchParams(window.location.search).get(\"activity_uuid\") || null : null;\n\n // UA 预判:小程序直接出局\n if (/miniProgram/i.test(ua)) {\n throw new UnsupportedError(\"weapp-not-supported\", \"guest\" as ClientContext);\n }\n\n // hello 握手(超时→ null = guest)\n const hello = await bridge.hello(sdkVersion, helloTimeout);\n\n if (!hello) {\n // 握手超时 → guest\n return {\n context: \"guest\",\n embedded: false,\n client: \"unknown\",\n appVersion: null,\n features: [],\n hello: null,\n activityUuid,\n };\n }\n\n // 宿主回包确认客户端类型(最终以握手结果为准,UA 仅用于 miniProgram 预判出局)\n const context: ClientContext = hello.client === \"ios\" || hello.client === \"android\" ? \"app\" : \"web-embedded\";\n\n return {\n context,\n embedded: true,\n client: hello.client,\n appVersion: hello.appVersion,\n features: hello.features,\n hello,\n activityUuid,\n };\n}\n\n/**\n * 根据 EnvResult 计算支持的 Capability 集合。\n */\nexport function buildCapabilities(env: EnvResult): Set {\n const caps = new Set();\n\n // 数据能力:所有上下文均支持\n caps.add(Capability.ReadTopic);\n caps.add(Capability.ReadStories);\n caps.add(Capability.ReadCharacters);\n caps.add(Capability.ReadCampaigns);\n caps.add(Capability.ReadLoreEvents);\n caps.add(Capability.ReadRank);\n // ReadActivity 仅当话题绑定了 activity(宿主注入 ?activity_uuid=)才授予;纯 hashtag 话题无活动数据,\n // 不报 ReadActivity,避免 can(ReadActivity) 恒 true 误导创作者渲染空活动区块。\n if (env.activityUuid) {\n caps.add(Capability.ReadActivity);\n }\n\n if (env.context !== \"guest\") {\n // 桥接能力(仅 App/Web-embedded)\n caps.add(Capability.Bridge);\n caps.add(Capability.NavInternal);\n caps.add(Capability.NavExternal);\n caps.add(Capability.NavApplyHost);\n caps.add(Capability.Toast);\n caps.add(Capability.Viewport);\n caps.add(Capability.EventBack);\n caps.add(Capability.EventTokenChanged);\n caps.add(Capability.EventViewport);\n }\n // guest(仅本地 dev 无宿主可达):仅只读数据能力;唤起 App 统一靠宿主 nav.internal,不再暴露独立 OpenApp 能力。\n\n return caps;\n}\n","import type { BridgeClient } from \"./bridge.js\";\nimport type { SDKEvents, ViewportInfo } from \"./types.js\";\n\ntype TokenChangedHandler = (newToken: string | null) => void;\ntype ViewportHandler = (info: ViewportInfo) => void;\ntype BackHandler = (ev: { preventDefault(): void }) => void;\ntype AnyHandler = TokenChangedHandler | ViewportHandler | BackHandler;\n\ninterface HandlerEntry {\n event: string;\n handler: AnyHandler;\n bridgeHandler: (data: unknown) => void;\n}\n\nexport class SDKEventsImpl implements SDKEvents {\n private readonly _entries: HandlerEntry[] = [];\n // tokenChanged 不直接订阅 bridge:宿主事件里 token 恒为 null,\n // 真正的新 token 由 SDK 内部 re-exchange 完成后经 notifyTokenChanged 下发。\n private readonly _tokenChangedHandlers = new Set();\n\n public constructor(private readonly _bridge: BridgeClient | null) {}\n\n public on(event: \"tokenChanged\", handler: TokenChangedHandler): () => void;\n public on(event: \"viewport\", handler: ViewportHandler): () => void;\n public on(event: \"back\", handler: BackHandler): () => void;\n public on(event: string, handler: AnyHandler): () => void {\n // tokenChanged:与 bridge 事件解耦,由 SDK 内部 re-exchange 后回调新 token\n if (event === \"tokenChanged\") {\n const h = handler as TokenChangedHandler;\n this._tokenChangedHandlers.add(h);\n return () => {\n this._tokenChangedHandlers.delete(h);\n };\n }\n\n if (!this._bridge) {\n // guest 模式:无宿主事件,返回空 unsubscribe\n return () => {};\n }\n\n let bridgeHandler: (data: unknown) => void;\n\n switch (event) {\n case \"viewport\": {\n const h = handler as ViewportHandler;\n bridgeHandler = (data: unknown) => {\n const raw = data as {\n safeTop?: number;\n safeBottom?: number;\n keyboardInset?: number;\n width?: number;\n height?: number;\n };\n h({\n safeTop: 0, // 固定 0,防双叠加\n safeBottom: raw.safeBottom ?? 0,\n keyboardInset: raw.keyboardInset ?? 0,\n width: raw.width ?? window.innerWidth,\n height: raw.height ?? window.innerHeight,\n });\n };\n break;\n }\n case \"back\": {\n const h = handler as BackHandler;\n bridgeHandler = (_data: unknown) => {\n let _defaultPrevented = false;\n h({\n preventDefault() {\n _defaultPrevented = true;\n },\n });\n // SDK 不需要把 defaultPrevented 回传给宿主;\n // 宿主侧设计为\"等待一帧确认\"(H8),此处不需要额外处理\n };\n break;\n }\n default:\n return () => {};\n }\n\n this._bridge.onEvent(event, bridgeHandler);\n const entry: HandlerEntry = { event, handler, bridgeHandler };\n this._entries.push(entry);\n\n return () => {\n this.off(event, handler as (...args: unknown[]) => void);\n };\n }\n\n /**\n * 由 SDK 内部在 re-exchange 完成后调用,向消费方下发最新 token。\n * 宿主 tokenChanged 事件本身不携带 token(恒 null),故新值取自 auth 当前 token。\n */\n public notifyTokenChanged(token: string | null): void {\n this._tokenChangedHandlers.forEach((h) => h(token));\n }\n\n public off(event: string, handler: (...args: unknown[]) => void): void {\n if (event === \"tokenChanged\") {\n this._tokenChangedHandlers.delete(handler as unknown as TokenChangedHandler);\n return;\n }\n const idx = this._entries.findIndex((e) => e.event === event && e.handler === handler);\n if (idx === -1) return;\n const entry = this._entries[idx]!;\n this._bridge?.offEvent(event, entry.bridgeHandler);\n this._entries.splice(idx, 1);\n }\n\n public destroy(): void {\n for (const entry of this._entries) {\n this._bridge?.offEvent(entry.event, entry.bridgeHandler);\n }\n this._entries.length = 0;\n this._tokenChangedHandlers.clear();\n }\n}\n","import type { BridgeClient } from \"./bridge.js\";\nimport { UnsupportedError } from \"./errors.js\";\nimport type { AllowedRoute, ClientContext, SDKNav } from \"./types.js\";\n\n/**\n * 自指路由:参数=「当前这个」,缺省时 SDK 从当前页 URL 自动填(创作者可显式覆盖)。\n * 宿主把 hashtag / activity_uuid 作为 query 注入 iframe src,故内嵌页 `location.search` 必有这些值。\n */\nconst SELF_PARAM_FROM_URL: Partial> = {\n \"/topic\": { param: \"hashtag\", urlKey: \"hashtag\" },\n \"/tag\": { param: \"hashtag\", urlKey: \"hashtag\" },\n \"/activity\": { param: \"uuid\", urlKey: \"activity_uuid\" },\n};\n\n/** per-item 路由:参数指向「具体某个」实体(来自被点卡片),SDK 无从代填 → 缺则抛错打回。 */\nconst REQUIRED_PARAMS: Partial> = {\n \"/oc\": [\"uuid\"],\n \"/user\": [\"uuid\"],\n \"/collection/interaction\": [\"uuid\"],\n};\n\nconst isBlank = (v: unknown): boolean => v === undefined || v === null || v === \"\";\n\nexport class SDKNavImpl implements SDKNav {\n public constructor(\n private readonly _bridge: BridgeClient | null,\n private readonly _context: ClientContext,\n ) {}\n\n /**\n * 解析最终 query:自指路由缺参从 URL 自动填;per-item 路由缺必需参数则抛错(开发期就被打回,而非线上白屏)。\n */\n private _resolveQuery(route: AllowedRoute, query?: Record): Record {\n const q: Record = { ...(query ?? {}) };\n\n // 1) 自指路由:缺参从当前页 URL 自动填\n const selfRef = SELF_PARAM_FROM_URL[route];\n if (selfRef && isBlank(q[selfRef.param]) && typeof window !== \"undefined\") {\n const fromUrl = new URLSearchParams(window.location.search).get(selfRef.urlKey);\n if (fromUrl) q[selfRef.param] = fromUrl;\n }\n\n // 2) 必需参数校验(自指填完后再查)\n const required = REQUIRED_PARAMS[route] ?? (selfRef ? [selfRef.param] : []);\n for (const p of required) {\n if (isBlank(q[p])) {\n const hint = selfRef\n ? `该自指路由通常由 SDK 从当前页 ?${selfRef.urlKey}= 自动填,但当前 URL 没有该值`\n : `该路由指向具体实体,请从被点卡片数据传入 ${p}(如 ${p}: story.uuid)`;\n throw new Error(`[topic-sdk] nav.internal('${route}') 缺少必需参数 '${p}'。${hint}。`);\n }\n }\n return q;\n }\n\n public async internal(route: AllowedRoute, query?: Record): Promise {\n const effectiveQuery = this._resolveQuery(route, query);\n if (this._context === \"guest\") {\n // guest(仅本地 dev 无宿主):_resolveQuery 已先跑(保留缺参 throw);无宿主不跳转,生产由宿主唤起 / 站内跳。\n console.info(\"[topic-sdk] nav.internal(route) 本地 dev 无宿主不跳转;生产由宿主唤起/站内跳\");\n return;\n }\n if (!this._bridge) {\n throw new UnsupportedError(\"nav.internal\", this._context);\n }\n await this._bridge.send(\"nav.internal\", { route, query: effectiveQuery });\n }\n\n public async external(url: string): Promise {\n if (this._context === \"guest\") {\n window.open(url, \"_blank\");\n return;\n }\n if (!this._bridge) {\n throw new UnsupportedError(\"nav.external\", this._context);\n }\n await this._bridge.send(\"nav.external\", { url });\n }\n\n public async applyHost(): Promise {\n if (this._context === \"guest\") {\n // guest(仅本地 dev 无宿主):拼 URL 需要的用户态/运营配置只有宿主本地有,dev 无宿主不跳转;生产由宿主承载。\n console.info(\"[topic-sdk] nav.applyHost() 本地 dev 无宿主不跳转;生产由宿主用本地用户态拼 URL 后跳转\");\n return;\n }\n if (!this._bridge) {\n throw new UnsupportedError(\"nav.applyHost\", this._context);\n }\n await this._bridge.send(\"nav.applyHost\");\n }\n}\n","import type { BridgeClient } from \"./bridge.js\";\nimport { UnsupportedError } from \"./errors.js\";\nimport type { ClientContext, SDKUi, ViewportInfo } from \"./types.js\";\n\nexport class SDKUiImpl implements SDKUi {\n public constructor(\n private readonly _bridge: BridgeClient | null,\n private readonly _context: ClientContext,\n ) {}\n\n public async toast(text: string, options?: { duration?: number; level?: \"info\" | \"warn\" }): Promise {\n if (this._context === \"guest\" || !this._bridge) {\n throw new UnsupportedError(\"ui.toast\", this._context);\n }\n await this._bridge.send(\"ui.toast\", {\n text,\n duration: options?.duration,\n level: options?.level ?? \"info\",\n });\n }\n\n public async viewport(): Promise {\n if (this._context === \"guest\" || !this._bridge) {\n throw new UnsupportedError(\"ui.viewport\", this._context);\n }\n const raw = await this._bridge.send<{\n safeTop?: number;\n safeBottom?: number;\n keyboardInset?: number;\n width?: number;\n height?: number;\n }>(\"ui.viewport\");\n\n // safeTop 固定返回 0(防双叠加,宿主已 safeTop:true/page.tsx:39)\n // 其余字段对宿主漏传做兜底,与 events.ts viewport 分支保持一致\n return {\n safeTop: 0,\n safeBottom: raw.safeBottom ?? 0,\n keyboardInset: raw.keyboardInset ?? 0,\n width: raw.width ?? window.innerWidth,\n height: raw.height ?? window.innerHeight,\n };\n }\n}\n","/**\n * OSS 图片处理参数化:拼接 `x-oss-process`(resize/format/quality)。\n * 与 talesofai 其余前端(weapp/event/bff 的 getImageLink)同一套 OSS 图片处理约定,\n * 移植到这里是为了让内嵌页也别再把原图尺寸直出——不同卡片位置渲染宽度不同,\n * 高分屏(devicePixelRatio > 1)不按比例多请求像素会糊,全量按比例请求又会浪费流量。\n */\n\nconst DEFAULT_QUALITY = 80;\n/** 设备像素比 clamp 上限:4x/5x 屏再按比例放大意义不大,只会白白增加流量。 */\nconst MAX_DPR = 3;\n\nexport interface OssImageOptions {\n /** 目标 CSS 展示宽度(px)。省略则不做 resize,只处理 format/quality。 */\n width?: number;\n /** 设备像素比倍率;省略则取 `window.devicePixelRatio`(非浏览器环境兜底 1),并 clamp 到 [1, MAX_DPR]。 */\n dpr?: number;\n /** 输出质量 1-100,默认 80(与其余前端一致)。 */\n quality?: number;\n /** 是否转 webp,默认 true。 */\n webp?: boolean;\n}\n\nfunction resolveDpr(explicit?: number): number {\n if (typeof explicit === \"number\" && explicit > 0) return Math.min(explicit, MAX_DPR);\n const detected = typeof window !== \"undefined\" && typeof window.devicePixelRatio === \"number\" ? window.devicePixelRatio : 1;\n return Math.min(Math.max(detected, 1), MAX_DPR);\n}\n\nfunction buildOssProcess(pixelWidth: number | undefined, quality: number, webp: boolean): string {\n const styles: string[] = [\"image\", \"auto-orient,1\"];\n if (pixelWidth && pixelWidth > 0) styles.push(`resize,m_lfit,w_${Math.round(pixelWidth)}`);\n if (webp) styles.push(\"format,webp\");\n styles.push(`quality,q_${quality}`);\n return styles.join(\"/\");\n}\n\n/**\n * 按目标展示宽度 + 设备像素比拼接 OSS 图片处理参数。\n * 卡片封面/头像等按实际渲染宽度传 `width`(如缩略图 200、大图 750),SDK 按屏幕 dpr 换算成实取像素宽。\n * 非 http(s)(如 `data:` 内联图)解析失败时原样返回,不强行拼参数——`data:` URL 拼 `?x-oss-process=`\n * 会把 base64 payload 直接拼坏。已经带 `x-oss-process` 的 URL(后端预处理过 / 重复调用)也原样返回,\n * 不再叠加第二个同名 query key(OSS 对重复 key 的解析行为未定义)。\n */\nexport function ossImage(source: string | null | undefined, options?: OssImageOptions): string | null {\n if (!source) return null;\n let url: URL;\n try {\n url = new URL(source);\n } catch {\n return source;\n }\n if (url.protocol !== \"http:\" && url.protocol !== \"https:\") return source;\n if (url.searchParams.has(\"x-oss-process\")) return source;\n const { width, quality = DEFAULT_QUALITY, webp = true, dpr } = options ?? {};\n const pixelWidth = width && width > 0 ? width * resolveDpr(dpr) : undefined;\n url.searchParams.set(\"x-oss-process\", buildOssProcess(pixelWidth, quality, webp));\n return url.toString();\n}\n\n/**\n * 生成 1x/2x/3x 三档 `srcset`(配合 `` 用),让浏览器按实际设备像素比自己选图。\n * `width` 是 1x(CSS px)基准宽度;2x/3x 档在此基础上按比例放大取图。\n */\nexport function ossImageSrcSet(\n source: string | null | undefined,\n width: number,\n options?: Omit,\n): string | null {\n if (!source) return null;\n const scales = [1, 2, 3];\n return scales.map((scale) => `${ossImage(source, { ...options, width, dpr: scale })} ${scale}x`).join(\", \");\n}\n","import { SDKAuthImpl } from \"./auth.js\";\nimport { BridgeClient } from \"./bridge.js\";\nimport { SDKActivityImpl, SDKRankImpl, SDKTopicImpl } from \"./data.js\";\nimport { buildCapabilities, detectEnv } from \"./env.js\";\nimport { SDKEventsImpl } from \"./events.js\";\nimport { SDKNavImpl } from \"./nav.js\";\nimport type { AllowedRoute, Capability, SDKAuth, TopicSDK, TopicSDKOptions } from \"./types.js\";\nimport { SDKUiImpl } from \"./ui.js\";\n\nexport type {\n AllowedRoute,\n BridgeClient as BridgeClientType,\n // 活动\n CampaignCard,\n // 角色\n CharacterCard,\n // 环境/能力\n ClientContext,\n CreatorCard,\n HelloResult,\n HighlightPage,\n Leaderboard,\n // 世界观\n LoreEvent,\n MyStoryKind,\n // 通用\n Page,\n RankEntity,\n RankEntry,\n // 榜单\n RankWindow,\n RichText,\n SDKActivity,\n // 接口\n SDKAuth,\n SDKEvents,\n SDKNav,\n SDKRank,\n SDKTopic,\n SDKUi,\n // 作品\n StoryCard,\n TopicDetail,\n TopicSDK,\n TopicSDKOptions,\n // 话题\n TopicTab,\n ViewportInfo,\n} from \"./types.js\";\n\nexport { BridgeClient } from \"./bridge.js\";\nexport { PageCursor } from \"./data.js\";\nexport { BridgeError, TopicApiError, UnsupportedError } from \"./errors.js\";\nexport { ossImage, ossImageSrcSet } from \"./image.js\";\nexport type { OssImageOptions } from \"./image.js\";\nexport { Capability } from \"./types.js\";\n\nconst SDK_VERSION = \"0.1.0\";\n\n/**\n * 全局拦截 点击。内嵌页跑在 sandbox iframe(无 allow-top-navigation)里,原生 /相对路径跳转会逃逸到\n * iframe 自身源(OSS)而非宿主 App。故在捕获阶段接管:\n * - 同源路径跳转 → nav.internal(命中 AllowedRoute 由宿主导航/游客转深链;未命中被宿主拦截、不逃逸);\n * - 跨站外链 → nav.external(宿主用系统浏览器打开);\n * - 同 path 仅改 hash(页面内视图切换)、target=_blank、download、非 http(s) scheme → 放行。\n * 这样创作者即便写了原生链接,也不会把 iframe 跳出内嵌页。返回卸载函数。\n */\nfunction installLinkInterceptor(nav: SDKNavImpl): () => void {\n const onClick = (e: MouseEvent) => {\n if (e.defaultPrevented || e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;\n const targetEl = e.target instanceof Element ? e.target : ((e.target as Node | null)?.parentElement ?? null);\n const a = targetEl?.closest(\"a\") as HTMLAnchorElement | null;\n if (!a) return;\n const href = a.getAttribute(\"href\");\n if (!href) return;\n const target = a.getAttribute(\"target\");\n if (target && target !== \"_self\") return;\n if (a.hasAttribute(\"download\")) return;\n let url: URL;\n try {\n url = new URL(href, location.href);\n } catch {\n return;\n }\n if (url.protocol !== \"http:\" && url.protocol !== \"https:\") return;\n if (url.origin === location.origin) {\n // 同 path 仅改 hash(页面内视图切换)→ 放行,交给页面自己处理\n if (url.pathname === location.pathname && url.search === location.search && url.hash) return;\n const query: Record = {};\n url.searchParams.forEach((v, k) => {\n query[k] = v;\n });\n e.preventDefault();\n nav.internal(url.pathname as AllowedRoute, query).catch(() => {});\n return;\n }\n e.preventDefault();\n nav.external(url.href).catch(() => {});\n };\n document.addEventListener(\"click\", onClick, true);\n return () => document.removeEventListener(\"click\", onClick, true);\n}\n\n/**\n * 守卫 history.pushState/replaceState。内嵌页禁用(会污染 Android WebView 返回栈,返回键先消费 iframe history),\n * 应改用 hash 路由 / 内存路由。embedded 下安装,违例当场 throw(dev 期即暴露);destroy 恢复原函数。\n */\nfunction installPushStateGuard(): () => void {\n if (typeof history === \"undefined\") return () => {};\n const origPush = history.pushState.bind(history);\n const origReplace = history.replaceState.bind(history);\n const guard = (method: string) => () => {\n const msg =\n `[topic-sdk] 内嵌页禁止 history.${method}(会污染 App 返回栈);` +\n `请改用 hash 路由(location.hash)或内存路由做页面内视图切换。`;\n console.error(msg);\n throw new Error(msg);\n };\n history.pushState = guard(\"pushState\") as typeof history.pushState;\n history.replaceState = guard(\"replaceState\") as typeof history.replaceState;\n return () => {\n history.pushState = origPush;\n history.replaceState = origReplace;\n };\n}\n\n/**\n * 初始化并返回 TopicSDK 实例。\n *\n * 初始化序列:\n * ① 端探测(UA 嗅探预判)\n * ② hello 握手(超时→ guest)\n * ③ 鉴权(embedded:bridge getEmbedToken;guest:匿名)\n * ④ 能力协商(填充 Capability 位图)\n * ⑤ 事件订阅(tokenChanged / viewport / back)\n */\nexport async function createTopicSDK(options: TopicSDKOptions = {}): Promise {\n const {\n apiBaseUrl = \"https://pre.api.talesofai.cn\",\n helloTimeout = 1500,\n tokenTimeout = 3000,\n tokenRefreshEarlyMs = 5 * 60 * 1000,\n onAuthLost,\n } = options;\n\n // tokenTimeout 下界 clamp:500ms 是 v1 bridge 已知坏值;低于 1000ms 上调并告警(SKILL 固定 3000)。\n let effectiveTokenTimeout = tokenTimeout;\n if (effectiveTokenTimeout < 1000) {\n console.warn(\n `[topic-sdk] tokenTimeout ${effectiveTokenTimeout}ms 低于下限 1000ms(500ms 是 v1 bridge 已知坏值),已上调到 1000ms。`,\n );\n effectiveTokenTimeout = 1000;\n }\n\n // ① + ② 端探测 + hello 握手\n // BridgeClient は常に作成するが、guest の場合はすぐ破棄する\n const bridge = new BridgeClient(effectiveTokenTimeout);\n const env = await detectEnv(bridge, SDK_VERSION, helloTimeout);\n\n // embedded 上下文:守卫 history.pushState/replaceState(内嵌页禁用,会污染 App 返回栈);违例当场 throw,dev 期即暴露。\n const removePushStateGuard = env.embedded ? installPushStateGuard() : () => {};\n\n // guest の場合は bridge を使わない\n const activeBridge = env.context === \"guest\" ? null : bridge;\n if (env.context === \"guest\") {\n // guest 模式:bridge 不再需要,但保留 message listener 无害(destroy 可选)\n // 为节约资源,立即销毁\n bridge.destroy();\n }\n\n // ③ 鉴权\n const auth = new SDKAuthImpl(activeBridge, effectiveTokenTimeout, tokenRefreshEarlyMs, onAuthLost);\n await auth.init();\n\n // ④ 能力协商\n const capabilities = buildCapabilities(env);\n\n // ⑤ 事件订阅:tokenChanged → 自动重取 token\n const eventsImpl = new SDKEventsImpl(activeBridge);\n\n if (activeBridge) {\n activeBridge.onEvent(\"tokenChanged\", (_data: unknown) => {\n // 宿主事件不携带新 token(恒 null),SDK 内部 re-exchange 后再把真实 token 下发给消费方\n auth\n .handleTokenChanged()\n .then(() => eventsImpl.notifyTokenChanged(auth.getToken()))\n .catch(() => eventsImpl.notifyTokenChanged(null));\n });\n }\n\n // ————— 构造子模块 —————\n const topicImpl = new SDKTopicImpl(apiBaseUrl, auth as SDKAuth);\n const activityImpl = new SDKActivityImpl(apiBaseUrl, auth as SDKAuth);\n const rankImpl = new SDKRankImpl(apiBaseUrl, auth as SDKAuth);\n const navImpl = new SDKNavImpl(activeBridge, env.context);\n const uiImpl = new SDKUiImpl(activeBridge, env.context);\n\n // 根治\"原生 /相对跳转在 sandbox iframe 内逃逸到 OSS 源\"的问题:全局接管 点击,改走 bridge 导航。\n const removeLinkInterceptor = installLinkInterceptor(navImpl);\n\n // ————— TopicSDK 对象 —————\n const sdk: TopicSDK = {\n env: {\n context: env.context,\n embedded: env.embedded,\n client: env.client,\n appVersion: env.appVersion,\n features: env.features,\n },\n\n auth: auth as SDKAuth,\n topic: topicImpl,\n activity: activityImpl,\n rank: rankImpl,\n nav: navImpl,\n ui: uiImpl,\n events: eventsImpl,\n\n can(cap: Capability): boolean {\n return capabilities.has(cap);\n },\n\n destroy(): void {\n removeLinkInterceptor();\n removePushStateGuard();\n auth.destroy();\n eventsImpl.destroy();\n activeBridge?.destroy();\n },\n };\n\n return sdk;\n}\n"],"mappings":"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","names":["delay","window","Capability"]} \ No newline at end of file diff --git a/skill-internal-publish/SKILL.md b/skill-internal-publish/SKILL.md index 0fc76a6..4feee37 100644 --- a/skill-internal-publish/SKILL.md +++ b/skill-internal-publish/SKILL.md @@ -12,6 +12,18 @@ is_internal: true # nieta-app 内嵌话题页 — 内部上线 runbook +## ⚠ 开工前必做:自我更新(不允许跳过) + +每次执行本 runbook、开始任何实际操作之前,先确认手上这份 `skill-internal-publish/`(来自 `topic-sdk` 仓库根目录)不是过期副本: + +> **例外**:如果你正在对 `topic-sdk` 仓库本身开发/提交代码,你就是版本源头,跳过下面这一步。 + +- 本目录是 git 仓库、且当前在 `main` 分支:`git pull --ff-only origin main`。拉不动 → 停下来向用户说明情况,不要在不确定的版本上对 prod 环境操作。当前不在 `main`:`git fetch origin main` 看落后多少,不要擅自切分支/强制同步。 +- 本目录是从别处复制来的纯拷贝:去 `https://github.com/talesofai/topic-sdk` 重新拉取 `skill-internal-publish/` 最新内容覆盖本地(仅覆盖这个目录自身文件,不动你正在操作的创作者项目里的 `.env`)。 +- **没有"看起来没变就跳过"这种例外**——每次都要重新做一次。这个 runbook 操作的是 prod 上线(`target=prod`/`activate`/`unbind`),权限校验和流程细节随时可能改动,用过期版本操作 prod 的风险比多花几秒钟高得多。 + +--- + 本 runbook 供**内部运营(`is_internal` 账号)**使用。创作者完成开发、发草稿并完成 dev 真机调试后,由内部团队执行本流程将页面正式上线(激活)。 > **权限说明**:`target=prod`(上线)/ `activate`(切换已有草稿为 active)/ `unbind`(下线)仅允许 `is_internal` 完整登录态,scoped dev 令牌和创作者账号会被后端直接拒绝(403)。请务必使用**内部有权限的账号**(`is_internal=true`)。 diff --git a/skill/SKILL.md b/skill/SKILL.md index ea41b06..8e1a2c6 100644 --- a/skill/SKILL.md +++ b/skill/SKILL.md @@ -13,6 +13,19 @@ description: >- # 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`(**交付前必须逐项过**)。 @@ -84,8 +97,9 @@ description: >- - `listCharacters` 的 `parentType` 是 `string[]`(省略时后端默认 `['oc','elementum']`)。 - `sdk.rank.get(entity, window, at)`:`oc`/`elementum` **只支持 `at='latest'`**,传时间戳会抛错。 - 分页用 `page.hasNext` 判断是否还有下一页(不要自己用 total 推算)。 +- **`coverUrl`/`avatarUrl` 一律用 `ossImage(url, { width })` 包一层再塞进 ``**(`width` 传该卡片实际渲染宽度,不是原图宽度),不要把原图直出。详见 cheatsheet「图片」一节的推荐宽度参考值。 -**校验门**:`getDetail` + `listStories` 能渲染;对所有可空字段已判空(grep 一遍 `.uuid`/`.aspect`/`.startTime` 的使用点)。 +**校验门**:`getDetail` + `listStories` 能渲染;对所有可空字段已判空(grep 一遍 `.uuid`/`.aspect`/`.startTime` 的使用点);所有图片字段都经过 `ossImage` 处理(grep 一遍 `coverUrl`/`avatarUrl` 的使用点,确认没有裸传进 ``)。 ## 5. 导航(唯一漏斗:nav.internal) diff --git a/skill/assets/scaffold/src/App.tsx b/skill/assets/scaffold/src/App.tsx index 361d447..77e782b 100644 --- a/skill/assets/scaffold/src/App.tsx +++ b/skill/assets/scaffold/src/App.tsx @@ -1,5 +1,5 @@ import { useEffect, useState } from "react"; -import { type StoryCard, type TopicDetail, type TopicSDK } from "@talesofai/topic-sdk"; +import { ossImage, type StoryCard, type TopicDetail, type TopicSDK } from "@talesofai/topic-sdk"; import { getHashtag, getSdk } from "./sdk"; /** @@ -83,7 +83,8 @@ export function App() { > {s.coverUrl && ( {s.title?...w_340 1x, ?...w_680 2x, ?...w_1020 3x" +``` + +- `width` 传**卡片实际渲染的 CSS 宽度**(不是原图宽度);SDK 按屏幕 `devicePixelRatio`(clamp 到 3x)自动换算成应取的像素宽,不用自己算。 +- 所有 `coverUrl` / `avatarUrl` 字段都是 OSS 直出图,**务必用 `ossImage` 包一层**再塞进 ``;纯展示图不需要原图分辨率,直出既拖慢加载也浪费流量。 +- 常见位置参考宽度(CSS px,按实际布局调整):瀑布流/列表卡片封面 300–400、头像 40–80、Banner/大图 750。 +- 返回 `null`(输入为 `null`/`undefined` 时);`` 上用 `?? undefined` 兜底。 +- 与其余产品线(weapp/event/bff 的 `getImageLink`)同一套 OSS 处理参数约定,不是内嵌页独有的新概念。 + ## 导航 — AllowedRoute v1 白名单 `sdk.nav.internal(route, query?)` 的 route 必须 ∈下表。**参数契约按路由性质分两类,传错/漏传会被 SDK 拦下(构建期类型 + 运行期 throw),不会再静默白屏**: diff --git a/src/image.ts b/src/image.ts new file mode 100644 index 0000000..b6a564c --- /dev/null +++ b/src/image.ts @@ -0,0 +1,72 @@ +/** + * OSS 图片处理参数化:拼接 `x-oss-process`(resize/format/quality)。 + * 与 talesofai 其余前端(weapp/event/bff 的 getImageLink)同一套 OSS 图片处理约定, + * 移植到这里是为了让内嵌页也别再把原图尺寸直出——不同卡片位置渲染宽度不同, + * 高分屏(devicePixelRatio > 1)不按比例多请求像素会糊,全量按比例请求又会浪费流量。 + */ + +const DEFAULT_QUALITY = 80; +/** 设备像素比 clamp 上限:4x/5x 屏再按比例放大意义不大,只会白白增加流量。 */ +const MAX_DPR = 3; + +export interface OssImageOptions { + /** 目标 CSS 展示宽度(px)。省略则不做 resize,只处理 format/quality。 */ + width?: number; + /** 设备像素比倍率;省略则取 `window.devicePixelRatio`(非浏览器环境兜底 1),并 clamp 到 [1, MAX_DPR]。 */ + dpr?: number; + /** 输出质量 1-100,默认 80(与其余前端一致)。 */ + quality?: number; + /** 是否转 webp,默认 true。 */ + webp?: boolean; +} + +function resolveDpr(explicit?: number): number { + if (typeof explicit === "number" && explicit > 0) return Math.min(explicit, MAX_DPR); + const detected = typeof window !== "undefined" && typeof window.devicePixelRatio === "number" ? window.devicePixelRatio : 1; + return Math.min(Math.max(detected, 1), MAX_DPR); +} + +function buildOssProcess(pixelWidth: number | undefined, quality: number, webp: boolean): string { + const styles: string[] = ["image", "auto-orient,1"]; + if (pixelWidth && pixelWidth > 0) styles.push(`resize,m_lfit,w_${Math.round(pixelWidth)}`); + if (webp) styles.push("format,webp"); + styles.push(`quality,q_${quality}`); + return styles.join("/"); +} + +/** + * 按目标展示宽度 + 设备像素比拼接 OSS 图片处理参数。 + * 卡片封面/头像等按实际渲染宽度传 `width`(如缩略图 200、大图 750),SDK 按屏幕 dpr 换算成实取像素宽。 + * 非 http(s)(如 `data:` 内联图)解析失败时原样返回,不强行拼参数——`data:` URL 拼 `?x-oss-process=` + * 会把 base64 payload 直接拼坏。已经带 `x-oss-process` 的 URL(后端预处理过 / 重复调用)也原样返回, + * 不再叠加第二个同名 query key(OSS 对重复 key 的解析行为未定义)。 + */ +export function ossImage(source: string | null | undefined, options?: OssImageOptions): string | null { + if (!source) return null; + let url: URL; + try { + url = new URL(source); + } catch { + return source; + } + if (url.protocol !== "http:" && url.protocol !== "https:") return source; + if (url.searchParams.has("x-oss-process")) return source; + const { width, quality = DEFAULT_QUALITY, webp = true, dpr } = options ?? {}; + const pixelWidth = width && width > 0 ? width * resolveDpr(dpr) : undefined; + url.searchParams.set("x-oss-process", buildOssProcess(pixelWidth, quality, webp)); + return url.toString(); +} + +/** + * 生成 1x/2x/3x 三档 `srcset`(配合 `` 用),让浏览器按实际设备像素比自己选图。 + * `width` 是 1x(CSS px)基准宽度;2x/3x 档在此基础上按比例放大取图。 + */ +export function ossImageSrcSet( + source: string | null | undefined, + width: number, + options?: Omit, +): string | null { + if (!source) return null; + const scales = [1, 2, 3]; + return scales.map((scale) => `${ossImage(source, { ...options, width, dpr: scale })} ${scale}x`).join(", "); +} diff --git a/src/index.ts b/src/index.ts index d7e2114..bb8edd4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -51,6 +51,8 @@ export type { export { BridgeClient } from "./bridge.js"; export { PageCursor } from "./data.js"; export { BridgeError, TopicApiError, UnsupportedError } from "./errors.js"; +export { ossImage, ossImageSrcSet } from "./image.js"; +export type { OssImageOptions } from "./image.js"; export { Capability } from "./types.js"; const SDK_VERSION = "0.1.0";