Compare commits

..

12 Commits

Author SHA1 Message Date
suzmii 96b44d8660 补充上游元数据漂移的拒绝用例
Project CI / AI game creator shell Rust crates (pull_request) Successful in 1m23s
Project CI / Backend tests (pull_request) Failing after 13s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 1m56s
Project CI / Frontend tests (pull_request) Successful in 2m11s
Project CI / Repository checks (pull_request) Failing after 17s
Project CI / AI game creator shell web tests (pull_request) Failing after 1m28s
Project CI / Native shell tests (pull_request) Successful in 5m31s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 8m31s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 8m41s
- 准备步骤用例新增一条:上游 codex-package.json 的 version/layoutVersion/entrypoint/resourcesDir 任一与声明不一致时必须拒绝且不落产物(共 10 条)
- 同步决策日志与 M2 里程碑规范里的用例计数
2026-09-27 19:01:00 +08:00
suzmii 33308a93e8 校正 M2 交付后的文档口径
Project CI / AI game creator shell Rust crates (pull_request) Successful in 1m21s
Project CI / Backend tests (pull_request) Failing after 14s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 2m7s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Has been cancelled
Project CI / Repository checks (pull_request) Has been cancelled
Project CI / AI game creator shell web tests (pull_request) Has been cancelled
Project CI / Native shell tests (pull_request) Has been cancelled
Project CI / Frontend tests (pull_request) Has been cancelled
- 技术方案修正症状层补丁清单:prune_staging/STAGED_* 只存在于未合入的补丁,主线从未有过
- 技术方案 §5.2/§5.3 记录过渡期已完成、.taurignore staging 条目保留到 M3 的原因,§8 M2 行标注已交付与 Windows 待复验
- M2 里程碑规范按证据勾选 macOS 侧可达的验收项,未验证项保持未勾选
- M1 实施计划标注 codex_package_metadata.rs 已在 M2 退役
2026-09-27 18:57:54 +08:00
suzmii f6dad1950f AGC 随包资源准备步骤接入 dev 与发布入口,构建脚本退出写入
Project CI / AI game creator shell Rust smoke (pull_request) Has been cancelled
Project CI / Backend tests (pull_request) Has been cancelled
Project CI / AI game creator shell Rust crates (pull_request) Has been cancelled
Project CI / Native shell tests (pull_request) Has been cancelled
Project CI / Frontend tests (pull_request) Has been cancelled
Project CI / Repository checks (pull_request) Has been cancelled
Project CI / AI game creator shell web tests (pull_request) Has been cancelled
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Has been cancelled
- start-tauri-dev.mjs 在前端与配套后端就绪后、spawn Tauri CLI 之前调用准备步骤,并保留依赖注入供入口测试断言顺序
- build-release.mjs 的 runTauriBuild 与既有 stageRuntime 并列调用 stageBundledResources,tauri build --no-bundle 仍不强制 staging
- 声明新增 origin 字段:source 由准备步骤写,build 由构建脚本在产物生成后写;构建脚本删除 codex 与插件白名单的写入分支及 stage_plugin_file/copy_plugin_tree/copy_plugin_file,改为 stage_build_generated_plugin_payloads
- 插件随包工作区改为与仓库源码逐文件比对(清单 + 逐文件 sha256 + 整树符号链接),实现移入 build_support/package_layout.rs 复用单测
- 上游原生包元数据改由准备步骤按声明校验,build_support/codex_package_metadata.rs 因失去调用方删除
- 准备步骤改为同步实现,dev 与发布入口可直接调用而无需子进程
- 测试:build-release 39 passed(新增 stage、bundled、build 顺序与 no-bundle 不 staging)、dev 入口 12 passed(新增准备步骤先于 CLI 启动)、准备步骤 9 passed、package_layout 36 passed
- 文档:技术方案 §4.9 记录构建期写入边界,M1 里程碑标记完成,运维文档补入口接线与 resources/plugins 所有权,决策日志与排障经验同步
2026-09-27 18:56:44 +08:00
suzmii a4b103a1b0 补充 AGC 随包资源归位的 M1 决策与文档
Project CI / AI game creator shell Rust crates (pull_request) Successful in 1m25s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 1m54s
Project CI / Backend tests (pull_request) Successful in 3m56s
Project CI / Frontend tests (pull_request) Successful in 1m43s
Project CI / Native shell tests (pull_request) Successful in 6m13s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 9m4s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 9m8s
Project CI / Repository checks (pull_request) Successful in 2m6s
Project CI / AI game creator shell web tests (pull_request) Successful in 1m39s
- 技术方案新增 §4.8 单一声明与实现形态(混合),§8 更新 M1 交付与停止条件,§9 未决问题 1 收口为已定
- M1 里程碑规范记录已定决策与校验收口边界;实施计划补充已定决策与实际验证命令
- M2 里程碑规范的依赖与前置条件补记 M1 已交付的准备步骤与声明门禁
- 开发运维文档补充声明、生成器、门禁、准备步骤与只读校验的用法;决策日志与排障经验同步本次口径
2026-09-27 18:36:09 +08:00
suzmii 05d455d4ef 构建脚本改为按声明生成并只读校验随包资源
- 平台布局、插件随包子目录与跳过规则、Codex 上游候选路径与第三方声明来源全部改由声明提供,删除脚本内的字面量
- 新增只读校验:Codex 目录校验清单 schema/平台/版本、文件集合、逐文件 sha256、第三方声明、可执行位与白名单外文件;插件目录校验必需组件与整树符号链接
- AGC_BUILD_TARGET 的 rustc-env 移到无条件路径(保留职责),新增 AGC_SKIP_RESOURCE_STAGING=1 以在既有产物上只跑校验
- 删除脚本内与 package_layout 重复的 sha256 实现,复用同一份
2026-09-27 18:36:04 +08:00
suzmii d8b37e0da9 新增 AGC 随包资源准备步骤(Codex 与插件两条纯复制路径)
- 新增 scripts/prepare-bundled-resources.mjs:按声明解析目标与替换单位,用上游 package-lock 的 resolved/integrity 组成缓存 key,写临时目录后原子替换,命中缓存不重写任何文件,只替换本工具产物,失败即退出并给出可执行提示
- Codex 路径按声明复制组件、保留受版本控制的第三方声明、按现有字节格式生成 manifest.json;macOS 双架构整体 staging
- 插件路径按声明的子目录白名单与跳过规则复制,顶层出现非本工具条目即失败
- 新增 scripts/prepare-bundled-resources.test.mjs:9 条用例覆盖 staging 结果、幂等(内容与时间戳)、上游缺失、目标不支持、目录被非本工具占用、dry-run 不落盘与白名单过滤
- .gitignore 忽略准备步骤产生的 staging 临时目录
2026-09-27 18:36:03 +08:00
suzmii 21c3bd7a2b 引入 AGC 随包资源的单一声明与生成门禁
- 新增 build_support/package-layout.json:Codex 三元表与组件白名单、上游候选路径、第三方声明来源、插件随包子目录与跳过规则、平台与 feature 门槛的唯一人工声明
- 新增 scripts/check-package-layout.mjs:由声明生成 build_support/package-layout.generated.rs,并校验目标唯一、可执行组件属于白名单、每个目标恰有一条第三方声明来源、codex.version 与应用锁定的 @openai/codex 一致
- 新增 build_support/package_layout.rs:声明类型、插件与平台门槛判定、逐块 sha256、随包 Codex 目录只读校验及其单测
- codex_bundle.rs 的布局与版本常量改由声明提供,公开接口与取值不变;src/main.rs 在测试期引入该模块以复用既有单测
- package.json 新增 bundled-resources:check/sync 并接进 typecheck 链,根 package.json 提供 agc:* 别名
2026-09-27 18:35:57 +08:00
suzmii 756297d2df 补充 AGC 随包资源 staging 归位里程碑与首段实施计划
Project CI / AI game creator shell Rust crates (pull_request) Successful in 1m29s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 2m0s
Project CI / Backend tests (pull_request) Successful in 4m21s
Project CI / Frontend tests (pull_request) Successful in 1m59s
Project CI / Native shell tests (pull_request) Successful in 6m0s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 9m25s
Project CI / AI game creator shell web tests (pull_request) Successful in 1m27s
Project CI / Repository checks (pull_request) Successful in 1m50s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 9m53s
- 新增三份里程碑规范:资源改由校验器读入、生成接入 dev 与发布入口、编辑器分支产物归位与症状层补丁清理
- 新增首个里程碑实施计划:修改边界、实现顺序、验证命令、风险与回滚点、待确认的实现形态
2026-09-27 16:59:42 +08:00
suzmii e02811588c 补充 AGC 随包资源 staging 归位技术方案
- 新增 docs/technical/【技术方案】AGC随包资源staging归位-2026-09-26.md:记录根因证据(tauri-build 把随包资源登记成 build script 输入)、准备步骤合同、构建脚本退化边界、入口接线、验收判据、风险与里程碑拆分
- docs/README.md 挂入文档索引
2026-09-27 16:59:42 +08:00
suzmii e1780b21b0 修复 AGC dev 客户端不再依赖本机 sccache 配置
- start-tauri-dev.mjs 启动 Tauri CLI 前复用 scripts/dev.mjs 的 buildLocalRustProcessEnv,本地 Rust wrapper 由脚本决定而不是本机 Cargo 配置
- 新增 buildTauriDevProcessEnv 组装 dev 端点环境并导出,供测试直接断言
- 补 3 条用例:配置为 sccache 时改用 /usr/bin/env 直连 rustc、未配置时清空两个 wrapper 变量、自定义 wrapper 保留且不改写调用方 env
- pitfalls 与开发运维文档补记 AGC dev 入口的 wrapper 口径
2026-09-27 16:59:40 +08:00
lhk229 6ed1fd26ed 修复 Linux 沙箱命令退出回收的偶发竞态
Project CI / AI game creator shell Rust crates (pull_request) Successful in 1m33s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 2m7s
Project CI / Backend tests (pull_request) Successful in 3m49s
Project CI / Frontend tests (pull_request) Successful in 1m53s
Project CI / Native shell tests (pull_request) Successful in 5m51s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 8m57s
Project CI / Repository checks (pull_request) Successful in 1m55s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 9m19s
Project CI / AI game creator shell web tests (pull_request) Successful in 1m24s
Project CI / AI game creator shell Rust crates (push) Successful in 1m30s
Project CI / AI game creator shell Rust smoke (push) Successful in 2m9s
Project CI / Backend tests (push) Successful in 4m49s
Project CI / Frontend tests (push) Successful in 2m9s
Project CI / Native shell tests (push) Successful in 6m54s
Project CI / AI game creator shell Rust lane 2/2 (push) Successful in 9m56s
Project CI / AI game creator shell Rust lane 1/2 (push) Successful in 10m14s
Project CI / Repository checks (push) Successful in 2m2s
Project CI / AI game creator shell web tests (push) Successful in 1m30s
统一正常退出、取消和超时的有界进程组退出确认,保留归属校验
提前记录启动身份,修补终态协议错误的清理及输出任务回收
补充确定性竞态回归并同步运行时规范和排障记忆
保持现有 CI 并行、分片和重试策略不变
2026-09-24 16:45:07 +00:00
lhk229 4912aa4df0 删除创作首页登录送泥点营销文案
Project CI / AI game creator shell Rust crates (pull_request) Successful in 1m34s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 2m14s
Project CI / Backend tests (pull_request) Successful in 3m51s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Failing after 6m40s
Project CI / Frontend tests (pull_request) Successful in 2m0s
Project CI / Native shell tests (pull_request) Successful in 5m52s
Project CI / Repository checks (pull_request) Successful in 2m8s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 8m50s
Project CI / AI game creator shell web tests (pull_request) Successful in 2m17s
删除 CreationLandingView hero 区“登录即送 100 泥点,可以免费制作 50 个素材”文案
删除 CreationLandingView.test.tsx 中对应的文案断言
清理 index.css 中已无调用方的 creation-landing__hero-benefit 样式
更正 decision-log 中已过期的“注册赠送固定为 100 泥点”决策记录,注明金额由线上钱包配置决定
2026-09-24 16:08:42 +00:00
288 changed files with 5099 additions and 23887 deletions
-9
View File
@@ -145,15 +145,6 @@ ELEVENLABS_BASE_URL="https://api.elevenlabs.io"
ELEVENLABS_API_KEY=""
ELEVENLABS_REQUEST_TIMEOUT_MS="180000"
# Tripo 3D 生成:地址与密钥必须显式给出,没有内置默认值。
# 缺任意一个,会提交或会执行 3D job 的进程(api / external-generation-worker / all)
# 在启动期直接拒绝启动——不会带着未知网关或空密钥把进程跑起来。
TRIPO_BASE_URL="https://openapi.tripo3d.com/v3"
TRIPO_API_KEY=""
# 两个运行旋钮可缺省(沿用内置值);显式给出时会校验,非法即拒绝启动。
TRIPO_REQUEST_TIMEOUT_MS="60000"
TRIPO_RETRIES="2"
# 阿里云 OSS 配置。
# Rust `server-rs` 的 `api-server` 会优先从 `.env` / `.env.local` 读取这些变量,
# 用于签发浏览器 PostObject 直传票据,并保持 `/generated-*` 旧路径习惯。
+2
View File
@@ -57,6 +57,8 @@ temp*build*/
/apps/ai-game-creator-shell/logs/
/apps/ai-game-creator-shell/src-tauri/resources/node-runtime/
/apps/ai-game-creator-shell/src-tauri/resources/node-runtime-staging-*/
/apps/ai-game-creator-shell/src-tauri/resources/plugins-staging-*/
/apps/ai-game-creator-shell/src-tauri/resources/codex/*-staging-*/
/apps/ai-game-creator-shell/.llm-drafts/
/apps/ai-game-creator-shell/game-creator.config.local.json
/apps/mobile-shell/.expo/
-84
View File
@@ -67,90 +67,6 @@ _Avoid_: 每步一个 sidecar 状态机、把切分专用的 SeparationState 泛
## Language
### 3D Model Generation
**3D 模型生成操作**:
由文本提示或平台图片引用驱动、可异步查询并最终产生模型文件的 provider-neutral 操作;操作状态和产品资源结果分开建模。
_Avoid_: 直接把第三方 provider task 当作产品资源、把模型 URL 当作永久资源地址
**Provider task**:
第三方 3D 生成平台返回的任务引用与任务快照,只存在于 provider adapter 的 Rust 边界内;它不是 Genarrative 的内部操作 ID。
_Avoid_: 把 Tripo task ID 当作稳定产品 operationId、让 SDK 类型穿透 api-server
**临时 provider URL**:
第三方任务完成后返回的短时模型下载地址,必须通过显式下载流程转换为本地或持久化资源,不能直接当作长期资产地址。
_Avoid_: 永久 OSS URL、资源 ID
**模型 artifact**:
由 text-to-model、image-to-model 或 multiview-to-model 任务产生、待应用层登记和持久化的模型文件结果;provider adapter 只提供可下载结果,不负责生成 resourceId 或 assetId。
_Avoid_: 仅 UI 中展示的模型链接、没有来源任务的静态文件
**Tripo API facade**:
面向站内已认证调用方的全新 Tripo 3D 生成 API 上下文,负责把 API operation、用户归属、异步状态、产物持久化和正式资源引用连接起来;它独立于历史 Hyper3D adapter,不复用其路由、契约或实现。
_Avoid_: 把 Tripo API 当成 Hyper3D 的 provider 替换、让 Hyper3D 路由悄悄切换到 Tripo
**Tripo operation**:
Genarrative 为一次 Tripo 生成请求分配的内部异步操作标识;客户端只看到 operationId,Tripo provider task 只作为服务端 checkpoint 保存,二者不是同一个 ID。
_Avoid_: 将 Tripo taskId 直接当公开 operationId、把 SDK task 状态模型穿透到 API
**模型产物持久化**:
Tripo 任务完成后,服务端下载模型与预览、写入受控对象存储并登记资源元数据,成功后才把 Tripo operation 置为 completed;本期按完整字节写入,流式上传是后续目标。
_Avoid_: 返回临时签名 URL作为永久资源、在 API handler 中把完整模型读成 Vec<u8>
**3D provider 配置门禁**:
Tripo 网关与密钥的可用性在「会提交或会执行 3D job 的进程」(`api` / `external-generation-worker` / `all`)启动期判定:`TRIPO_BASE_URL` 与 `TRIPO_API_KEY` 没有内置默认值,缺失、空白或网关非法即拒绝启动;请求期返回 503 只是第二道防线,不承担发现部署缺失的职责。
_Avoid_: 内置默认网关、缺配置时静默沿用某个地址或兜底密钥、把「少配置」留到用户提交后才发现
**3D 生成底价**:
一次 3D 生成在指定端点和模型版本下、按是否带贴图区分的基准泥点价,不包含任何叠加项,也不随请求的其它参数变化。
_Avoid_: 把底价与加价项合并成一个档位价、把 provider 的 credit 数值当底价
**3D 生成加价项**:
叠加在底价之上、由请求参数判定的可选泥点加价;每个端点各自持有一份加价项价目,同一加价项在不同端点可以是不同价。
_Avoid_: 全局唯一一份加价项价目、按用途而不是按请求参数判定加价
**定价配置归一化**:
读取持久化定价时把它与当前契约对齐的过程:缺的键用受控默认值补齐,契约之外的遗留键剔除并告警;它只发生在读取方向,写入方向仍要求完整合法。
_Avoid_: 把「省略即沿用」当作保存语义、把归一化当成静默改价
**模型生成落点**:
3D 生成结果要进入的平台容器,调用方必须在工程资源与素材库资产之间显式二选一;工程分支可附带画布占位框回填,素材库分支必须给出目录与标签。
_Avoid_: 两个都不给就默认落素材库、把落点混进 provider 请求参数
**模型输入资源引用**:
image-to-model 请求对平台已有图片的明确 tagged 引用,可指向项目资源或素材库资产;服务端负责 owner 校验和 OSS 对象解析,不接受把多种来源塞进裸 `input` 字符串。
_Avoid_: 任意远程 URL、data URL、未区分语义的 resourceId/assetId 字符串
**3D 模型生成工具**:
图片画布工程里从底部工具栏打开、给出「文生 3D 模型」与「图生 3D 模型」两个子项的画布生成工具;提交后按画布既有生成链路的占位框、任务侧栏与失败退款口径推进,产物落成 **3D 模型资源**。
_Avoid_: 独立 3D 生成页、把 3D 生成做成玩法类型或作品、把 3D 生成接进画布 Agent 对话
**3D 生成定价**:
3D 模型生成工具在提交前展示的泥点价格,来自编辑器生成定价查询的 `model3d` 段;该段缺失即入口不可提交,前端不内置兜底数值。该段的权威事实源是 SpacetimeDB 定价表里的两段强类型列,公开读取时投影回迁移前的旧形状。
_Avoid_: 前端写死 3D 价格、借用图片模型的兜底定价配置、按 0 元放行、把内部两段结构直接暴露给画布
**定价版本(乐观锁)**:
后台读取完整模型定价时一并拿到的版本号(SpacetimeDB 行的更新时间微秒值),保存时必须原样回传;与服务端当前版本不一致即整笔拒绝并返回 409,提示重新读取,避免静默覆盖他人的改价。
_Avoid_: 在前端自造或缓存该版本、把版本不匹配当成可重试的普通失败、把「省略字段」当成「沿用当前值」
### 3D 模型资源预览
**3D 模型资源**:
类别为 `model3d` 的平台资源 / 素材,一个资源同时持有模型本体与模型预览图两个对象;客户端默认消费预览图,模型本体只在 3D 查看器里加载。
_Avoid_: 只有模型文件的资源、把模型本体当图片素材、为模型新增并列媒体类型
**模型预览图**:
3D 模型资源的静态代表图,落在资源投影的 `imageSrc` 与 `thumbnailSrc` 上,供画布卡片、素材缩略图、后台列表和精选卡片使用。
_Avoid_: 用模型本体充当预览、按需实时渲染缩略图、把预览图当模型真相
**模型本体**:
3D 模型资源里的可下载模型文件,只由资源的 `objectKey` 指向,格式以对象内容类型为准。
_Avoid_: 把模型本体当图片地址、用预览图字段承载模型文件、按文件扩展名推断格式
**3D 查看器预览**:
在资源上打开模态把模型本体交给 3D 查看器渲染的只读能力,不修改资源、不产出新资源,也不是画布图层本身的编辑操作。
_Avoid_: 在画布里内联渲染模型、把预览当编辑、为预览产出新资源
### Puzzle Clear
**拼消消**:
+1 -2
View File
@@ -60,7 +60,6 @@ import type {
AdminUpdateAccountResponse,
AdminUpdateAgcTemplateRequest,
AdminUploadedEditorShowcaseCampaignImage,
AdminUpsertEditorGenerationPricingRequest,
AdminUpsertEditorShowcaseCampaignRequest,
AdminUpsertFeatureGateConfigRequest,
AdminUpsertProfileInviteCodeRequest,
@@ -520,7 +519,7 @@ export function getAdminEditorGenerationPricing(token: string) {
export function upsertAdminEditorGenerationPricing(
token: string,
payload: AdminUpsertEditorGenerationPricingRequest,
payload: EditorGenerationPricingConfigPayload,
) {
return request<EditorGenerationPricingConfigPayload>(
'/admin/api/editor-generation-pricing',
-35
View File
@@ -431,43 +431,8 @@ export interface EditorGenerationModelPricingPayload {
prices?: Record<string, number>;
}
/** 某个 3D 模型版本在「无贴图 / 带贴图」两种形态下的泥点底价。 */
export interface EditorGenerationModel3dVersionPricePayload {
noTexture: number;
texture: number;
}
/**
* 单个 3D 生成端点的整套定价:模型版本底价 + 该端点自己的加价项价目。
*
* 键集合由后端契约决定,后台只改数值:模型版本键与加价项键都直接来自接口返回,
* 不在前端硬编码清单,也不能新增、删除或停用。
*/
export interface EditorGenerationModel3dEndpointPricingPayload {
versionPrices: Record<string, EditorGenerationModel3dVersionPricePayload>;
addOnPrices: Record<string, number>;
}
/** 3D 生成定价段:两个端点并列,各持自己的设置。 */
export interface EditorGenerationModel3dPricingPayload {
textToModelPricing: EditorGenerationModel3dEndpointPricingPayload;
imageToModelPricing: EditorGenerationModel3dEndpointPricingPayload;
}
export interface EditorGenerationPricingConfigPayload {
models: Record<string, EditorGenerationModelPricingPayload>;
/** 后端尚未配置 3D 段时缺省;缺失即 3D 生成不可提交,后台也不能新建整段。 */
model3d?: EditorGenerationModel3dPricingPayload | null;
}
/**
* 后台保存定价的请求体:`models` 与 3D 两段都必须显式给出。
*
* 不带定价版本:保存即整段覆盖,服务端不比对版本(后台同时只有一个管理员在操作)。
*/
export interface AdminUpsertEditorGenerationPricingRequest {
models: Record<string, EditorGenerationModelPricingPayload>;
model3d: EditorGenerationModel3dPricingPayload | null;
}
export interface AdminEditorAssetListQuery {
@@ -1,131 +0,0 @@
import type {
EditorGenerationModel3dPricingPayload,
EditorGenerationModel3dVersionPricePayload,
} from '../api/adminApiTypes';
import {
EDITOR_MODEL3D_ENDPOINT_SECTIONS,
editorModel3dAddOnLabel,
updateEditorModel3dEndpointPricing,
} from './adminEditorGenerationPricing';
import { parsePositiveInteger } from './pageUtils';
/**
* 一个模型版本的两个底价字段。两栏只有字段键与展示名不同,用描述表驱动渲染,
* 避免两段近乎相同的 JSX 各自演化。
*/
const VERSION_PRICE_FIELDS = [
{ key: 'noTexture', label: '无贴图' },
{ key: 'texture', label: '带贴图' },
] as const satisfies ReadonlyArray<{
key: keyof EditorGenerationModel3dVersionPricePayload;
label: string;
}>;
interface AdminEditorGenerationModel3dPricingSectionProps {
model3d: EditorGenerationModel3dPricingPayload;
onChange: (next: EditorGenerationModel3dPricingPayload) => void;
}
/**
* 3D 生成定价区块:按端点分成「文生 3D」与「图生 3D」两组,每组是该端点自己的
* 「模型版本 × {无贴图, 带贴图}」底价表与加价项单值输入。
*
* 键集合全部来自接口返回,后台只能改数值:不能新增、删除键,也不能停用整段。
*/
export function AdminEditorGenerationModel3dPricingSection({
model3d,
onChange,
}: AdminEditorGenerationModel3dPricingSectionProps) {
function updateEndpoint(
section: (typeof EDITOR_MODEL3D_ENDPOINT_SECTIONS)[number]['key'],
updater: Parameters<typeof updateEditorModel3dEndpointPricing>[2],
) {
onChange(updateEditorModel3dEndpointPricing(model3d, section, updater));
}
return (
<section className="admin-panel admin-pricing-model-card">
<div className="admin-pricing-model-heading">
<strong>3D 生成定价</strong>
<span className="admin-pricing-unit">按次</span>
</div>
<p className="admin-pricing-model3d-hint">
模型版本与加价项由后端契约给出,只能改数值;两段必须完整给出,缺失即 3D
生成不可提交。
</p>
<div className="admin-pricing-grid">
{EDITOR_MODEL3D_ENDPOINT_SECTIONS.map(({ key, label }) => (
<section className="admin-panel admin-pricing-model-card" key={key}>
<div className="admin-pricing-model-heading">
<strong>{label}</strong>
</div>
<div className="admin-pricing-model3d-list">
{Object.entries(model3d[key].versionPrices).map(
([version, price]) => (
<div className="admin-pricing-model3d-row" key={version}>
<span className="admin-pricing-model3d-row-label">
{version}
</span>
{VERSION_PRICE_FIELDS.map(
({ key: field, label: fieldLabel }) => (
<label className="admin-field" key={field}>
<span>{fieldLabel}</span>
<input
aria-label={`${label} ${version} ${fieldLabel}`}
min={1}
step={1}
type="number"
value={price[field]}
onChange={(event) =>
updateEndpoint(key, (current) => ({
...current,
versionPrices: {
...current.versionPrices,
[version]: {
...price,
[field]: parsePositiveInteger(
event.target.value,
),
},
},
}))
}
/>
</label>
),
)}
</div>
),
)}
</div>
<div className="admin-form-row">
{Object.entries(model3d[key].addOnPrices).map(
([addOn, price]) => (
<label className="admin-field" key={addOn}>
<span>{editorModel3dAddOnLabel(addOn)}</span>
<input
aria-label={`${label} ${editorModel3dAddOnLabel(addOn)}`}
min={1}
step={1}
type="number"
value={price}
onChange={(event) =>
updateEndpoint(key, (current) => ({
...current,
addOnPrices: {
...current.addOnPrices,
[addOn]: parsePositiveInteger(event.target.value),
},
}))
}
/>
</label>
),
)}
</div>
</section>
))}
</div>
</section>
);
}
@@ -11,16 +11,14 @@ import {
import type { EditorGenerationPricingConfigPayload } from '../api/adminApiTypes';
import { AdminEditorGenerationPricingPage } from './AdminEditorGenerationPricingPage';
vi.mock('../api/adminApiClient', async () => {
const actual = await vi.importActual<typeof import('../api/adminApiClient')>(
'../api/adminApiClient',
);
return {
...actual,
getAdminEditorGenerationPricing: vi.fn(),
upsertAdminEditorGenerationPricing: vi.fn(),
};
});
vi.mock('../api/adminApiClient', () => ({
formatAdminApiError: vi.fn((error: unknown) =>
error instanceof Error ? error.message : '请求失败',
),
getAdminEditorGenerationPricing: vi.fn(),
isAdminApiError: vi.fn(() => false),
upsertAdminEditorGenerationPricing: vi.fn(),
}));
const pricing: EditorGenerationPricingConfigPayload = {
models: {
@@ -43,78 +41,36 @@ const pricing: EditorGenerationPricingConfigPayload = {
'audio1.0': { unit: 'perGeneration', price: 5 },
'chirp-v5': { unit: 'perGeneration', price: 12 },
},
model3d: {
textToModelPricing: {
versionPrices: {
'v3.1-20260211': { noTexture: 8, texture: 16 },
'P2-20260801': { noTexture: 80, texture: 88 },
},
addOnPrices: {
hdTexture: 8,
ultraTexture: 16,
quadMesh: 4,
generateParts: 16,
},
},
imageToModelPricing: {
versionPrices: {
'v3.1-20260211': { noTexture: 16, texture: 24 },
'P2-20260801': { noTexture: 80, texture: 88 },
},
addOnPrices: {
hdTexture: 8,
ultraTexture: 16,
quadMesh: 4,
generateParts: 16,
},
},
},
};
function savedPricing(
overrides: Partial<EditorGenerationPricingConfigPayload> = {},
) {
return {
...pricing,
...overrides,
};
}
beforeEach(() => {
vi.clearAllMocks();
vi.mocked(getAdminEditorGenerationPricing).mockResolvedValue(pricing);
vi.mocked(upsertAdminEditorGenerationPricing).mockResolvedValue(
savedPricing({
models: {
...pricing.models,
'gpt-image-2': {
unit: 'perGeneration',
prices: { '1K': 20, '2K': 58 },
},
vi.mocked(upsertAdminEditorGenerationPricing).mockResolvedValue({
...pricing,
models: {
...pricing.models,
'gpt-image-2': {
unit: 'perGeneration',
prices: { '1K': 20, '2K': 58 },
},
}),
);
},
});
});
async function renderPage() {
test('模型定价后台按模型展示单位并保存尺寸定价', async () => {
const user = userEvent.setup();
render(
<AdminEditorGenerationPricingPage
token="admin-token"
onUnauthorized={vi.fn()}
/>,
);
await screen.findByText('3D 生成定价');
}
test('模型定价后台按模型展示单位并保存尺寸定价', async () => {
const user = userEvent.setup();
await renderPage();
expect((await screen.findAllByText('按次')).length).toBeGreaterThan(0);
expect(screen.getAllByText('按秒').length).toBeGreaterThan(0);
fireEvent.change(screen.getByLabelText('gpt-image-2 2K'), {
target: { value: '58' },
});
const gptImage2kInput = screen.getByLabelText('gpt-image-2 2K');
fireEvent.change(gptImage2kInput, { target: { value: '58' } });
await user.click(screen.getByRole('button', { name: '保存定价' }));
await user.click(screen.getByRole('button', { name: '确认' }));
@@ -134,82 +90,3 @@ test('模型定价后台按模型展示单位并保存尺寸定价', async () =>
);
});
});
test('3D 区块按端点渲染版本底价与加价项,并随保存回传整段与定价版本', async () => {
const user = userEvent.setup();
await renderPage();
expect(screen.getByText('文生 3D')).toBeTruthy();
expect(screen.getByText('图生 3D')).toBeTruthy();
expect(screen.getAllByText('高清贴图').length).toBeGreaterThan(0);
fireEvent.change(screen.getByLabelText('文生 3D v3.1-20260211 带贴图'), {
target: { value: '18' },
});
fireEvent.change(screen.getByLabelText('图生 3D P2-20260801 无贴图'), {
target: { value: '86' },
});
fireEvent.change(screen.getByLabelText('文生 3D 四边形网格'), {
target: { value: '6' },
});
await user.click(screen.getByRole('button', { name: '保存定价' }));
await user.click(screen.getByRole('button', { name: '确认' }));
await waitFor(() => {
expect(upsertAdminEditorGenerationPricing).toHaveBeenCalledWith(
'admin-token',
expect.objectContaining({
model3d: {
textToModelPricing: expect.objectContaining({
versionPrices: expect.objectContaining({
'v3.1-20260211': { noTexture: 8, texture: 18 },
}),
addOnPrices: expect.objectContaining({ quadMesh: 6 }),
}),
imageToModelPricing: expect.objectContaining({
versionPrices: expect.objectContaining({
'P2-20260801': { noTexture: 86, texture: 88 },
}),
}),
},
}),
);
});
});
test('数值小于 1 时禁用保存并给出中文提示', async () => {
const user = userEvent.setup();
await renderPage();
fireEvent.change(screen.getByLabelText('文生 3D 四边形网格'), {
target: { value: '0' },
});
expect(await screen.findByText(/以下价格必须是不小于 1 的整数/)).toBeTruthy();
const saveButton = screen.getByRole('button', {
name: '保存定价',
}) as HTMLButtonElement;
expect(saveButton.disabled).toBe(true);
await user.click(saveButton);
expect(upsertAdminEditorGenerationPricing).not.toHaveBeenCalled();
});
test('后端未配置 3D 段时给出提示并禁止保存', async () => {
vi.mocked(getAdminEditorGenerationPricing).mockResolvedValue({
...pricing,
model3d: null,
});
render(
<AdminEditorGenerationPricingPage
token="admin-token"
onUnauthorized={vi.fn()}
/>,
);
expect(await screen.findByText(/当前部署未配置 3D 定价段/)).toBeTruthy();
expect(
(screen.getByRole('button', { name: '保存定价' }) as HTMLButtonElement)
.disabled,
).toBe(true);
});
@@ -6,18 +6,12 @@ import {
upsertAdminEditorGenerationPricing,
} from '../api/adminApiClient';
import type {
EditorGenerationModel3dPricingPayload,
EditorGenerationModelPricingPayload,
EditorGenerationPricingConfigPayload,
EditorGenerationPricingUnitPayload,
} from '../api/adminApiTypes';
import { useAdminWriteConfirm } from '../components/useAdminWriteConfirm';
import { AdminEditorGenerationModel3dPricingSection } from './AdminEditorGenerationModel3dPricingSection';
import {
buildAdminEditorGenerationPricingRequest,
collectInvalidEditorGenerationPricingLabels,
} from './adminEditorGenerationPricing';
import { handlePageError, parsePositiveInteger } from './pageUtils';
import { handlePageError } from './pageUtils';
interface AdminEditorGenerationPricingPageProps {
token: string;
@@ -26,7 +20,6 @@ interface AdminEditorGenerationPricingPageProps {
const emptyPricing: EditorGenerationPricingConfigPayload = {
models: {},
model3d: null,
};
const unitLabels: Record<EditorGenerationPricingUnitPayload, string> = {
@@ -64,7 +57,7 @@ export function AdminEditorGenerationPricingPage({
async function handleSave(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
if (isSaving || !canSave) {
if (isSaving) {
return;
}
setErrorMessage('');
@@ -78,12 +71,7 @@ export function AdminEditorGenerationPricingPage({
setIsSaving(true);
try {
setPricing(
await upsertAdminEditorGenerationPricing(
token,
buildAdminEditorGenerationPricingRequest(pricing),
),
);
setPricing(await upsertAdminEditorGenerationPricing(token, pricing));
} catch (error: unknown) {
handlePageError(error, onUnauthorized, setErrorMessage);
} finally {
@@ -132,15 +120,6 @@ export function AdminEditorGenerationPricingPage({
});
}
function updateModel3dPrice(next: EditorGenerationModel3dPricingPayload) {
setPricing((current) => ({ ...current, model3d: next }));
}
const invalidPriceLabels =
collectInvalidEditorGenerationPricingLabels(pricing);
const isModel3dMissing = !pricing.model3d;
const canSave = invalidPriceLabels.length === 0 && !isModel3dMissing;
return (
<section className="admin-page admin-page-wide">
<div className="admin-page-heading">
@@ -165,20 +144,6 @@ export function AdminEditorGenerationPricingPage({
</div>
) : null}
{isModel3dMissing ? (
<div className="admin-alert" role="status">
当前部署未配置 3D 定价段(缺失即 3D
生成不可提交):需先由受控默认配置提供两段价格,
后台不在页面里新建或停用整段。
</div>
) : null}
{invalidPriceLabels.length > 0 ? (
<div className="admin-alert" role="status">
{`以下价格必须是不小于 1 的整数:${invalidPriceLabels.join('、')}`}
</div>
) : null}
<form className="admin-stack" onSubmit={handleSave}>
<div className="admin-pricing-model-list admin-pricing-model-list--single">
{Object.entries(pricing.models).map(([model, modelPricing]) =>
@@ -191,16 +156,9 @@ export function AdminEditorGenerationPricingPage({
)}
</div>
{pricing.model3d ? (
<AdminEditorGenerationModel3dPricingSection
model3d={pricing.model3d}
onChange={updateModel3dPrice}
/>
) : null}
<button
className="admin-primary-button"
disabled={isSaving || !canSave}
disabled={isSaving}
type="submit"
>
<Save size={17} aria-hidden="true" />
@@ -265,3 +223,8 @@ function renderModelPricingCard({
</section>
);
}
function parsePositiveInteger(value: string) {
const parsed = Number.parseInt(value, 10);
return Number.isFinite(parsed) && parsed > 0 ? parsed : 0;
}
@@ -7,7 +7,7 @@ import {
} from '../api/adminApiClient';
import type { ProfileWalletConfigAdminResponse } from '../api/adminApiTypes';
import { useAdminWriteConfirm } from '../components/useAdminWriteConfirm';
import { handlePageError, parsePositiveInteger } from './pageUtils';
import { handlePageError } from './pageUtils';
interface AdminProfileWalletConfigPageProps {
token: string;
@@ -197,3 +197,8 @@ export function AdminProfileWalletConfigPage({
</section>
);
}
function parsePositiveInteger(value: string) {
const parsed = Number(value);
return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : 0;
}
@@ -11,7 +11,7 @@ import type {
ProfileRechargeProductKind,
} from '../api/adminApiTypes';
import { useAdminWriteConfirm } from '../components/useAdminWriteConfirm';
import { handlePageError, parsePositiveInteger } from './pageUtils';
import { handlePageError } from './pageUtils';
interface AdminRechargeProductPageProps {
token: string;
@@ -546,6 +546,11 @@ function formatPrice(priceCents: number) {
return `¥${(priceCents / 100).toFixed(2)}`;
}
function parsePositiveInteger(value: string) {
const parsed = parseInteger(value);
return parsed > 0 ? parsed : 0;
}
function parseNonNegativeInteger(value: string) {
const parsed = parseInteger(value);
return parsed > 0 ? parsed : 0;
@@ -12,7 +12,7 @@ import type {
ProfileRedeemCodeMode,
} from '../api/adminApiTypes';
import { useAdminWriteConfirm } from '../components/useAdminWriteConfirm';
import { handlePageError, parsePositiveInteger, splitLines } from './pageUtils';
import { handlePageError, splitLines } from './pageUtils';
interface AdminRedeemCodePageProps {
token: string;
@@ -422,6 +422,11 @@ export function AdminRedeemCodePage({
);
}
function parsePositiveInteger(value: string) {
const parsed = Number.parseInt(value, 10);
return Number.isFinite(parsed) && parsed > 0 ? parsed : 0;
}
function redeemModeLabel(value: ProfileRedeemCodeMode) {
return redeemModes.find((item) => item.value === value)?.label ?? value;
}
@@ -19,7 +19,7 @@ import {
filterAdminTrackingEventKeyOptions,
findAdminTrackingEventDefinition,
} from '../config/trackingEventDefinitions';
import { handlePageError, parsePositiveInteger } from './pageUtils';
import { handlePageError } from './pageUtils';
interface AdminTaskConfigPageProps {
token: string;
@@ -571,6 +571,11 @@ export function AdminTaskConfigPage({
);
}
function parsePositiveInteger(value: string) {
const parsed = Number.parseInt(value, 10);
return Number.isFinite(parsed) && parsed > 0 ? parsed : 0;
}
function parseInteger(value: string) {
const parsed = Number.parseInt(value, 10);
return Number.isFinite(parsed) ? parsed : 0;
@@ -1,87 +0,0 @@
import { expect, test } from 'vitest';
import type { EditorGenerationPricingConfigPayload } from '../api/adminApiTypes';
import {
buildAdminEditorGenerationPricingRequest,
collectInvalidEditorGenerationPricingLabels,
editorModel3dAddOnLabel,
} from './adminEditorGenerationPricing';
import { parsePositiveInteger } from './pageUtils';
function pricingFixture(): EditorGenerationPricingConfigPayload {
return {
models: {
'gpt-image-2': { unit: 'perGeneration', prices: { '1K': 3, '2K': 5 } },
'audio1.0': { unit: 'perGeneration', price: 5 },
},
model3d: {
textToModelPricing: {
versionPrices: { 'v3.1-20260211': { noTexture: 8, texture: 16 } },
addOnPrices: { hdTexture: 8, unknownAddOn: 7 },
},
imageToModelPricing: {
versionPrices: { 'v3.1-20260211': { noTexture: 16, texture: 24 } },
addOnPrices: { hdTexture: 8 },
},
},
};
}
test('数值解析把空串、非数字、小数与 0 收敛成 0', () => {
expect(parsePositiveInteger('12')).toBe(12);
expect(parsePositiveInteger('')).toBe(0);
expect(parsePositiveInteger('abc')).toBe(0);
expect(parsePositiveInteger('12abc')).toBe(0);
expect(parsePositiveInteger('3.9')).toBe(0);
expect(parsePositiveInteger('0')).toBe(0);
expect(parsePositiveInteger('-3')).toBe(0);
});
test('未登记的 3D 加价项键回退显示原键名', () => {
expect(editorModel3dAddOnLabel('hdTexture')).toBe('高清贴图');
expect(editorModel3dAddOnLabel('unknownAddOn')).toBe('unknownAddOn');
});
test('校验覆盖模型档位与 3D 两段的全部数值', () => {
expect(collectInvalidEditorGenerationPricingLabels(pricingFixture())).toEqual(
[],
);
const invalid = pricingFixture();
const invalidImagePricing = invalid.models['gpt-image-2'];
const invalidTextEndpoint = invalid.model3d?.textToModelPricing;
const invalidImageEndpoint = invalid.model3d?.imageToModelPricing;
if (
!invalidImagePricing?.prices ||
!invalidTextEndpoint ||
!invalidImageEndpoint
) {
throw new Error('夹具必须包含图片档位与 3D 两段');
}
const imageVersionPrice = invalidImageEndpoint.versionPrices['v3.1-20260211'];
if (!imageVersionPrice) {
throw new Error('夹具必须包含图生 3D 的版本底价');
}
invalidImagePricing.prices['2K'] = 0;
invalidTextEndpoint.addOnPrices.hdTexture = 0;
imageVersionPrice.noTexture = 0;
expect(collectInvalidEditorGenerationPricingLabels(invalid)).toEqual([
'gpt-image-2 2K',
'文生 3D 高清贴图',
'图生 3D v3.1-20260211 无贴图',
]);
});
test('保存请求整段回传两段且不带定价版本,3D 缺失时回传 null', () => {
const pricing = pricingFixture();
expect(buildAdminEditorGenerationPricingRequest(pricing)).toEqual({
models: pricing.models,
model3d: pricing.model3d,
});
expect(
buildAdminEditorGenerationPricingRequest({ ...pricing, model3d: null }),
).toEqual({
models: pricing.models,
model3d: null,
});
});
@@ -1,115 +0,0 @@
import type {
AdminUpsertEditorGenerationPricingRequest,
EditorGenerationModel3dEndpointPricingPayload,
EditorGenerationModel3dPricingPayload,
EditorGenerationPricingConfigPayload,
} from '../api/adminApiTypes';
/** 3D 端点在两个并列段里的字段名与后台展示名。 */
export const EDITOR_MODEL3D_ENDPOINT_SECTIONS = [
{ key: 'textToModelPricing', label: '文生 3D' },
{ key: 'imageToModelPricing', label: '图生 3D' },
] as const satisfies ReadonlyArray<{
key: keyof EditorGenerationModel3dPricingPayload;
label: string;
}>;
/**
* 3D 加价项键到中文标签。键来自接口返回,这里只负责展示;遇到未登记的键回退显示原键名,
* 不因为标签缺失就把该项漏掉。
*/
const EDITOR_MODEL3D_ADD_ON_LABELS: Record<string, string> = {
hdTexture: '高清贴图',
ultraTexture: '超高清贴图',
hdGeometry: '高清几何',
quadMesh: '四边形网格',
smartLowPoly: '智能低面',
generateParts: '分件生成',
};
export function editorModel3dAddOnLabel(addOn: string) {
return EDITOR_MODEL3D_ADD_ON_LABELS[addOn] ?? addOn;
}
export function isPositiveMudPoints(value: number | null | undefined) {
return typeof value === 'number' && Number.isInteger(value) && value >= 1;
}
/**
* 提交前校验:所有泥点数值都必须是不小于 1 的整数,返回不合法项的展示名。
*
* 后端仍会独立校验一遍;这里只是不把明显非法的 payload 发出去,避免保存后才报错。
*/
export function collectInvalidEditorGenerationPricingLabels(
pricing: EditorGenerationPricingConfigPayload,
) {
const labels: string[] = [];
for (const [model, modelPricing] of Object.entries(pricing.models)) {
const tieredPrices = modelPricing.prices;
// `prices: {}` 没有档位可校验,不能算「这一项走分层计价」——否则底价会被顺手跳过校验。
if (tieredPrices && Object.keys(tieredPrices).length > 0) {
for (const [tier, price] of Object.entries(tieredPrices)) {
if (!isPositiveMudPoints(price)) {
labels.push(`${model} ${tier}`);
}
}
continue;
}
if (!isPositiveMudPoints(modelPricing.price)) {
labels.push(`${model} 泥点`);
}
}
const model3d = pricing.model3d;
if (model3d) {
for (const { key, label } of EDITOR_MODEL3D_ENDPOINT_SECTIONS) {
// 段与子表都来自接口返回:缺段或缺子表时只跳过这一段,不能让整页校验崩掉。
const endpointPricing = model3d[key];
if (!endpointPricing) {
continue;
}
for (const [version, price] of Object.entries(
endpointPricing.versionPrices ?? {},
)) {
if (!isPositiveMudPoints(price.noTexture)) {
labels.push(`${label} ${version} 无贴图`);
}
if (!isPositiveMudPoints(price.texture)) {
labels.push(`${label} ${version} 带贴图`);
}
}
for (const [addOn, price] of Object.entries(
endpointPricing.addOnPrices ?? {},
)) {
if (!isPositiveMudPoints(price)) {
labels.push(`${label} ${editorModel3dAddOnLabel(addOn)}`);
}
}
}
}
return labels;
}
/**
* 组装保存请求:`models` 与 3D 两段整段回传,不带定价版本(保存即覆盖,服务端不比对版本)。
* 3D 段缺失时回传 `null`——服务端会以 400 拒绝,后台不允许在这里静默新建或丢弃整段。
*/
export function buildAdminEditorGenerationPricingRequest(
pricing: EditorGenerationPricingConfigPayload,
): AdminUpsertEditorGenerationPricingRequest {
return {
models: pricing.models,
model3d: pricing.model3d ?? null,
};
}
/** 单个端点段的不可变更新:底价与加价项各改一处,其余原样保留。 */
export function updateEditorModel3dEndpointPricing(
model3d: EditorGenerationModel3dPricingPayload,
section: keyof EditorGenerationModel3dPricingPayload,
updater: (
current: EditorGenerationModel3dEndpointPricingPayload,
) => EditorGenerationModel3dEndpointPricingPayload,
): EditorGenerationModel3dPricingPayload {
return { ...model3d, [section]: updater(model3d[section]) };
}
-13
View File
@@ -1,18 +1,5 @@
import { formatAdminApiError, isAdminApiError } from '../api/adminApiClient';
/**
* 泥点 / 积分类输入框文本转正整数:空串、非数字、小数、0 与负数一律收敛成 0,
* 由各页的提交前校验拦住。
*
* 用 `Number` 而不是 `parseInt`:`parseInt` 会把 `3.9` 截成 3、`12abc` 截成 12,
* 等于把编辑中的半截输入静默当成合法值存下去;再用 `isSafeInteger` 挡掉超出安全整数
* 范围的值(那种值在别处会被静默改写)。后台各页统一用这一份,不再各写一遍。
*/
export function parsePositiveInteger(value: string) {
const parsed = Number(value);
return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : 0;
}
export function handlePageError(
error: unknown,
onUnauthorized: (message?: string) => void,
-29
View File
@@ -2972,34 +2972,6 @@ button:disabled {
gap: 10px;
}
.admin-pricing-model3d-list {
display: grid;
gap: 10px;
}
.admin-pricing-model3d-row {
display: grid;
grid-template-columns: minmax(0, 1.2fr) repeat(2, minmax(0, 1fr));
align-items: end;
gap: 10px;
}
.admin-pricing-model3d-row-label {
color: #5c3a26;
font-size: 13px;
font-weight: 700;
line-height: 1.4;
overflow-wrap: anywhere;
padding-bottom: 10px;
}
.admin-pricing-model3d-hint {
color: #7b5a45;
font-size: 13px;
line-height: 1.5;
margin: 0;
}
.admin-pricing-unit {
flex: 0 0 auto;
border-radius: 999px;
@@ -3050,7 +3022,6 @@ button:disabled {
.admin-two-column-wide,
.admin-accounts-layout,
.admin-pricing-grid,
.admin-pricing-model3d-row,
.admin-form-row,
.admin-filter-grid,
.admin-table-query-grid,
+5 -1
View File
@@ -13,6 +13,10 @@
"skill-pack:check": "node scripts/check-skill-pack.mjs",
"skill-pack:sync": "node scripts/check-skill-pack.mjs --write",
"skill-pack:test": "node --test scripts/check-skill-pack.test.mjs",
"bundled-resources:check": "node scripts/check-package-layout.mjs",
"bundled-resources:sync": "node scripts/check-package-layout.mjs --write",
"bundled-resources:prepare": "node scripts/prepare-bundled-resources.mjs",
"bundled-resources:test": "node --test scripts/prepare-bundled-resources.test.mjs",
"llm-status": "node scripts/run-cli-with-config.mjs --llm-status",
"agent-task": "node scripts/run-cli-with-config.mjs --agent-task",
"config": "node scripts/game-creator-config-wizard.mjs",
@@ -24,7 +28,7 @@
"agent-runtime:supervisor-swarm-tool-plan-handoff-runner-kill-real-e2e": "node scripts/agent-runtime-real-e2e.mjs --suite supervisor-swarm-tool-plan-handoff-runner-kill",
"agent-runtime:steer-real-e2e": "node scripts/agent-runtime-steer-real-e2e.mjs",
"agent-runtime:steer-runner-kill-real-e2e": "node scripts/agent-runtime-real-e2e.mjs --suite steer-runner-kill",
"typecheck": "tsc -p tsconfig.json --noEmit && npm run skill-pack:check && node scripts/check-config.mjs"
"typecheck": "tsc -p tsconfig.json --noEmit && npm run skill-pack:check && npm run bundled-resources:check && node scripts/check-config.mjs"
},
"dependencies": {
"@cubone/react-file-manager": "^1.35.0",
@@ -18,6 +18,7 @@ import {
resolveReleaseChannel,
} from './channel-identity.mjs';
import { prepareNsisToolsetForRelease } from './nsis-toolset.mjs';
import { prepareBundledResources } from './prepare-bundled-resources.mjs';
import { stageNodeRuntime } from './stage-node-runtime.mjs';
const appRoot = fileURLToPath(new URL('..', import.meta.url));
@@ -431,10 +432,29 @@ function writeChannelConfigFile(channel, target, includeNodeRuntime = false) {
return configPath;
}
/// 随包资源必须在打包工具之前生成:构建脚本只做只读校验,不再生成。
export function stageBundledResources(
target,
{ prepare = prepareBundledResources } = {},
) {
const summaries = prepare({
target,
features: new Set(defaultEditorFeatures(target)),
log: (line) => console.log(`[ai-game-creator-shell] ${line}`),
});
for (const summary of summaries) {
console.log(`[ai-game-creator-shell] ${summary}`);
}
}
export function runTauriBuild(
args = [],
context = resolveReleaseContext(args),
{ spawn = spawnSync, stageRuntime = stageNodeRuntime } = {},
{
spawn = spawnSync,
stageRuntime = stageNodeRuntime,
stageBundled = stageBundledResources,
} = {},
) {
if (
explicitBuildTarget(args) &&
@@ -444,7 +464,10 @@ export function runTauriBuild(
}
const tauriArguments = buildTauriBuildArguments(args, context.target);
const { channel, target } = context;
if (!args.includes('--no-bundle')) stageRuntime(target);
if (!args.includes('--no-bundle')) {
stageRuntime(target);
stageBundled(target);
}
const configPath = writeChannelConfigFile(
channel,
target,
@@ -363,6 +363,8 @@ test('packaged renderer receives the same channel as the updater manifest', () =
// 必须 stub:真实 staging 会用宿主平台(如 macOS 的 darwin/arm64)去对默认的
// Windows 目标做一致性校验,在非 Windows 主机上直接失败——本用例只关心渠道注入。
stageRuntime: () => {},
// 同上:随包资源准备会读取真实上游包,本用例只关心渠道环境变量。
stageBundled: () => {},
spawn: (_binary, _args, options) => {
spawnOptions = options;
return { status: 0 };
@@ -456,6 +458,8 @@ test('explicit macOS target drives version lookup, Tauri endpoint, artifact and
seenContexts.push(context);
runTauriBuild(args, context, {
stageRuntime: () => {},
// 必须 stub:随包资源准备会读取真实上游包与仓库插件工作区,本用例只关心参数。
stageBundled: () => {},
spawn: (_binary, command) => {
const configIndex = command.lastIndexOf('--config');
const config = JSON.parse(
@@ -705,6 +709,7 @@ test('Windows remains the default and explicit Windows overrides macOS environme
context,
{
stageRuntime: () => {},
stageBundled: () => {},
spawn: (_binary, command) => {
assert.ok(
command.includes(
@@ -818,6 +823,10 @@ test('release stages Node before Tauri and injects its resource mapping only for
assert.equal(target, windowsTarget);
events.push('stage');
},
stageBundled(target) {
assert.equal(target, windowsTarget);
events.push('bundled');
},
spawn(_binary, args) {
events.push('build');
const config = JSON.parse(
@@ -829,11 +838,14 @@ test('release stages Node before Tauri and injects its resource mapping only for
return { status: 0 };
},
});
assert.deepEqual(events, ['stage', 'build']);
assert.deepEqual(events, ['stage', 'bundled', 'build']);
runTauriBuild(['--no-bundle', '--target', windowsTarget], context, {
stageRuntime() {
assert.fail('no-bundle must not stage resources');
},
stageBundled() {
assert.fail('no-bundle must not stage bundled resources');
},
spawn(_binary, args) {
const config = JSON.parse(
readFileSync(args[args.lastIndexOf('--config') + 1], 'utf8'),
@@ -848,6 +860,9 @@ test('release stages Node before Tauri and injects its resource mapping only for
stageRuntime() {
throw new Error('missing runtime');
},
stageBundled() {
assert.fail('invalid runtime must prevent bundled staging');
},
spawn() {
assert.fail('invalid runtime must prevent build');
},
@@ -960,6 +975,8 @@ for (const channel of ['release', 'beta-2']) {
);
runTauriBuild([`--target=${target}`], context, {
stageRuntime: () => {},
// 必须 stub:随包资源准备会读取真实上游包与仓库插件工作区,本用例只关心参数。
stageBundled: () => {},
spawn: (_binary, command) => {
const config = JSON.parse(
readFileSync(
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,460 @@
import assert from 'node:assert/strict';
import { createHash } from 'node:crypto';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { test } from 'node:test';
import {
DECLARATION_PATH,
findCodexSource,
pluginDirectories,
prepareBundledResources,
readDeclaration,
resolveHostTarget,
stagingUnit,
} from './prepare-bundled-resources.mjs';
const WINDOWS_TARGET = 'x86_64-pc-windows-msvc';
const MAC_TARGET = 'aarch64-apple-darwin';
function sha256File(file) {
return createHash('sha256').update(fs.readFileSync(file)).digest('hex');
}
/// 造一个最小工作区:app(含 node_modules 上游包)、repo(含 plugins 工作区)、lockfile。
function buildFixture({ targets = [WINDOWS_TARGET], plugins = true } = {}) {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'agc-resources-'));
const appRoot = path.join(root, 'app');
const repoRoot = path.join(root, 'repo');
const destinationRoot = path.join(appRoot, 'src-tauri');
const declaration = readDeclaration(DECLARATION_PATH);
const lockfile = { packages: {} };
for (const target of targets) {
const layout = declaration.codex.targets.find(
(entry) => entry.target === target,
);
assert.ok(layout, `声明缺少目标 ${target}`);
const vendor = path.join(
appRoot,
`node_modules/@openai/codex-${layout.platform}/vendor/${target}`,
);
for (const relative of layout.files) {
const file = path.join(vendor, relative);
fs.mkdirSync(path.dirname(file), { recursive: true });
fs.writeFileSync(
file,
relative === declaration.codex.packageMetadataFileName
? `${JSON.stringify(
{
layoutVersion: declaration.codex.packageMetadata.layoutVersion,
version: declaration.codex.version,
target,
entrypoint: layout.executable,
resourcesDir: declaration.codex.packageMetadata.resourcesDir,
pathDir: declaration.codex.packageMetadata.pathDir,
},
null,
2,
)}\n`
: `component ${target} ${relative}\n`,
);
if (relative === layout.executable) {
fs.chmodSync(file, 0o755);
}
}
lockfile.packages[`node_modules/@openai/codex-${layout.platform}`] = {
resolved: `https://registry.npmjs.org/@openai/codex-${layout.platform}/-/${layout.platform}.tgz`,
integrity: `sha512-${target}`,
};
}
fs.mkdirSync(path.join(destinationRoot, 'resources/codex'), {
recursive: true,
});
for (const entry of declaration.codex.noticeSources) {
if (entry.preserve) {
continue;
}
const file = path.join(destinationRoot, entry.source);
fs.mkdirSync(path.dirname(file), { recursive: true });
fs.writeFileSync(file, 'mac codex notice\n');
}
if (targets.includes(WINDOWS_TARGET)) {
const tracked = path.join(
destinationRoot,
'resources/codex/win-x64/NOTICE.md',
);
fs.mkdirSync(path.dirname(tracked), { recursive: true });
fs.writeFileSync(tracked, 'windows codex notice\n');
}
if (plugins) {
const pluginRoot = path.join(repoRoot, 'plugins/agc-demo-editor');
fs.mkdirSync(path.join(pluginRoot, 'src'), { recursive: true });
fs.mkdirSync(path.join(pluginRoot, 'panels'), { recursive: true });
fs.mkdirSync(path.join(pluginRoot, 'target'), { recursive: true });
fs.mkdirSync(path.join(pluginRoot, '.git'), { recursive: true });
fs.writeFileSync(
path.join(pluginRoot, 'plugin.json'),
'{"name":"agc-demo-editor"}\n',
);
fs.writeFileSync(
path.join(pluginRoot, 'src/entry.mjs'),
'export const entry = 1;\n',
);
fs.writeFileSync(
path.join(pluginRoot, 'panels/panel.html'),
'<html></html>\n',
);
fs.writeFileSync(path.join(pluginRoot, 'panels/panel.test.mjs'), 'test\n');
fs.writeFileSync(path.join(pluginRoot, 'target/junk.rs'), 'junk\n');
fs.writeFileSync(path.join(pluginRoot, '.git/HEAD'), 'ref\n');
fs.writeFileSync(path.join(pluginRoot, '.env'), 'secret\n');
}
const lockfilePath = path.join(root, 'package-lock.json');
fs.writeFileSync(lockfilePath, JSON.stringify(lockfile, null, 2));
return {
root,
appRoot,
repoRoot,
destinationRoot,
lockfilePath,
recordPath: path.join(root, 'record.json'),
declaration,
cleanup: () => fs.rmSync(root, { recursive: true, force: true }),
};
}
function snapshot(directory) {
const entries = [];
const stack = [['', directory]];
while (stack.length > 0) {
const [prefix, current] = stack.pop();
for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
const relative = prefix ? `${prefix}/${entry.name}` : entry.name;
const full = path.join(current, entry.name);
if (entry.isDirectory()) {
stack.push([relative, full]);
} else {
const info = fs.statSync(full);
entries.push({
relative,
size: info.size,
mtimeMs: info.mtimeMs,
mode: info.mode & 0o777,
sha256: sha256File(full),
});
}
}
}
return entries.sort((left, right) =>
left.relative.localeCompare(right.relative),
);
}
function prepare(fixture, overrides = {}) {
return prepareBundledResources({
target: WINDOWS_TARGET,
destinationRoot: fixture.destinationRoot,
declarationPath: DECLARATION_PATH,
recordPath: fixture.recordPath,
lockfilePath: fixture.lockfilePath,
repoRoot: fixture.repoRoot,
appRoot: fixture.appRoot,
...overrides,
});
}
test('stages declared codex components with manifest and preserved notice', () => {
const fixture = buildFixture();
try {
const summaries = prepare(fixture);
assert.match(summaries[0], /codex x86_64-pc-windows-msvc 重新生成/);
const declaration = fixture.declaration;
const layout = declaration.codex.targets.find(
(entry) => entry.target === WINDOWS_TARGET,
);
const unit = path.join(
fixture.destinationRoot,
'resources/codex',
layout.directory,
);
for (const relative of layout.files) {
assert.ok(
fs.existsSync(path.join(unit, relative)),
`缺少组件 ${relative}`,
);
}
assert.equal(
fs.readFileSync(path.join(unit, 'NOTICE.md'), 'utf8'),
'windows codex notice\n',
'受版本控制的第三方声明必须原地保留',
);
const manifest = JSON.parse(
fs.readFileSync(path.join(unit, 'manifest.json'), 'utf8'),
);
assert.deepEqual(Object.keys(manifest), [
'files',
'platform',
'schemaVersion',
'version',
]);
assert.equal(manifest.platform, layout.platform);
assert.equal(manifest.schemaVersion, declaration.codex.manifestSchema);
assert.equal(
manifest.version,
`${declaration.codex.cliVersionPrefix}${declaration.codex.version}`,
);
assert.deepEqual(
Object.keys(manifest.files).sort(),
[...layout.files].sort(),
'清单文件集合必须等于组件白名单',
);
for (const relative of layout.files) {
assert.equal(
manifest.files[relative],
sha256File(path.join(unit, relative)),
);
}
} finally {
fixture.cleanup();
}
});
test('second run is a no-op: identical content and timestamps', () => {
const fixture = buildFixture();
try {
prepare(fixture);
const unit = path.join(
fixture.destinationRoot,
'resources/codex',
'win-x64',
);
const plugins = path.join(fixture.destinationRoot, 'resources/plugins');
const before = { codex: snapshot(unit), plugins: snapshot(plugins) };
const summaries = prepare(fixture);
assert.match(summaries[0], /命中缓存/);
assert.match(summaries[1], /命中缓存/);
assert.deepEqual(
snapshot(unit),
before.codex,
'codex 产物内容与时间戳必须不变',
);
assert.deepEqual(
snapshot(plugins),
before.plugins,
'插件产物内容与时间戳必须不变',
);
} finally {
fixture.cleanup();
}
});
test('stages the macOS universal group with both architectures', () => {
const fixture = buildFixture({
targets: [MAC_TARGET, 'x86_64-apple-darwin'],
});
try {
const summaries = prepare(fixture, { target: MAC_TARGET });
assert.match(summaries[0], /mac-native/);
const unit = path.join(
fixture.destinationRoot,
'resources/codex/mac-native',
);
for (const directory of ['darwin-arm64', 'darwin-x64']) {
for (const file of ['bin/codex', 'manifest.json', 'NOTICE.md']) {
assert.ok(
fs.existsSync(path.join(unit, directory, file)),
`缺少 ${directory}/${file}`,
);
}
assert.equal(
fs.readFileSync(path.join(unit, directory, 'NOTICE.md'), 'utf8'),
'mac codex notice\n',
);
}
assert.deepEqual(fs.readdirSync(unit).sort(), [
'darwin-arm64',
'darwin-x64',
]);
} finally {
fixture.cleanup();
}
});
test('copies only whitelisted plugin subdirectories', () => {
const fixture = buildFixture();
try {
prepare(fixture);
const staged = path.join(
fixture.destinationRoot,
'resources/plugins/agc-demo-editor',
);
assert.ok(fs.existsSync(path.join(staged, 'plugin.json')));
assert.ok(fs.existsSync(path.join(staged, 'src/entry.mjs')));
assert.ok(fs.existsSync(path.join(staged, 'panels/panel.html')));
assert.ok(
!fs.existsSync(path.join(staged, 'panels/panel.test.mjs')),
'测试文件不随包',
);
assert.ok(
!fs.existsSync(path.join(staged, 'target')),
'构建产物目录不随包',
);
assert.ok(!fs.existsSync(path.join(staged, '.git')), '隐藏目录不随包');
assert.ok(!fs.existsSync(path.join(staged, '.env')), '隐藏文件不随包');
} finally {
fixture.cleanup();
}
});
test('fails closed when the upstream package metadata drifts from the declaration', () => {
const fixture = buildFixture();
try {
const declaration = fixture.declaration;
const layout = declaration.codex.targets.find(
(entry) => entry.target === WINDOWS_TARGET,
);
const metadataFile = path.join(
fixture.appRoot,
`node_modules/@openai/codex-${layout.platform}/vendor/${WINDOWS_TARGET}/codex-package.json`,
);
const metadata = JSON.parse(fs.readFileSync(metadataFile, 'utf8'));
assert.equal(metadata.version, declaration.codex.version);
for (const [key, value] of [
['version', '0.0.0'],
['layoutVersion', 2],
['entrypoint', 'bin/other.exe'],
['resourcesDir', '../private'],
]) {
fs.writeFileSync(
metadataFile,
`${JSON.stringify({ ...metadata, [key]: value }, null, 2)}\n`,
);
assert.throws(
() => prepare(fixture),
/上游包元数据与声明不一致/,
`${key} 漂移必须被拒绝`,
);
}
assert.ok(
!fs.existsSync(
path.join(
fixture.destinationRoot,
'resources/codex/win-x64/manifest.json',
),
),
'拒绝时不得留下产物',
);
} finally {
fixture.cleanup();
}
});
test('fails closed when the upstream package is missing', () => {
const fixture = buildFixture();
try {
fs.rmSync(path.join(fixture.appRoot, 'node_modules'), {
recursive: true,
force: true,
});
assert.throws(() => prepare(fixture), /npm ci/);
assert.ok(
!fs.existsSync(
path.join(
fixture.destinationRoot,
'resources/codex/win-x64/manifest.json',
),
),
'失败时不得留下半成品清单',
);
assert.ok(
!fs.existsSync(path.join(fixture.destinationRoot, 'resources/plugins')),
);
} finally {
fixture.cleanup();
}
});
test('fails closed for unsupported targets', () => {
const fixture = buildFixture();
try {
assert.throws(
() => prepare(fixture, { target: 'x86_64-unknown-linux-gnu' }),
/声明不含目标/,
);
assert.throws(() => resolveHostTarget('linux', 'x64'), /不支持的目标平台/);
} finally {
fixture.cleanup();
}
});
test('fails closed when the destination is owned by something else', () => {
const fixture = buildFixture();
try {
const unit = path.join(fixture.destinationRoot, 'resources/codex/win-x64');
fs.writeFileSync(path.join(unit, 'foreign.bin'), 'foreign\n');
assert.throws(() => prepare(fixture), /被非本工具内容占用/);
const plugins = path.join(fixture.destinationRoot, 'resources/plugins');
fs.rmSync(path.join(unit, 'foreign.bin'), { force: true });
fs.mkdirSync(path.join(plugins, 'someone-elses-plugin'), {
recursive: true,
});
assert.throws(() => prepare(fixture), /插件随包目录被非本工具内容占用/);
} finally {
fixture.cleanup();
}
});
test('dry run writes nothing', () => {
const fixture = buildFixture();
try {
const summaries = prepare(fixture, { dryRun: true });
assert.match(summaries[0], /需要重新生成(dry-run 未写入)/);
const unit = path.join(fixture.destinationRoot, 'resources/codex/win-x64');
assert.deepEqual(
fs.readdirSync(unit),
['NOTICE.md'],
'dry-run 不得写入任何组件或清单',
);
assert.ok(
!fs.existsSync(path.join(fixture.destinationRoot, 'resources/plugins')),
);
assert.ok(!fs.existsSync(fixture.recordPath));
} finally {
fixture.cleanup();
}
});
test('declaration drives source lookup and staging units', () => {
const declaration = readDeclaration(DECLARATION_PATH);
const windows = stagingUnit(declaration, WINDOWS_TARGET);
assert.equal(windows.directory, 'win-x64');
assert.deepEqual(
windows.targets.map((member) => member.target),
[WINDOWS_TARGET],
);
const mac = stagingUnit(declaration, MAC_TARGET);
assert.equal(mac.directory, 'mac-native');
assert.deepEqual(
mac.targets.map((member) => member.target),
['aarch64-apple-darwin', 'x86_64-apple-darwin'],
);
const fixture = buildFixture();
try {
const source = findCodexSource(declaration, WINDOWS_TARGET, {
app: fixture.appRoot,
repo: fixture.repoRoot,
});
assert.match(source, /codex-win32-x64\/vendor\/x86_64-pc-windows-msvc$/);
assert.equal(pluginDirectories(declaration, fixture.repoRoot).length, 1);
} finally {
fixture.cleanup();
}
});
@@ -1,6 +1,7 @@
import { resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { buildLocalRustProcessEnv } from '../../../scripts/dev.mjs';
import {
defaultEditorFeatures,
withDefaultCargoFeatures,
@@ -10,6 +11,10 @@ import {
resolveAgcDevEndpoint,
withAgcDevEndpointEnv,
} from './dev-port.mjs';
import {
prepareBundledResources,
supportedHostTarget,
} from './prepare-bundled-resources.mjs';
import {
isAiGameCreatorServer,
preflightExistingVite,
@@ -67,6 +72,29 @@ function withDevCargoFeatures(argv, features = readDevCargoFeatures()) {
return withDefaultCargoFeatures(argv, features);
}
/// 随包资源必须在 Tauri 之前生成:构建脚本只做只读校验,不再生成资源。
/// 命中缓存的重复调用不写任何文件,因此每次 dev 启动都会先跑一次。
function prepareBundledResourcesBeforeTauri(
features = readDevCargoFeatures(),
{ prepare = prepareBundledResources, log = console.log } = {},
) {
const target = supportedHostTarget();
if (!target) {
log(
'[ai-game-creator-shell] 当前平台不受随包资源声明覆盖,跳过随包资源准备',
);
return;
}
const summaries = prepare({
target,
features: new Set(features),
log: (line) => log(`[ai-game-creator-shell] ${line}`),
});
for (const summary of summaries) {
log(`[ai-game-creator-shell] ${summary}`);
}
}
function spawnTauriCli(argv, { env = process.env } = {}) {
return spawnChild(process.execPath, [tauriCliPath, ...argv], {
cwd: appRoot,
@@ -75,6 +103,17 @@ function spawnTauriCli(argv, { env = process.env } = {}) {
});
}
/// Tauri dev 的 Cargo 直接继承启动器环境,用户级 / 仓库级 Cargo 配置里的
/// `rustc-wrapper`(本地常见为 sccache)会在这里生效。本地 sccache daemon 状态
/// 一旦损坏,`cargo` 的首次 rustc 探测就会失败并阻断整个 AGC 启动;因此这里复用
/// `npm run dev` 的本地 Rust 环境规则,由脚本而不是本机 Cargo 配置决定 wrapper。
function buildTauriDevProcessEnv(endpoint, env = process.env) {
return buildLocalRustProcessEnv({
...withAgcDevEndpointEnv(endpoint, env),
[AGC_DESIGN_DEBUG_ENV]: designDebugEnabled,
});
}
async function runTauriDev(
argv = process.argv.slice(2),
{
@@ -84,6 +123,7 @@ async function runTauriDev(
spawnCli = spawnTauriCli,
waitForCli = waitForChildTermination,
terminateTree = terminateChildTree,
prepareResources = prepareBundledResourcesBeforeTauri,
} = {},
) {
const endpoint = await resolveDevEndpoint();
@@ -130,15 +170,14 @@ async function runTauriDev(
shutdownRequested.then(() => false),
]);
if (!prepared || shutdownSignal) return 1;
const devFeatures = readDevCargoFeatures();
prepareResources(devFeatures);
const tauriArguments = buildTauriArguments(
withDevCargoFeatures(argv),
withDevCargoFeatures(argv, devFeatures),
endpoint.url,
);
child = spawnCli(tauriArguments, {
env: {
...withAgcDevEndpointEnv(endpoint),
[AGC_DESIGN_DEBUG_ENV]: designDebugEnabled,
},
env: buildTauriDevProcessEnv(endpoint),
});
const childResult = waitForCli(child);
const outcome = await Promise.race([
@@ -225,7 +264,9 @@ function isDirectModuleExecution() {
export {
buildTauriArguments,
buildTauriDevProcessEnv,
isDirectModuleExecution,
prepareBundledResourcesBeforeTauri,
runTauriDev,
spawnTauriCli,
withDevCargoFeatures,
+4 -3
View File
@@ -6312,7 +6312,8 @@ dependencies = [
[[package]]
name = "ts-rs"
version = "12.0.1"
source = "git+https://github.com/Aleph-Alpha/ts-rs?rev=fd8679ef20f866f747c046b30bd29c15e3d7a003#fd8679ef20f866f747c046b30bd29c15e3d7a003"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "756050066659291d47a554a9f558125db17428b073c5ffce1daf5dcb0f7231d8"
dependencies = [
"thiserror 2.0.18",
"ts-rs-macros 12.0.1",
@@ -6333,9 +6334,9 @@ dependencies = [
[[package]]
name = "ts-rs-macros"
version = "12.0.1"
source = "git+https://github.com/Aleph-Alpha/ts-rs?rev=fd8679ef20f866f747c046b30bd29c15e3d7a003#fd8679ef20f866f747c046b30bd29c15e3d7a003"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38d90eea51bc7988ef9e674bf80a85ba6804739e535e9cab48e4bb34a8b652aa"
dependencies = [
"indexmap 2.14.0",
"proc-macro2",
"quote",
"syn 2.0.118",
@@ -29,10 +29,7 @@ tauri-build = { version = "2.6.2", features = [] }
[dependencies]
codex-patch-parser = { path = "vendor/codex-patch-parser" }
chrono = { version = "0.4", default-features = false, features = ["std"] }
# 与 server-rs 同一口径:已发布的 ts-rs 12.0.1 解析不了 enum 级 `serde(deny_unknown_fields)`
# (见 agent/direct_codex_user_item/model.rs),上游修复 #491 未发版,故固定到该 commit;
# 等 crates.io 发布 > 12.0.1 后改回 registry 版本。不要改用 `no-serde-warnings` 关告警。
ts-rs = { git = "https://github.com/Aleph-Alpha/ts-rs", rev = "fd8679ef20f866f747c046b30bd29c15e3d7a003" }
ts-rs = "12.0.1"
typed_floats = { version = "1.0.7", features = ["serde"] }
nalgebra = { version = "0.35.0", features = ["serde-serialize"] }
agent-runtime-core = { path = "../../../server-rs/crates/agent-runtime-core" }
@@ -69,7 +66,7 @@ platform-agent = { path = "../../../server-rs/crates/platform-agent" }
portable-pty = "0.9"
reqwest = { version = "0.12", default-features = false, features = ["json", "multipart", "native-tls", "stream"] }
regex = "1"
shared-contracts = { path = "../../../server-rs/crates/shared-contracts", default-features = false }
shared-contracts = { path = "../../../server-rs/crates/shared-contracts", default-features = false, features = ["ts-bindings"] }
tauri = { version = "2.11.2", features = [] }
tauri-plugin-dialog = "2.7.1"
tauri-plugin-http = { version = "2.5.9", default-features = false, features = ["charset", "cookies", "http2", "rustls-tls"] }

Some files were not shown because too many files have changed in this diff Show More