扩展宿主调用链扫描
自动发现 H5 HostBridge 真实能力调用链 补齐 runtime 回读与 wrapper 消费者扫描兜底 区分壳源码和 H5 业务调用链的禁替身词表 将视觉小说生产预览数据从 mock 命名收口为本地预览命名 更新原生壳验收文档和共享决策记录
This commit is contained in:
@@ -29,7 +29,7 @@
|
||||
- 2026-06-18 宿主 runtime 回读:主 App 启动时会通过真实 `host.getRuntime` 回读 Expo / Tauri runtime 并缓存过滤后的能力清单,能力来源为 URL `hostCapabilities` 与宿主真实回包的并集;裁剪壳或旧入口 URL 缺少 `hostCapabilities` 时也能启用真实声明能力,但仍不会仅凭 `native_app` 或 transport 存在推断能力可用。
|
||||
- 2026-06-18 壳能力防漂移:`npm run mobile-shell:typecheck` 与 `npm run desktop-shell:typecheck` 会校验 Expo / Tauri 壳声明的 capability 均来自共享 HostBridge 白名单,并校验壳 runtime 回包、H5 URL `hostCapabilities` 和实现分支保持一致;微信小程序 `WECHAT_HOST_CAPABILITIES` 也由 `miniprogram/host-bridge/protocol.test.js` 反查同一共享白名单。新增能力必须先更新契约和真实壳实现,再通过这些检查。
|
||||
- 2026-06-18 原生壳统一验收门禁:根级 `npm run check:native-shells` 统一执行 H5 HostBridge 关键测试、Expo 壳 typecheck / test / config smoke / Metro export smoke、Tauri 壳 typecheck / cargo test、桌面 release `--no-bundle` 构建烟测,以及可分发壳与 H5 HostBridge 真实调用链的临时替身词扫描;根级 `npm run check` 会在 lint、主站测试、构建和内容检查后继续执行该门禁,避免 HostBridge、三端壳、Expo managed config、移动端 production bundle、桌面 release 入口和 H5 HostBridge 真实调用链禁替身验收散落成容易漏跑的单项命令。
|
||||
- 2026-06-19 原生壳临时替身扫描范围:`npm run check:native-shells` 的生产替身词扫描必须覆盖微信小程序壳生产 `.js`、Expo / Tauri 壳源码与配置、共享 HostBridge 契约、H5 native transport,以及已接入的 H5 直接调用链文件;新增 H5 调用点接入 HostBridge 时,必须同步把对应文件纳入扫描或在同等门禁中证明生产代码没有 mock / fake / placeholder / TODO / FIXME / 占位 / 伪造。
|
||||
- 2026-06-19 原生壳临时替身扫描范围:`npm run check:native-shells` 的生产替身词扫描必须覆盖微信小程序壳生产 `.js`、Expo / Tauri 壳源码与配置、共享 HostBridge 契约、H5 native transport,并自动覆盖已接入真实宿主能力 facade 的 H5 生产调用链文件;新增 H5 调用点接入 HostBridge 时,必须让自动扫描覆盖对应文件或在同等门禁中证明生产代码没有 mock / fake / stub / TODO / FIXME / 模拟 / 伪造。
|
||||
- 2026-06-18 微信壳桥接层纳入统一验收:`npm run check:native-shells` 还会运行 `miniprogram/host-bridge/`、`miniprogram/shell/`、`pages/web-view` 样式和 `scripts/miniprogram-web-view-auth.test.ts` 的微信壳测试,覆盖 WebView 入口、登录触发、分享目标、支付结果、订阅消息结果和九宫切图行为;三端桥接层文件结构检查只证明目录边界,行为回归必须由同一门禁中的微信壳测试证明。
|
||||
- 2026-06-18 登录 / 支付能力禁伪声明:`auth.requestLogin` 和 `payment.request` 保留在共享 HostBridge 契约中供未来真实接入,但 Expo / Tauri 壳在真实 SDK、渠道流程和后端契约落地前不得声明这些 capability,也不得把它们写入入口 URL `hostCapabilities`;两端检查脚本会拒绝伪声明,请求实际到达壳层时必须返回明确 `unsupported_method` 并让 H5 fallback,两端壳测试直接覆盖这两个 method。
|
||||
- 2026-06-18 移动壳触觉反馈边界:`haptics.impact` 只接受 `light`、`medium`、`heavy` 三档 impact style,缺省为 `light`;未知值必须返回 `invalid_request`,不得静默降级成真实设备触觉反馈。桌面壳不声明该 capability,H5 继续按 HostBridge fallback 处理。
|
||||
@@ -90,6 +90,7 @@
|
||||
- 2026-06-18 HostBridge capability / handler 关系门禁:两端壳声明 request method capability 时必须有对应 HostBridge handler;壳 handler 处理的 method 必须已被该壳声明,登录 / 支付等 SDK-backed method 只能保留明确 `unsupported_method` 路径。事件类 capability 不要求 request handler。
|
||||
- 2026-06-18 桌面壳 CSP 分层:Tauri release `csp` 不得包含 `http://127.0.0.1:*`、`ws://127.0.0.1:*` 或其它本机调试源,本机 Vite、HMR WebSocket 和开发 frame 只允许出现在 `devCsp`。桌面壳配置检查会同时拒绝 release CSP 混入本机调试源、dev CSP 缺失本机开发源,以及 release / dev CSP 加入 `unsafe-eval`、`tauri:` 或 `file:`。
|
||||
- 2026-06-18 壳生产代码禁用临时替身:微信 / Expo / Tauri 三端壳的生产源码和配置不得出现 mock / fake / placeholder / stub / TODO / FIXME 以及对应中文脚手架词;测试文件仍可使用 mock。两端原生壳配置检查会扫描生产入口、配置和壳实现,根级 `npm run check:native-shells` 会统一扫描 `miniprogram`、`apps/mobile-shell`、`apps/desktop-shell`、H5 HostBridge transport 和共享 HostBridge 契约生产源码,防止把临时替身、占位文案或伪实现带进可分发壳或真实调用链。
|
||||
- 2026-06-19 H5 HostBridge 调用链自动扫描:根级 `npm run check:native-shells` 从 `src/` 生产文件自动收集真实宿主能力 facade 的直接消费者,以及 `useHostLifecycleActive`、`useHostNetworkOnline`、`platformProfileHostClipboard` 等薄 wrapper 的消费者。H5 业务文件允许正常表单 `placeholder` 属性和业务占位图文案,但不得出现 mock / fake / stub / TODO / FIXME / 模拟 / 伪造等替身痕迹;壳源码和配置仍继续禁用 placeholder / 占位。
|
||||
- 2026-06-18 原生壳本地生成物边界:Expo `.expo/`、Expo export smoke 临时目录、Tauri `target/`、Tauri schema `gen/` 和 Tauri 自动生成权限目录都必须保持 gitignored,不作为生产源码敏感词扫描输入;手写 capability / 权限配置仍在扫描范围内。
|
||||
- 2026-06-18 移动壳启动页与 adaptive icon:Expo 移动壳启动页和 Android adaptive icon 复用现有真实品牌图标 `apps/mobile-shell/assets/icon.png`,背景色固定为 H5 壳根背景 `#fffdf9`。该 PNG 是 1024x1024 RGBA 透明前景品牌资产,不新增占位图;配置检查会校验图标尺寸、透明像素、splash 和 adaptive icon 指向,避免后续换成非品牌或占位素材。
|
||||
- 2026-06-18 桌面壳 bundle 图标集:Tauri 桌面壳从现有真实品牌 PNG `apps/desktop-shell/src-tauri/icons/icon.png` 派生 `32x32.png`、`128x128.png`、`128x128@2x.png`、`icon.ico` 和 `icon.icns`,并在 `bundle.icon` 中同时声明这些平台图标。检查脚本会校验 PNG 尺寸、ICO 多尺寸头部、ICNS 容器长度和 bundle 图标列表,避免后续退回单图标或替换为非品牌 / 占位素材。
|
||||
|
||||
@@ -216,7 +216,7 @@ npm run build
|
||||
npm run check:native-shells
|
||||
```
|
||||
|
||||
该命令会覆盖 H5 HostBridge 关键测试、微信 / Expo / Tauri 三端桥接层文件结构门禁、Expo 壳 typecheck / test / config smoke / Metro export smoke、Tauri 壳 typecheck / cargo test、桌面壳 release `--no-bundle` 构建烟测,以及可分发壳与 H5 HostBridge 真实调用链的临时替身词扫描,确认 Expo managed config、移动端 iOS / Android production bundle、打包 H5 资产、Tauri release 入口和 H5 HostBridge 真实调用链没有漂移;扫描范围包含微信小程序壳生产 `.js`、共享 HostBridge 契约、H5 native transport 和已接入的 H5 直接调用链文件。
|
||||
该命令会覆盖 H5 HostBridge 关键测试、微信 / Expo / Tauri 三端桥接层文件结构门禁、Expo 壳 typecheck / test / config smoke / Metro export smoke、Tauri 壳 typecheck / cargo test、桌面壳 release `--no-bundle` 构建烟测,以及可分发壳与 H5 HostBridge 真实调用链的临时替身词扫描,确认 Expo managed config、移动端 iOS / Android production bundle、打包 H5 资产、Tauri release 入口和 H5 HostBridge 真实调用链没有漂移;扫描范围包含微信小程序壳生产 `.js`、共享 HostBridge 契约、H5 native transport,并自动覆盖已接入真实宿主能力 facade 的 H5 生产调用链文件。壳源码和配置继续严格禁止 mock / fake / placeholder / stub / TODO / FIXME / 占位 / 模拟 / 伪造;H5 业务调用链允许正常表单 `placeholder` 属性和业务占位图文案,但仍禁止 mock / fake / stub / TODO / FIXME / 模拟 / 伪造等替身痕迹。
|
||||
该命令同时会运行微信小程序 `miniprogram/host-bridge/`、`miniprogram/shell/`、`pages/web-view` 样式和 `scripts/miniprogram-web-view-auth.test.ts` 的壳层测试,保证微信桥接层拆分后的支付、订阅消息、九宫切图、分享目标和 WebView 登录 / 分享入口行为与 Expo、Tauri 壳一起验收。
|
||||
|
||||
内容检查:
|
||||
|
||||
@@ -441,7 +441,9 @@ GameBridge 禁止:
|
||||
|
||||
2026-06-18 追加:登录 / 支付能力伪声明进入门禁。`auth.requestLogin` 和 `payment.request` 保留在共享 HostBridge 契约中供未来真实接入,但 Expo 与 Tauri 壳在没有真实 SDK、渠道流程和后端契约前不得声明这些 capability,也不得把它们写入 `hostCapabilities`;请求到达壳层时必须返回明确 `unsupported_method`,让 H5 fallback 承接。两端壳测试会直接覆盖这两个 method,避免后续半接入时返回伪成功。
|
||||
|
||||
2026-06-18 追加:微信 / Expo / Tauri 三端壳的生产源码和配置禁止出现 mock / fake / placeholder / stub / TODO / FIXME 以及对应中文脚手架词;测试文件仍可使用 `vi.mock` 或等价测试替身。`apps/mobile-shell/scripts/check-config.mjs` 与 `apps/desktop-shell/scripts/check-config.mjs` 会扫描各自生产入口、配置和壳实现;根级 `npm run check:native-shells` 会统一扫描 `miniprogram`、`apps/mobile-shell`、`apps/desktop-shell`、H5 HostBridge transport 和共享 HostBridge 契约生产源码,防止把临时替身或占位文案带进可分发壳或真实调用链。
|
||||
2026-06-18 追加:微信 / Expo / Tauri 三端壳的生产源码和配置禁止出现 mock / fake / placeholder / stub / TODO / FIXME 以及对应中文脚手架词;测试文件仍可使用 `vi.mock` 或等价测试替身。`apps/mobile-shell/scripts/check-config.mjs` 与 `apps/desktop-shell/scripts/check-config.mjs` 会扫描各自生产入口、配置和壳实现;根级 `npm run check:native-shells` 会统一扫描 `miniprogram`、`apps/mobile-shell`、`apps/desktop-shell`、H5 HostBridge transport 和共享 HostBridge 契约生产源码,防止把临时替身或占位文案带进可分发壳。
|
||||
|
||||
2026-06-19 追加:H5 HostBridge 真实调用链扫描改为自动发现。根级 `npm run check:native-shells` 会从 `src/` 生产文件里收集直接导入并调用真实宿主能力 facade 的文件,以及 `useHostLifecycleActive`、`useHostNetworkOnline`、`platformProfileHostClipboard` 等薄 wrapper 的消费者;新增 `reloadHostWebView`、`showHostLocalNotification`、`setHostAppBadgeCount`、`importHostTextFile`、`importHostImageFile`、`captureHostImageFile`、`subscribeHostImageDrop`、`readHostClipboardText`、`openHostExternalUrl` 等 H5 调用点时,不再手工维护单个扫描文件清单。H5 业务文件允许正常表单 `placeholder` 属性和业务占位图文案,但仍会拒绝 mock / fake / stub / TODO / FIXME / 模拟 / 伪造等替身痕迹。
|
||||
|
||||
2026-06-18 追加:微信壳行为测试进入原生壳统一验收。`npm run check:native-shells` 会运行 `miniprogram/host-bridge/`、`miniprogram/shell/`、`pages/web-view` 样式和 `scripts/miniprogram-web-view-auth.test.ts` 测试,覆盖微信 WebView 入口、登录触发、分享目标、支付结果、订阅消息结果和九宫切图桥接行为;三端桥接层文件结构检查只锁定职责边界,真实行为回归必须由同一门禁中的微信壳测试证明。
|
||||
|
||||
@@ -491,7 +493,7 @@ GameBridge 禁止:
|
||||
- AI sandbox 无法调用 HostBridge,也无法读取 H5 登录态。
|
||||
- Tauri release 包不允许任意远端页面调用桌面命令。
|
||||
- Expo WebView 外链离开主站后不保留完整 HostBridge。
|
||||
- 根级验收入口 `npm run check:native-shells` 必须同时覆盖 H5 HostBridge 关键路径、三端桥接层文件结构门禁、Expo 壳 typecheck / test / config smoke / Metro export smoke、Tauri 壳 typecheck / cargo test、桌面 release `--no-bundle` 构建烟测,以及可分发壳与 H5 HostBridge 真实调用链的临时替身词扫描。该扫描范围必须包含微信小程序壳生产 `.js`、共享 HostBridge 契约、H5 native transport 和已接入的 H5 直接调用链文件。
|
||||
- 根级验收入口 `npm run check:native-shells` 必须同时覆盖 H5 HostBridge 关键路径、三端桥接层文件结构门禁、Expo 壳 typecheck / test / config smoke / Metro export smoke、Tauri 壳 typecheck / cargo test、桌面 release `--no-bundle` 构建烟测,以及可分发壳与 H5 HostBridge 真实调用链的临时替身词扫描。该扫描范围必须包含微信小程序壳生产 `.js`、共享 HostBridge 契约、H5 native transport,并自动覆盖已接入真实宿主能力 facade 的 H5 生产调用链文件。
|
||||
|
||||
## 参考资料
|
||||
|
||||
|
||||
@@ -83,7 +83,7 @@ HostBridge 事件名以 `packages/shared/src/contracts/hostBridge.ts` 的 `HOST_
|
||||
- 小程序支付仍跳转 `/pages/wechat-pay/index` 并保留支付结果 hash 回灌确认。
|
||||
- 小程序订阅授权仍跳转 `/pages/subscribe-message/index`,且返回不阻断生成主链路。
|
||||
- 普通浏览器分享、H5 支付和 Native 二维码支付不受影响。
|
||||
- 原生壳统一验收入口 `npm run check:native-shells` 通过,能力白名单、共享 capability profile、HostBridge event 白名单、壳 runtime 回包、URL `hostCapabilities`、H5 fallback、三端桥接层结构、两端壳实现、Expo managed config、移动端 production bundle、桌面 release 构建入口,以及可分发壳与 H5 HostBridge 真实调用链的临时替身词扫描没有漂移;扫描范围包含微信小程序壳生产 `.js`、共享 HostBridge 契约、H5 native transport 和已接入的 H5 直接调用链文件。
|
||||
- 原生壳统一验收入口 `npm run check:native-shells` 通过,能力白名单、共享 capability profile、HostBridge event 白名单、壳 runtime 回包、URL `hostCapabilities`、H5 fallback、三端桥接层结构、两端壳实现、Expo managed config、移动端 production bundle、桌面 release 构建入口,以及可分发壳与 H5 HostBridge 真实调用链的临时替身词扫描没有漂移;扫描范围包含微信小程序壳生产 `.js`、共享 HostBridge 契约、H5 native transport,并自动覆盖已接入真实宿主能力 facade 的 H5 生产调用链文件。H5 业务文件允许正常表单 `placeholder` 属性和业务占位图文案,但不得出现 mock / fake / stub / TODO / FIXME / 模拟 / 伪造等替身痕迹。
|
||||
|
||||
## 后续
|
||||
|
||||
|
||||
+233
-11
@@ -20,15 +20,74 @@ const productionShellScanRoots = [
|
||||
'packages/shared/src/contracts/hostBridge.ts',
|
||||
'src/services/host-bridge',
|
||||
];
|
||||
const h5HostBridgeCallChainScanFiles = [
|
||||
'src/services/clipboard.ts',
|
||||
'src/services/appTitle.ts',
|
||||
'src/services/runtimeAudioFeedback.ts',
|
||||
const h5HostBridgeFacadeModule = 'src/services/host-bridge/hostBridge';
|
||||
const h5HostBridgeScannedFacadeImports = new Set([
|
||||
'canUseHostShareGrid',
|
||||
'captureHostImageFile',
|
||||
'exportHostAudioFile',
|
||||
'exportHostImageFile',
|
||||
'exportHostTextFile',
|
||||
'getHostAppearanceColorScheme',
|
||||
'getHostNetworkStatus',
|
||||
'getNativeAppHostRuntime',
|
||||
'importHostAudioFile',
|
||||
'importHostImageFile',
|
||||
'importHostTextFile',
|
||||
'navigateHostNativePage',
|
||||
'openHostExternalUrl',
|
||||
'openHostShare',
|
||||
'openHostShareGrid',
|
||||
'readHostClipboardText',
|
||||
'refreshNativeAppHostRuntime',
|
||||
'reloadHostWebView',
|
||||
'requestHostHapticsImpact',
|
||||
'requestHostLogin',
|
||||
'requestHostPayment',
|
||||
'setHostAppBadgeCount',
|
||||
'setHostAppTitle',
|
||||
'setHostShareTarget',
|
||||
'showHostLocalNotification',
|
||||
'subscribeHostAppLifecycle',
|
||||
'subscribeHostImageDrop',
|
||||
'subscribeHostNavigationCanGoBack',
|
||||
'subscribeHostNetworkStatusChange',
|
||||
'subscribeHostRuntimeChange',
|
||||
'writeHostClipboardText',
|
||||
]);
|
||||
const h5HostBridgeCallChainWrapperFiles = [
|
||||
'src/hooks/useHostLifecycleActive.ts',
|
||||
'src/hooks/useHostNetworkOnline.ts',
|
||||
'src/components/platform-entry/platformProfileHostClipboard.ts',
|
||||
];
|
||||
const h5HostBridgeRequiredCallChainFiles = [
|
||||
'src/App.tsx',
|
||||
'src/components/auth/AuthGate.tsx',
|
||||
'src/components/common/CreativeAudioInputPanel.tsx',
|
||||
'src/components/common/CreativeImageInputPanel.tsx',
|
||||
'src/components/common/PublishShareModal.tsx',
|
||||
'src/components/common/publishShareCardImage.ts',
|
||||
'src/components/common/CreativeAudioInputPanel.tsx',
|
||||
'src/components/creation-agent/CreationAgentWorkspace.tsx',
|
||||
'src/components/match3d-runtime/Match3DRuntimeShell.tsx',
|
||||
'src/components/platform-entry/PlatformEntryFlowShellImpl.tsx',
|
||||
'src/components/platform-entry/PlatformFeedbackView.tsx',
|
||||
'src/components/platform-entry/PlatformProfilePrimitives.tsx',
|
||||
'src/components/platform-entry/PlatformProfileReferralModal.tsx',
|
||||
'src/components/platform-entry/PlatformProfileRewardCodeRedeemModal.tsx',
|
||||
'src/components/platform-entry/usePlatformProfileCenterController.ts',
|
||||
'src/components/puzzle-runtime/PuzzleRuntimeShell.tsx',
|
||||
'src/components/rpg-creation-result/RpgCreationAssetDebugPanel.tsx',
|
||||
'src/components/rpg-entry/RpgEntryHomeView.tsx',
|
||||
'src/components/square-hole-result/SquareHoleResultView.tsx',
|
||||
'src/hooks/useBackgroundMusic.ts',
|
||||
'src/hooks/useHostLifecycleActive.ts',
|
||||
'src/hooks/useHostNetworkOnline.ts',
|
||||
'src/main.tsx',
|
||||
'src/services/appTitle.ts',
|
||||
'src/services/clipboard.ts',
|
||||
'src/services/runtimeAudioFeedback.ts',
|
||||
'src/services/wechatMiniProgramShareTarget.ts',
|
||||
'src/services/wechatMiniProgramSubscribe.ts',
|
||||
'src/services/wechatMiniProgramShareGrid.ts',
|
||||
];
|
||||
const expectedWechatHostBridgeFiles = [
|
||||
'dispatch.js',
|
||||
@@ -126,6 +185,7 @@ const productionShellExtensions = new Set([
|
||||
'.wxml',
|
||||
'.wxss',
|
||||
]);
|
||||
const h5ProductionSourceExtensions = new Set(['.js', '.jsx', '.ts', '.tsx']);
|
||||
const productionShellExcludedSegments = new Set([
|
||||
'.expo',
|
||||
'.expo-export-smoke',
|
||||
@@ -148,6 +208,15 @@ const productionShellDevScaffoldTerms = [
|
||||
'模' + '拟',
|
||||
'伪' + '造',
|
||||
];
|
||||
const h5HostBridgeCallChainDevScaffoldTerms = [
|
||||
'mo' + 'ck',
|
||||
'fa' + 'ke',
|
||||
'st' + 'ub',
|
||||
'TO' + 'DO',
|
||||
'FIX' + 'ME',
|
||||
'模' + '拟',
|
||||
'伪' + '造',
|
||||
];
|
||||
|
||||
const h5HostBridgeTests = [
|
||||
'packages/shared/src/contracts/hostBridge.test.ts',
|
||||
@@ -234,6 +303,19 @@ function shouldScanProductionShellFile(filePath) {
|
||||
return productionShellExtensions.has(path.extname(filePath));
|
||||
}
|
||||
|
||||
function shouldScanH5ProductionSourceFile(filePath) {
|
||||
const normalizedPath = filePath.split(path.sep).join('/');
|
||||
if (
|
||||
normalizedPath.includes('.test.') ||
|
||||
normalizedPath.includes('/test-utils/') ||
|
||||
normalizedPath.includes('/services/host-bridge/')
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return h5ProductionSourceExtensions.has(path.extname(filePath));
|
||||
}
|
||||
|
||||
function collectProductionShellFiles(entryPath) {
|
||||
const normalizedPath = entryPath.split(path.sep).join('/');
|
||||
if (productionShellExcludedPaths.has(normalizedPath)) {
|
||||
@@ -259,16 +341,145 @@ function collectProductionShellFiles(entryPath) {
|
||||
return shouldScanProductionShellFile(entryPath) ? [entryPath] : [];
|
||||
}
|
||||
|
||||
function assertNoProductionShellDevScaffoldTerms() {
|
||||
const files = [
|
||||
...productionShellScanRoots,
|
||||
...h5HostBridgeCallChainScanFiles,
|
||||
].flatMap(collectProductionShellFiles);
|
||||
function collectFiles(entryPath, shouldIncludeFile) {
|
||||
if (!fs.existsSync(entryPath)) {
|
||||
throw new Error(`scan path does not exist: ${entryPath}`);
|
||||
}
|
||||
|
||||
const stats = fs.statSync(entryPath);
|
||||
if (stats.isDirectory()) {
|
||||
const name = path.basename(entryPath);
|
||||
if (productionShellExcludedSegments.has(name)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return fs
|
||||
.readdirSync(entryPath, { withFileTypes: true })
|
||||
.flatMap((entry) => collectFiles(path.join(entryPath, entry.name), shouldIncludeFile));
|
||||
}
|
||||
|
||||
return shouldIncludeFile(entryPath) ? [entryPath] : [];
|
||||
}
|
||||
|
||||
function normalizeModulePath(modulePath) {
|
||||
return modulePath.split(path.sep).join('/').replace(/\.(jsx?|tsx?)$/, '');
|
||||
}
|
||||
|
||||
function importedModulePath(fromFile, specifier) {
|
||||
if (specifier === '@') {
|
||||
return '.';
|
||||
}
|
||||
if (specifier.startsWith('@/')) {
|
||||
return normalizeModulePath(specifier.slice(2));
|
||||
}
|
||||
if (!specifier.startsWith('.')) {
|
||||
return specifier;
|
||||
}
|
||||
|
||||
return normalizeModulePath(path.normalize(path.join(path.dirname(fromFile), specifier)));
|
||||
}
|
||||
|
||||
function extractImportSpecifiers(source) {
|
||||
const specifiers = [];
|
||||
for (const match of source.matchAll(/import\s+(?:type\s+)?[\s\S]*?\s+from\s+['"]([^'"]+)['"]/g)) {
|
||||
specifiers.push(match[1]);
|
||||
}
|
||||
for (const match of source.matchAll(/export\s+(?:type\s+)?\{[\s\S]*?\}\s+from\s+['"]([^'"]+)['"]/g)) {
|
||||
specifiers.push(match[1]);
|
||||
}
|
||||
for (const match of source.matchAll(/import\s*['"]([^'"]+)['"]/g)) {
|
||||
specifiers.push(match[1]);
|
||||
}
|
||||
|
||||
return specifiers;
|
||||
}
|
||||
|
||||
function extractNamedImportsFromModule(source, fromFile, targetModule) {
|
||||
const importedNames = new Set();
|
||||
const importPattern =
|
||||
/import\s+(?:type\s+)?(?:\{([\s\S]*?)\}|[A-Za-z0-9_$]+\s*,\s*\{([\s\S]*?)\})\s+from\s+['"]([^'"]+)['"]/g;
|
||||
const exportPattern =
|
||||
/export\s+(?:type\s+)?\{([\s\S]*?)\}\s+from\s+['"]([^'"]+)['"]/g;
|
||||
const collectNamedImports = (importBody) => {
|
||||
for (const rawImport of importBody.split(',')) {
|
||||
const cleanedImport = rawImport
|
||||
.replace(/\btype\s+/g, '')
|
||||
.trim();
|
||||
if (!cleanedImport) {
|
||||
continue;
|
||||
}
|
||||
importedNames.add(cleanedImport.split(/\s+as\s+/)[0]?.trim() ?? '');
|
||||
}
|
||||
};
|
||||
|
||||
for (const match of source.matchAll(importPattern)) {
|
||||
if (importedModulePath(fromFile, match[3]) !== targetModule) {
|
||||
continue;
|
||||
}
|
||||
|
||||
collectNamedImports(match[1] ?? match[2] ?? '');
|
||||
}
|
||||
|
||||
for (const match of source.matchAll(exportPattern)) {
|
||||
if (importedModulePath(fromFile, match[2]) !== targetModule) {
|
||||
continue;
|
||||
}
|
||||
|
||||
collectNamedImports(match[1] ?? '');
|
||||
}
|
||||
|
||||
return importedNames;
|
||||
}
|
||||
|
||||
function collectH5HostBridgeCallChainFiles() {
|
||||
const sourceFiles = collectFiles('src', shouldScanH5ProductionSourceFile);
|
||||
const wrapperModules = new Set(
|
||||
h5HostBridgeCallChainWrapperFiles.map(normalizeModulePath),
|
||||
);
|
||||
const scannedFiles = new Set();
|
||||
|
||||
for (const file of sourceFiles) {
|
||||
const source = fs.readFileSync(file, 'utf8');
|
||||
const imports = extractImportSpecifiers(source).map((specifier) =>
|
||||
importedModulePath(file, specifier),
|
||||
);
|
||||
const facadeImports = extractNamedImportsFromModule(
|
||||
source,
|
||||
file,
|
||||
h5HostBridgeFacadeModule,
|
||||
);
|
||||
const importsScannedFacadeCapability = [...facadeImports].some((importName) =>
|
||||
h5HostBridgeScannedFacadeImports.has(importName),
|
||||
);
|
||||
if (
|
||||
importsScannedFacadeCapability ||
|
||||
imports.some((specifier) => wrapperModules.has(specifier))
|
||||
) {
|
||||
scannedFiles.add(file);
|
||||
}
|
||||
}
|
||||
|
||||
for (const wrapperFile of h5HostBridgeCallChainWrapperFiles) {
|
||||
scannedFiles.add(wrapperFile);
|
||||
}
|
||||
|
||||
const missingRequiredFiles = h5HostBridgeRequiredCallChainFiles.filter(
|
||||
(file) => !scannedFiles.has(file),
|
||||
);
|
||||
if (missingRequiredFiles.length > 0) {
|
||||
throw new Error(
|
||||
`H5 HostBridge call chain scan is missing required files: ${missingRequiredFiles.join(', ')}`,
|
||||
);
|
||||
}
|
||||
|
||||
return [...scannedFiles].sort();
|
||||
}
|
||||
|
||||
function assertNoDevScaffoldTermsInFiles(files, terms) {
|
||||
for (const file of files) {
|
||||
const source = fs.readFileSync(file, 'utf8');
|
||||
const lowerSource = source.toLowerCase();
|
||||
for (const term of productionShellDevScaffoldTerms) {
|
||||
for (const term of terms) {
|
||||
const matchIndex = lowerSource.indexOf(term.toLowerCase());
|
||||
if (matchIndex === -1) {
|
||||
continue;
|
||||
@@ -283,6 +494,17 @@ function assertNoProductionShellDevScaffoldTerms() {
|
||||
}
|
||||
}
|
||||
|
||||
function assertNoProductionShellDevScaffoldTerms() {
|
||||
assertNoDevScaffoldTermsInFiles(
|
||||
productionShellScanRoots.flatMap(collectProductionShellFiles),
|
||||
productionShellDevScaffoldTerms,
|
||||
);
|
||||
assertNoDevScaffoldTermsInFiles(
|
||||
collectH5HostBridgeCallChainFiles().flatMap(collectProductionShellFiles),
|
||||
h5HostBridgeCallChainDevScaffoldTerms,
|
||||
);
|
||||
}
|
||||
|
||||
function assertSameList(actual, expected, label) {
|
||||
if (
|
||||
actual.length !== expected.length ||
|
||||
|
||||
@@ -416,7 +416,7 @@ import {
|
||||
buildVisualNovelEntryGenerationProgress,
|
||||
type VisualNovelEntryFormPayload,
|
||||
} from '../visual-novel-creation/visualNovelEntryGeneration';
|
||||
import { createMockVisualNovelRunFromDraft } from '../visual-novel-runtime/visualNovelMockData';
|
||||
import { createLocalPreviewVisualNovelRunFromDraft } from '../visual-novel-runtime/visualNovelLocalPreviewData';
|
||||
import {
|
||||
buildBarkBattleDraftConfigFromWorkSummary,
|
||||
buildBarkBattlePublishedConfigFromDraft,
|
||||
@@ -7267,7 +7267,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
const profileId =
|
||||
visualNovelWork?.summary.profileId?.trim() ?? draft.profileId?.trim();
|
||||
if (!profileId) {
|
||||
const run = createMockVisualNovelRunFromDraft(draft);
|
||||
const run = createLocalPreviewVisualNovelRunFromDraft(draft);
|
||||
setVisualNovelRun(run);
|
||||
setVisualNovelRuntimeReturnStage('visual-novel-result');
|
||||
setSelectionStage('visual-novel-runtime');
|
||||
@@ -7287,7 +7287,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
} catch (error) {
|
||||
// 中文注释:VN-07 只要求结果页进入测试运行时;真实 runtime 接口未就绪时,
|
||||
// 使用当前 draft 生成本地 test run,不扩展到正式玩家运行链路。
|
||||
setVisualNovelRun(createMockVisualNovelRunFromDraft(draft));
|
||||
setVisualNovelRun(createLocalPreviewVisualNovelRunFromDraft(draft));
|
||||
setVisualNovelRuntimeReturnStage('visual-novel-result');
|
||||
setSelectionStage('visual-novel-runtime');
|
||||
setVisualNovelError(
|
||||
|
||||
@@ -5,7 +5,7 @@ import type { ComponentProps } from 'react';
|
||||
import { expect, test, vi } from 'vitest';
|
||||
|
||||
import { buildVisualNovelForbiddenCopyPattern } from '../visual-novel-runtime/visualNovelForbiddenCopy';
|
||||
import { mockVisualNovelSession } from '../visual-novel-runtime/visualNovelMockData';
|
||||
import { visualNovelLocalPreviewSession } from '../visual-novel-runtime/visualNovelLocalPreviewData';
|
||||
import { VisualNovelAgentWorkspace } from './VisualNovelAgentWorkspace';
|
||||
import {
|
||||
buildVisualNovelEntryGenerationAnchorEntries,
|
||||
@@ -107,7 +107,7 @@ test('visual novel workspace submits idea and selected visual style as seed text
|
||||
});
|
||||
|
||||
test('visual novel workspace restores idea text from existing session', () => {
|
||||
renderWorkspace({ session: mockVisualNovelSession });
|
||||
renderWorkspace({ session: visualNovelLocalPreviewSession });
|
||||
|
||||
expect(
|
||||
(screen.getByLabelText('一句话创作') as HTMLTextAreaElement).value,
|
||||
|
||||
@@ -5,7 +5,7 @@ import userEvent from '@testing-library/user-event';
|
||||
import { expect, test, vi } from 'vitest';
|
||||
|
||||
import { buildVisualNovelForbiddenCopyPattern } from '../visual-novel-runtime/visualNovelForbiddenCopy';
|
||||
import { mockVisualNovelDraft } from '../visual-novel-runtime/visualNovelMockData';
|
||||
import { visualNovelLocalPreviewDraft } from '../visual-novel-runtime/visualNovelLocalPreviewData';
|
||||
import { VisualNovelResultView } from './VisualNovelResultView';
|
||||
|
||||
vi.mock('../../services/visual-novel-creation', () => ({
|
||||
@@ -26,7 +26,7 @@ vi.mock('../../services/assetReadUrlService', () => ({
|
||||
|
||||
test('visual novel profile tab uses PlatformSubpanel shells', () => {
|
||||
const { container } = render(
|
||||
<VisualNovelResultView draft={mockVisualNovelDraft} onBack={() => {}} />,
|
||||
<VisualNovelResultView draft={visualNovelLocalPreviewDraft} onBack={() => {}} />,
|
||||
);
|
||||
|
||||
const profilePanels = Array.from(
|
||||
@@ -48,7 +48,7 @@ test('visual novel result header back button reuses shared compact back action b
|
||||
|
||||
render(
|
||||
<VisualNovelResultView
|
||||
draft={mockVisualNovelDraft}
|
||||
draft={visualNovelLocalPreviewDraft}
|
||||
onBack={onBack}
|
||||
isBusy
|
||||
/>,
|
||||
@@ -69,7 +69,7 @@ test('visual novel result header back button reuses shared compact back action b
|
||||
|
||||
test('visual novel profile media previews use PlatformMediaFrame aspects', () => {
|
||||
const draftWithCover = {
|
||||
...mockVisualNovelDraft,
|
||||
...visualNovelLocalPreviewDraft,
|
||||
coverImageSrc: '/visual-novel/cover.png',
|
||||
};
|
||||
|
||||
@@ -104,7 +104,7 @@ test('visual novel upload-only asset picker uses PlatformEmptyState chrome', asy
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(
|
||||
<VisualNovelResultView draft={mockVisualNovelDraft} onBack={() => {}} />,
|
||||
<VisualNovelResultView draft={visualNovelLocalPreviewDraft} onBack={() => {}} />,
|
||||
);
|
||||
|
||||
await user.click(screen.getByRole('button', { name: '封面' }));
|
||||
@@ -130,7 +130,7 @@ test('visual novel entity list items use interactive PlatformSubpanel shells', a
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(
|
||||
<VisualNovelResultView draft={mockVisualNovelDraft} onBack={() => {}} />,
|
||||
<VisualNovelResultView draft={visualNovelLocalPreviewDraft} onBack={() => {}} />,
|
||||
);
|
||||
|
||||
await user.click(screen.getByRole('button', { name: '角色' }));
|
||||
@@ -146,7 +146,7 @@ test('visual novel entity list items use interactive PlatformSubpanel shells', a
|
||||
test('visual novel empty entity list uses PlatformEmptyState shell', async () => {
|
||||
const user = userEvent.setup();
|
||||
const emptyCharacterDraft = {
|
||||
...mockVisualNovelDraft,
|
||||
...visualNovelLocalPreviewDraft,
|
||||
characters: [],
|
||||
};
|
||||
|
||||
@@ -173,7 +173,7 @@ test('visual novel result opens complex editors as a dialog', async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(
|
||||
<VisualNovelResultView draft={mockVisualNovelDraft} onBack={() => {}} />,
|
||||
<VisualNovelResultView draft={visualNovelLocalPreviewDraft} onBack={() => {}} />,
|
||||
);
|
||||
|
||||
await user.click(screen.getByRole('button', { name: '角色' }));
|
||||
@@ -190,7 +190,7 @@ test('visual novel result exposes test run action with current draft', async ()
|
||||
|
||||
render(
|
||||
<VisualNovelResultView
|
||||
draft={mockVisualNovelDraft}
|
||||
draft={visualNovelLocalPreviewDraft}
|
||||
onBack={() => {}}
|
||||
onStartTestRun={onStartTestRun}
|
||||
/>,
|
||||
@@ -209,7 +209,7 @@ test('visual novel result sends edited character draft to save and test run', as
|
||||
|
||||
render(
|
||||
<VisualNovelResultView
|
||||
draft={mockVisualNovelDraft}
|
||||
draft={visualNovelLocalPreviewDraft}
|
||||
onBack={() => {}}
|
||||
onSaveDraft={onSaveDraft}
|
||||
onStartTestRun={onStartTestRun}
|
||||
@@ -251,7 +251,7 @@ test('visual novel result uploads scene and character assets into platform refer
|
||||
|
||||
render(
|
||||
<VisualNovelResultView
|
||||
draft={mockVisualNovelDraft}
|
||||
draft={visualNovelLocalPreviewDraft}
|
||||
onBack={() => {}}
|
||||
onSaveDraft={onSaveDraft}
|
||||
/>,
|
||||
@@ -304,7 +304,7 @@ test('visual novel result generates scene background from asset picker', async (
|
||||
|
||||
render(
|
||||
<VisualNovelResultView
|
||||
draft={mockVisualNovelDraft}
|
||||
draft={visualNovelLocalPreviewDraft}
|
||||
onBack={() => {}}
|
||||
onSaveDraft={onSaveDraft}
|
||||
/>,
|
||||
@@ -330,7 +330,7 @@ test('visual novel result generates scene background from asset picker', async (
|
||||
expect.objectContaining({
|
||||
kind: 'scene_background',
|
||||
scene: expect.objectContaining({
|
||||
sceneId: mockVisualNovelDraft.scenes[0]?.sceneId,
|
||||
sceneId: visualNovelLocalPreviewDraft.scenes[0]?.sceneId,
|
||||
}),
|
||||
prompt: '默认图片提示词',
|
||||
}),
|
||||
|
||||
@@ -57,7 +57,7 @@ import {
|
||||
PlatformTextField,
|
||||
} from '../common/PlatformTextField';
|
||||
import { PlatformToggleRow } from '../common/PlatformToggleRow';
|
||||
import { mockVisualNovelDraft } from '../visual-novel-runtime/visualNovelMockData';
|
||||
import { visualNovelLocalPreviewDraft } from '../visual-novel-runtime/visualNovelLocalPreviewData';
|
||||
|
||||
type VisualNovelResultViewProps = {
|
||||
draft?: VisualNovelResultDraft | null;
|
||||
@@ -1994,7 +1994,7 @@ function VisualNovelEditorDialog({
|
||||
}
|
||||
|
||||
export function VisualNovelResultView({
|
||||
draft = mockVisualNovelDraft,
|
||||
draft = visualNovelLocalPreviewDraft,
|
||||
isBusy = false,
|
||||
error = null,
|
||||
onBack,
|
||||
@@ -2004,7 +2004,7 @@ export function VisualNovelResultView({
|
||||
onPublish,
|
||||
}: VisualNovelResultViewProps) {
|
||||
const [editDraft, setEditDraft] = useState(() =>
|
||||
cloneDraft(draft ?? mockVisualNovelDraft),
|
||||
cloneDraft(draft ?? visualNovelLocalPreviewDraft),
|
||||
);
|
||||
const [activeTab, setActiveTab] = useState<VisualNovelResultTab>('profile');
|
||||
const [editorTarget, setEditorTarget] =
|
||||
|
||||
@@ -5,14 +5,14 @@ import { expect, test } from 'vitest';
|
||||
|
||||
import { VisualNovelAttributePanel } from './VisualNovelAttributePanel';
|
||||
import { VisualNovelHistoryPanel } from './VisualNovelHistoryPanel';
|
||||
import { mockVisualNovelRun } from './visualNovelMockData';
|
||||
import { visualNovelLocalPreviewRun } from './visualNovelLocalPreviewData';
|
||||
import { VisualNovelSavePanel } from './VisualNovelSavePanel';
|
||||
|
||||
test('visual novel history panel uses platform empty state', () => {
|
||||
render(
|
||||
<VisualNovelHistoryPanel
|
||||
run={{
|
||||
...mockVisualNovelRun,
|
||||
...visualNovelLocalPreviewRun,
|
||||
history: [],
|
||||
}}
|
||||
/>,
|
||||
@@ -28,7 +28,7 @@ test('visual novel attribute panel uses platform empty state', () => {
|
||||
render(
|
||||
<VisualNovelAttributePanel
|
||||
run={{
|
||||
...mockVisualNovelRun,
|
||||
...visualNovelLocalPreviewRun,
|
||||
metrics: {},
|
||||
}}
|
||||
/>,
|
||||
@@ -43,7 +43,7 @@ test('visual novel attribute panel uses platform empty state', () => {
|
||||
test('visual novel save panel uses platform empty states', () => {
|
||||
const { rerender } = render(
|
||||
<VisualNovelSavePanel
|
||||
run={mockVisualNovelRun}
|
||||
run={visualNovelLocalPreviewRun}
|
||||
saveArchives={[]}
|
||||
isLoadingArchives
|
||||
/>,
|
||||
@@ -52,7 +52,7 @@ test('visual novel save panel uses platform empty states', () => {
|
||||
expect(screen.getByText('读取中').className).toContain('bg-white/74');
|
||||
expect(screen.queryByText('暂无存档')).toBeNull();
|
||||
|
||||
rerender(<VisualNovelSavePanel run={mockVisualNovelRun} saveArchives={[]} />);
|
||||
rerender(<VisualNovelSavePanel run={visualNovelLocalPreviewRun} saveArchives={[]} />);
|
||||
|
||||
expect(screen.getByText('暂无存档').className).toContain('bg-white/74');
|
||||
expect(screen.queryByText('读取中')).toBeNull();
|
||||
@@ -60,7 +60,7 @@ test('visual novel save panel uses platform empty states', () => {
|
||||
|
||||
test('visual novel runtime list cards use platform subpanel chrome', () => {
|
||||
const { rerender } = render(
|
||||
<VisualNovelHistoryPanel run={mockVisualNovelRun} />,
|
||||
<VisualNovelHistoryPanel run={visualNovelLocalPreviewRun} />,
|
||||
);
|
||||
|
||||
const historyCard = screen.getByText('#1').closest('article');
|
||||
@@ -71,12 +71,12 @@ test('visual novel runtime list cards use platform subpanel chrome', () => {
|
||||
|
||||
rerender(
|
||||
<VisualNovelSavePanel
|
||||
run={mockVisualNovelRun}
|
||||
run={visualNovelLocalPreviewRun}
|
||||
saveArchives={[
|
||||
{
|
||||
worldKey: 'visual-novel:archive-1',
|
||||
ownerUserId: 'mock-user',
|
||||
profileId: 'vn-profile-mock-1',
|
||||
ownerUserId: 'local-preview-user',
|
||||
profileId: 'vn-profile-preview-1',
|
||||
worldType: 'visual-novel',
|
||||
worldName: '雪线电台',
|
||||
subtitle: '风雪站台',
|
||||
|
||||
@@ -5,7 +5,7 @@ import userEvent from '@testing-library/user-event';
|
||||
import { expect, test, vi } from 'vitest';
|
||||
|
||||
import { buildVisualNovelForbiddenCopyPattern } from './visualNovelForbiddenCopy';
|
||||
import { mockVisualNovelDraft, mockVisualNovelRun } from './visualNovelMockData';
|
||||
import { visualNovelLocalPreviewDraft, visualNovelLocalPreviewRun } from './visualNovelLocalPreviewData';
|
||||
import { VisualNovelRuntimeShell } from './VisualNovelRuntimeShell';
|
||||
|
||||
test('visual novel runtime renders mock play surface and opens panels as dialogs', async () => {
|
||||
@@ -13,8 +13,8 @@ test('visual novel runtime renders mock play surface and opens panels as dialogs
|
||||
|
||||
render(
|
||||
<VisualNovelRuntimeShell
|
||||
draft={mockVisualNovelDraft}
|
||||
run={mockVisualNovelRun}
|
||||
draft={visualNovelLocalPreviewDraft}
|
||||
run={visualNovelLocalPreviewRun}
|
||||
onBack={() => {}}
|
||||
/>,
|
||||
);
|
||||
@@ -35,8 +35,8 @@ test('visual novel runtime submits free text action with client event id', async
|
||||
|
||||
render(
|
||||
<VisualNovelRuntimeShell
|
||||
draft={mockVisualNovelDraft}
|
||||
run={mockVisualNovelRun}
|
||||
draft={visualNovelLocalPreviewDraft}
|
||||
run={visualNovelLocalPreviewRun}
|
||||
onBack={() => {}}
|
||||
onSubmitAction={onSubmitAction}
|
||||
/>,
|
||||
@@ -61,14 +61,14 @@ test('visual novel runtime submits choice and continue actions', async () => {
|
||||
const onSubmitAction = vi.fn();
|
||||
const onContinue = vi.fn();
|
||||
const runWithoutChoices = {
|
||||
...mockVisualNovelRun,
|
||||
...visualNovelLocalPreviewRun,
|
||||
availableChoices: [],
|
||||
};
|
||||
|
||||
const { rerender } = render(
|
||||
<VisualNovelRuntimeShell
|
||||
draft={mockVisualNovelDraft}
|
||||
run={mockVisualNovelRun}
|
||||
draft={visualNovelLocalPreviewDraft}
|
||||
run={visualNovelLocalPreviewRun}
|
||||
onBack={() => {}}
|
||||
onSubmitAction={onSubmitAction}
|
||||
onContinue={onContinue}
|
||||
@@ -88,7 +88,7 @@ test('visual novel runtime submits choice and continue actions', async () => {
|
||||
|
||||
rerender(
|
||||
<VisualNovelRuntimeShell
|
||||
draft={mockVisualNovelDraft}
|
||||
draft={visualNovelLocalPreviewDraft}
|
||||
run={runWithoutChoices}
|
||||
onBack={() => {}}
|
||||
onSubmitAction={onSubmitAction}
|
||||
@@ -108,8 +108,8 @@ test('visual novel runtime panels call regeneration and platform archive actions
|
||||
|
||||
render(
|
||||
<VisualNovelRuntimeShell
|
||||
draft={mockVisualNovelDraft}
|
||||
run={mockVisualNovelRun}
|
||||
draft={visualNovelLocalPreviewDraft}
|
||||
run={visualNovelLocalPreviewRun}
|
||||
onBack={() => {}}
|
||||
onRegenerateHistoryEntry={onRegenerateHistoryEntry}
|
||||
onSaveRun={onSaveRun}
|
||||
@@ -117,8 +117,8 @@ test('visual novel runtime panels call regeneration and platform archive actions
|
||||
saveArchives={[
|
||||
{
|
||||
worldKey: 'visual-novel:archive-1',
|
||||
ownerUserId: 'mock-user',
|
||||
profileId: 'vn-profile-mock-1',
|
||||
ownerUserId: 'local-preview-user',
|
||||
profileId: 'vn-profile-preview-1',
|
||||
worldType: 'visual-novel',
|
||||
worldName: '雪线电台',
|
||||
subtitle: '风雪站台',
|
||||
@@ -147,8 +147,8 @@ test('visual novel runtime shows raw text only as transient stream text', () =>
|
||||
|
||||
render(
|
||||
<VisualNovelRuntimeShell
|
||||
draft={mockVisualNovelDraft}
|
||||
run={mockVisualNovelRun}
|
||||
draft={visualNovelLocalPreviewDraft}
|
||||
run={visualNovelLocalPreviewRun}
|
||||
streamingText={transientText}
|
||||
onBack={() => {}}
|
||||
/>,
|
||||
|
||||
@@ -25,7 +25,7 @@ import type {
|
||||
} from '../../../packages/shared/src/contracts/visualNovel';
|
||||
import { ResolvedAssetImage } from '../ResolvedAssetImage';
|
||||
import { useVisualNovelRuntimeController } from './useVisualNovelRuntimeController';
|
||||
import { mockVisualNovelDraft, mockVisualNovelRun } from './visualNovelMockData';
|
||||
import { visualNovelLocalPreviewDraft, visualNovelLocalPreviewRun } from './visualNovelLocalPreviewData';
|
||||
import {
|
||||
VisualNovelRuntimePanel,
|
||||
type VisualNovelRuntimePanelKind,
|
||||
@@ -218,8 +218,8 @@ function resolveChoices(
|
||||
}
|
||||
|
||||
export function VisualNovelRuntimeShell({
|
||||
draft = mockVisualNovelDraft,
|
||||
run = mockVisualNovelRun,
|
||||
draft = visualNovelLocalPreviewDraft,
|
||||
run = visualNovelLocalPreviewRun,
|
||||
isBusy = false,
|
||||
isSaving = false,
|
||||
isLoadingArchives = false,
|
||||
@@ -243,8 +243,8 @@ export function VisualNovelRuntimeShell({
|
||||
const [localTextModeEnabled, setLocalTextModeEnabled] = useState(
|
||||
run?.textModeEnabled ?? draft?.runtimeConfig.defaultTextMode ?? false,
|
||||
);
|
||||
const displayDraft = draft ?? mockVisualNovelDraft;
|
||||
const baseRun = run ?? mockVisualNovelRun;
|
||||
const displayDraft = draft ?? visualNovelLocalPreviewDraft;
|
||||
const baseRun = run ?? visualNovelLocalPreviewRun;
|
||||
const runtimeController = useVisualNovelRuntimeController({
|
||||
draft: displayDraft,
|
||||
initialRun: baseRun,
|
||||
|
||||
+30
-30
@@ -5,11 +5,11 @@ import type {
|
||||
VisualNovelSourceMode,
|
||||
} from '../../../packages/shared/src/contracts/visualNovel';
|
||||
|
||||
const MOCK_NOW = '2026-05-05T12:00:00.000Z';
|
||||
const LOCAL_PREVIEW_NOW = '2026-05-05T12:00:00.000Z';
|
||||
|
||||
// 中文注释:VN-04 只提供 UI 骨架,mock 数据必须保持在前端组件域内,避免被误用成业务真相源。
|
||||
export const mockVisualNovelDraft: VisualNovelResultDraft = {
|
||||
profileId: 'vn-profile-mock-1',
|
||||
// 中文注释:VN-04 只提供 UI 骨架,本地预览数据必须保持在前端组件域内,避免被误用成业务真相源。
|
||||
export const visualNovelLocalPreviewDraft: VisualNovelResultDraft = {
|
||||
profileId: 'vn-profile-preview-1',
|
||||
workTitle: '雪线电台',
|
||||
workDescription: '一列停在雪夜边境的列车,牵出旧电台、失踪乘客和未寄出的告白。',
|
||||
workTags: ['悬疑', '冬夜', '双人叙事'],
|
||||
@@ -140,12 +140,12 @@ export const mockVisualNovelDraft: VisualNovelResultDraft = {
|
||||
message: '封面待补齐。',
|
||||
},
|
||||
],
|
||||
updatedAt: MOCK_NOW,
|
||||
updatedAt: LOCAL_PREVIEW_NOW,
|
||||
};
|
||||
|
||||
export const mockVisualNovelSession: VisualNovelAgentSessionSnapshot = {
|
||||
sessionId: 'vn-session-mock-1',
|
||||
ownerUserId: 'mock-user',
|
||||
export const visualNovelLocalPreviewSession: VisualNovelAgentSessionSnapshot = {
|
||||
sessionId: 'vn-session-preview-1',
|
||||
ownerUserId: 'local-preview-user',
|
||||
sourceMode: 'idea',
|
||||
status: 'ready',
|
||||
messages: [
|
||||
@@ -154,24 +154,24 @@ export const mockVisualNovelSession: VisualNovelAgentSessionSnapshot = {
|
||||
role: 'user',
|
||||
kind: 'chat',
|
||||
text: '想做一个雪夜列车和旧电台有关的悬疑视觉小说。',
|
||||
createdAt: MOCK_NOW,
|
||||
createdAt: LOCAL_PREVIEW_NOW,
|
||||
},
|
||||
{
|
||||
id: 'vn-message-2',
|
||||
role: 'assistant',
|
||||
kind: 'summary',
|
||||
text: '底稿已整理到世界观、角色、场景和前两段剧情阶段。',
|
||||
createdAt: MOCK_NOW,
|
||||
createdAt: LOCAL_PREVIEW_NOW,
|
||||
},
|
||||
],
|
||||
draft: mockVisualNovelDraft,
|
||||
draft: visualNovelLocalPreviewDraft,
|
||||
pendingAction: {
|
||||
actionId: 'vn-action-compile-mock',
|
||||
actionId: 'vn-action-compile-preview',
|
||||
kind: 'compile_work_profile',
|
||||
label: '生成结果页',
|
||||
},
|
||||
createdAt: MOCK_NOW,
|
||||
updatedAt: MOCK_NOW,
|
||||
createdAt: LOCAL_PREVIEW_NOW,
|
||||
updatedAt: LOCAL_PREVIEW_NOW,
|
||||
};
|
||||
|
||||
function createVisualNovelDraftId(prefix: string) {
|
||||
@@ -278,7 +278,7 @@ export function createBlankVisualNovelDraft(
|
||||
};
|
||||
}
|
||||
|
||||
export function createMockVisualNovelSessionFromDraft(
|
||||
export function createLocalPreviewVisualNovelSessionFromDraft(
|
||||
draft: VisualNovelResultDraft,
|
||||
): VisualNovelAgentSessionSnapshot {
|
||||
const now = new Date().toISOString();
|
||||
@@ -307,7 +307,7 @@ export function createMockVisualNovelSessionFromDraft(
|
||||
};
|
||||
}
|
||||
|
||||
export function createMockVisualNovelRunFromDraft(
|
||||
export function createLocalPreviewVisualNovelRunFromDraft(
|
||||
draft: VisualNovelResultDraft,
|
||||
): VisualNovelRunSnapshot {
|
||||
const now = new Date().toISOString();
|
||||
@@ -388,10 +388,10 @@ export function createMockVisualNovelRunFromDraft(
|
||||
};
|
||||
}
|
||||
|
||||
export const mockVisualNovelRun: VisualNovelRunSnapshot = {
|
||||
runId: 'vn-run-mock-1',
|
||||
ownerUserId: 'mock-user',
|
||||
profileId: 'vn-profile-mock-1',
|
||||
export const visualNovelLocalPreviewRun: VisualNovelRunSnapshot = {
|
||||
runId: 'vn-run-preview-1',
|
||||
ownerUserId: 'local-preview-user',
|
||||
profileId: 'vn-profile-preview-1',
|
||||
mode: 'test',
|
||||
status: 'active',
|
||||
currentSceneId: 'vn-scene-platform',
|
||||
@@ -408,7 +408,7 @@ export const mockVisualNovelRun: VisualNovelRunSnapshot = {
|
||||
history: [
|
||||
{
|
||||
entryId: 'vn-history-1',
|
||||
runId: 'vn-run-mock-1',
|
||||
runId: 'vn-run-preview-1',
|
||||
turnIndex: 1,
|
||||
source: 'assistant',
|
||||
actionText: null,
|
||||
@@ -432,24 +432,24 @@ export const mockVisualNovelRun: VisualNovelRunSnapshot = {
|
||||
},
|
||||
],
|
||||
snapshotBeforeHash: null,
|
||||
snapshotAfterHash: 'mock-hash-1',
|
||||
createdAt: MOCK_NOW,
|
||||
snapshotAfterHash: 'preview-hash-1',
|
||||
createdAt: LOCAL_PREVIEW_NOW,
|
||||
},
|
||||
{
|
||||
entryId: 'vn-history-2',
|
||||
runId: 'vn-run-mock-1',
|
||||
runId: 'vn-run-preview-1',
|
||||
turnIndex: 2,
|
||||
source: 'player',
|
||||
actionText:
|
||||
'靠近广播柜,确认频段来源,同时留意林遥是否在隐瞒什么。',
|
||||
steps: [],
|
||||
snapshotBeforeHash: 'mock-hash-1',
|
||||
snapshotAfterHash: 'mock-hash-2',
|
||||
createdAt: MOCK_NOW,
|
||||
snapshotBeforeHash: 'preview-hash-1',
|
||||
snapshotAfterHash: 'preview-hash-2',
|
||||
createdAt: LOCAL_PREVIEW_NOW,
|
||||
},
|
||||
],
|
||||
availableChoices: mockVisualNovelDraft.opening.initialChoices,
|
||||
availableChoices: visualNovelLocalPreviewDraft.opening.initialChoices,
|
||||
textModeEnabled: true,
|
||||
createdAt: MOCK_NOW,
|
||||
updatedAt: MOCK_NOW,
|
||||
createdAt: LOCAL_PREVIEW_NOW,
|
||||
updatedAt: LOCAL_PREVIEW_NOW,
|
||||
};
|
||||
Reference in New Issue
Block a user