feat: integrate platform backend and application interfaces

This commit is contained in:
Qiufeng
2026-09-02 21:09:47 +08:00
parent cd9ac1af70
commit b16390dd41
648 changed files with 102488 additions and 17737 deletions
+1 -1
View File
@@ -5,7 +5,7 @@ description: Free use and licensing terms for MiragenFlow
# MIT License
MiragenFlow is licensed under the [MIT License](https://github.com/basketikun/infinite-canvas/blob/main/LICENSE). Anyone may use it free of charge for personal, academic, internal, SaaS, private deployment, open-source, or closed-source commercial purposes.
MiragenFlow is licensed under the [MIT License](https://git.awaioi.com/awaioi/MiragenFlow/src/branch/main/LICENSE). Anyone may use it free of charge for personal, academic, internal, SaaS, private deployment, open-source, or closed-source commercial purposes.
You may use, copy, modify, merge, publish, distribute, sublicense, and sell copies of the project. You do not need to purchase a commercial license or publish your modifications or application source code.
+1 -1
View File
@@ -5,7 +5,7 @@ description: 元境幻生的免费使用和授权说明
# MIT 开源协议
元境幻生采用 [MIT License](https://github.com/basketikun/infinite-canvas/blob/main/LICENSE)。任何个人、团队和企业都可以免费、自由地使用本项目,没有使用场景和商业用途限制。
元境幻生采用 [MIT License](https://git.awaioi.com/awaioi/MiragenFlow/src/branch/main/LICENSE)。任何个人、团队和企业都可以免费、自由地使用本项目,没有使用场景和商业用途限制。
## 项目初衷
@@ -5,17 +5,17 @@ description: Frontend-first local development
# Local Development
The main application lives in `web/`. The browser sends AI requests directly to the user's OpenAI-compatible endpoint.
The local application runs `web/`, `admin/`, and `server/` together. The frontend and admin proxy through one public development origin, and the server gateway handles AI requests.
## Start the frontend
```bash
cd web
bun install
bun run dev
bun run dev:all
```
Open `http://localhost:3000`, then enter your `Base URL`, `API Key`, and model names in Settings. AI providers and WebDAV are accessed directly by the browser.
Open `http://localhost:3000`, register a user, and use `/admin/` to configure public products and channels. Users do not enter provider URLs or API keys; WebDAV is only for personal generated data.
## Start the documentation site
@@ -5,14 +5,14 @@ description: 前端优先的本地开发方式
# 本地开发
当前主应用以 `web/` 前端为主,AI 请求由浏览器前台直连用户自己的 OpenAI 兼容接口。
当前开发环境由 `web/`、`admin/` 和 `server/` 共同组成;前后台通过 Vite 代理访问同一公开端口,AI 请求由服务端网关转发。
## 1. 启动前端
```bash
cd web
bun install
bun run dev
bun run dev:all
```
前端默认访问:
@@ -23,7 +23,7 @@ http://localhost:3000
## 2. 配置模型
打开右上角配置弹窗,填写自己的 `Base URL`、`API Key` 和模型名。AI 服务与 WebDAV 都由浏览器前端直连。
打开 `/` 注册用户,管理员访问 `/admin/` 配置公开模型、渠道和价格。用户端不填写 `Base URL` 或 `API Key`;WebDAV 仅用于保存个人生成数据。
## 3. 启动文档站
+16 -28
View File
@@ -5,22 +5,27 @@ description: Deploy MiragenFlow with Docker Compose
# Docker Deployment
## Use the published image
## Production Compose
```bash
git clone git@github.com:basketikun/infinite-canvas.git
cd infinite-canvas
docker compose up -d
git clone https://git.awaioi.com/awaioi/MiragenFlow.git
cd MiragenFlow
cp .env.example .env
# Set PostgreSQL, access-token, admin, MFA, and channel-encryption secrets in .env.
docker compose up -d --build
```
Open `http://localhost:3000`.
The stack builds the frontend/admin bundle and starts the API, PostgreSQL, and
Redis services. After migrations complete, open `http://localhost:3000`.
## Build locally
## Local fixture
```bash
docker compose -f docker-compose.local.yml up -d --build
```
The local fixture uses file persistence and an in-process queue. It is useful for UI work, but it does not validate the PostgreSQL/Redis production profile.
## Documentation image
The documentation in `docs/` is a separate Next.js standalone application with server capabilities.
@@ -37,26 +42,9 @@ cd docs
docker compose -f docker-compose.local.yml up -d --build
```
The main application container only serves the web application. Canvases, My Assets, and API keys are stored in the browser by default. WebDAV can synchronize canvases, assets, and their referenced image or audio files, so no application-managed data volume is required for this data.
The Compose deployment starts the frontend, admin bundle, API gateway, and server. Authentication, balance, tasks, channel routing, and WebSocket events are handled by the server. Canvases and assets remain browser-local by default; media can be synchronized to WebDAV and provider credentials never enter browser business storage.
## Optional analytics
No analytics are enabled by default. A deployment can enable GA4, Baidu Analytics, or both at runtime:
| Variable | Description |
| --- | --- |
| `ANALYTICS_GA4_ID` | Google Analytics 4 measurement ID (`G-XXXX`) |
| `ANALYTICS_BAIDU_ID` | Baidu site ID, the hash after `hm.js?` |
```yaml
services:
app:
image: ghcr.io/basketikun/infinite-canvas:latest
ports:
- "3000:3000"
environment:
ANALYTICS_GA4_ID: G-XXXXXXXXXX
ANALYTICS_BAIDU_ID: e8263761cc5c32f5f7a855b8203b965a
```
SPA route changes automatically report page views to enabled providers. Only configure analytics IDs on sites you control, and do not commit them to the repository.
The frontend does not inject third-party analytics scripts or send page data to
external analytics providers. Authentication, balances, jobs, queues, and
WebSocket events still require the frontend, server, PostgreSQL, and Redis
services in the same Compose stack.
+13 -46
View File
@@ -5,23 +5,25 @@ description: 使用 Docker Compose 部署元境幻生
# Docker 部署
如果你希望在自己的机器或服务器上运行项目,可以直接使用 Docker Compose。
主应用的生产配置依赖 PostgreSQL、Redis 和服务端容器;请先准备生产密钥,再使用 Docker Compose 构建整套同源服务。当前仓库未发布可直接替代整套服务的单容器镜像。
## 使用发布镜像
## 生产 Compose
```bash
git clone git@github.com:basketikun/infinite-canvas.git
cd infinite-canvas
docker compose up -d
git clone https://git.awaioi.com/awaioi/MiragenFlow.git
cd MiragenFlow
cp .env.example .env
# 在 .env 中填写 PostgreSQL、Access Token、管理员、MFA 和渠道加密密钥。
docker compose up -d --build
```
启动后访问:
首次启动会执行数据库迁移;构建和迁移完成后访问:
```text
http://localhost:3000
```
## 本地构建镜像
## 本地 fixture
如果需要基于当前源码构建镜像:
@@ -29,6 +31,8 @@ http://localhost:3000
docker compose -f docker-compose.local.yml up -d --build
```
本地 fixture 使用文件持久化和进程内队列,只适合前台/后台开发,不能替代 PostgreSQL/Redis 生产联调。
## 文档站镜像
文档站位于 `docs/` 目录,按带服务端能力的 Next.js standalone 应用单独构建,不打进主应用镜像。
@@ -49,43 +53,6 @@ docker compose -f docker-compose.local.yml up -d --build
## 数据说明
当前主应用镜像只提供网页应用。画布、我的资产和 AI API Key 默认保存在浏览器本地;画布、资产及其引用的图片或音频文件可以通过 WebDAV 同步,不需要由项目服务器维护数据目录。
主应用 Compose 同时启动前台、后台静态资源、API 网关和服务端;认证、余额、任务、渠道路由和 WebSocket 由服务端处理。画布和资产默认保存在浏览器本地,媒体可通过 WebDAV 同步,provider 凭证不会进入浏览器业务存储。
## 统计分析(可选)
项目**默认不接入任何统计**:不设置下列环境变量时,页面不会加载任何统计脚本,也不会发送任何外部请求。你在自己的部署上想查看流量时,设置对应环境变量即可,无需修改代码或重新构建镜像——镜像启动时会据此生成运行期配置。
统计按「每家一个独立变量」配置:**填了谁就启用谁,可同时启用多家**(例如 GA4 + 百度一起开)。仅支持 GA4 与百度,两者都只接受 ID,脚本地址由代码固定拼接,不接受任意脚本地址或内联 JS。
| 变量 | 说明 |
| --- | --- |
| `ANALYTICS_GA4_ID` | Google Analytics 4 衡量 ID(`G-XXXX`) |
| `ANALYTICS_BAIDU_ID` | 百度统计站点 ID(`hm.js?` 后面那串 hash) |
在 `docker-compose.yml` 的 `environment` 中配置(文件内已附注释示例),下例同时启用 GA4 与百度:
```yaml
services:
app:
image: ghcr.io/basketikun/infinite-canvas:latest
ports:
- "3000:3000"
environment:
ANALYTICS_GA4_ID: G-XXXXXXXXXX
ANALYTICS_BAIDU_ID: e8263761cc5c32f5f7a855b8203b965a
```
或使用 `docker run`:
```bash
docker run -d -p 3000:3000 \
-e ANALYTICS_GA4_ID=G-XXXXXXXXXX \
-e ANALYTICS_BAIDU_ID=e8263761cc5c32f5f7a855b8203b965a \
ghcr.io/basketikun/infinite-canvas:latest
```
单页应用的路由切换会自动向所有已启用的统计上报 pageview。
<Callout type="warn">
请只在你自己的站点上配置统计 ID,不要把 ID 提交进仓库;fork 或自托管本项目的用户默认不会产生任何统计。
</Callout>
前端不会注入第三方统计脚本,也不会把页面访问数据发送到外部统计服务。认证、余额、任务、队列和 WebSocket 仍依赖同一 Compose 中的前台、服务端、PostgreSQL 与 Redis。
+7 -7
View File
@@ -27,9 +27,9 @@ description: Major features available in the current project
- Use image, text, audio, generation-configuration, and group nodes.
- Connect prompts and reference resources to configuration nodes.
- Generate images, text, and audio through user-configured OpenAI-compatible or Gemini-compatible channels.
- Generate images, text, and audio through the same-origin asynchronous task gateway; provider channels are managed by administrators.
- Preserve image ratios by default, organize batch results in one expandable image group, and retry failed outputs independently.
- Configure models, image parameters, text reasoning effort, audio voice, format, speed, and per-model invocation scripts.
- Select published model products, capability schemas, resolutions, quantities, and balance-unit prices.
## Image tools
@@ -40,14 +40,14 @@ description: Major features available in the current project
## Assets
- Store reusable text and image assets in the browser.
- Store reusable text, image, and audio assets in the browser and mirror them to the platform asset API.
- Search, filter, import, export, edit, download, and insert assets into a canvas.
- Insert assets from the canvas side panel without leaving the current project.
## Configuration and synchronization
- Configure multiple model channels and assign image, text, or audio capabilities to each model.
- Import or export local configuration, including credentials and WebDAV settings.
- Configure public model products, channel groups, routes, prices, plans, and balance units in the admin console.
- Keep provider credentials server-side; the browser stores only public settings, local assets, and WebDAV connection status.
- Inspect local IndexedDB usage.
- Synchronize canvases, assets, and their image or audio files through WebDAV.
@@ -57,6 +57,6 @@ The repository retains the node registry, runtime contracts, canvas operation AP
## Current limitations
- The project is frontend-first and does not provide built-in cloud accounts or application-managed cloud storage.
- API keys remain in browser storage and requests are sent directly to configured providers.
- Browser canvas/media remains local-first; the server stores task, billing, asset references, and temporary staging metadata.
- PostgreSQL persistence writes the core V1 relational rows and a revision-fenced compatibility projection in one transaction; Redis queue leases use Lua fencing, and provider reconciliation plus offline WebDAV workers are included in the server runtime. Remaining administrative record mappings and concrete external provider protocols stay deployment-specific.
- Production Docker static-resource paths still require broader deployment verification.
@@ -27,9 +27,9 @@ description: 当前项目已实现的主要功能
- 保留图片、文本、音频、生成配置和组节点。
- 文本提示词、图片和音频参考可以通过连线输入生成配置节点。
- 浏览器前端直连用户配置的 OpenAI 兼容或 Gemini 兼容渠道,生成图片、文本和音频。
- 通过同源平台网关提交图片、文本和音频异步任务,渠道由管理后台统一路由。
- 图片节点默认保持原始比例;批量生图结果收纳在同一个可展开图片组中,失败槽位可独立重试。
- 支持配置模型、图片参数、文本推理强度、音频声音/格式/语速,以及每个模型的自定义调用脚本。
- 支持选择公开模型产品、能力 schema、分辨率、数量和金币价格;provider 渠道由管理台配置。
## 图片工具
@@ -40,14 +40,14 @@ description: 当前项目已实现的主要功能
## 我的资产
- 浏览器本地保存文本和图片资产。
- 浏览器本地保存文本、图片和音频资产,并与平台资产 API 镜像。
- 支持搜索、筛选、新增、编辑、删除、导入、导出、复制文本和下载图片。
- 可从画布侧栏直接插入资产,不必离开当前项目。
## 配置与同步
- 支持多个模型渠道,并为每个模型指定生图、文本或音频能力。
- 支持导入和导出本地配置;配置文件包含 API Key 与 WebDAV 凭据,需要自行妥善保管。
- 管理台支持模型产品、渠道组、路由、价格、套餐和金币单位配置。
- 支持本地画布/资产导入导出;provider 凭证不进入浏览器业务存储,WebDAV 凭证由服务端密文保存。
- 支持查看 IndexedDB 本地存储占用。
- WebDAV 可同步画布、我的资产及其图片或音频文件,不需要把全部生成资产存放在项目服务器。
@@ -57,6 +57,7 @@ description: 当前项目已实现的主要功能
## 当前限制
- 当前不提供云端账号系统或由项目服务器托管的素材云盘。
- AI API Key 保存在浏览器本地,并由浏览器直接请求已配置的模型服务。
- 浏览器画布和媒体仍是本地优先;服务端保存任务、账本、资产引用和临时 staging 元数据。
- Access Token 仅保存在前端内存,Refresh Token 使用 HttpOnly Cookie,模型请求由服务端网关转发。
- PostgreSQL 持久化在同一事务中写入 V1 核心关系行和带 revision fencing 的兼容投影,Redis 队列使用 Lua 租约 fencing,并包含 provider 对账与 WebDAV 离线 worker;剩余管理记录映射和具体外部供应商协议仍需在部署环境配置。
- Docker 静态资源路径仍需要更广泛的生产部署验证。
+10 -10
View File
@@ -9,14 +9,15 @@ To try the project, start the `web/` frontend and `admin/` management console th
## Deploy with Vercel
Import the repository into Vercel. The root `vercel.json` builds both `web/` and `admin/`. AI requests are sent directly from the browser to your OpenAI-compatible endpoint, so no additional application server is required.
Production deployment must provide `server/` together with the frontend and admin bundles. Static-only hosting does not provide authentication, balance, task APIs, or the task WebSocket.
## Run locally
```bash
git clone git@github.com:basketikun/infinite-canvas.git
cd infinite-canvas/web
git clone https://git.awaioi.com/awaioi/MiragenFlow.git
cd MiragenFlow
npm --prefix ../admin install --ignore-scripts
cd web
bun install
bun run dev:all
```
@@ -25,17 +26,16 @@ Open the frontend at `http://localhost:3000/` or the admin console at `http://lo
## Run with Docker
The Docker image includes both applications and exposes only port 3000.
Use Compose so the API gateway, server, and dependencies are started with the two applications.
```bash
docker build -t miragenflow .
docker run --rm -p 3000:3000 miragenflow
docker compose -f docker-compose.local.yml up -d --build
```
## First-time setup
- Open Settings and enter your `Base URL`, `API Key`, and model names.
- Assign image, text, or audio capabilities to the models you want to use.
- Configure WebDAV if you want to synchronize canvases, assets, and their image or audio files across devices.
- Register and complete email or phone verification, then choose a public model product and resolution in the workspace.
- Configure channels, model products, prices, balance units, and plans in `/admin/`; users never enter provider URLs or API keys.
- Configure WebDAV in Storage Settings if you want to synchronize canvases, assets, and media across devices.
Canvas projects and My Assets are primarily stored in the browser. The API key is also stored locally and used by the browser to call OpenAI-compatible endpoints directly.
Canvas projects and My Assets are primarily stored in IndexedDB/localForage. Access tokens stay in memory, refresh tokens use an HttpOnly cookie, and provider credentials remain encrypted on the server.
@@ -9,13 +9,13 @@ description: 用最少步骤把元境幻生跑起来
## Vercel 部署
在 Vercel 中导入仓库即可,根目录 `vercel.json` 会构建前台 `web/` 和后台 `admin/`。当前版本的 AI 请求由浏览器前台直连用户自己的 OpenAI 兼容地址,不需要额外配置服务端。
生产部署必须同时提供 `server/`、前后台静态资源和依赖服务;只构建静态资源不包含认证、余额、任务 API 或任务 WebSocket。
## 本地启动
```bash
git clone git@github.com:basketikun/infinite-canvas.git
cd infinite-canvas
git clone https://git.awaioi.com/awaioi/MiragenFlow.git
cd MiragenFlow
npm --prefix admin install --ignore-scripts
cd web
bun install
@@ -31,20 +31,19 @@ bun run dev:all
## Docker 启动
如果你需要基于当前源码构建镜像,镜像会同时包含前台和后台:
推荐使用 Compose 同时启动 API 网关、服务端、依赖和前后台:
```bash
docker build -t miragenflow .
docker run --rm -p 3000:3000 miragenflow
docker compose -f docker-compose.local.yml up -d --build
```
## 首次使用建议
- 先打开右上角配置弹窗,填入自己的 `Base URL`、`API Key` 和模型名。
- 为准备使用的模型指定生图、文本或音频能力。
- 如果需要跨设备同步画布、资产及其图片或音频文件,可在配置弹窗中填写 WebDAV。
- 先注册并完成邮箱或手机号验证,再在工作台选择公开模型产品和分辨率。
- 管理员在 `/admin/` 配置渠道、模型、价格、金币单位和套餐;用户端不填写 provider 地址或 API Key。
- 如需跨设备同步画布、资产及媒体文件,可在存储设置中填写 WebDAV。
## 说明
- 当前画布项目和“我的素材”主要保存在浏览器本地,WebDAV 同步需要用户自行配置。
- AI API Key 保存在浏览器本地,并由前端直接请求 OpenAI 兼容接口。
- Access Token 只保存在前端内存,Refresh Token 使用 HttpOnly Cookie;provider 凭证只在服务端密文保存。
+1 -1
View File
@@ -5,7 +5,7 @@ description: Deploy MiragenFlow with Render
# Deploy on Render
[Deploy to Render](https://render.com/deploy?repo=https://github.com/basketikun/infinite-canvas)
This repository is hosted on a self-managed Git service, so a Render one-click link is not published. To use Render, mirror the repository to GitHub first and configure PostgreSQL, Redis, and the server environment variables described in the deployment guide.
1. Select **Deploy to Render**.
2. Sign in and connect GitHub as prompted.
+1 -1
View File
@@ -7,7 +7,7 @@ description: 使用 Render 部署元境幻生
点击下面链接即可部署到 Render:
[部署到 Render](https://render.com/deploy?repo=https://github.com/basketikun/infinite-canvas)
当前仓库使用自托管 Git 服务;Render 一键部署链接暂未发布。若要使用 Render,请先将仓库镜像到 GitHub,再按 Render 文档配置 PostgreSQL、Redis 和服务端环境变量。
## 部署步骤
+267 -6
View File
@@ -5,18 +5,279 @@ description: Implemented changes that still need manual verification
# Pending Tests
- Latest regression evidence: `npm run build:all` passed server typecheck, the web production build, and the admin production build; `npm run test:all` passed 102/102 server tests plus web/admin typechecks. Exact forbidden-reference scans found no user-side Ant Design, ProComponents, Radix controls, Lucide, native `<select>`, `.ant-*`, CDN, or WebFont usage; remaining matches are the project-owned TDesign `Select` adapter names and native file-input API types. Existing large-chunk, dynamic-import, and Browserslist warnings remain informational.
- An isolated browser tab at 390x844 verified `/`, `/auth/reset`, `/style-preview`, and `/admin/login`: each rendered Chinese content, had no horizontal overflow, no native `<select>`, and no captured console errors. Authenticated admin workflows, protected user routes, canvas internals, and external provider/WebDAV scenarios remain pending.
- A fresh same-origin administrator session signed in with the documented local credentials and loaded `/admin/dashboard/base`; the overview rendered KPI values, seven-day trend sections, runtime alerts, and the grouped navigation without browser console errors. The first request briefly showed an empty main region before the dashboard data settled; authenticated write flows and the remaining 21 admin pages still require manual verification.
- Admin non-GET requests now reuse the original idempotency key after a 401 session refresh, and channel generation tests translate a selected display model ID through the configured request-model mapping before calling the upstream. Verify a response-lost 401 retry does not duplicate a write and that a mapped model reaches the compatible provider endpoint as the request ID.
- User canvas crop, split, angle, and upscale operations now reject broken image data and time out instead of remaining pending forever; crop failures show a localized retry message. Verify invalid or interrupted image data in each editor and confirm the loading state and editor can be used again.
- User account password and MFA mutations now invalidate stale responses after dialog close or back-navigation, keep errors in the active dialog, and isolate session-revoke errors on the account page. MFA confirmation content scrolls inside short dialogs, and profile dropdown visibility is owned by TDesign with route remount fencing. Verify close/ESC/overlay during delayed requests, retry behavior, focus return, short mobile heights, and reopening the profile menu after navigation.
- Password-reset code requests now retain the channel and normalized target used at submission; editing the identifier or switching channel invalidates the old request and its CAPTCHA. Verify reversed responses, CAPTCHA refresh, target text, and reset submission in a delayed browser session.
- User canvas crop/mask editors, asset image/audio/cover selection, and verification/reset CAPTCHA loading now fence async results to the current input. Stale image metadata, earlier file uploads, or older CAPTCHA responses cannot overwrite the current draft, and file read/upload failures show localized errors. Browser verification is still needed by delaying the first image metadata result, reversing two file uploads, and reversing two CAPTCHA requests to confirm the final preview, dimensions, title, question, and submitted challenge ID belong to the latest selection.
- User canvas upscale and split editors now also discard stale image-dimension results when the image URL or open state changes, preventing a previous image's target size, ratio, or split grid from leaking into the current editor. Authenticated canvas verification is still needed by switching quickly between differently sized images while delaying the first metadata result.
- The latest clean-browser gate check visited all 22 admin business deep links and each settled on `/admin/login` without exposing business content; desktop width remained within the viewport and no console errors were captured. This confirms unauthenticated route protection only; authenticated admin rendering, permissions, writes, persistence, and error/retry behavior remain pending.
- The default admin login shell exposes only the email and password fields while administrator CAPTCHA/MFA are disabled; no verification, one-time-code, or recovery-code input is rendered. Its page-settings drawer exposes accessible theme/layout radio groups, closes on Escape, unmounts after the transition, and returns focus to the `页面设置` trigger without console errors. Authenticated settings and business-shell behavior remain pending.
- An isolated local server using a fresh temporary store and `MIRAGENFLOW_DEMO_DATA=false` returned ready status and allowed the configured administrator to sign in; authenticated counts were users 0, channels 0, payment providers 0, message providers/templates/outbox 0, tasks 0, recharge orders 0, and payment events 0. The default catalog still exposed five built-in public products and one empty channel group, as intended for the unconfigured development shell. The existing long-lived preview store was not modified; explicit test-mode fixture behavior still needs a separate check.
- Latest command regression passed: `npm run test:all` completed 102/102 server tests plus web/admin typechecks, and `npm run build:all` completed server typecheck, web production build, and admin production build. Existing large-chunk, dynamic-import, deprecation, and stale Browserslist warnings remain informational; authenticated browser workflows are still pending.
- Latest unauthenticated browser regression verified the user homepage, login, registration, verification, password reset, component preview, user 404, and admin login shell at desktop and 390px widths: all rendered Chinese content, had no native `<select>`, no horizontal overflow, and no captured console errors. At 390px the user Select opened with Enter, moved with ArrowDown, committed `风格模型` with Enter, and closed with Escape with `aria-expanded=false` and a hidden popup. Authenticated admin pages and all write/error/retry flows remain pending.
- Image creation and admin reconciliation now use project Button controls for visible file-upload triggers; hidden file inputs remain only as the browser file API exception. Verify file chooser focus, invalid type/size feedback, preview replacement, cancel, and submit behavior in authenticated desktop and 390px sessions.
- Narrow-screen overlay closing is now wired for the admin settings/detail/attempt drawers and the user asset/mobile navigation surfaces; admin drawers use a viewport-bounded width so a clickable mask remains visible at 390px. Browser checks confirmed admin settings mask close with focus returned to the settings trigger, user sidebar mask close, no horizontal overflow, and no console errors. Authenticated business drawers and their async failure/retry behavior still need verification.
- Authentication bootstrap now recovers legacy user/admin refresh sessions that lack a CSRF cookie and issues the missing CSRF cookie on successful rotation; ordinary browser-backed business writes still require the double-submit token. Server regression coverage passes for the administrator path. Verify a real browser hard refresh after an older login, then confirm an unprotected business write is still rejected and authenticated writes continue to work.
- Current browser evidence: in temporary same-origin tabs, the desktop user homepage and public authentication/404 routes loaded without horizontal overflow; the 390px homepage and mobile navigation also passed, including Escape close and light/dark theme toggling. All six protected user routes settled on `/login?redirect=...`. After logging into the local preview administrator, all 22 admin business deep links loaded with their expected Chinese page titles, default administrator CAPTCHA/MFA fields remained hidden, the user-creation drawer retained invalid drafts after Chinese validation errors, and the channel editor exposed only the three required tabs with separate display/request model IDs and retained an invalid URL draft. The user-side themed Select opened from Enter, exposed its listbox options, and closed on Escape. This verifies route/page-shell loading and a few low-risk control paths only; authenticated writes, permission matrices, error/retry flows, canvas internals, and external provider/WebDAV scenarios remain pending.
- Follow-up authenticated 390px evidence covered `/assets`, `/tasks`, `/account`, `/settings`, `/tools/image`, and `/tools/audio`: each route retained the login shell, rendered its key content, and stayed within the 390px viewport without horizontal overflow. The task status Select opened with Enter, selected an option with ArrowDown/Enter, and closed with Escape. The account revoke flow showed the confirmation dialog, preserved the session after cancel, and closed on Escape with focus returned. Settings showed validation feedback for an invalid WebDAV test and updated local-storage usage from `--` to `0 MB`; image advanced settings toggled correctly, and audio generation became enabled after text input. The user-side imperative confirmation implementation was changed from `DialogPlugin.confirm` to a controlled TDesign Dialog because the plugin path rendered no dialog under the current React 19 runtime; web typecheck and `git diff --check` pass. Failure/retry mutations, real generation, WebDAV, detailed canvas interaction, and desktop authenticated visual review remain pending.
- The shared TDesign Button adapter now forces ordinary buttons to render as native `button` elements even when disabled, while leaving link buttons on their normal anchor path. A desktop 1440px image-tool check confirmed the disabled generation control is `BUTTON[disabled]`, the page has no horizontal overflow, and a dark-to-light theme switch updates the TDesign page variable without layout overflow. Verify disabled/loading semantics across the admin and user applications, including any custom `href` or `tag` callers.
- Admin generic confirmation actions now have an exception fallback for freeze/unfreeze, message retry, and task cancel/retry entries; failed requests keep the confirmation dialog open and show a Chinese error message. Verify authenticated 409/422/503 and offline/recovery responses, including that the action can be retried without duplicate submission.
- The user-side confirmation-dialog adapter now catches rejected async callbacks, keeps the dialog open, and shows a Chinese error by default, with an optional caller error handler for session revoke, task cancel, and result deletion. Verify failure/retry, loading reset, focus return, and no unhandled Promise in authenticated desktop and 390px flows.
- The user settings page now handles local storage usage read failures with a Chinese error message and a loading state instead of leaving an unhandled click promise. Verify the disabled/loading state and recovery after IndexedDB or browser-storage permission errors in an authenticated desktop and 390px session.
- Admin edit, approval, balance-adjustment, refund, and reconciliation dialogs now catch asynchronous request failures in the shared confirmation flow, keep the draft open, and show a Chinese error message so a failed mutation can be retried. Verify authenticated 409/422/503 and network-failure responses across these mutation types, including that the form values remain intact and no unhandled browser error is emitted.
- The user homepage carousel no longer displays legacy product or out-of-scope 3D promotional banners. It now uses the existing character, scene, and asset reference images with MiragenFlow-specific Chinese overlay labels while preserving arrow, dot, and responsive behavior. Independent same-origin checks verified the next-slide transform, no legacy banner text, no horizontal overflow at desktop and 390px, light/dark theme rendering, and no browser warnings; review the final image crop and visual contrast in the authenticated shell.
- Normal development store startup no longer seeds the mock payment adapter; payment fixtures are limited to `NODE_ENV=test` or explicit `MIRAGENFLOW_TEST_MODE=true`, while production remains empty until a real adapter is configured. Verify a clean development store shows an empty payment-adapter list and that explicit test mode still exercises the payment contract.
- The unauthenticated admin-route smoke test revisited all 22 visible business routes (`/admin/dashboard/base`, users, auth messages, MFA security, channels, channel health/groups, model products, pricing, billing, plans, recharges, payments, payment providers, approvals, tasks, WS metrics, storage, settings, and both audit routes) in a clean browser tab. Each route settled on the admin login surface without exposing business content; authenticated rendering, permissions, writes, and error/retry states remain pending.
- The shared user-side TDesign Switch no longer renders nested `label` elements; labeled switches now expose a named `role="switch"` control and clicking the visible label text toggles the state. The component-center browser check verified no nested labels, `aria-checked` changed from `true` to `false`, and no console errors; verify labeled switches again in canvas image-tool settings and light/dark authenticated layouts.
- Admin audit dashboard and detailed-log pages now show a fixed right-side “view log details” action column when the current administrator has audit scope and rows have detail IDs; verify action-column visibility, detail loading/error/retry, permission-hidden behavior, fixed columns, and 390px horizontal scrolling.
- User canvas project cards are now focusable links that open with Enter, and the admin brand logo now uses the shared TDesign Button while preserving the home navigation. Verify visible focus, keyboard opening, nested checkbox/action behavior, rename mode, and desktop/390px layouts in guest and authenticated states.
- The user workbench header now exposes a light/dark theme toggle sharing the canvas theme store, TDesign Button adapter, and animated transition logic. Guest homepage checks at desktop and 390px verified button visibility, dark-theme activation, the updated accessible label, persistence after reload, no horizontal overflow, and no console errors. Authenticated-shell behavior, contrast review, and broader page states remain to verify.
- The user-side TDesign TextInput and TextArea adapters now bridge native input events for controlled values while retaining the TDesign uncontrolled path and clear behavior. Web typecheck and production build pass; verify real keyboard, IME, paste, clear, validation, and controlled-draft behavior across authentication, assets, settings, and canvas at desktop and 390px.
- The developer-only component center no longer presents a planned 3D rendering card, matching the V1 exclusion of real 3D. Verify the business preview contains only retained homepage, AI creation, canvas, assets, and account areas.
- User canvas crop-ratio, generation-mode, grid-style, lens, upscale-target, and upscale-algorithm controls now use `group` containers matching their TDesign `aria-pressed` buttons instead of invalid `radiogroup` semantics. Verify the selected visual state and keyboard activation remain unchanged in the authenticated canvas/editor flows.
- User canvas resource-mention menus now support Home/End navigation and expose vertical listbox semantics while keeping the focused editor synchronized through `aria-activedescendant`. Verify image, audio, text, and config-resource mention flows with IME input, filtering, selection, Escape, and narrow-screen positioning.
- Admin channel-group model-product and provider-channel multi-select lists now use a single roving-focus option per list, support Arrow/Home/End navigation while skipping disabled choices, and preserve Enter/Space toggling without double-changing nested checkboxes. Verify the authenticated group editor with real enabled, disabled, selected, mode-mismatched, and cross-group-owned choices at desktop and 390px.
- Admin page-settings color, theme-mode, and navigation-layout controls now expose radio semantics with roving focus, Arrow-key navigation, Home/End navigation, and keyboard access to the custom color picker. The login-route smoke test verified the groups and selected-state updates; verify the same behavior after authentication and across desktop/390px light and dark layouts.
- The admin theme-color selector now uses an explicit controlled `value`, so reopening settings follows the current theme color instead of an initial-value prop. Verify preset and custom colors, close/reopen behavior, selected borders, and custom-panel readback after authentication.
- The admin page-settings drawer now restores focus to its trigger after closing; desktop and 390px checks verified Escape close, drawer unmount, light-theme restoration, no horizontal overflow, and no console errors. Verify overlay and close-button behavior, plus focus continuity after layout changes, in the authenticated admin shell.
- The full-page admin login route now mounts the page-settings drawer without inheriting the `Layout/Content` shell. Desktop and 390px checks confirmed opening, `light/dark` theme switching, `radiogroup/radio` semantics, no horizontal overflow, and Escape close. Verify that the settings continue to affect navigation layout, theme color, and element toggles after an authenticated page is mounted.
- The shared UI package now exposes the `@miragenflow/ui/button` subpath and resolves it to the local Button adapter; the export-integrity check and web/admin typechecks pass. Verify the standalone subpath import in development and production builds.
- The latest same-origin read-only smoke test covered the user home, login, registration, public previews, protected entries, and `/admin/` at desktop and 390px viewports. Public/auth pages had no horizontal overflow or console errors, protected routes settled on login, and the admin entry settled on `/admin/login`. Authenticated business pages, all 22 admin entries, and write operations still require authorized page-by-page verification.
- The user task-history page now uses the shared TDesign Table with columns for task links, type, public model, status, credits, creation time, and view actions, while retaining the narrow-screen horizontal scroll. Verify the authenticated status filter, timed refresh, status tags, task links, empty state, and 390px table scrolling.
- User-side Select call sites now pass structured `options` data, and focus traps no longer include a native `select` branch. The shared adapter renders the same TDesign combobox/listbox contract; the component center verified zero native `select`/`option` elements, Enter-to-open, Escape-to-close, focus retention, no horizontal overflow, and no console errors. Authenticated image/audio/task/asset workflows still need their full visual and value-selection review.
- Management-console structured detail values now render only fields with registered Chinese labels, so unknown nested response keys are omitted instead of appearing as unmapped content. Verify representative nested objects, empty-after-filter objects, and sensitive-key responses in detail drawers and audit payloads.
- User-detail tab tables now apply the same registered-label filter as detail drawers, omitting unknown response keys instead of creating an “other information” column. Verify account, sessions, tasks, ledger, entitlements, and audit tabs with partial and extended responses.
- The admin login submit button now uses a plain layout container instead of an unnamed `FormItem`, keeping dynamic CAPTCHA/MFA fields as the only form-managed controls. Verify Enter submission, validation errors, loading/disabled state, and the desktop/390px layout.
- Asset ZIP import now clears the hidden file input immediately after capturing the selected file, so selecting the same package again during or after an import can trigger another change event. Verify repeated same-file import, invalid packages, cancellation, and concurrent import feedback.
- Local file persistence now uses one atomic write path for synchronous/asynchronous saves and unique temporary names for concurrent repository saves. Verify repeated requests and service reloads keep `/api/ready` ready, preserve the latest valid snapshot, and leave no corrupted snapshot after an interrupted save.
- Channel-group task-mode options now show only Chinese labels such as “图片” and “文本”; the underlying `image`/`text` values remain unchanged for API submission. Verify the displayed labels and saved capability values in the authenticated channel-group editor.
- Public-route browser regression now covers `/`, `/login`, `/register`, `/auth/verify`, `/auth/reset`, `/components`, `/style-preview`, and the 404 route at desktop and 390px widths with no horizontal overflow or console errors. The protected routes `/assets`, `/tools/image`, `/tools/audio`, `/tasks`, `/account`, `/settings`, and `/canvas` first show the Chinese auth-loading state and then redirect to `/login` with the requested `redirect`; the same-origin `/admin/` entry is also gated and settles on `/admin/login` without credentials. The admin login page renders at desktop and 390px without horizontal overflow or console errors, while authenticated admin pages and user-business-flow verification remain pending. The migration architecture HTML was regenerated from the current JSON and passed Archify showcase validation (9/9 checks), desktop light/dark containment, and the 1440px visual spot-check.
- Shared UI now exposes `Form`, `Table`, `Tabs`, `Menu`, `Layout`, and `Row/Col` subpaths and root exports. The admin login, business tables, navigation shells, settings grid, and page layouts now use the shared entries while admin retains permission checks, route guards, dynamic table columns/drag sorting, and form instance behavior. Verify composite static members, table pagination/horizontal scrolling/drag sorting, dynamic CAPTCHA and MFA fields, menu permissions, and desktop/390px light/dark rendering page by page.
- The user-side themed Select adapter now exposes a combobox/listbox ARIA relationship, supports Enter/Space opening and selection, Arrow/Home/End navigation, Escape close with focus return, disabled-option skipping, and active-option feedback. The admin low-risk Dropdown, Space, Breadcrumb, Popup, date-range picker, and color-picker usage now enters through `@miragenflow/ui`; verify keyboard selection, selected/active announcements, overlay/backdrop/Escape behavior, date value preservation, popup positioning, and desktop/390px light/dark rendering.
- The shared UI root and subpath exports now include the existing Dropdown, Pagination, Slider, and Tooltip adapters plus Space, Breadcrumb, Popup, date-picker, color-picker, `Form`, `Table`, `Tabs`, `Menu`, `Layout`, and `Row/Col` adapters. Admin login, business tables, navigation shells, settings grid, and page layouts now enter through these shared boundaries while retaining admin-owned state and permissions. Verify package resolution in dev/build, static-member compatibility, and the remaining high-risk controls before changing the cleanup checklist.
- The shared UI package now exports Card and Statistic adapters, and admin board, business charts, and statistics use them without changing the existing TDesign card DOM contract. Verify card headers, chart sizing, zero/empty statistics, light/dark themes, and 390px layouts across dashboard, business, and finance pages.
- The admin error page, login form, headers, mobile menu backdrop, and business-page actions now use the shared TDesign Button adapter. The adapter adds explicit outline and danger-outline semantics while keeping native submit behavior under `htmlType`; verify navigation, login validation and Enter submission, popup actions, loading/disabled states, danger styling, and the business page at desktop and 390px widths.
- The shared UI package now exports TDesign `Alert` and `Loading` adapters, and the admin business page uses the shared entry for alerts, detail errors, and loading states. The admin Vite development config also pre-bundles `prop-types` and `react-transition-group` so TDesign Alert's ESM/CommonJS interop does not blank the development page. The same-origin admin dashboard has been checked at desktop and 390px widths for rendered content, an alert node, no horizontal overflow, and no console errors; the remaining 22 admin entries still need page-by-page loading, error, empty-state, and light/dark review.
- The admin page-settings drawer now makes the theme and navigation-layout previews and labels one clickable selection area, exposes `radiogroup`/`radio` semantics with visible focus, and supports Enter/Space selection. Controlled state follows Redux updates immediately, and “follow system” now applies correctly when the OS prefers light mode. Verify the full option click target, Tab/Enter/Space behavior, light/dark/system switching, layout switching, and close behavior through the header button, backdrop, and Escape at desktop and 390px widths.
- Channel groups now follow the explicit `task mode -> upper model product -> lower request channel pool` relationship. The list separates model ID, product name, base price, and a compact resolution matrix; the editor provides visible checkable product and channel choices with mode compatibility, ownership, health, and selection counts. Saving a group refreshes both the group rows and the product/channel candidate relationships. The group ID no longer occupies a list column, and product publication state is not mixed into the product-name column. The API rejects incompatible capabilities, silent cross-group moves, and mode-only updates that would leave invalid bindings, while explicitly unbound published products are taken offline. Verify create/edit backfill, Chinese conflict feedback, explicit unbind/rebind, refresh persistence, fallback ordering, desktop table scrolling, compact resolution rows, and the 390px dialog.
- Admin model product, membership plan, balance unit, storage policy, and message provider forms now validate numeric boundaries and endpoint URLs before submitting. Invalid values keep the draft open with a Chinese message, and a blank endpoint during provider editing preserves the saved endpoint. Verify valid and invalid values on each page, confirm no request is sent for invalid input, and reload after a successful save.
- The admin login mobile breakpoint now reuses the desktop transparent left-side content rail; the title, form, and footer share the `5%` left offset without an extra full-width content container. Verify background cropping, scrolling, and login error states at 390px, 320px, landscape, and short viewports.
- Admin user list/detail responses now include a resource `version`; freeze, unfreeze, security edits, and administrator password resets run inside a transaction with `If-Match` checks and increment the version. A stale submission returns a Chinese 409 without changing the current state. Verify refresh readback, concurrent stale conflicts, freeze/unfreeze persistence, and failed-form/detail retention.
- System overview statistic cards now distinguish a real numeric zero from a missing field by showing “暂无数据” for missing values; trend, model, channel, message, payment, and audit charts filter malformed array entries, and loading includes a Chinese status. Verify empty responses, partial fields, all-zero data, malformed entries, retry failures, and light/dark themes.
- The shared user-side Tabs adapter now moves real focus with Arrow, Home, and End navigation and clears stale focus requests when a controlled value does not change. Verify disabled-tab navigation, delayed controlled updates, mouse activation, and focus return across desktop and 390px routes.
- Canvas image/media/project file inputs now clear their value immediately after taking a FileList, so selecting the same file again can trigger another import. Verify repeated same-file selection, invalid files, cancellation, and an interrupted upload/import.
- User-side imperative confirmation dialogs now add dialog semantics, move focus into the dialog, trap Tab, and restore focus after close. Verify cancel, close, overlay, Escape, async confirm success/failure, and stacked confirmations in desktop and 390px sessions.
- Canvas image, audio, and text settings popovers now expose trigger state and dialog relationships, move focus into the panel, trap Tab, and restore the trigger after close. Verify each popover with mouse/keyboard, nested TDesign Select popups, outside/Escape close, canvas pan/zoom, desktop, and 390px.
- Canvas settings popovers now ignore their themed TDesign select overlays when deciding whether a pointer is outside. Verify selecting an option in image/audio/text settings does not close the parent popover, while real outside clicks still close it.
- Canvas node and connection create menus now use roving focus for Arrow Up/Down, Home, and End and initially focus the first menu item instead of the close control. Verify wraparound, activation, Escape/outside close, focus return, dynamic plugin items, and edge positioning.
- The shared user-side Field wrapper now supports a labelled group mode for compound fields, so mention menus and buttons are not nested inside a label while simple fields keep their implicit label association. Verify accessible names and click/focus behavior for simple inputs, Selects, switches, and the image prompt mention list.
- Saving admin authentication settings now refreshes the current administrator profile, so the MFA page no longer makes enable/disable decisions from stale policy state. In one session, toggle the administrator two-step policy and immediately open the security page; verify the action guard matches the server policy and a profile-refresh failure does not roll back the saved settings.
- The dashboard now renders when a dashboard object is present even if an optional metric is missing; each KPI and chart keeps its own numeric or empty-state fallback instead of hiding the whole overview. Verify empty, partial, and real zero-value responses separately.
- The user Select adapter now exposes select-like `multiple`, `options`, `selectedOptions`, `selectedIndex`, and event cancellation methods while keeping the themed TDesign popup and an accessible empty state. Verify a future multi-select caller and the existing single-select pages after keyboard selection and Escape.
- Protected user routes now keep the user shell out of the tree until authentication hydration settles; the guard renders a full-viewport Chinese loading state before redirecting unauthenticated visitors. Verify direct `/tools/image`, `/assets`, `/tasks`, `/account`, `/settings`, and `/canvas` navigation at desktop and 390px during slow refresh/session expiry, confirming no stale navigation flash and no console errors.
- Store reloads now wait for queued persistence writes, serialize concurrent reloads, and retry when a write starts during the read; PostgreSQL snapshot reads run in one repeatable-read transaction. Fake-repository regressions cover both in-flight write orderings; verify against real PostgreSQL with concurrent admin writes, task creation, worker updates, restart recovery, and two instances to confirm revision fencing and mixed-generation reads remain impossible.
- The admin task center now keeps the task-attempt drawer open when its request fails, shows a Chinese error, and exposes a retry action before restoring the attempts table. Verify 403, 5xx, offline, and recovery flows, including stale-request protection and close/overlay/Escape dismissal.
- Canvas asset overlays, side-panel hover feedback, and image/audio/text settings triggers now use the active canvas theme tokens; shortcut labels and live-task connection failures are localized, and legacy `Copy` suffixes normalize to the Chinese duplicate suffix. Verify contrast, focus, and titles in authenticated light/dark, 390px, keyboard, and asset-hover scenarios.
- User task details now show only the snapshot public model ID and fall back to “平台模型”; revoking a login session from Account requires confirmation; the homepage “我的资产” tab now renders real local image assets with a matching empty state. Verify authenticated snapshots with and without public IDs, confirm/cancel behavior, and asset create/delete/filter flows.
- Homepage carousel arrows stay visible and vertically centered on touch screens, artwork actions expose a visible keyboard focus state, negative letter spacing was removed from login/account/task/create/preview styles, and modal backdrops use theme tokens. Verify desktop/390px light/dark and keyboard Tab paths.
- The standalone home example now uses a local real artwork asset instead of gradient-orb decoration; it is not a production route and still needs a development-entry visual check for cropping, narrow screens, and theme contrast.
- Latest command regression: all 100 server tests pass; web/admin typechecks, web/admin builds, admin lint, `npm run build:all`, and `git diff --check` pass. Builds retain only the existing large-chunk and dynamic-import notices; authenticated writes and in-canvas menu interaction remain pending below.
- This turn's 390px core-route smoke covered `/`, `/login`, `/register`, `/components`, `/style-preview`, plus canvas, assets, tasks, account, settings, and image/audio creation entries: public pages had no captured console errors, protected entries redirected to login after auth hydration, and every route had no horizontal overflow.
- The admin page-settings drawer now closes through an idempotent state setter, and the custom theme-color panel is controlled so repeated close events cannot reopen the drawer or leave a stale color. Verify rapid repeated clicks, overlay/Escape/close-button dismissal, and reopening the custom color panel after selecting a preset.
- The regular canvas prompt editor and node resource textarea mention menus now expose `listbox/option` semantics, active-item associations, and localized labels in both languages. Verify keyboard arrows, Enter, Escape, mouse selection, focus, and inserted references in an authenticated canvas.
- This turn's independent browser smoke used `http://localhost:3008/` and `http://localhost:3009/admin/login`: desktop and 390×844 user home plus admin login had no horizontal overflow or captured console errors, and the admin mobile layout had no extra card container. Unauthenticated `/canvas` showed a Chinese loading state before redirecting to login. Screenshots are saved at `/tmp/miragenflow-web-home-desktop.png`, `/tmp/miragenflow-web-home-mobile.png`, `/tmp/miragenflow-admin-login-desktop.png`, and `/tmp/miragenflow-admin-login-mobile.png`; authenticated canvas-menu, dark-theme, and write-flow checks still require user-authorized credentials.
- Canvas node/connection creation menus now render in an untransformed viewport overlay, keeping a stable screen size and applying viewport translation/zoom only once with edge clamping. Verify in an authenticated canvas at desktop, 320px, and 390px sizes after pan/zoom, including edge placement, keyboard navigation, Escape/outside dismissal, and focus return.
- The production home inspiration list no longer injects prototype artwork records, authors, or popularity counts; it is built only from real local assets and exposes a Chinese empty state when the library is empty. Audit status charts now distinguish HTTP < 400 from HTTP >= 400 or business errors, and message provider/template writes are persisted in the store snapshot. Verify asset create/edit/delete and newest/oldest sorting in an authenticated browser, then restart a local service to confirm message configuration survives.
- The admin login page now uses a locally optimized `admin-login-background.webp`; the login route mounts the full-screen login surface directly, so desktop and mobile share the left-side content rail without inheriting the admin `Layout/Content` shell or adding a card container/top blur strip. The form keeps the desktop width until the viewport requires proportional reduction with symmetric side spacing, and short screens can scroll vertically. Verify crop, contrast, login-error states, and scrolling across aspect ratios and light/dark themes.
- Admin user, task, message, and audit detail drawers now stay open on request failure and expose a Chinese error with a retry action; the settings and task-attempt drawers explicitly enable close button, overlay dismissal, and Escape. Verify offline, 403, 404, and recovery retries without stale-route overwrites.
- Canvas audio/text settings popovers now shrink to the viewport, while create and context menus add menu semantics, Escape/outside dismissal, and narrow-screen width limits. Verify 320px/390px placement, theme changes, keyboard navigation, and focus return near canvas edges.
- The user account menu now uses the shared TDesign Dropdown adapter while preserving the custom profile visual, guest/login branch, and signed-in account actions. Verify the signed-in options, focus return, Escape/outside-click close, theme contrast, and 390px placement.
- Protected user routes now show a visible Chinese loading status during authentication hydration before redirecting or rendering the page. Verify slow refresh/session-expiry transitions do not leave only the shell navigation visible.
- Channel-group ordering now uses TDesign's supported full-row drag mode, updating the failover draft and save field after a reorder. Verify dragging multiple rows in the edit dialog, saving, and refreshing to confirm persistence.
- Admin detail and task-attempt drawers now show a loading state and cancel/invalidate in-flight requests on close. Verify under a slow network by opening user, task, and audit details then immediately closing them; confirm no drawer reopens and Escape/overlay close work.
- The user routes now show a visible Chinese loading state and a controlled error page with reload/home actions. Device identification falls back to an in-memory value when `localStorage` is unavailable. Verify slow loading, refresh, and restricted-storage scenarios do not produce a blank page.
- User shell navigation and developer preview isolation were hardened: `/components` and `/style-preview` are explicitly excluded from `UserLayout`, preview menus use left-start alignment at every breakpoint, and account/settings/task detail lists wrap on narrow screens instead of forcing a 560px row. Verify desktop and 390px screenshots, direct preview URLs, and keyboard navigation.
- The image creation page now uses a themed controlled disclosure for advanced settings instead of the browser-native `details` control. Verify expand/collapse, keyboard focus, and mobile spacing in light and dark themes.
- 首页作品筛选现在会在本地资产新增、编辑或删除后重新计算;需在登录态创建和删除图片资产后确认筛选、排序和卡片状态同步更新。
- User asset sync failures now keep the edit dialog and local draft open, and retrying a new asset reuses the same pending local record. Image/audio task waits now have a ten-minute ceiling, attempt cancellation on timeout, and tell the user to inspect task history for the final state. Verify retry behavior, cancellation races, and task-history status with an authenticated session.
- 详细日志列宽已收紧到桌面内容区,状态、分类、行为、操作人和对象无需横向拖动即可查看;窄屏仍保留列表横向滚动。已在管理员会话的桌面截图确认对象列完整显示,仍需在 390px 下确认横滑手势和固定时间列。
- 用户端主导航、用户头像菜单和侧栏底部入口现在统一从左侧内边距开始排列,不再继承通用按钮的居中布局;桌面与 390px 浏览器截图已确认主导航和游客菜单的图标/文字起始线一致。登录态菜单入口仍需在真实会话下复核。
- 用户端桌面与 390px 菜单已实测为左对齐,开发者组件中心确认不带正式页头、侧栏或导航且无横向溢出;画布库操作区现可在窄屏换行,账户/任务/同步记录在手机端保留紧凑行并横向滑动,认证页使用 `100dvh`。前台固定为中文,模型目录错误提供重试,图片用量、任务错误和结果格式不再直出内部英文键;公开首页、组件中心和登录页截图及控制台已验证,登录态画布/账户页仍待授权会话截图确认。
- 用户端资产页标题已统一为左对齐,资产/画布列表改为由工作台单层滚动承载;浅色滚动条和开发者组件中心业务卡改用主题变量,组件中心根节点现在独立挂载用户端主题并拥有自己的长页滚动承载。路由根级加载占位已补齐,新的独立浏览器标签页启动无控制台警告;仍需登录后在桌面、390px 和深色主题截图确认资产/画布底部滚动与标题位置。
- 共享用户端 Dialog/Drawer 现在由适配层显式监听 ESC,移动导航打开时增加遮罩并支持 ESC 关闭;组件预览已用实际键盘和坐标路径验证 Dialog 的 ESC、右上角、遮罩关闭,以及导航遮罩/ESC 关闭。仍需登录后逐页确认业务弹窗的焦点回收和异步失败保留表单。
- 用户端侧栏按钮已覆盖共享 TDesign 按钮的居中规则,图标和文字从导航内边距开始左对齐。`/components` 组件中心仅在开发环境存在,并挂载在 `UserLayout`、`AppShell` 之外,不带正式导航或系统壳层;桌面与 390×844 截图已确认左对齐、无手机横向溢出、无正式导航且控制台错误为零,生产构建也不包含组件中心文案。登录态路由仍需继续完成完整回归。
- 管理台 Business 页面现在为跨路由的列表刷新、详情/调用记录、渠道探活和生图测试加入视图代次与 `AbortController` 保护;旧页面完成的 mutation 不会取消或覆盖新页面请求,快速切换记录、关闭弹窗或切换路由时旧响应也不会回写当前界面。`npm --prefix admin run typecheck` 与新增文件安静 lint 已通过;仍需在浏览器延迟请求后快速切换 `/admin/users`、`/admin/channels`、`/admin/tasks`,并验证详情、探活和生成弹窗没有串数据或残留加载态。
- 代码更新后的独立浏览器回归已打开 `/admin/users`、`/admin/channels` 和 `/admin/tasks`,三页均正常渲染且控制台错误为 0;用户列表连续打开两条详情也未出现运行时错误。真实延迟网络下的跨路由旧响应覆盖场景仍待专门限速验证。
- 用户端画布资源提及输入已迁移到共享 TDesign `TextArea`,并保留真实文本框引用、@ 提及菜单、选区/光标、滚动同步和输入法行为;画布节点、侧栏、图片创作、资产入口、参数选项和主题切换的可见按钮,以及管理台移动菜单遮罩也统一使用共享 TDesign `Button`。`npm --prefix web run typecheck` 与 `npm --prefix web run build` 已通过;需登录后在画布编辑、图片提及、批量操作、深浅主题和 390px 视口下确认视觉与焦点行为。
- 本轮修复了用户端作品卡片的无效视频播放伪按钮,卡片与首页主视觉改用主题语义色,移动端无需悬停即可看到收藏、分类和“一键同款”;已在 `http://localhost:3008/` 的 390×844 首页确认无横向溢出、控制台无错误和入口可见。登录后的创作/资产页面与深色主题仍需继续回归。
- 管理台渠道生成测试的网络/HTTP 失败现在会在弹窗中保留 `/v1/images/generations`、请求状态、测试时间和中文错误;用户安全编辑、冻结/解冻等写操作在详情抽屉打开时会刷新详情。需在管理员登录后验证失败保留表单、详情同步和 409/503 路径。
- 本轮补充浏览器证据:管理员会话下 22 个可见管理台页面(含日志大板、详细日志和支付适配器)在 390×844 视口均无横向溢出、无控制台错误;桌面系统概览挂载 10 个 ECharts 画布。新增用户弹窗确认仅有用户名、邮箱、手机号、初始密码,渠道弹窗确认三 Tab 可自由切换且取消/ESC 可关闭。真实写入和登录用户画布仍待继续验证。
- In the authenticated same-origin development session at `http://localhost:3008`, all 22 admin entry routes rendered with zero captured console errors; real writes, permission/error paths, and narrow-screen visuals still require page-by-page verification.
- A 390×844 same-origin regression covered the homepage, login, dashboard, channels, and users pages with no horizontal overflow or captured console errors; the collapsed admin sidebar is absent from the accessibility tree. Authenticated canvas, assets, and creation flows remain pending.
- The user task history now assigns a request sequence to status filters, manual refreshes, and polling refreshes, so stale responses cannot overwrite the current rows, loading state, or error message. Verify rapid status changes while requests overlap.
- 管理台修复了用户数字序号与充值目标不一致的问题:余额页现在加载真实用户列表并用 `1 · 用户名 · 脱敏邮箱` 选择目标,提交直接使用真实用户标识;渠道基础信息补回优先级编辑,支付适配器编辑使用开关,布尔状态筛选显示中文,详细日志按 `createdAt` 倒序。已在开发浏览器检查控件和列表,仍需完成写入后刷新回读的完整验收。
- 共享 TDesign 开关现在输出并更新 `aria-checked`;用户端和管理台弹窗增加遮罩点击事件兜底。已用坐标点击在 `/components` 和 `/admin/channels` 验证遮罩关闭,取消、右上角和 ESC 也已验证;其他业务弹窗仍需逐页回归。
- 本轮目标规则已统一落库:`docs/miragenflow-long-task-goal-prompt.md` 第 8 节是唯一可复制的长期目标提示词,`goal.md`、`admin-plan.md` 和 `docs/miragenflow-tdesign-unification-goal.md` 已明确服从其 OpenAI 单一供应方式、统一弹窗和三 Tab 规则;目标平台状态仍为 `active`,后续实现和浏览器证据尚未全部完成。
- 共享 TDesign `TextInput` 适配器现在会把负数、非有限和小数 `minLength`/`maxLength` 归一化为合法值或移除,避免浏览器 `IndexSizeError` 导致白屏;需在登录、注册、密码重置和画布标题编辑场景验证真实输入节点属性、原生校验和空值行为。
- 独立浏览器回归已验证 `http://localhost:3008/login` 的真实输入节点保留 `required` 与密码 `minLength=8`,`/components` 的主题化选择器可展开并用 ESC 关闭,两个页面均无控制台错误;负长度边界仍需在专门表单或自动化夹具中验证。
- 本轮命令回归已通过:用户端 typecheck/build、管理端 typecheck/build/lint、服务端 typecheck/test(101/101)、`npm run build:all` 和 `git diff --check`;构建产生的大包提示与既有动态导入提示不影响退出码,仍需保留浏览器逐页验收。
- 本轮综合目标与可复制提示词已落库至 `docs/miragenflow-long-task-goal-prompt.md`;当前状态仍为进行中。以下项目是实现后待人工确认的证据清单,不能用文档或接口烟测替代浏览器逐页验收。
- 验收索引:用户端 8 个核心路由、管理台 22 个页面、系统概览/渠道健康/任务中心/实时指标/日志大板五类 ECharts、OpenAI 渠道三 Tab,以及组件依赖扫描,均需覆盖桌面与 390px、浅色与深色、刷新、加载/空态/错误态和弹窗键盘关闭路径。
- 管理台列表请求已加入请求代次与 `AbortController` 保护,旧响应不会覆盖当前行数据、加载态或错误态;仍需浏览器验证:延迟 `/admin/users` 后快速切换 `/admin/channels`,确认页面不出现用户字段或错误操作入口,详见 `admin/src/pages/Business/index.tsx` 的 `load`。
- 系统概览只要收到概览对象就会继续渲染 KPI 和图表;可选字段缺失时由单项统计或图表显示数字兜底/中文空态,不再因为一个字段缺失隐藏整块页面。仍需用空对象、部分字段和真实全零数据分别验证空态与零值的区分。
- 静态源码扫描已通过:用户端和管理端没有业务层原生 `<select>`、Ant Design、ProComponents、Radix、Lucide、`.ant-*` 或第三方统计脚本引用;TDesign 的传递图标依赖仍由组件库内部使用,本地品牌字体仍作为视觉资产保留。最终交付仍需保留扫描命令和结果证据。
- 用户端用户菜单已改为受控弹层,支持头像点击展开、外部点击关闭、ESC 关闭、焦点回收和路由切换自动收起;画布库、节点悬浮工具栏、顶栏快捷键、项目卡片、画布节点选中态和批量图片主图标已改用画布主题令牌。需在游客/登录状态、键盘操作、浅色/深色和 390px 下验证菜单关闭路径、工具栏层次与选中态对比度。
- 用户端导航、账户菜单、首页轮播控制、快捷入口和作品收藏按钮已迁移到共享 TDesign `Button`/`IconButton` 适配层,并保留原有 CSS 和焦点回收逻辑;需在桌面/390px、游客/登录状态、悬停/键盘和轮播切换下确认尺寸、点击与 `aria` 状态无回归。
- 画布资产选择卡、项目标题入口、右键菜单和节点创建菜单已迁移到共享 TDesign 按钮适配层;需在浅色/深色、节点坐标边界、点击外部/ESC 关闭、资产插入和项目重命名流程中确认没有布局或事件回归。
- 本轮新增代码证据:`npm --prefix web run typecheck`、`npm --prefix web run build`、`npm --prefix admin run typecheck`、`npm --prefix admin run build`、`npm --prefix server test`(101/101)和 `git diff --check` 均通过;管理台 `Business/index.tsx` 与图表文件的安静 lint 检查通过。全量页面和管理员登录后的人工验收仍未完成。
- 用户端顶部导航、移动导航、资产选择、生成中状态和 404 页面已改用统一主题语义色;需在浅色/深色、窄屏和弹层打开状态下确认文字、边框、按钮和遮罩对比度。
- Frontend analytics is now a local no-op and the runtime `config.js` injection was removed, so Google/Baidu scripts are no longer loaded; verify production static preview and container startup have no third-party scripts or extra outbound requests. The local brand font remains an intentional visual asset.
- 审计日志操作人映射已修正为普通用户优先显示用户名、管理员直接显示邮箱,并兼容历史 `admin:` 标识;需在管理员和普通用户产生的真实日志中确认展示结果。
- 本轮同源运行在 `http://localhost:3006/` 下完成未登录回归:用户端首页正常渲染,受保护的 `/canvas`、`/assets`、`/tasks`、`/account`、`/settings` 和 `/tools/image` 均正确跳转登录页;首页 390px 视口宽度与页面滚动宽度一致且无横向溢出,首页和管理台登录页控制台无运行时错误。管理台登录后的 22 个页面、弹窗和业务写操作仍需在管理员凭据确认后逐页验证。
- 浏览器补充证据:游客头像菜单已验证点击展开、ESC 关闭、点击导航外部关闭,关闭后 `aria-expanded=false` 且弹层不再进入可访问树;管理台登录页桌面结构和 390px 宽度均无控制台错误。登录用户画布项目内部的节点工具栏、深色主题和真实写操作仍待授权登录后验证。
- `scripts/dev-all.mjs` 现在默认将 `MIRAGENFLOW_DEMO_DATA` 设为 `false`,演示任务/支付/日志只有显式 opt-in 才会写入;不会自动删除已有持久化记录。需在干净存储重启后确认列表保持真实空态,并在显式开启演示模式时确认展示数据仍能按需出现。
- 同源 API 烟测使用本地管理员会话完成登录与退出:`dashboard`、用户、渠道、模型、计费、支付、审批、任务、实时指标、存储、设置、审计等 21 个管理查询接口全部返回 `200 ok=true`,未输出令牌。该结果证明后端路由链路可用,仍不能替代登录后的页面交互和写操作回归。
- 隔离内存服务的管理写入回归已通过:用户创建/编辑/回读、金币申请与审批、OpenAI 渠道新增/编辑/主动拉取模型、渠道分组、模型产品、套餐、金币换算、系统设置和支付适配器均返回预期状态并可回读;该回归不修改现有开发存储,仍需在浏览器表单中确认交互和持久化。
- 管理台路由元数据现在将尚未接入的消息供应商和消息模板标记为隐藏;认证消息列表仍可见,直达路由与后端接口保留。需在登录后确认不同角色菜单均不出现这两个未完成配置入口。
- The first TDesign unification slice is now wired through `packages/ui`: both applications use the shared TDesign Provider, the user client maps its light/dark custom tokens to TDesign variables, and the canvas project library uses the shared button adapter. Verify user-console visual parity, TDesign popup stacking, locale switching, React 19 behavior, and the unchanged admin baseline after a fresh browser load.
- User-side TDesign controls now use `@miragenflow/ui/*` component subpath exports: input, select, switch, textarea, dialog, drawer, dropdown, tooltip, slider, input-number, pagination, tag, and message no longer import `tdesign-react/es/*` directly; subpaths preserve tree-shaking. Verify control visuals, themed popups, and React 19 behavior across user pages.
- Simple text areas in the image tool, audio tool, authentication enrollment, and account MFA flow now use the shared TDesign `TextArea` adapter. The adapter forwards its `textareaElement` ref so image mentions keep selection and caret placement. Verify the generated field height, focus/selection behavior, read-only enrollment content, and light/dark styling after a fresh browser load.
- Canvas image dimension/count fields and the audio settings speed field now use the shared TDesign `InputNumber` adapter. Dimension blur/Enter commits still align to the optional 16-pixel step, count and speed keep their min/max and empty-value behavior, and themed wrapper styles remain local to the canvas. Verify keyboard entry, blur/Enter commits, range correction, disabled resolution fields, and light/dark popup styling.
- Canvas project selection now uses a shared TDesign `Checkbox` subpath with canvas theme tokens. Native file inputs remain limited to hidden upload triggers, and the mention editor keeps its native textarea because selection, scroll, IME, and overlay highlighting depend on the real DOM node. Verify checkbox selection/bubbling, keyboard focus, and the documented native-control exceptions.
- Canvas node title editing now uses the shared TDesign `TextInput` adapter with its real `inputElement` ref. Auto-focus/select, Enter/Escape, blur commit, outside-pointer detection, compact width, and dashed underline styling need browser verification in both themes.
- The local component center verified the shared controls after this migration: the themed select opens and hides on Escape, the example dialog opens and closes from its close button, and the page has no runtime console errors; remaining page dialogs and drawers still require authenticated review.
- This slice also migrated the image/audio/auth/account text areas, canvas image/audio numeric inputs, project checkbox, and canvas-node title input to shared TDesign adapters. `npm --prefix web run typecheck`, `npm --prefix web run build`, `npm --prefix server test` (101/101), `npm --prefix admin run typecheck`, `npm --prefix admin run lint`, and `npm run build:all` pass; public login/component-center browser smoke has no runtime errors. Authenticated canvas settings still need visual and interaction review.
- 用户端与管理台的左上角品牌标题、登录主标题/副标题和画布标题现在统一左对齐。需在登录页、首页和画布页的桌面与窄屏尺寸确认品牌文字起始线一致。
- 总览 P50/P95 延迟现在使用双环图,无调用样本时显示“暂无样本”;渠道生图测试会在成功和失败结果中保留实际请求路径、是否发起、测试时间和中文原因,编辑渠道不会清除独立的模型映射。需验证有无样本、上游拒绝/超时、未发起请求和保存后重新编辑的状态。
- 未执行过测试的渠道现在显示“未测试”,总览图表颜色会跟随深浅主题。需在新建渠道、切换主题和无调用样本时确认状态与颜色。
- 渠道健康单行测试现在使用独立结果弹窗,测试期间显示上游模型列表请求加载状态,完成后展示实际请求路径、是否发起、模型数量、测试时间和中文失败原因;最近一次结果也会回填到健康列表。验证成功、地址/权限/网络失败、取消、重新测试、刷新后的结果持久化,以及深浅色主题下的可读性。
- Finance ledger, recharge-order, and payment-event pages now derive statistic cards and ECharts from real rows and share a date-range filter. Batch channel health checks retain the request path, attempted flag, and test time, while health checks and image-generation tests use distinct labels; admin requests now report timeout, cancellation, and network failures in Chinese. Verify populated/empty states, narrow layouts, date selection, and an upstream timeout.
- Canvas project roots, workspaces, and loading placeholders now use one dynamic viewport height without conflicting `vh/svh` minimum constraints. Catalog synchronization is isolated from the canvas page effects, no-op config writes are skipped, and malformed legacy viewports are normalized. Verify complete top-to-bottom visibility and no update-depth errors at 1280×720, 390×844, during viewport switches, and in projects with persisted nodes.
- The admin settings permission explanation now reports the security-management scope accurately; storage-policy edits use `If-Match` version protection; empty numeric inputs remain empty instead of displaying 0. Verify concurrent-edit conflicts, the settings permission message, and empty-value validation.
- Canvas project pages now keep every root/flex canvas layer at the dynamic viewport height, merge resize observations per animation frame, and ignore no-op viewport/node updates that could cause a React update loop. The user console shell also uses a shrinkable dynamic-viewport content area, and image-tool preview measurements ignore unchanged values. Client bootstrap requests are deduplicated under StrictMode. On narrow screens the side panel opens as an overlay without covering the bottom toolbar, the minimap moves to the upper-right, and the main toolbar is separated from the zoom dock. A signed-in browser smoke verified 1440×900 and 390×844 sizes, text/config node creation, no body overflow, and no console errors; verify image/audio/group interactions and a real project with persisted nodes after a fresh login.
- Switching canvas projects now cancels the previous project's resource restore and generation requests, clears pending timers, and fences persistence/viewport saves to the project that actually finished loading. Verify rapid A-to-B navigation, a generation in flight during navigation, and that each project's nodes and viewport remain isolated.
- Asset platform synchronization now skips no-op replacements and keeps the notification API out of the initialization effect dependencies. Verify opening `/assets` after hydration does not repeatedly request or replace the same list, and that a failed sync leaves local assets usable.
- Editing a model product now explicitly clears a stale channel-group binding when the product is disabled and the group field is emptied; publishing still requires a valid enabled group. Verify unbinding, republishing validation, and version-conflict feedback.
- The account center now labels login devices as “Current browser” / “Device N” instead of exposing device UUIDs; session revocation still uses the server-side identifier. Verify multi-device ordering, current-device detection, and list refresh after revocation.
- The admin sidebar now disappears completely when collapsed instead of retaining an icon rail. On narrow screens it opens above the page with a backdrop and closes after navigation or backdrop dismissal. Business lists remain compact tables with horizontal touch scrolling, while page and card side spacing is reduced. Verify collapse/restore on desktop, backdrop behavior at 390px, and horizontal scrolling across every admin list.
- AI creation panels, result/reference containers, and asset previews/placeholders now use the shared theme surfaces instead of hard-coded near-black or Stone backgrounds. Guests no longer see “Profile” or “Log out” in the lower-left sidebar, while authenticated users retain both actions. Verify `/tools/image`, `/tools/audio`, and `/assets` in both themes, then compare guest and signed-in sidebar states.
- Shared panels, stat cards, and dialogs on the user account, task, and sync-settings pages now use theme surfaces as well, so light mode no longer shows dark containers. Verify populated and empty states on each page.
- Canvas crop and split previews now follow the active canvas theme, and the node information dialog no longer exposes a raw JSON tab; it keeps fielded ID, type, size, position, and status details. Non-essential English on the homepage, style preview, and component verification route is translated, and showcase buttons now have explicit demo navigation/dialog behavior. Verify these surfaces in both themes and development routes.
- The user client and admin now use localized Lineicons subsets instead of Lucide or direct TDesign business-icon imports. Common user-client glyphs render at 22px with at least a 36px hit area; admin menu, header, and primary action glyphs render at 24px with 40px hit areas. Undo, redo, stop, sidebar, grouping, text, generation, and favorite states use distinct semantic shapes, and no CDN, webfont, or full icon catalog is loaded. Verify all routes, dark theme, collapsed navigation, canvas rotation/undo controls, dropdowns, dense action columns, and 390px layout.
- User-facing model, task, image-parameter, and canvas image-setting dropdowns now use the shared styled popup rather than native browser `<select>` controls. The outer popup and TDesign content surface share the user theme, with consistent hover, selected, and disabled states. The adapter preserves the existing controlled `onChange(event.target.value)` contract, supports keyboard selection, outside/Escape dismissal, disabled and empty states, and keeps portal menus inside the canvas popover interaction boundary. Verify each dropdown in light/dark themes, keyboard navigation, mobile widths, and selection persistence after reopening.
- User management, the dedicated balance page, and user details now place separate add-coins and deduct-coins icon actions beside the balance. The amount and reason labels are associated with their controls; the dialog accepts only a positive integer amount, shows the current and projected available balance, and requires a reason. Local super administrators automatically execute the recorded approval and refresh the visible balance after confirmation; if automatic execution fails, the saved request opens in the approval center instead of inviting a duplicate submission. Production still requires a second finance administrator. Verify both directions, MFA-enabled local execution, failure recovery, subsequent task spending, stale-version/replay rejection, insufficient-balance prevention, dialog closing, and the approval-center actions.
- Admin create/edit fields now use a single controlled state source instead of unnamed TDesign `FormItem` state, so persisted values are no longer replaced by empty internal form values. Channel base information, model-list and model-mapping backfill, tab preservation, cancel closing, and an error-free console were browser-verified; verify the remaining resource editors.
- Channel models are now persisted independently from optional display-to-request mappings. Upstream discovery renders a checkbox list and “Add selected models” action; it never creates mappings. Manual model entry remains available when discovery is not used, and development startup no longer seeds display-only mock channels. Verify create/edit backfill, selection, removal, an empty mapping list, and a fresh development store.
- Channel probing now marks a request as attempted only immediately before the upstream `fetch` call. Generic providers no longer infer health from a configured API key, and protocol, private-network, or DNS validation failures are reported as not attempted. Closing and reopening the dialog cannot be overwritten by an old probe; create/edit save no longer probes implicitly, and the independent loading state is only shown after clicking the model-list button. Verify these success, failure, and race paths.
- Channel model probing now has an independent loading state and reports the actual request path (for example `/v1/models`), whether an upstream request was attempted, and a Chinese reason for DNS, HTTP, empty-list, or network failures. Editing the URL or API key clears stale results; verify success, upstream failure, and unresolvable hosts while confirming the form remains open after failure.
- Channel forms now show complete request URL examples (`https://example.com` and `https://example.com/v1`). API keys are trimmed and only required to be non-empty; the former 16-character minimum is removed consistently for probing, creation, editing, and rotation. Verify a short but valid upstream key through each flow.
- The admin channel dialog uses consistent tabs for base information, model discovery, and model mapping. Tabs are freely switchable, and the dialog-level draft keeps base fields, probe results, and mappings when panels change; the footer always uses Save/Cancel. The model map explicitly separates the platform display model ID from the provider request model ID. Local development enables private provider URLs only through an explicit launcher flag; production keeps the SSRF restriction. Verify free tab switching, failed probes preserving the form, unified save/edit backfill, and a real local-compatible endpoint.
- The admin create-user dialog now contains only username, email, phone, and initial password; status defaults to active, while plans, allowed models, welcome messages, concurrency, and daily limits are configured separately after creation. Verify the minimal form and the existing email/phone requirement.
- The full server regression suite now passes all 101 tests, and web/admin typechecks plus admin lint pass; only existing formatting warnings remain. Browser-based page-by-page acceptance is still required for the checklist below.
- The OpenAI image ratio matrix now covers 1.91:1, 2.35:1, and 21:9 across 1K/2K/4K sizes, and the server accepts the documented wide 1K sizes; new image products use a 3840×2160 4K default preset. Verify every wide ratio in the standalone image page and canvas, including displayed size, actual output dimensions, and billing tier.
- The server now has 69 OpenAI image endpoint regression tests covering ratio conversion, expired/revoked/missing staging references, invalid image URLs, capability schema generation, idempotency headers, and real image metadata during manual reconciliation. A live compatible endpoint is still needed to verify URL outputs, transparent PNG/WebP, and long-running 4K tasks.
- OpenAI image, text, and audio requests now send the standard `Idempotency-Key`; when a provider transport result is unknown, the user image tool keeps the task as “待对账” and links to task history instead of showing a generic failure. Verify the header with a compatible endpoint and the unknown-result browser flow.
- The OpenAI image adapter now fails closed for invalid reference sequences, missing `@图片X` references, oversized reference objects, and more than 16 references, and adds the documented default reference instruction when only images are supplied. Verify these boundaries through both the task API and the internal adapter path.
- The admin security menu now points to a valid settings route, logout is isolated from the generic menu navigation handler, approval actions reject self-approval by administrator ID/email/name, and audit category/actor fallbacks remain Chinese. Verify with multiple administrator identities and sparse audit records.
- Admin pagination no longer shows an unnecessary total-count summary, and create/edit dialogs explicitly enable overlay, Escape, cancel, and close-button dismissal. Verify every write dialog closes on each path and closes automatically after a successful async save.
- The admin channel editor now uses one consistent OpenAI Images form for both creation and editing. The provider selector, generic-provider branch, and step wizard are removed; the form contains the HTTPS base URL, bearer API key, base model ID, optional 1K/2K/4K model mapping, priority, and `/v1/models` probing. Verify that no generic/provider-choice fields appear, both dialogs use the same layout, and generation/edit requests work against a real compatible endpoint.
- PNG/JPEG/WebP uploads now use the multipart endpoint so a single image can reach the 50 MB limit without the JSON body cap; Chinese reference mentions match longer names first (for example `@图片十一`), canvas base aspect ratios are stored as `ratio`, and reference-image fidelity defaults to high. Verify large uploads, images eleven through sixteen, aspect switching, and legacy canvas settings.
- The user image tool now supports up to 16 ordered reference images, 50 MB per image, Chinese reference names, and `@图片一` mentions. Verify ordering, removal, missing-reference validation, structured output metadata, and manual reconciliation behavior for unknown provider results.
- Admin success reconciliation now uploads the real PNG/JPEG/WebP output as multipart and validates image magic bytes; channel edits can probe `/v1/models` with the stored credential, and model-product creation includes a graphical resolution matrix. Verify large files, existing-output reconciliation, invalid-file rejection, and retained form state after failed probes.
- The development launcher reserves distinct ports before starting the three processes. Verify the printed frontend `/admin/` proxy URL remains correct when the default ports are occupied.
- Admin visualization now uses ECharts for the system overview, channel health, task center, realtime metrics, and audit dashboard. Audit is split into a log dashboard and detailed logs; the dashboard shows category share, normal HTTP 200 versus error 4xx/5xx share, and time trend, while detailed logs add a status column. Verify populated and empty states, narrow screens, dark theme, readable Chinese labels, and note that realtime metrics are a short frontend polling sample rather than durable history.
- The admin sidebar now uses distinct semantic icons for business entries; groups with one visible page render that page directly, while the Audit group exposes both a dashboard and detailed-log entry and other multi-page groups remain expandable. Verify icon contrast, current-route highlighting, role-filtered menus, collapsed mode, and narrow-screen behavior.
- Admin business lists and detail drawers now use Chinese field labels and structured values throughout: balances show available/reserved coins, nested objects are no longer rendered as raw JSON, and credential/hash/idempotency fields are hidden. Only the user list renders its `ID` column as 1, 2, 3 in current-list order; model, plan, and channel identifiers keep their real values, while allowed models use readable names. Verify every admin route has no “other information” columns, raw JSON, untranslated action/status values, or unintended English.
- Audit logs now use fixed time, category, behavior, actor, object, identifier, request, and source columns, with newest records first. The server supplies Chinese behavior mappings and readable actors: administrator email, ordinary-user name, system, or payment callback; verify role/action coverage and that no empty Actions column or raw action code is visible.
- The admin system overview now falls back to safe values for missing statistic fields instead of crashing TDesign Statistic; verify `/admin/dashboard/base` with an empty task history and after session refresh.
- The admin system overview now renders a loading state and a Chinese empty-data panel with a refresh action when the dashboard response has no row, while request errors retain the retry alert. Verify the empty response and recovery path in `/admin/dashboard/base` after an authenticated session.
- Desktop authentication pages now use symmetric vertical padding so the bilingual brand heading and form sit closer to the viewport center with less top whitespace; verify login and registration at common desktop heights while preserving the mobile spacing.
- The user profile dropdown now remains visible for guests with a refined visual placeholder slot, a login prompt, and future artwork space; signed-in users see identity details and account/task/logout actions. Verify hover/focus behavior, light/dark contrast, and narrow-screen fit.
- The notification popover now uses a responsive short height with a viewport-based maximum while preserving its rounded corners; verify empty and populated states at desktop, tablet, and narrow viewport heights.
- The user-console sidebar is now 224px wide on desktop and mobile, with nav rows and lower links slightly shorter; verify the wider drawer, content offset, and label fit at desktop and narrow-screen widths.
- Password login now uses a single “Account” field; verify that email, phone (including spaced or hyphenated input), and username values are detected and accepted without showing separate email/phone/username tabs.
- 管理台入口现在会把绝对站点 base URL 规范化为 React Router 的路径 basename;请在 `/admin/dashboard/base` 直达、刷新和 site 静态构建预览下确认页面不再白屏,并确认未登录时会落到 `/admin/login`。
- The login reward entry no longer renders the oversized floating reward card or transparent hit overlay. The GIF sweep is replaced with a CSS gradient border on a transparent background; verify that the label remains readable in the light theme without crowding the avatar/navigation.
- The gradient border now uses a masked border layer so the button center stays fully transparent; verify that no fill appears inside the rounded frame.
- The reward label now follows the “作品灵感” tab typography with a lighter brand weight while keeping only “免费” highlighted; verify that all three text groups fit on desktop and mobile.
- The canvas and one-click-draft entries now use full-card image surfaces instead of the former CSS-only gradient cards; verify image cropping, overlay readability, and the existing navigation targets across desktop and mobile.
- The two mobile tool cards below the carousel now use the same aspect ratio as the two banners above; verify aligned edges and consistent image/text cropping on narrow screens.
- The visitor avatar artwork and “加入设计交流群” sidebar card are temporarily removed; the mobile header hides the left brand logo while keeping the collapse-menu button. Verify that guest avatars no longer open a large image, signed-in profile links still work, and the mobile drawer/side links remain usable.
## V1 Current Verification
- The `MiragenFlow-front` visual foundation is copied under `web/src/user-console/`; verify desktop/mobile shell spacing, the removed search/model-square navigation, compact-brand component-center login/register/reset/verify screens (brand text above the form title without the MF icon/container), the “登录免费送积分” login reward copy with “免费” highlighted and its original visual treatment, same-ratio mobile/desktop advertising images without crop, AI image/audio creation cards, and that real canvas/assets/tools routes remain functional after navigation.
- A `/style-preview` page now compares four candidate palettes—Twilight Blue-Gray, Warm Paper Cobalt, Mist Glass, and Turquoise Graphite—without changing production tokens; compare the four cards on desktop and narrow screens, then select one direction before applying it across the user-facing pages.
- The confirmed Mist Glass direction is now applied to the user client: lavender backgrounds, translucent white surfaces, violet primary actions, and coral accents cover the homepage, authentication, creation, canvas, and assets surfaces; verify contrast, TDesign overlays, narrow-screen scrolling, and the readable dark violet variant after toggling the theme.
- Authentication now separates password login (email/phone/username) from SMS code login; registration has email and phone paths with independent admin switches for registration-email and registration-phone verification. Verify disabled direct registration, enabled code-first registration/password confirmation, SMS send/resend/rate-limit errors, and MFA chaining.
- Authentication switches now control independent admin CAPTCHA, stale MFA challenge invalidation, refresh-session state, and audit retention; verify browser forms hide CAPTCHA/MFA/recovery fields when disabled and admin login accepts email/password only.
- Admin list pages now provide section-specific facet/date filters, the dashboard includes a seven-day task trend and runtime alerts, and API-level 403 responses navigate to the recoverable permission page with roles, required scope, and request ID; verify filters preserve pagination and both login recovery buttons clear the correct session state.
- PostgreSQL dispatch can hydrate a task claimed by another instance before publishing it; verify a worker with an empty local task map eventually enqueues and executes a task created by a different instance.
- PostgreSQL snapshot saves now fence against the revision captured when the snapshot was taken; verify a queued stale snapshot cannot overwrite a direct lease/outbox update under concurrent workers.
- Queue claim, Redis active leases, PostgreSQL task-heartbeat/expiry recovery, verification snapshot loading, and message dead-letter boundaries were hardened; verify worker ownership, stale ack/nack rejection, retries, and restart recovery with PostgreSQL/Redis multi-instance deployment.
- PostgreSQL message outbox leases renew while provider requests are in flight and completion remains owner/expiry fenced; real provider timeout and two-instance duplicate-send verification is still pending.
- Message enqueue accepts a caller idempotency key and forwards a stable `Idempotency-Key` to providers; verify the semantics with each real email/SMS provider.
- The task center now exposes a redacted task-detail drawer and approvals support version-guarded rejection with a reason; verify timeline/output redaction and stale-version retry behavior.
- Unknown tasks now offer success/failure reconciliation with an optional charged-coin amount, balance-unit configuration preserves precision and rounding, and channel creation probes the channel before saving; verify settlement, release, credential validation, and probe feedback.
- Finance administrators now have a dedicated user-balance adjustment page backed by a minimal user target list containing only redacted identity and balance fields. Approval requesters no longer see self-approval actions, and detailed audit logs expose a closable detail drawer; verify finance-role visibility, adjustment approval, action-column visibility, and all drawer close paths.
- Management console refactor pending browser verification: grouped `/admin/` navigation, scope-hidden menu items, recoverable 403/404 buttons, user creation drawer, channel/group/model/price creation dialogs, fixed-width tables, Chinese health/capability labels, and authentication-message detail/retry.
- Channel, channel-group, and model-product edit drawers are available, channel health supports probing all channels, and message lists no longer return target hashes. Verify If-Match conflicts, batch-probe failures/retries, Chinese status labels, and fixed columns in narrow windows.
- Management console user workbench now includes one-time admin password reset, verification-message resend, session/entitlement detail, dashboard P50/P95 and channel/message/payment anomaly metrics; verify these actions in a browser with a non-super-admin scope matrix.
- Management console user details now expose account, sessions, tasks, ledger, entitlements, and audit tabs; the independent `/api/v1/admin/settings` resource persists security switches and applies authentication policy changes immediately. Verify tab data, If-Match conflicts, switch-off cleanup, and retry states in a browser.
- Current service checks: `npm run typecheck:server`, `npm run test:server` (101 tests, all passing), `npm run typecheck:web`, `npm run typecheck:admin`, `npm --prefix web run build`, `npm --prefix admin run build`, `npm --prefix admin run lint`, `npm run build:all`, and `git diff --check`; admin lint has no errors and only existing formatting warnings.
- Local preview defaults to administrator `admin@admin.com` with password `admin`; administrator MFA and CAPTCHA remain off by default and can be re-enabled through environment variables.
- Authentication: ordinary users default to email/password only; email/phone verification, CAPTCHA, and user MFA remain switchable enhancements, alongside the independent admin audience, multi-admin roles, admin MFA setup/disable, and refresh reuse.
- Account APIs: user plan entitlements and session listing/revocation should preserve ownership checks and revoke the selected refresh-session family.
- Authentication/privacy: the admin MFA CAPTCHA exemption is bound to the current admin session, user/admin MFA secrets and payment-adapter credentials remain encrypted and restart-persistent server-side, and user-facing payment responses do not expose adapter identifiers or provider keys.
- Billing: pending user recharge orders, signed mock/adapter webhook contracts for recharge and plan orders, payment failure/refund transitions, refund idempotency against the original balance bucket, plan purchase/entitlement snapshots, approval-only manual recharge, reserve allocation, partial settle, release, refund, and idempotency conflict.
- Billing invariants: expired buckets must append idempotent adjustment entries, and plan entitlement availability must follow the actual plan credit bucket after task reserve/settle/release.
- Balance expiry accounting clamps each expiration adjustment to the remaining aggregate balance so the ledger and account projection stay consistent.
- Task gateway: image/text/audio/reverse-prompt/multi-angle/split/upscale tasks, channel fallback, unknown reconciliation, lease recovery, cancellation, WebSocket cursor replay, and task-detail HTTP polling fallback.
- Storage: upload MIME/magic validation and scanner quarantine, private staging/GC, retention-aware output objects, local browser assets, WebDAV encrypted credentials, manifest ETags/version, conflict jobs, tombstones, archive queue, and sync UI.
- WebDAV retention: manifest retention status is persisted and users can extend it within the configured policy limit.
- Asset ownership: saving a task output must reject another user's task/output and return the existing asset on repeated saves.
- V1 exclusions: no video, animation, real 3D, training, teams, content governance, Local Agent, remote Prompt, or plugin marketplace routes/menu entries.
The current scope reduction needs manual verification in these areas:
- The homepage, browser title, configuration storage, export files, WebDAV default directory, and internal plugin SDK use the new MiragenFlow / 元境幻生 naming.
- The `web/` frontend and `admin/` console start together with `bun run dev:all` and both load through port 3000; the production build serves `/` and `/admin/` correctly.
- The top-right navigation no longer shows a GitHub entry; documentation, settings, locale, theme, and version controls remain available.
- Navigation exposes only the homepage, canvas library, canvas project, My Assets, and Settings; removed routes should show the not-found page.
- The top-right navigation no longer shows a GitHub entry; documentation, storage settings, locale, and theme controls remain available. Frontend release/version controls are not part of the user surface.
- Navigation exposes only the homepage, AI creation, canvas library, My Assets, and account/task actions; removed model-square and search routes should show the not-found page.
- No independent image workspace, video workspace or video node, Prompt Center, remote prompt source, local Agent connection, or remote plugin marketplace remains in the interface.
- Canvas projects can still be created, renamed, duplicated, imported, exported, and deleted without losing their nodes, connections, viewport, or appearance settings.
- Image, text, audio, generation-configuration, and group nodes can still be created, connected, edited, generated, retried, copied, grouped, and exported.
- Image generation, reference-image editing, text generation, and audio generation use the selected channel and model; cancellation and failure states remain usable.
- Reverse prompt, multi-angle generation, crop, mask edit, split, and local upscale create the expected connected result nodes.
- My Assets still supports text and image records, including import/export, search, editing, canvas insertion, copying, downloading, and deletion.
- Settings contains Channels, Preferences, WebDAV, and Local Storage only. Configuration import/export and local storage statistics should still work.
- WebDAV synchronizes canvases, assets, and referenced image/audio files while leaving AI credentials local.
- English and Simplified Chinese remain complete across retained navigation, settings, canvas controls, messages, and Ant Design components.
- My Assets still supports text, image, and audio records, including import/export, search, editing, canvas insertion, copying, downloading, and deletion.
- `/settings` contains only WebDAV and local storage; provider channels, model preferences, and custom scripts are managed by the service and are not exposed to users.
- The user client no longer exposes provider channels, Base URLs, API keys, custom scripts, or remote plugin configuration; `/settings` contains only local storage and WebDAV controls.
- WebDAV synchronizes canvases, assets, and referenced image/audio files while leaving AI credentials local; the settings page reloads saved WebDAV connection status from the server after refresh.
- English and Simplified Chinese remain complete across retained navigation, settings, canvas controls, messages, and TDesign components.
- The new `server/` process starts with `npm run dev:server`, exposes the shared contracts version through `GET /api/health`, and passes its typecheck and health test.
- During local development `/api` and task WebSocket traffic are proxied to port 3100 instead of the TDesign template's Tencent example API.
- Registration, verification, password login, public model catalog, balance, task idempotency, and event history still need end-to-end browser verification; 3D requests must return `CAPABILITY_NOT_ENABLED`.
- Unknown tasks without a provider request ID now release expired reservations; live staging objects are protected from TTL cleanup, and production Compose refuses to start without explicit secrets. A production-profile smoke with temporary PostgreSQL and Redis verified `/api/ready`, all eighteen migrations, canonical user/balance/ledger/channel/task/attempt/event rows, task snapshots, dispatch outbox, payment fingerprints, message outbox lease fields, PostgreSQL write/read-after-reload, final `sent` outbox dispatch state, revision fencing, and Redis lease claim/renew/ack/nack; the temporary dependencies were removed after verification.
- Same-origin route smoke verified `/`, `/admin/`, `/api/v1/`, and `/api/ready` through the development origin; unauthenticated `/api/v1/ws/tasks` is rejected as expected. Full browser interaction remains a manual UI check.
- The admin console now exposes a create-after-publish switch for model products, per-channel batch-probe results, read-only task details/call records, and session-clearing recovery actions on error pages; verify each drawer, permission state, retained form, and 409 retry in a browser.
- Administrator MFA setup/verification is now gated by the system enhancement switch; verify that the login page, security page, and recovery-code fields stay hidden while disabled and appear only after enabling the switch.
- PostgreSQL WebDAV completion now commits file/manifest, retention/archive state, and conflict-copy jobs with the owner-fenced job update; retention scanning now uses a row-locked PostgreSQL transaction and retries terminal jobs. Verify restart recovery, conflict retry, expired-lease fencing, and two-worker behavior against real PostgreSQL/WebDAV.
- Isolated browser smoke verified default account/password registration and login, canvas library, account sessions, plan purchase, settings, desktop navigation labels, mobile navigation, and removed-route 404 behavior. Enhanced CAPTCHA/MFA switches and external WebDAV/provider interaction remain deployment-environment work.
- Docker Compose interpolation and healthcheck configuration were verified; the image build itself was blocked by the current environment's Docker Hub TLS certificate mismatch, so production image delivery still needs a registry/network retry.
- CAPTCHA responses only expose the human challenge, never the answer or reversible plaintext; WS refresh-cookie family revocation, invalid WebDAV paths, readiness failures, 3D catalog filtering, public model IDs, and WebDAV checksum/conflict handling have regression coverage.
- The V1 regression suite now also covers explicit MFA recovery, user session listing/revocation, `/me/plans`, separate-admin approval execution, asset ownership/deduplication, WebDAV manifest retention limits and valid writes, expired bucket ledger entries, and channel-group route ordering.
- Access tokens now carry a session-family binding; revoking a session invalidates its bearer token and WebSocket subscriptions immediately. The documented admin singleton/detail method aliases have regression coverage.
- Admin create forms now omit blank resource IDs so the server generates stable identifiers; plan, user, and model forms validate server-side constraints before submission, finance configuration is restricted to finance scope, and provider/template edits enforce version matching. Verify invalid-form feedback, role visibility, publish prerequisites, and stale-edit conflicts in the browser.
@@ -5,18 +5,280 @@ description: 当前版本已实现但仍需人工验证的变更项
# 待测试
- 最新回归证据:`npm run build:all` 已通过服务端类型检查、用户端生产构建和管理端生产构建;`npm run test:all` 已通过服务端 102/102 测试及用户端/管理端类型检查。精确禁用项扫描未发现用户端 Ant Design、ProComponents、Radix 控件、Lucide、原生 `<select>`、`.ant-*`、CDN 或 WebFont;剩余命中仅为项目自有 TDesign `Select` 适配器名称和原生文件输入 API 类型。构建中的大包、动态导入和 Browserslist 提示仍为既有提示。
- 独立浏览器标签在 390×844 下复核了 `/`、`/auth/reset`、`/style-preview` 和 `/admin/login`:页面均显示中文内容,无横向溢出、原生 `<select>` 或控制台错误。管理员登录态业务流程、受保护用户路由、画布内部交互以及外部 provider/WebDAV 场景仍待验证。
- 管理台非 GET 请求在 401 会话刷新后现在会复用原幂等键,渠道生成测试在请求上游前会把选中的平台显示模型 ID 转换为已配置的供应商请求模型 ID。需验证响应丢失后的 401 重试不会重复写入,并确认带映射的模型最终以请求 ID 发往兼容供应商端点。
- 用户端画布裁剪、切分、角度和放大操作遇到损坏图片时现在会拒绝并在超时后结束,不会永久停留在处理中;裁剪失败会显示中文重试提示。需在各编辑器注入无效或中断的图片数据,确认加载态恢复且编辑器可以再次使用。
- 用户端账户密码和两步验证写操作现在会在弹窗关闭或返回上一步后使旧响应失效,错误保留在当前弹窗内,会话撤销错误独立显示在账户页;两步验证确认内容可在矮屏弹窗内滚动,用户菜单由 TDesign 下拉组件维护唯一可见状态并在路由切换时重建。需验证延迟请求期间关闭/ESC/遮罩、失败重试、焦点回收、矮屏滚动和路由后重新打开用户菜单。
- 找回密码验证码请求现在保留提交时的登录方式和规范化账户目标;编辑账户或切换方式会使旧请求及其 CAPTCHA 失效。需在浏览器延迟并反转响应时验证验证码目标文案、CAPTCHA 刷新和最终重置提交。
- 用户端画布裁剪/蒙版编辑、资产图片/音频/封面选择和验证/重置页人机验证已增加当前输入代次保护;过期图片元数据、较早文件上传或旧验证码响应不会覆盖当前草稿,文件读取/上传失败会显示中文错误。需通过延迟第一份图片元数据、反转两次文件上传和反转两次验证码请求的浏览器场景,确认最终预览、尺寸、标题、验证码题目和提交 ID 始终属于最后一次选择。
- 用户端画布放大和切分编辑器现在也会在图片地址或打开状态变化时废弃旧的图片尺寸响应,避免快速切换图片后沿用上一张图片的目标尺寸、比例或切分网格。需在登录后的画布中快速切换不同尺寸图片,并延迟第一份元数据响应确认最终尺寸属于当前图片。
- 最新干净浏览器门禁复核访问了管理台全部 22 个业务深链,均回到 `/admin/login` 且未暴露业务内容;桌面视口无横向溢出,也未捕获控制台错误。该结果只证明未登录路由保护,不替代登录后的页面渲染、权限、写入、持久化及错误/重试验收。
- 管理员 CAPTCHA/MFA 默认关闭时,登录壳只显示管理员邮箱和密码输入框,不显示验证码、动态码或恢复码;页面设置抽屉的主题/布局选项具有可访问 radio 语义,按 Escape 关闭并在动画结束后卸载,焦点回到“页面设置”按钮,且无控制台错误。登录后的设置和业务壳层行为仍待验证。
- 使用全新临时存储、`MIRAGENFLOW_DEMO_DATA=false` 启动隔离本地服务后,健康状态正常,配置的管理员可以登录;登录后用户 0、渠道 0、支付适配器 0、消息供应商/模板/发送队列 0、任务 0、充值订单 0、支付事件 0。默认目录仍提供 5 个内置公开模型和 1 个空渠道组,符合未配置开发环境的页面结构需要;现有长期预览存储未被修改,显式测试模式的 fixture 行为仍需单独复核。
- 最新命令回归已通过:`npm run test:all` 完成服务端 102/102 测试及用户端/管理台类型检查,`npm run build:all` 完成服务端类型检查、用户端生产构建和管理台生产构建。仅保留既有大包、动态导入、弃用提示和 Browserslist 数据过期提示;登录态浏览器业务流程仍待验证。
- 最新未登录浏览器回归已在桌面和 390px 验证用户首页、登录、注册、验证、密码重置、组件预览、用户端 404 和管理台登录壳:均渲染中文内容,无原生 `<select>`、横向溢出或控制台错误。390px 下用户端 Select 可用 Enter 打开、ArrowDown 移动、Enter 选中“风格模型”,Escape 关闭后 `aria-expanded=false` 且弹层隐藏;管理台登录态页面和所有写入、错误、重试流程仍待验证。
- 图片创作和管理台对账现在使用项目 Button 作为可见文件上传入口,隐藏文件输入仅作为浏览器文件 API 例外保留。仍需在登录态桌面和 390px 验证文件选择焦点、非法类型/大小提示、预览替换、取消和提交行为。
- 管理台设置/详情/任务调用抽屉以及用户端资产/移动导航现在都显式绑定遮罩关闭;管理台抽屉在 390px 下按视口限制宽度,保留可点击的遮罩区域。浏览器已确认管理台设置遮罩关闭并将焦点回收到设置按钮、用户端侧栏遮罩关闭、无横向溢出且无控制台错误;登录态业务抽屉及其异步失败/重试仍待验证。
- 认证引导现在可以恢复缺少 CSRF Cookie 的旧用户/管理员 refresh 会话,并在成功轮换后补发缺失的 CSRF Cookie;普通浏览器业务写请求仍要求双提交令牌。服务端已覆盖管理员路径回归测试并通过。仍需在真实浏览器中用旧登录态硬刷新确认恢复,再验证无 CSRF 证明的业务写请求仍被拒绝、带合法认证的写请求正常执行。
- 本轮浏览器证据:在临时同源标签中,用户端桌面首页和公开认证/404 路由均正常加载且无横向溢出;390px 首页与移动导航也已通过,包括 ESC 关闭和浅深色主题切换。六个受保护用户路由均进入带 `redirect` 参数的 `/login`。使用本地预览管理员登录后,管理台 22 个业务深链均能加载对应中文页面标题,默认管理员 CAPTCHA/MFA 输入项保持隐藏;新增用户抽屉的非法表单会显示中文校验并保留草稿,渠道编辑器只展示要求的三个 Tab,明确区分平台显示模型 ID 与供应商请求模型 ID,无效地址提交也会保留草稿。用户端主题化 Select 可通过 Enter 打开、显示 listbox 选项并用 ESC 关闭。以上只证明路由/页面壳层和少量低风险控件路径;登录态写入、权限矩阵、错误/重试、画布内部交互以及外部 provider/WebDAV 场景仍待验证。
- 后续登录态 390px 证据覆盖 `/assets`、`/tasks`、`/account`、`/settings`、`/tools/image` 和 `/tools/audio`:六个页面均保持登录壳层、显示关键内容,且在 390px 视口内没有横向溢出。任务状态 Select 可用 Enter 打开、ArrowDown/Enter 选择并用 Escape 关闭;账户撤销设备会显示确认弹窗,取消后会话仍保留,按 Escape 可关闭并回收焦点。设置页对无效 WebDAV 测试显示校验反馈,本地存储统计可从 `--` 更新为 `0 MB`;图片高级设置可展开/收起,音频填写文本后“开始生成”会解除禁用。当前 React 19 运行时下 `DialogPlugin.confirm` 路径实际不渲染弹窗,因此已将用户端命令式确认实现改为受控 TDesign Dialog,同时保留取消、遮罩、ESC、加载和焦点行为;web 类型检查与 `git diff --check` 已通过。失败重试、真实生成、WebDAV、复杂画布交互及登录态桌面视觉仍待验证。
- 共享 TDesign Button 适配器现在让普通按钮即使禁用也保持原生 `button` 标签,同时保留链接按钮的自动锚点行为。桌面 1440px 图片创作页已确认禁用的生成控件为 `BUTTON[disabled]`,页面无横向溢出,深色切换浅色后 TDesign 页面变量同步更新且布局无溢出。仍需在前后台各类禁用/加载控件中确认语义,并复核自定义 `href` 或 `tag` 调用方。
- 管理台通用确认操作现在为冻结/解冻、消息重试、任务取消/重试等入口增加异常兜底;请求失败时保留确认弹窗并显示中文错误,需在登录态验证 409/422/503、断网和恢复网络场景,确认可以重试且不会重复提交。
- 用户端确认弹窗适配器现在统一捕获异步回调异常,默认显示中文错误并保留弹窗,可按调用方提供错误处理;需在登录态会话撤销、任务取消和结果删除的失败/重试场景确认加载态恢复、弹窗不误关闭、焦点可回收且没有未处理 Promise。
- 用户端设置页现在会处理本地存储统计读取失败,显示中文错误提示和刷新中的加载状态,不再让异步点击产生未处理错误。需在登录态桌面和 390px 环境模拟 IndexedDB 或浏览器存储权限失败,确认按钮禁用/恢复和重试行为。
- 管理台编辑、审批、金币调账、退款和任务对账弹窗现在统一捕获异步请求失败,保留当前草稿并显示中文错误提示,失败后可以继续重试。需在登录态分别验证 409/422/503 和网络失败,确认表单内容不丢失且浏览器不产生未处理错误。
- 用户端首页轮播不再展示旧产品或不纳入 V1 的 3D 宣传横幅,改用现有角色、场景和资产参考图,并叠加当前 MiragenFlow 工作台的中文语义标签,同时保留箭头、圆点和响应式切换。独立同源检查已确认下一张切换位移正常、页面不含旧横幅文字、桌面和 390px 无横向溢出、浅色/深色主题均能渲染且浏览器无 warning;仍需在登录态外壳中复核最终裁切和文字对比度。
- 普通开发环境启动时不再自动注入模拟支付适配器,支付 fixture 仅在 `NODE_ENV=test` 或显式设置 `MIRAGENFLOW_TEST_MODE=true` 时启用;生产环境在配置真实适配器前保持为空。需在干净开发存储中确认支付适配器列表为空,并确认显式测试模式仍能覆盖支付契约。
- 本轮在独立浏览器标签中重新覆盖管理台 22 个可见业务路由(系统概览、用户与认证、管理员安全、渠道/健康/分组、模型产品、价格、计费、套餐、充值、支付、支付适配器、审批、任务、实时指标、存储、设置及日志两页)。未登录时每个路由均稳定呈现管理台登录主体,没有暴露业务内容;登录后的页面渲染、权限、写操作及错误/重试状态仍待验证。
- 用户端共享 TDesign Switch 不再生成嵌套 `label`;带文字的开关现在提供带名称的 `role="switch"` 控件,点击可见文字也会切换状态。组件中心浏览器检查已确认无嵌套 label、`aria-checked` 从 `true` 切换为 `false` 且无控制台错误;仍需在画布图片工具设置和登录态浅深色布局中复核带文字开关。
- 管理台日志大板和详细日志在当前管理员具备审计权限且记录包含详情编号时,会显示右侧固定的“查看日志详情”操作列;需验证操作列显示条件、详情加载/失败重试、无权限时隐藏、固定列行为以及 390px 下横向滚动。
- 用户端画布项目卡片现在可通过 Tab 聚焦并按 Enter 打开,管理台品牌 Logo 改用共享 TDesign Button 且保留返回首页行为;需在游客/登录态、项目卡片含复选框与操作按钮、重命名状态以及桌面/390px 下复核焦点可见性、键盘打开和嵌套控件操作不串行。
- 用户端主工作台头部现在提供浅色/深色主题切换按钮,并与画布共用主题状态、TDesign Button 适配器和动画切换逻辑;游客首页已在桌面和 390px 验证按钮可见、切换到深色、无障碍名称更新、刷新后持久化、无横向溢出且无控制台错误。登录态外壳、内容对比度和更多页面状态仍待验证。
- 用户端共享 TDesign TextInput 和 TextArea 适配器现在会为受控值桥接原生输入事件,同时保留 TDesign 非受控路径和清空行为;web typecheck 与生产构建已通过。需在认证、资产、同步设置和画布中,于桌面和 390px 用真实键盘、输入法、粘贴、清空、校验及受控草稿复核。
- 仅开发环境的组件中心已移除未纳入 V1 的“三维渲染”规划卡,符合不提供真实 3D 的范围约束;需确认业务预览只保留首页、AI 创作、自由画布、资产和账户区域。
- 用户端画布裁剪比例、生成模式、网格样式、镜头、放大目标和放大算法控件现在使用与 TDesign `aria-pressed` 按钮一致的 `group` 容器,不再错误声明为 `radiogroup`;需在认证后的画布和编辑器流程中确认选中视觉状态与键盘激活行为不变。
- 用户端画布资源提及菜单现在支持 Home/End 导航并提供垂直 listbox 语义,继续通过 `aria-activedescendant` 保持编辑器与活动项同步;需在图片、音频、文本和配置资源提及流程中,用输入法、筛选、选择、Escape 关闭及窄屏定位复核。
- 管理台渠道分组的模型产品和供应渠道多选列表现在每组只保留一个可 Tab 进入的 option,支持方向键、Home/End 导航并跳过禁用项,Enter/空格切换时不会与嵌套 Checkbox 重复变更;需在认证后的分组编辑器中用真实可选、禁用、已选、模式不匹配和跨分组归属项,在桌面和 390px 下复核。
- 管理台页面设置的主题色、主题模式和导航布局控件现在提供 radio 语义、循环焦点、方向键、Home/End 导航,自定义颜色选择器也可用键盘打开;登录页冒烟验证已确认分组和选中态更新,仍需在认证后的管理台以及桌面/390px、浅色/深色布局中复核。
- 管理台主题色选择器现在使用明确的受控 `value` 状态,避免重新打开设置时沿用旧的初始颜色。需在登录后切换预设色和自定义色,关闭并重新打开设置抽屉,确认选中边框和颜色面板回填一致。
- 管理台页面设置抽屉关闭后现在会将焦点回收到“页面设置”触发按钮;已在桌面和 390px 验证 ESC 关闭、抽屉卸载、浅色主题恢复、无横向溢出和无控制台错误。仍需在登录后普通后台布局中验证遮罩、右上角关闭和布局切换后的焦点连续性。
- 管理台全屏登录页现在也挂载页面设置抽屉,登录主体仍不继承 `Layout/Content`;已在桌面和 390px 验证抽屉打开、主题模式 `明亮/黑暗` 切换、`radiogroup/radio` 语义、无横向溢出和 ESC 关闭。仍需在登录后页面验证设置对导航布局、主题色和元素开关的持续影响。
- 共享 UI 包现在补齐 `@miragenflow/ui/button` 子路径,指向本地 Button 适配器;包导出完整性检查、web/admin 类型检查均通过。需在独立子路径导入场景和生产构建中确认解析结果。
- 本轮同源只读回归已在桌面和 390px 视口覆盖用户端首页、登录、注册、公开预览、受保护入口及 `/admin/`;公开/认证页面无横向溢出和控制台错误,受保护路由稳定跳转登录,管理台入口稳定落到 `/admin/login`。登录态业务页面、管理台 22 个页面和写操作仍需授权后逐页验证。
- 用户端任务历史列表现在通过共享 TDesign Table 的列定义渲染任务链接、类型、公开模型、状态、金币、创建时间和查看操作,并保留窄屏横向滚动。需在登录态确认任务筛选、定时刷新、状态标签、查看链接、空数据和 390px 表格横滑没有回归。
- 用户端 Select 调用点现在统一传入结构化 `options` 数据,焦点陷阱也不再包含原生 `select` 分支;共享适配层继续渲染同一套 TDesign combobox/listbox 契约。组件中心已确认 DOM 中没有原生 `select`/`option`,Enter 可打开、Escape 可关闭并保留 combobox 焦点,页面无横向溢出且无控制台错误;图片、音频、任务和资产真实业务流程仍需完整确认视觉与选中值。
- 管理台结构化详情现在只渲染已登记中文字段,未知嵌套字段会被忽略,不再显示未映射内容。需用包含未知字段、过滤后为空对象以及敏感字段的详情响应,确认详情抽屉和审计 payload 展示安全。
- 管理台用户详情各 Tab 的动态表格现在沿用已登记字段过滤规则,未知响应键不会生成“其他信息”列。需在账户、会话、任务、账本、权益和审计 Tab 中用部分字段及扩展响应确认。
- 管理台登录提交按钮现在使用普通布局容器,不再作为无名 `FormItem` 参与表单状态管理;动态 CAPTCHA/MFA 字段仍由表单控制。需验证回车提交、校验错误、加载/禁用态,以及桌面和 390px 布局。
- 用户端资产 ZIP 导入现在会在取得文件后立即清空隐藏输入值,导入中或失败后重新选择同一压缩包也能再次触发。需验证重复导入同一文件、无效压缩包、取消选择和并发导入提示。
- 本地文件快照现在让同步/异步保存共用原子写入路径,并为并发仓储保存使用唯一临时文件名。需验证重复请求和服务重载后 `/api/ready` 保持就绪、最新有效快照可恢复,异常中断不会留下损坏快照。
- 渠道分组的任务模式选择器现在只显示“图片”“文本”等中文名称,提交 API 时仍保留 `image`、`text` 等真实能力值。需在登录后的渠道分组编辑弹窗中确认显示文案和保存后的能力值。
- 已在桌面和 390px 宽度回归公共路由 `/`、`/login`、`/register`、`/auth/verify`、`/auth/reset`、`/components`、`/style-preview` 和 404 页面,均无横向溢出或控制台错误。未登录访问 `/assets`、`/tools/image`、`/tools/audio`、`/tasks`、`/account`、`/settings` 和 `/canvas` 会先显示中文认证加载态,再带原始 `redirect` 回到 `/login`;同源管理台 `/admin/` 入口也会在无凭证时稳定落到 `/admin/login`。管理台登录页已在桌面和 390px 下确认无横向溢出、无控制台错误;登录后的管理台页面、用户端登录态业务流程和 dashboard 仍待凭证授权后逐页验证。统一迁移架构图 HTML 已从当前 JSON 重新生成,并通过 Archify showcase 9/9 项结构校验、桌面浅深色 containment 和 1440px 视觉抽查。
- 共享 UI 已新增 `Form`、`Table`、`Tabs`、`Menu`、`Layout` 和 `Row/Col` 子路径及根入口;管理台登录、业务页表格、导航壳层、设置网格和页面布局已改用共享入口,权限判断、路由保护、表格动态列/拖拽排序和登录表单实例仍由管理端保留。需逐页验证复合静态成员、表格分页/横滑/拖拽、登录动态验证码与 MFA 字段、菜单权限和桌面/390px 浅色/深色表现。
- 用户端主题化 Select 适配器现在补齐 combobox/listbox 的 ARIA 关系,支持 Enter/空格打开与选择、方向键/Home/End 导航、Escape 关闭并回收焦点、跳过禁用项和活动项反馈;管理端低风险 Dropdown、Space、Breadcrumb、Popup、日期范围选择器和颜色选择器已改用 `@miragenflow/ui` 入口。需验证键盘选择、已选/活动项播报、遮罩/外部点击/ESC、日期值保持、弹层定位,以及桌面/390px 浅色/深色表现。
- 共享 UI 根入口和子路径现在包含已有的 Dropdown、Pagination、Slider、Tooltip 适配器,以及 Space、Breadcrumb、Popup、date-picker、color-picker、`Form`、`Table`、`Tabs`、`Menu`、`Layout`、`Row/Col` 适配器。管理台登录、业务表格、导航壳层、设置网格和页面布局已通过共享边界接入,同时保留管理端自己的状态与权限逻辑。需先验证包解析、开发/生产构建和复合组件静态成员兼容,再调整清理清单。
- 共享 UI 包新增 Card、Statistic 适配器,管理台看板、业务图表和统计卡片改用共享入口,同时保留现有 TDesign 卡片 DOM 样式契约。需在 dashboard、业务和财务页面验证卡片头部、图表尺寸、零值/空态统计、浅深色主题及 390px 布局。
- 管理台错误页、登录表单、顶部导航、移动菜单遮罩和业务页操作按钮现在统一使用共享 TDesign Button 适配器;适配器新增明确的描边与危险描边语义,并通过 `htmlType` 保留原生表单提交行为。需验证导航、登录校验与回车提交、弹层操作、加载/禁用态、扣减金币危险样式,以及桌面和 390px 视口下的业务页。
- 共享 UI 包新增 `Alert` 和 `Loading` TDesign 适配器,管理台业务页的告警、详情错误和加载态已改用共享入口;admin Vite 开发配置同时预构建 `prop-types` 与 `react-transition-group`,避免 TDesign Alert 的 ESM/CommonJS 互操作导致开发页面白屏。已在同源管理台 dashboard 的桌面和 390px 视口确认页面渲染、告警节点、无横向溢出和无控制台错误;仍需在其他 22 个入口的加载、错误、空态和浅深色主题下逐页复核。
- 管理台页面设置抽屉中的主题模式和导航布局现在将预览与文字放入同一选择区域,提供 `radiogroup`/`radio` 语义、焦点样式以及 Enter/空格键选择;受控状态会随 Redux 设置即时更新,浅色系统偏好下选择“跟随系统”也会正确生效。需在桌面和 390px 视口验证整项点击、Tab/Enter/空格、浅色/深色/跟随系统切换、布局切换,以及右上角、遮罩和 ESC 关闭。
- 渠道分组页面已按“任务模式 → 上层模型产品 → 下层渠道池”重组:列表将模型 ID、产品名称、基础价格和分辨率矩阵拆成独立字段,分辨率改为紧凑对齐矩阵,编辑弹窗可直接勾选模型产品和供应渠道,并显示模式匹配、其他分组归属、渠道健康状态和已选数量;新增或编辑保存后会同时刷新分组列表以及模型产品、渠道候选关系。分组 ID 不再占用列表列,产品上架状态也不混入产品名称。服务端会拒绝能力不匹配、跨分组静默迁移及只改模式后遗留不兼容产品,取消关联的已上架产品会自动下架。需验证新增/编辑回填、冲突中文提示、显式解绑后重新绑定、刷新持久化、故障切换顺序、紧凑分辨率行,以及桌面和 390px 的表格横滑与弹窗滚动。
- 管理台模型产品、套餐、金币单位、存储策略和消息供应商表单已增加前端边界校验:负数、小数、非数字和无效接口地址会在提交前显示中文提示,失败时保留弹窗草稿;消息供应商编辑留空接口地址时会沿用原配置。需在各页面分别输入合法与非法值,确认不会发出请求、成功保存后刷新回读正常。
- 管理台登录页手机断点现在直接沿用桌面透明左侧内容轨道,标题、表单和页脚共用 `5%` 左边距,不再增加全宽内容容器;需在 390px、320px、横屏和短屏验证背景裁切、滚动与登录错误态。
- 管理台用户列表和详情现在返回资源 `version`;冻结、解冻、安全编辑及管理员重置密码均在事务内执行 `If-Match` 校验并递增版本,旧版本提交返回中文 409 且不会改变当前状态。需验证刷新回读版本、并发旧版本冲突、冻结/解冻状态保持和失败后表单/详情状态。
- 系统概览统计卡片现在区分真实数值 0 与缺失字段,缺失值显示“暂无数据”;趋势、模型、渠道、消息、支付和审计图表会过滤异常数组元素,加载时显示中文状态。需验证空响应、部分字段缺失、全零数据、异常数组元素、失败重试和浅深色主题。
- 用户端共享 Tabs 适配器现在会在方向键、Home、End 切换时同步真实焦点,并在受控值未变化时清理过期的待聚焦目标。需在桌面和 390px 页面验证禁用项跳转、延迟受控更新、鼠标切换和焦点回收。
- 画布图片/媒体/项目导入文件选择器会在读取 `FileList` 后立即清空值,重复选择同一文件可以再次触发导入。需验证重复选择同一文件、无效文件、取消选择以及上传/导入中断。
- 用户端插件式确认弹窗现在补齐对话框语义、打开时焦点进入、Tab 循环和关闭后焦点回收。需在桌面和 390px 验证取消、关闭、遮罩、ESC、异步确认成功/失败及连续打开多个确认框。
- 画布图片、音频和文本设置浮层现在统一提供触发状态与对话框关联,打开后焦点进入浮层、Tab 在内部循环,关闭后回到触发按钮。需验证鼠标/键盘、嵌套 TDesign 下拉、外部点击/ESC、画布缩放平移以及桌面和 390px。
- 画布设置浮层现在会忽略主题化 TDesign 下拉选项的点击,不会误判为外部点击而关闭。需验证图片、音频、文本设置中选择参数后浮层仍可继续操作,真实外部点击仍能关闭。
- 画布节点创建与连线创建菜单现在使用方向键、Home、End 的 roving 焦点,并默认聚焦第一个菜单项而不是关闭按钮。需验证首尾循环、执行、ESC/外部关闭、焦点回收、动态插件项和画布边缘定位。
- 用户端共享 Field 现在为复合字段提供带可访问名称的 group 模式,引用菜单和按钮不再嵌套在 label 中,普通字段仍保留隐式 label 关联。需验证普通输入、Select、开关和图片提示词引用列表的名称与点击/焦点行为。
- 管理台系统设置保存成功后会重新读取当前管理员状态,管理员 MFA 页面不再使用旧的策略缓存。需在同一会话中开启/关闭管理员两步验证后立即进入安全页面,确认启停操作判断与服务端策略一致,并验证状态刷新失败时已保存的设置不会被回滚。
- 系统概览现在只要收到概览对象就会继续渲染;可选指标缺失时各个统计卡和图表独立显示数字兜底或中文空态,不会隐藏整块页面。需分别用空对象、部分字段和真实全零数据验证空态与零值。
- 用户端 Select 适配器现在补齐 `multiple`、`options`、`selectedOptions`、`selectedIndex` 以及事件取消方法,并保留主题化 TDesign 弹层和空态。需在后续多选调用方及现有单选页面中验证键盘选择与 ESC 关闭。
- 用户端受保护路由现在会等认证初始化完成后再挂载用户壳层;认证期间显示完整视口的中文加载状态,未登录时直接跳转而不会闪现旧导航。需在桌面和 390px 下慢速刷新/会话过期时直接打开 `/tools/image`、`/assets`、`/tasks`、`/account`、`/settings`、`/canvas`,确认没有旧导航闪现且控制台无错误。
- Store 重载现在会等待排队中的持久化写入并串行化并发重载;如果数据库读取期间开始新写入,会丢弃旧快照并自动重读,PostgreSQL 快照读取改为同一个可重复读事务。假仓储回归已覆盖两种写入/重载交错场景。仍需在真实 PostgreSQL 中并发执行管理写入、任务创建、工作进程更新、重启恢复和双实例操作,确认 revision 防护有效且不会读到混合版本。
- 管理台任务中心的调用记录抽屉现在会保留请求失败状态,显示中文错误并可点击“重试”,成功后恢复调用记录表格;需在 403、5xx、断网和恢复网络场景确认抽屉不会卸载、旧请求不会回写新任务,关闭/ESC/遮罩仍可退出。
- 用户端画布资产遮罩、侧栏悬停反馈和图片/音频/文字设置入口已改用当前画布主题令牌,快捷键提示与实时任务连接错误已本地化,复制旧英文 `Copy` 标题时会归一化为“副本”;需在已登录画布的浅色/深色、390px、键盘和资源卡悬停场景确认对比度、焦点和标题结果。
- 用户端任务详情现在只显示快照中的公开模型 ID,缺失时显示“平台模型”;账户中心撤销登录设备需要二次确认;首页“我的资产”标签会复用真实本地图片资产列表并提供正确空态。需在登录态分别验证有/无模型快照、确认取消/确认撤销、资产新增删除和筛选排序。
- 用户端首页轮播箭头在触摸端始终可见并按轮播高度垂直居中,作品卡片“一键同款”获得键盘焦点可见态;登录、账户、任务、创作和开发预览页面的负字距已清零,弹层遮罩改用主题令牌。需在桌面、390px、浅色/深色和键盘 Tab 路径下确认视觉与交互。
- 独立首页示例视觉改用本地真实创作素材,移除渐变球体装饰;该页面当前不是正式生产路由,仍需在开发入口加载确认素材、窄屏裁切和主题对比度。
- 当前命令回归最新结果为:服务端 101/101 测试通过,用户端与管理端类型检查、用户端/管理端构建、管理端 lint、`npm run build:all` 和 `git diff --check` 均通过;构建仅保留大包及动态导入提示。浏览器登录态写操作和画布内部菜单仍按下方条目待人工验证。
- 本轮 390px 核心路由烟测覆盖 `/`、`/login`、`/register`、`/components`、`/style-preview` 及画布、资产、任务、账户、设置、图片/音频创作入口:公开页面均无控制台错误,受保护入口在认证初始化后正确跳转登录,所有页面无横向溢出。
- 管理台页面设置抽屉的关闭回调改为幂等设置关闭,主题色自定义面板改为受控值;需在快速重复点击、遮罩/ESC/右上角关闭,以及切换预设色后重新打开自定义色面板时确认不会回弹或显示旧值。
- 画布普通提示词编辑器和节点资源文本框的 @ 提及菜单现在提供 `listbox/option` 语义、活动项关联及中英文无障碍标签;需在已登录画布中用键盘上下键、Enter、ESC 和鼠标选择验证列表状态、焦点和插入结果。
- 本轮独立浏览器烟测已在 `http://localhost:3008/` 与 `http://localhost:3009/admin/login` 完成:用户端首页桌面/390×844、管理端登录桌面/390×844 均无横向溢出和控制台错误,管理端移动端没有额外卡片容器;未登录访问 `/canvas` 会显示中文加载态后跳转登录。截图保存在 `/tmp/miragenflow-web-home-desktop.png`、`/tmp/miragenflow-web-home-mobile.png`、`/tmp/miragenflow-admin-login-desktop.png`、`/tmp/miragenflow-admin-login-mobile.png`;登录态画布菜单、深色主题和业务写操作仍需用户授权后验证。
- 画布创建菜单现在作为未缩放的视口覆盖层渲染,菜单尺寸保持稳定,平移/缩放后按画布实际视口重新定位并在边缘钳制;需在已登录画布中于 320px、390px 和桌面视口打开节点创建与连线创建菜单,验证缩放、平移、边缘定位、键盘导航、ESC/外部点击关闭和焦点回收。
- 首页作品灵感已移除原型演示数据,只从真实本地资产库生成列表;审计日志状态图改为“HTTP < 400 / HTTP >= 400 或业务错误”,消息供应商和模板的管理写入也已补齐快照持久化。需在登录态创建、编辑和删除资产后确认首页列表与“最新/最早”排序同步,并重启服务确认消息配置不会丢失。
- 管理台登录页已接入本地优化的 `admin-login-background.webp` 背景图;登录路由直接挂载全屏主体,桌面与移动端共用左侧内容轨道,不继承后台 `Layout/Content` 外层容器,也不增加卡片容器或顶部模糊带。表单沿用桌面宽度并在窄屏按视口缩小且保持左右留白对称,短屏允许纵向滚动。需在不同屏幕比例、浅色/深色主题和实际登录错误态下确认背景裁切、文字对比度与滚动行为。
- 管理台详情抽屉在用户、任务、认证消息和日志详情请求失败时会保留当前抽屉并显示中文错误及“重试”,页面设置和任务调用抽屉显式支持右上角、遮罩和 ESC 关闭。需在断网、403、404 和恢复网络后验证重试不会串入旧路由或覆盖新记录。
- 画布音频/文字设置面板会根据视口收缩,创建菜单和右键菜单补齐菜单语义、ESC/外部点击关闭与窄屏最大宽度。需在 320px/390px、主题切换、键盘导航和画布边缘打开菜单时验证定位与焦点回收。
- 用户端账户菜单已改用共享 TDesign Dropdown 适配层承载,同时保留自定义资料视觉、游客/登录分支和账户操作入口;需在登录态确认全部菜单项、焦点回收、ESC/外部点击关闭、主题对比度及 390px 定位。
- 用户端受保护路由在认证状态初始化期间现在显示明确的“正在加载”中文状态,再决定跳转登录或渲染页面;需在慢速刷新和会话过期场景确认不会只剩壳层导航。
- 渠道分组顺序表改用 TDesign 整行拖拽模式,拖动后会同步更新故障切换草稿和保存字段。需在渠道分组编辑弹窗中实际拖动多行、保存并刷新确认顺序持久化。
- 管理台详情和任务调用抽屉现在会显示加载态,关闭时取消未完成请求并使旧响应失效。需在慢速网络下打开用户、任务、日志详情后立即关闭,确认抽屉不会回弹且 ESC/遮罩关闭正常。
- 用户端路由新增可见的中文加载态和受控错误页,设备标识在 `localStorage` 不可用时会以内存值继续工作。需在慢速加载、刷新和存储受限环境中确认不会出现空白页,错误页的重新加载与返回首页入口可用。
- 用户端壳层导航与开发预览边界已加固:`/components`、`/style-preview` 显式绕过 `UserLayout`,预览菜单在所有断点统一左起布局,账户/设置/任务详情列表在窄屏允许换行、不再强制 560px 行宽。需用桌面和 390px 截图复核直接访问预览地址及键盘导航。
- 图片创作页的高级设置已改为主题化受控折叠栏,不再使用浏览器原生 `details` 控件;需在浅色/深色主题和 390px 视口验证展开、收起、键盘焦点与间距。
- 首页作品筛选依赖已补齐,资产新增、编辑或删除后会重新计算作品列表;需在登录态实际创建和删除图片资产后确认筛选、排序和卡片状态同步更新。
- 用户端资产同步失败时会保留编辑弹窗和本地草稿,新增资产重试会复用同一条待同步记录;图片/音频任务等待现在有十分钟上限,超时会尝试取消并提示到任务历史查看最终状态。需在真实登录态验证失败重试、取消竞态和任务历史状态。
- 详细日志列宽已收紧到桌面内容区,状态、分类、行为、操作人和对象无需横向拖动即可查看;窄屏仍保留列表横向滚动。已在管理员会话的桌面截图确认对象列完整显示,仍需在 390px 下确认横滑手势和固定时间列。
- 用户端主导航、用户头像菜单和侧栏底部入口现在统一从左侧内边距开始排列,不再继承通用按钮的居中布局;桌面与 390px 浏览器截图已确认主导航和游客菜单的图标/文字起始线一致。登录态菜单入口仍需在真实会话下复核。
- 用户端资产页标题已统一为左对齐,资产/画布列表改为由工作台单层滚动承载;浅色滚动条和开发者组件中心业务卡改用主题变量,组件中心根节点独立挂载用户端主题并拥有自己的长页滚动承载。路由根级加载占位已补齐,新的独立浏览器标签页启动无控制台警告;仍需登录后在桌面、390px 和深色主题截图确认资产/画布底部滚动与标题位置。
- 共享用户端 Dialog/Drawer 现在由适配层显式监听 ESC,移动导航打开时增加遮罩并支持 ESC 关闭;组件预览已用实际键盘和坐标路径验证 Dialog 的 ESC、右上角、遮罩关闭,以及导航遮罩/ESC 关闭。仍需登录后逐页确认业务弹窗的焦点回收和异步失败保留表单。
- 用户端侧栏按钮已覆盖共享 TDesign 按钮的默认居中布局,图标和文字从导航项左内边距开始排列;`/components` 组件中心仅在开发环境保留,并挂载在 `UserLayout`/`AppShell` 之外,不进入正式用户导航。桌面端和 390×844 截图已确认左对齐、手机端无横向溢出、组件预览页没有正式侧栏且控制台错误为 0;`npm --prefix web run build` 已通过,生产构建产物不包含组件中心文案。登录后的业务路由仍需随整体验收继续确认。
- 管理台 Business 页面现在为跨路由的列表刷新、详情/调用记录、渠道探活和生图测试加入视图代次与 `AbortController` 保护;旧页面完成的写操作不会取消或覆盖新页面请求,快速切换记录、关闭弹窗或切换路由时旧响应也不会回写当前界面。`npm --prefix admin run typecheck` 与新增文件安静 lint 已通过;仍需在浏览器延迟请求后快速切换 `/admin/users`、`/admin/channels`、`/admin/tasks`,确认详情、探活和生成弹窗没有串数据或残留加载态。
- 代码更新后的独立浏览器回归已打开 `/admin/users`、`/admin/channels` 和 `/admin/tasks`,三页均正常渲染且控制台错误为 0;用户列表连续打开两条详情也未出现运行时错误。真实延迟网络下的跨路由旧响应覆盖场景仍待专门限速验证。
- 用户端画布资源提及输入已迁移到共享 TDesign `TextArea`,并保留真实文本框引用、@ 提及菜单、选区/光标、滚动同步和输入法行为;画布节点、侧栏、图片创作、资产入口、参数选项和主题切换的可见按钮,以及管理台移动菜单遮罩也统一使用共享 TDesign `Button`。`npm --prefix web run typecheck` 与 `npm --prefix web run build` 已通过;需登录后在画布编辑、图片提及、批量操作、深浅主题和 390px 视口下确认视觉与焦点行为。
- 本轮修复了用户端作品卡片的无效视频播放伪按钮,卡片与首页主视觉改用主题语义色,移动端无需悬停即可看到收藏、分类和“一键同款”;已在 `http://localhost:3008/` 的 390×844 首页确认无横向溢出、控制台无错误和入口可见。登录后的创作/资产页面与深色主题仍需继续回归。
- 管理台渠道生成测试的网络/HTTP 失败现在会在弹窗中保留 `/v1/images/generations`、请求状态、测试时间和中文错误;用户安全编辑、冻结/解冻等写操作在详情抽屉打开时会刷新详情。需在管理员登录后验证失败保留表单、详情同步和 409/503 路径。
- 本轮补充浏览器证据:管理员会话下 22 个可见管理台页面(含日志大板、详细日志和支付适配器)在 390×844 视口均无横向溢出、无控制台错误;桌面系统概览挂载 10 个 ECharts 画布。新增用户弹窗确认仅有用户名、邮箱、手机号、初始密码,渠道弹窗确认三 Tab 可自由切换且取消/ESC 可关闭。真实写入和登录用户画布仍待继续验证。
- 本轮在已登录同源开发环境 `http://localhost:3008` 逐页打开管理台 22 个入口,页面均能渲染,控制台错误数为 0;仍需继续验证每页真实写操作、错误态、权限和窄屏视觉。
- 同源窄屏回归使用 390×844 视口验证首页、登录页、系统概览、渠道管理和用户管理:页面无横向溢出、控制台错误为 0;管理台侧栏折叠后从可访问树中完全隐藏。用户端登录后的画布、资产和创作流程仍待验证。
- 用户端任务历史列表现在为每次状态筛选、手动刷新和定时刷新分配请求代次;过期响应不会覆盖当前任务、加载状态或错误提示。需在快速切换状态和刷新期间确认列表始终对应当前筛选。
- 管理台修复了用户数字序号与充值目标不一致的问题:余额页现在加载真实用户列表并用“1 · 用户名 · 脱敏邮箱”选择目标,提交直接使用真实用户标识;渠道基础信息补回优先级编辑,支付适配器编辑使用开关,布尔状态筛选显示中文,详细日志按 `createdAt` 倒序。已在开发浏览器检查控件和列表,仍需完成写入后刷新回读的完整验收。
- 共享 TDesign 开关现在输出并更新 `aria-checked`;用户端和管理台弹窗增加遮罩点击事件兜底。已用坐标点击在 `/components` 和 `/admin/channels` 验证遮罩关闭,取消、右上角和 ESC 也已验证;其他业务弹窗仍需逐页回归。
- 本轮目标规则已统一落库:`docs/miragenflow-long-task-goal-prompt.md` 第 8 节是唯一可复制的长期目标提示词,`goal.md`、`admin-plan.md` 和 `docs/miragenflow-tdesign-unification-goal.md` 已明确服从其 OpenAI 单一供应方式、统一弹窗和三 Tab 规则;目标平台状态仍为 `active`,后续实现和浏览器证据尚未全部完成。
- 共享 TDesign `TextInput` 适配器现在会把负数、非有限和小数 `minLength`/`maxLength` 归一化为合法值或移除,避免浏览器 `IndexSizeError` 导致白屏;需在登录、注册、密码重置和画布标题编辑场景验证真实输入节点属性、原生校验和空值行为。
- 独立浏览器回归已验证 `http://localhost:3008/login` 的真实输入节点保留 `required` 与密码 `minLength=8`,`/components` 的主题化选择器可展开并用 ESC 关闭,两个页面均无控制台错误;负长度边界仍需在专门表单或自动化夹具中验证。
- 本轮命令回归已通过:用户端 typecheck/build、管理端 typecheck/build/lint、服务端 typecheck/test(101/101)、`npm run build:all` 和 `git diff --check`;构建产生的大包提示与既有动态导入提示不影响退出码,仍需保留浏览器逐页验收。
- 本轮综合目标与可复制提示词已落库至 `docs/miragenflow-long-task-goal-prompt.md`;当前状态仍为进行中。以下项目是实现后待人工确认的证据清单,不能用文档或接口烟测替代浏览器逐页验收。
- 验收索引:用户端 8 个核心路由、管理台 22 个页面、系统概览/渠道健康/任务中心/实时指标/日志大板五类 ECharts、OpenAI 渠道三 Tab,以及组件依赖扫描,均需覆盖桌面与 390px、浅色与深色、刷新、加载/空态/错误态和弹窗键盘关闭路径。
- 管理台列表请求已加入请求代次与 `AbortController` 保护,旧响应不会覆盖当前行数据、加载态或错误态;仍需浏览器验证:延迟 `/admin/users` 后快速切换 `/admin/channels`,确认页面不出现用户字段或错误操作入口,详见 `admin/src/pages/Business/index.tsx` 的 `load`。
- 系统概览只要收到概览对象就会继续渲染 KPI 和图表;可选字段缺失时由单项统计或图表显示数字兜底/中文空态,不再因为一个字段缺失隐藏整块页面。仍需用空对象、部分字段和真实全零数据分别验证空态与零值的区分。
- 静态源码扫描已通过:用户端和管理端没有业务层原生 `<select>`、Ant Design、ProComponents、Radix、Lucide、`.ant-*` 或第三方统计脚本引用;TDesign 的传递图标依赖仍由组件库内部使用,本地品牌字体仍作为视觉资产保留。最终交付仍需保留扫描命令和结果证据。
- 用户端用户菜单已改为受控弹层,支持头像点击展开、外部点击关闭、ESC 关闭、焦点回收和路由切换自动收起;画布库、节点悬浮工具栏、顶栏快捷键、项目卡片、画布节点选中态和批量图片主图标已改用画布主题令牌。需在游客/登录状态、键盘操作、浅色/深色和 390px 下验证菜单关闭路径、工具栏层次与选中态对比度。
- 用户端导航、账户菜单、首页轮播控制、快捷入口和作品收藏按钮已迁移到共享 TDesign `Button`/`IconButton` 适配层,并保留原有 CSS 和焦点回收逻辑;需在桌面/390px、游客/登录状态、悬停/键盘和轮播切换下确认尺寸、点击与 `aria` 状态无回归。
- 画布资产选择卡、项目标题入口、右键菜单和节点创建菜单已迁移到共享 TDesign 按钮适配层;需在浅色/深色、节点坐标边界、点击外部/ESC 关闭、资产插入和项目重命名流程中确认没有布局或事件回归。
- 本轮新增代码证据:`npm --prefix web run typecheck`、`npm --prefix web run build`、`npm --prefix admin run typecheck`、`npm --prefix admin run build`、`npm --prefix server test`(101/101)和 `git diff --check` 均通过;管理台 `Business/index.tsx` 与图表文件的安静 lint 检查通过。全量页面和管理员登录后的人工验收仍未完成。
- 用户端顶部导航、移动导航、资产选择、生成中状态和 404 页面已改用统一主题语义色;需在浅色/深色、窄屏和弹层打开状态下确认文字、边框、按钮和遮罩对比度。
- 前端埋点已改为本地空实现并移除运行期 `config.js` 注入,不再加载 Google/Baidu 统计脚本;需在生产静态预览与容器启动后确认没有第三方脚本和额外外联请求,品牌字体仍作为本地视觉资产保留。
- 审计日志操作人映射已修正为普通用户优先显示用户名、管理员直接显示邮箱,并兼容历史 `admin:` 标识;需在管理员和普通用户产生的真实日志中确认展示结果。
- 本轮同源运行在 `http://localhost:3006/` 下完成未登录回归:用户端首页正常渲染,受保护的 `/canvas`、`/assets`、`/tasks`、`/account`、`/settings` 和 `/tools/image` 均正确跳转登录页;首页 390px 视口宽度与页面滚动宽度一致且无横向溢出,首页和管理台登录页控制台无运行时错误。管理台登录后的 22 个页面、弹窗和业务写操作仍需在管理员凭据确认后逐页验证。
- 浏览器补充证据:游客头像菜单已验证点击展开、ESC 关闭、点击导航外部关闭,关闭后 `aria-expanded=false` 且弹层不再进入可访问树;管理台登录页桌面结构和 390px 宽度均无控制台错误。登录用户画布项目内部的节点工具栏、深色主题和真实写操作仍待授权登录后验证。
- `scripts/dev-all.mjs` 现在默认将 `MIRAGENFLOW_DEMO_DATA` 设为 `false`,演示任务/支付/日志只有显式 opt-in 才会写入;不会自动删除已有持久化记录。需在干净存储重启后确认列表保持真实空态,并在显式开启演示模式时确认展示数据仍能按需出现。
- 同源 API 烟测使用本地管理员会话完成登录与退出:总览、用户、渠道、模型、计费、支付、审批、任务、实时指标、存储、设置、审计等 21 个管理查询接口全部返回 `200 ok=true`,未输出令牌。该结果证明后端路由链路可用,仍不能替代登录后的页面交互和写操作回归。
- 隔离内存服务的管理写入回归已通过:用户创建/编辑/回读、金币申请与审批、OpenAI 渠道新增/编辑/主动拉取模型、渠道分组、模型产品、套餐、金币换算、系统设置和支付适配器均返回预期状态并可回读;该回归不修改现有开发存储,仍需在浏览器表单中确认交互和持久化。
- 管理台路由元数据现在将尚未接入的消息供应商和消息模板标记为隐藏;认证消息列表仍可见,直达路由与后端接口保留。需在登录后确认不同角色菜单均不出现这两个未完成配置入口。
- 首批 TDesign 统一迁移已通过 `packages/ui` 接入:两端共用 TDesign Provider,用户端浅色/深色自定义令牌会映射为 TDesign 变量,画布项目库已使用共享按钮适配器。需在全新浏览器加载、语言切换、React 19、弹层层级和管理台原有视觉基线下确认用户端视觉不回退、弹层不遮挡且管理台不受影响。
- 用户端基础 TDesign 控件已通过 `@miragenflow/ui/*` 按组件子路径导出并迁移:输入框、选择器、开关、文本域、弹窗、抽屉、下拉、提示、滑块、分页、标签和消息不再直接耦合 `tdesign-react/es/*` 内部路径;子路径保持按需加载。需在用户端各页面验证控件视觉、主题弹层和 React 19 行为。
- 图片工具、音频工具、认证绑定和账户 MFA 流程中的简单文本域现在统一使用共享 TDesign `TextArea` 适配器;适配器转发 `textareaElement` 引用,图片提及仍可准确恢复光标和选区。需在全新浏览器加载后验证文本域高度、焦点/选区行为、只读配置内容以及浅色/深色主题样式。
- 画布图片的宽高/数量输入和音频设置中的语速输入现在统一使用共享 TDesign `InputNumber` 适配器;宽高在失焦或回车时仍按可选的 16 像素步长提交,数量和语速保留最小/最大值及空值处理,画布主题样式由外层包装控制。需验证键盘输入、失焦/回车提交、范围修正、分辨率模式下的禁用状态以及浅色/深色主题样式。
- 画布项目选择复选框现在使用共享 TDesign `Checkbox` 子路径并沿用画布主题。文件选择器仍仅作为隐藏的上传触发器保留原生实现,提及编辑器因依赖真实 DOM 的选区、滚动、输入法和高亮叠层而继续使用原生文本域。需验证复选框选中/冒泡、键盘焦点,以及这些已记录的原生控件例外。
- 画布节点标题编辑现在使用共享 TDesign `TextInput` 适配器,并通过 `inputElement` 引用访问真实输入节点。自动聚焦/全选、回车与 ESC、失焦提交、外部点击判断、紧凑宽度和虚线下划线样式仍需在两种主题下浏览器验证。
- 组件中心已在本地开发浏览器验证共享控件行为:主题化选择器可以打开并通过 ESC 隐藏,示例弹窗可以打开并通过关闭按钮关闭,页面控制台无运行时错误;其他页面的弹窗和抽屉仍需登录后逐项确认。
- 本轮还将图片/音频/认证/账户文本域、画布图片宽高与数量、音频语速、项目复选框和画布节点标题输入迁移到共享 TDesign 适配器。`npm --prefix web run typecheck`、`npm --prefix web run build`、`npm --prefix server test`(101/101)、`npm --prefix admin run typecheck`、`npm --prefix admin run lint` 和 `npm run build:all` 均通过;未登录浏览器的登录页和组件中心无运行时错误。登录后的画布设置仍需人工检查视觉和交互。
- 用户端与管理台的左上角品牌标题、登录主标题/副标题和画布标题现在统一左对齐。需在登录页、首页和画布页的桌面与窄屏尺寸确认品牌文字起始线一致。
- 总览 P50/P95 延迟现在使用双环图,无调用样本时显示“暂无样本”;渠道生图测试会在成功和失败结果中保留实际请求路径、是否发起、测试时间和中文原因,编辑渠道不会清除独立的模型映射。需验证有无样本、上游拒绝/超时、未发起请求和保存后重新编辑的状态。
- 未执行过测试的渠道现在显示“未测试”,总览图表颜色会跟随深浅主题。需在新建渠道、切换主题和无调用样本时确认状态与颜色。
- 渠道健康单行测试现在使用独立结果弹窗,测试期间显示上游模型列表请求加载状态,完成后展示实际请求路径、是否发起、模型数量、测试时间和中文失败原因;最近一次结果也会回填到健康列表。需验证成功、地址/权限/网络失败、取消、重新测试、刷新后的结果持久化,以及深浅色主题下的可读性。
- 财务页面的余额账本、充值订单和支付事件现在根据真实列表生成统计卡与 ECharts 图表,并支持统一日期范围筛选;批量渠道健康检查会保留实际请求路径、是否发起和测试时间,健康检查与生图测试使用不同动作名称;管理请求增加超时、取消和网络失败提示。需在有数据、空数据、窄屏和请求超时场景下确认图表、筛选和状态提示。
- 画布项目页的根节点、工作区和加载占位现在统一使用动态视口高度,移除 `vh/svh` 冲突最小高度;目录同步拆为独立副作用并对配置无变化更新短路,历史项目的异常视口会自动归一化。需在桌面 1280×720、手机 390×844、切换视口和带旧节点项目中确认画布上下区域完整可见且控制台无更新深度错误。
- 画布项目页在加载完成后重新连接尺寸观察器,并为父子层级补齐显式全高与帧合并,避免下半屏塌陷;用户端壳层改用可收缩的动态视口内容区,图片工具预览测量对未变化值不再写入状态;窄屏侧栏、小地图、工具栏和缩放控件分层布局,侧栏遮罩不再压住底部工具栏,顶栏折叠入口保持可用,画布操作更新增加无变化短路以避免渲染循环。客户端初始化请求也会合并,避免 StrictMode 重复刷新会话。需在重新登录后验证图片、音频、分组交互,以及带持久化节点的真实项目。
- 切换画布项目时现在会取消旧项目的资源恢复和生成请求、清理待执行计时器,并将持久化/视口保存限制在真正完成加载的项目上。需快速从项目 A 切到项目 B、在生成请求进行中切换,并确认两个项目的节点和视口不会串线。
- 平台资产同步现在只在合并结果确实变化时替换列表,并将提示 API 移出初始化 effect 依赖。需打开 `/assets` 验证 hydration 后不会重复请求或反复替换同一列表,失败时本地资产仍可使用。
- 管理台设置页的权限提示现在准确显示安全管理范围;存储策略编辑改用带 `If-Match` 的版本保护;数字输入为空时保持为空,不再默认显示 0。需验证并发编辑冲突、设置页权限提示和空值提交校验。
- 模型产品编辑在下架并清空渠道组时会显式解除旧绑定;重新上架仍要求选择有效且启用的渠道组。需验证下架解绑、重新上架校验和版本冲突提示。
- 账户中心的登录设备列表现在只显示“当前浏览器 / 设备 N”等可读名称,不再把设备 UUID 暴露给用户;撤销操作仍使用服务端真实会话标识。需验证多设备列表排序、当前设备识别和撤销后列表刷新。
- 管理台侧栏折叠后现在会完全隐藏,不再保留图标窄栏;窄屏展开时覆盖在页面上方,并可通过遮罩或选择菜单项关闭。业务列表在手机端仍保持紧凑表格,可触摸横向滑动,页面与卡片左右间距同步缩小。需验证桌面折叠/恢复、390px 遮罩交互和全部管理列表的横向滚动。
- AI 创作面板、结果/参考图容器和资产预览/占位区域已统一使用主题语义色,不再硬编码近黑色或 Stone 背景;游客左下角不再显示“个人主页”和“退出登录”,登录用户仍保留这两个入口。需在浅色/深色主题下验证 `/tools/image`、`/tools/audio`、`/assets`,并分别确认游客与登录状态。
- 用户端账户、任务和同步设置共用的面板、统计卡与弹窗也已改用主题表面,浅色模式不再出现深色容器。需在有数据和空状态下分别检查这些页面。
- 画布图片裁剪和切分预览也已改为跟随画布主题的背景,节点信息弹窗移除原始 JSON 标签,仅保留 ID、类型、尺寸、位置、状态等字段化信息;首页、样式预览和组件验收页的非必要英文已改为中文,演示按钮会打开明确的交互提示或跳转真实创作页。需在浅色/深色主题和开发路由下确认预览容器、信息弹窗和按钮行为。
- 用户端与管理台通用 UI 图标已统一改为本地化 Lineicons 精选子集,不再使用 Lucide 或直接引用 TDesign 业务图标;用户端常用图标为 22px 且点击区域至少 36px,管理台菜单、页头和主要操作图标为 24px、点击区域为 40px。撤销、重做、停止、侧栏、分组、文本、生成和收藏状态已使用独立语义图形,不加载 CDN、WebFont 或全量图标目录。需逐页确认深色主题、折叠菜单、画布旋转与撤销控件、下拉菜单、密集操作列和 390px 布局。
- 用户端模型选择、任务筛选、图片参数和画布图片设置下拉统一使用带主题样式的自定义弹层,不再渲染浏览器原生 `<select>`;外层弹层与 TDesign 内容层共用用户端主题,并统一悬停、选中和禁用态。适配器保留原有受控 `onChange(event.target.value)` 接口,支持键盘选择、点击外部/Escape 关闭和空状态;画布 Portal 菜单不会再被误判为面板外部点击。需在浅色/深色主题、键盘操作、手机宽度和重新打开后的选中状态下逐项确认。
- 用户管理已增加独立的增加金币和扣减金币图标入口;纯财务管理员可从“计费与套餐 > 用户金币调整”使用仅含脱敏身份与余额的专用列表。审批、MFA 防重放计数、总余额、可消费金币批次、关联账本和幂等响应会在同一事务内提交,批准与驳回都会在事务内重新检查最新版本和状态。金额与原因标签已关联对应输入控件;弹窗只接受正整数数量,展示当前可用金币与审批后预计余额,并要求填写调整原因。本地超级管理员(包括仅绑定回环地址的本地 Docker 环境)可执行自己的申请;自动执行失败时会保留原申请并进入审批中心继续处理,避免重复提交。生产环境仍要求另一名财务管理员审批。需分别验证超级管理员与财务角色、加减两个方向、自动执行失败恢复、后续任务消费、旧版本与验证码重放拦截、余额不足、弹窗关闭和审批中心操作。
- 管理台新增/编辑字段已改为单一受控状态源,不再被无名称的 TDesign `FormItem` 内部空值覆盖。已在浏览器验证渠道基础信息、模型列表、模型映射完整回填,切换 Tab 保值、取消关闭及控制台无报错;其余资源编辑弹窗仍需逐项验收。
- 渠道模型列表已与可选模型映射独立保存:上游拉取结果使用复选框和“添加所选模型”加入渠道模型,不会自动创建映射;未拉取时仍可手动添加模型。开发环境也不再自动生成展示用 mock 渠道。需验证新增、编辑回填、勾选、移除、空映射保存以及全新开发存储。
- 渠道探活现在只在真正调用上游 `fetch` 前标记“已发起”;通用供应商不会凭 API Key 判断健康,协议、私网和解析校验失败会明确标记为尚未请求。关闭或重开弹窗后旧请求不会覆盖新表单;新增/编辑保存不再隐式探活,只有点击“拉取模型列表”才显示独立加载状态;需验证这些成功、失败和竞态路径。
- 渠道“拉取可用模型”现在使用独立加载状态,按钮会显示正在请求,结果会标明实际请求路径(如 `/v1/models`)、是否真正发起上游请求以及中文失败原因(域名解析、HTTP 状态、空列表或网络超时)。修改地址或 API Key 会清除旧结果;需在可用、错误和无法解析的地址下确认提示准确,且失败时表单仍保留。
- 渠道表单已补充完整请求地址示例(`https://example.com` 和 `https://example.com/v1`)。API Key 会先去掉首尾空白,只要求非空,不再限制 16 位最小长度;探活、新增、编辑和轮换凭证的规则已统一。需用较短但有效的上游密钥分别验证各流程。
- 管理台渠道弹窗使用统一的“基础信息 / 拉取模型 / 模型映射”Tab,Tab 可自由切换,基础信息、探测结果和映射草稿不会因面板切换而清空,底部统一通过“保存/取消”提交或放弃;映射明确区分平台显示模型 ID 与供应商请求模型 ID。非生产环境默认允许本机/私网供应商地址,显式设置 `MIRAGENFLOW_ALLOW_LOCAL_PROVIDER_URLS=false` 或生产环境仍会拒绝。需在浏览器中验证自由切换、探活失败保留表单、统一保存和编辑回填,并用真实本地兼容端点确认请求模型参数。
- 管理台新增用户弹窗已收敛为用户名、邮箱、手机号和初始密码四项;状态默认启用,套餐、可用模型、欢迎消息、并发和每日上限不再要求创建时填写。需验证最小表单创建成功及邮箱/手机号必填约束。
- 本轮已完成服务端全量回归 101 项、用户端与管理台类型检查、管理台 lint;仅保留既有格式化警告。浏览器仍需继续按下方清单进行逐页验收。
- OpenAI 图片比例矩阵已覆盖 1.91:1、2.35:1 和 21:9 的 1K/2K/4K 尺寸换算,服务端允许文档矩阵的宽屏 1K 尺寸;管理台新增模型默认 4K 预设为 3840×2160。需在独立图片页和画布中切换所有宽屏比例,确认显示尺寸、实际输出尺寸和价格档位一致。
- 服务端已补充 OpenAI 图片端点的 69 项回归测试,覆盖比例矩阵、参考对象过期/撤销/staging 丢失、异常图片 URL、模型能力声明、幂等请求头和人工对账真实图片元数据;仍需使用真实兼容端点验证供应商返回 URL、透明 PNG/WebP 和 4K 长任务。
- OpenAI 图片、文本和音频请求现在统一发送标准 `Idempotency-Key`;供应商传输结果未知时,用户端图片工作台会保留“待对账”状态并提供任务历史入口,不再误报普通失败。需使用兼容端点和浏览器验证幂等请求头及未知结果流程。
- OpenAI 图片供应商适配层新增参考图序号和缺失引用兜底:非法序号、超过 16 张、单图超过 50MB 或提示词引用未上传图片时会拒绝请求;只有参考图而没有提示词时会自动追加参考图说明。需补充内部适配器调用、文生图和参考图生图的边界回归。
- 管理台安全设置入口改为有效路由,退出登录不会再被菜单通用点击处理覆盖;审批列表会按管理员 ID、邮箱和名称阻止申请人自审,审计分类和操作人缺失时使用中文兜底。需用不同管理员身份和审计记录验证。
- 管理台分页已隐藏总条数汇总,新增/编辑弹窗显式支持遮罩、ESC、取消和右上角关闭,渠道凭证字段改为中文“访问密钥”;需在渠道、用户、套餐、模型产品和财务配置页面分别验证关闭路径及异步成功自动关闭。
- 管理台渠道新增和编辑现在共用统一风格的 OpenAI 图片端点 Tab 弹窗;已移除通用供应商分支。需确认基础信息、模型拉取和显示/请求模型映射三个 Tab 布局一致,并使用真实 OpenAI 兼容端点验证 `/v1/images/generations`、`/v1/images/edits`、透明背景与 PNG/JPEG/WebP 输出格式。
- 图片上传现在对 PNG/JPEG/WebP 走 multipart,单图 50MB 限制不再受普通 JSON 请求体限制;参考图序号匹配按长序号优先(如“图片十一”),画布基础比例写入 `ratio` 而不是非法的 `size`。需人工验证大图、图片十一至十六、比例切换和旧画布配置。
- 用户端图片创作已支持最多 16 张参考图、50MB 单图限制、中文图片编号和 `@图片一` 提及;需验证排序、删除、未上传图片引用拦截、参考图顺序和结果元数据展示。未知上游结果应保留预留金币,管理员成功对账必须上传真实图片,失败对账释放预留金币。
- 管理台成功对账现在使用 multipart 直接上传真实 PNG/JPEG/WebP,服务端会校验图片魔数;渠道编辑可复用已保存 API Key 探测模型,模型产品新增图形化分辨率矩阵。需验证大于普通 JSON 请求体的图片、已有输出任务的成功对账、无效图片拒绝和探测失败时表单保留。
- 开发启动脚本会为前台、管理台和服务端预留互不冲突的端口;需验证脚本打印的前台 `/admin/` 代理地址与实际管理台端口一致,并确认 3000 端口被占用时仍能正确显示动态端口。
- 管理台新增 ECharts 可视化:系统概览展示任务趋势、模型成功率和任务状态;渠道健康展示成功率/延迟和健康状态;任务中心展示状态与创建趋势;实时指标轮询展示连接、订阅、队列和事件积压短时序;审计拆分为“日志大板”和“详细日志”,日志大板展示分类、正常 HTTP 200/异常 4xx/5xx 占比及趋势,详细日志增加状态列。需在有数据、无数据、窄屏和深色主题下检查图表不重叠、不空白、中文标签完整;实时指标当前为前端短时采样,不代表持久历史。
- 管理台图表空数据现在显示明确的等待提示,深色主题的标题、图例、坐标轴和饼图标签会自动切换为高对比度文字;系统概览统计卡改为数值加单位,模型成功率图优先显示模型中文名称。需验证无任务、无实时指标和无模型数据时不会出现伪造的零点或白屏。
- 管理台左侧菜单已按业务入口使用差异化图标;只有一个可见页面的分组会直接显示页面入口,审计分组现在包含“日志大板”和“详细日志”两个入口,其他多页面分组仍可展开。需在不同权限角色、折叠侧栏和窄屏下确认图标、层级和当前路由高亮一致。
- 管理台所有业务列表和详情已统一使用中文字段与结构化展示:余额显示可用/预留金币,嵌套对象不再直接输出 JSON,内部凭证/哈希/幂等标识会隐藏;仅用户列表的 ID 按当前列表显示为 1、2、3 等纯数字,模型、套餐、渠道等其他 ID 保留真实值,可用模型显示名称。需逐页确认无“其他信息”、原始 JSON、未翻译动作或异常英文。
- 审计日志已改为固定的时间、分类、行为、操作人、对象、编号、请求编号和来源地址列,并按最新时间倒序展示;服务端提供中文行为映射与操作人名称,管理员只显示邮箱,普通用户显示用户名,系统任务和支付回调使用固定中文名称,且不再显示空的“操作”列;需验证不同角色和动作下的分类、中文文案及操作人显示。
- 管理台系统概览在统计字段缺失时会回退到安全值,不再因 TDesign Statistic 崩溃白屏;需在无任务历史和会话刷新后访问 `/admin/dashboard/base` 验证。
- 管理台系统概览在接口没有返回数据时现在显示加载态或中文空数据面板,并保留刷新入口;请求错误仍显示可重试提示。需在登录后模拟空响应和恢复路径,确认不会出现白屏。
- 桌面端登录/注册页已改为对称垂直留白,让中英文品牌标题、表单和底部提示更接近视口水平/垂直居中并减少顶部空白;需验证常见桌面高度,同时确认移动端间距保持正常。
- 用户菜单下拉在未登录时也会显示精致的资料视觉占位容器和“登录工作台”入口,登录后显示身份信息、账户中心、任务历史和退出登录操作;需验证悬浮/聚焦交互、浅色/深色对比度和窄屏适配。
- 通知悬浮窗已改为响应式短面板,按视口高度限制最大高度并保留原有圆角;需在桌面、平板和窄屏高度下分别验证无通知与有通知状态。
- 用户端侧边导航已统一加宽至 224px,导航项和底部链接高度略微压缩;需在桌面端与窄屏抽屉中确认宽度、主内容偏移、文字完整显示和滚动体验。
- 密码登录已改为单一“账号”输入框;需验证邮箱、手机号(含带空格/连字符输入)和用户名均能自动识别并登录,且密码登录界面不再出现邮箱/手机号/用户名切换标签。
- 管理台入口现在会把绝对站点 base URL 规范化为 React Router 的路径 basename;请在 `/admin/dashboard/base` 直达、刷新和 site 静态构建预览下确认页面不再白屏,并确认未登录时会落到 `/admin/login`。
- 登录奖励入口已移除大尺寸悬浮奖励卡和透明点击覆盖层,取消 GIF 扫光并改为仅有 CSS 渐变边框的透明背景;需在桌面和窄屏确认浅色主题下文字清晰,且移动端不会挤压右侧头像和导航。
- 登录奖励渐变已改用遮罩边框实现;需确认渐变只显示在边缘,按钮中心与页面背景保持透明。
- 登录奖励文案已改用“作品灵感”标签同款品牌字体和较轻字重;需确认“登录 / 免费 / 送积分”在桌面和移动端均完整显示,且“免费”高亮仍清晰。
- 自由画布与一键成稿入口已改为图片铺满卡片;需确认两张图片在桌面、平板和手机端比例完整、文字叠加可读,并保持原有跳转。
- 移动端两个工具入口已恢复为与上方横幅相同的宽高比例;需在窄屏确认四个卡片边缘、文字和图片裁切保持一致。
- 用户头像访客浮图和“加入设计交流群”容器已暂时移除,窄屏隐藏左侧品牌 logo、保留折叠菜单按钮;需确认访客悬浮头像不再弹出大图,已登录用户菜单、移动端抽屉导航和侧栏底部链接仍可用。
## V1 当前验收
- `MiragenFlow-front` 的用户控制台视觉基础已复制到 `web/src/user-console/` 并接入用户壳层;需人工确认桌面/移动端侧栏、首页空资产状态、`/components` 组件验收页、登录奖励原有视觉样式及“免费”高亮字距、三段文字的填充和“免费”前后对称间距,以及登录/注册页仅在表单标题上方显示放大的品牌文字、不再显示 MF 图标和顶部容器;移动端广告轮播和活动图片应保持与桌面端一致比例且不再被裁切。
- 用户端新增 `/style-preview` 配色候选页;需在桌面和窄屏分别比较“暮光蓝灰、暖纸钴蓝、雾紫玻璃、松石石墨”四套方案的层次、文字对比度、卡片密度和滚动体验,确认候选后再应用到正式页面。
- 用户端已按确认的“雾紫玻璃”方案应用淡紫背景、玻璃卡片、紫色主色和珊瑚辅助色;需人工确认首页、登录注册、AI 创作、自由画布、资产页在浅色模式下的对比度、TDesign 弹层层次和窄屏滚动,并确认主题切换后的深色紫夜版本仍可读。
- 认证流程已拆分为密码登录(邮箱/手机号/用户名)与手机号验证码登录;注册分为邮箱/手机号两种入口,后台系统设置新增“注册邮箱验证”和“注册手机号验证”独立开关。需人工验证开关关闭时直接注册、开启时先发验证码再完成密码设置,以及验证码登录发送/重发、错误码、限流和 MFA 串联行为。
- 认证增强开关已补齐独立管理员 CAPTCHA、遗留 MFA 挑战失效、刷新会话状态和审计保留;需浏览器确认关闭开关时普通用户验证页/账户页不出现 CAPTCHA、MFA 或恢复码输入框,并确认管理员仅邮箱密码登录。
- 队列 claim、Redis 活跃租约、PostgreSQL 任务心跳/过期恢复、验证码快照和消息死信边界已修复;需在 PostgreSQL/Redis 多实例环境验证租约 owner、失败重试、过期 ack/nack 和重启恢复。
- PostgreSQL 消息 outbox 在发送期间会续租并在完成时校验 owner/租约有效期;仍需真实 provider 超时与双实例重复发送验证。
- 消息入队支持调用方幂等键并向 Provider 传递稳定 `Idempotency-Key`;需验证各真实邮件/SMS Provider 的幂等语义。
- 任务中心新增脱敏任务详情抽屉,审批中心新增带 If-Match 的驳回操作;需浏览器验证输入/输出/事件时间线、驳回原因校验和版本冲突重试。
- 管理员登录已兼容管理台的 `account` 字段与 API 的 `email` 字段,默认 `admin@admin.com` / `admin` 可进入后台;需在浏览器中确认错误提示、刷新会话和权限菜单。
- 管理台新增/配置弹窗已覆盖套餐、金币单位、换算、支付适配器、存储、消息模板和管理员安全;用户新增支持套餐/并发/每日任务上限/可见模型/欢迎消息,渠道健康可直接探活,需浏览器逐项确认成功、失败和重试态。
- 未知任务对账弹窗支持“供应商成功/供应商失败”两种结果,并可填写实际扣除金币;金币单位配置支持小数位数和舍入方式,新增渠道保存后可按需点击模型列表探活。需验证成功结算、失败释放预留金币、凭证校验和探活提示。
- 管理台新增渠道、分组、模型产品和支付适配器时由服务端生成可靠 ID;套餐、用户和模型提交前会校验必填项与数值边界,财务配置入口按 finance 权限显示,供应商和模板编辑会检测版本冲突。需验证空表单提示、无效上架条件、不同角色按钮可见性以及旧数据冲突提示。
- 审批申请人不会再看到自己的审批按钮;详细日志列表提供可关闭的详情抽屉,需验证操作列仅在确有查看动作时出现,且抽屉可通过取消、右上角和遮罩关闭。
- 渠道、渠道分组和模型产品已增加编辑抽屉,渠道健康支持全部探活,认证消息列表不再返回目标哈希;需浏览器确认 If-Match 冲突、批量探活失败重试、中文状态和窄窗口固定列布局。
- 管理台用户工作台已增加一次性重置密码、重发认证消息、会话/套餐权益详情;系统概览和渠道健康补充 P50/P95、成功率及消息/支付异常指标,需在浏览器中验证操作、权限矩阵和错误重试。
- 用户详情抽屉已拆分为账户、会话、任务、账本、权益和审计 Tab;新增独立 `/api/v1/admin/settings` 系统设置资源,支持 If-Match、认证开关即时生效和关闭增强后的用户状态清理,需在浏览器中验证冲突重试与权限。
- 已执行 `npm run typecheck:server`、`npm run test:server`(101 项全部通过)、`npm run typecheck:web`、`npm run typecheck:admin`、`npm --prefix web run build`、`npm --prefix admin run build`、`npm --prefix admin run lint` 和 `npm run build:all`;`git diff --check` 通过,管理台 lint 仅有既有格式化警告、无错误。
- 本地预览默认管理员账号为 `admin@admin.com`、密码为 `admin`;管理员 MFA 与人机验证码默认关闭,可通过环境变量重新开启。
- 本地一体化预览默认写入 `/tmp/miragenflow-dev/store.json` 私有快照;服务端热重载后已创建的用户仍可使用原账号密码登录,若显式设置 `MIRAGENFLOW_STORE_FILE` 则以自定义路径为准。
- 认证:普通用户默认账号密码直登;邮箱/手机号验证、图形验证码和用户 MFA 可由后台开关启用,独立管理员 audience、多管理员角色、管理员 MFA 管理和 refresh 重放撤销仍需人工确认。
- 账户 API:用户套餐权益查询与登录设备会话列表/撤销应保持所有权校验,并撤销选中会话族。
- 认证与隐私:管理员 MFA 的 CAPTCHA 豁免严格绑定当前管理员会话;用户/管理员 MFA 密钥和支付适配器凭证仅在服务端加密保存并可在重启后恢复,用户支付响应不暴露适配器标识或 provider key。
- 计费:用户充值待支付、充值和套餐订单的签名 mock/adapter webhook 契约、支付失败/退款状态、按原余额 bucket 幂等退款、套餐购买/权益快照、人工充值审批、reserve allocation、部分结算、释放、退款和幂等冲突。
- 账本不变量:过期 bucket 必须追加幂等 adjustment 流水,套餐权益可用余额必须跟随实际 plan bucket 的预扣/结算/释放变化。
- 余额过期扣减会按账户实际剩余余额钳制 adjustment 金额,确保账本与余额投影一致。
- 任务:图片/文本/音频/反推/多角度/拆分/放大统一异步,渠道 fallback、unknown 对账、租约恢复、取消、WS cursor 重放和任务详情 HTTP 降级轮询。
- 存储:上传 MIME/魔数校验与 quarantine 扫描、私有 staging/GC、按保留期管理结果对象、本地资产、WebDAV 密文凭证、manifest ETag/版本、冲突任务、墓碑、归档队列和同步界面。
- WebDAV 保留期:manifest 保留状态持久化,用户可在策略上限内延长保留时间。
- 资产归属:保存任务输出时必须拒绝其他用户的任务/输出,重复保存同一输出应返回原资产而不是创建副本。
- V1 不包含视频、动画、真实 3D、训练、团队、内容治理、Local Agent、远程 Prompt 或插件市场入口。
本次产品范围收缩后需要人工确认:
- 首页、浏览器标题、配置存储、导出文件、WebDAV 默认目录和内部插件 SDK 应显示或使用 MiragenFlow / 元境幻生的新名称。
- `web/` 前台与 `admin/` 后台应通过 `bun run dev:all` 一起启动,并都能从 3000 端口访问;生产构建后 `/` 与 `/admin/` 均应正常加载。
- 右上角导航不再显示 GitHub 入口,文档、设置、语言、主题和版本入口仍可正常使用。
- 右上角导航不再显示 GitHub 入口,文档、存储设置、语言和主题入口仍可正常使用;前台不提供版本号/更新入口。
- 导航只保留首页、画布库、画布项目、我的资产和配置;访问已经删除的路由时应进入页面不存在提示。
- 界面中不再出现独立生图工作台、视频工作台或视频节点、提示词中心或远程提示词源、本地 Agent 接入、远程插件市场或 URL 安装入口。
- 画布项目仍可正常创建、重命名、复制、导入、导出和删除,节点、连线、视口与外观设置可以正确保存和恢复。
- 图片、文本、音频、生成配置和组节点仍可创建、连接、编辑、生成、重试、复制、分组和导出。
- 图片生成、参考图编辑、文本生成和音频生成应使用当前选中的渠道与模型;停止、失败和重试状态应保持正常。
- 反推提示词、多角度生成、裁剪、遮罩局部编辑、切分和本地放大应生成内容正确、连线正确的新节点。
- 我的资产仍支持文本和图片的新增、编辑、搜索、筛选、导入导出、插入画布、复制、下载和删除。
- 配置弹窗只保留渠道、偏好设置、WebDAV 和本地存储;配置导入导出与 IndexedDB 统计应正常工作。
- WebDAV 应同步画布、我的资产及其引用的图片或音频文件,同时不上传 AI API Key。
- 简体中文与 English 在保留的导航、配置、画布控件、提示信息和 Ant Design 组件中应完整切换。
- 我的资产仍支持文本、图片和音频的新增、编辑、搜索、筛选、导入导出、插入画布、复制、下载和删除。
- `/settings` 只保留 WebDAV 与本地存储;供应商渠道、偏好配置和自定义脚本已移至服务端管理,不在用户端展示。
- 用户端不再显示供应商渠道、Base URL、API Key、自定义脚本或远程插件配置;`/settings` 仅提供本地存储与 WebDAV。
- WebDAV 应同步画布、我的资产及其引用的图片或音频文件,同时不上传 AI API Key;刷新设置页后会从服务端恢复已保存的 WebDAV 连接状态。
- 简体中文与 English 在保留的导航、配置、画布控件、提示信息和 TDesign 组件中应完整切换。
- `server/` 应能通过 `npm run dev:server` 启动,`GET /api/health` 返回 contracts 版本;`packages/contracts` 能被服务端类型检查解析。
- 本地开发时 `/api` 和 WebSocket 应由前台 Vite 代理到 3100 端口,不再请求 TDesign 模板的腾讯云示例接口。
- 注册、验证码验证、密码登录、公开模型目录、金币余额、任务幂等和任务事件历史需用真实 API 联调;3D 请求应返回 `CAPABILITY_NOT_ENABLED`。
- 没有 provider request ID 的未知任务在预扣过期后会释放余额;已使用临时 PostgreSQL 和 Redis 完成生产配置 smoke,验证 `/api/ready`、23 个 SQL 迁移文件、用户/余额/账本/渠道/任务/attempt/event canonical 行、任务 snapshot、dispatch outbox、支付指纹、消息 outbox lease 字段、PostgreSQL 写入后重新加载、outbox 派发最终 `sent`、revision fencing,以及 Redis 租约 claim/renew/ack/nack;验证后的临时依赖已清理。
- 已通过同源开发入口验证 `/`、`/admin/`、`/api/v1/` 和 `/api/ready`;未鉴权访问 `/api/v1/ws/tasks` 会按预期拒绝。完整浏览器交互仍需人工 UI 验收。
- 管理台新增模型产品上架开关、批量探活逐项结果、只读任务详情和错误页会话清理;需在浏览器逐页验证弹窗、表单保留、409 重试和权限恢复路径。
- 管理员两步验证设置/验证现在只有系统增强开关开启后可用;需分别验证开关关闭时登录页、管理台安全页和恢复码均不显示,开启后才出现挑战流程。
- PostgreSQL WebDAV worker 已将文件、manifest、保留/归档状态和冲突副本写入同一完成事务;过期保留扫描已改为 PostgreSQL 行锁入队并允许终态重试。仍需用真实 PostgreSQL + WebDAV 远端验证重启恢复、冲突重试、租约过期 fencing 和多实例并发。
- 已使用独立浏览器页面验证默认账号密码注册/登录、画布库、账户会话、套餐购买、设置页、桌面/移动导航文案和删除路由 404;增强开关开启后的验证码/MFA 以及真实 WebDAV/provider 交互仍需部署环境验收。
- Docker Compose 插值和 healthcheck 配置已核验;镜像构建因当前环境访问 Docker Hub 时出现 TLS 证书不匹配而阻断,生产镜像仍需在可用的 registry/网络环境重试。
- CAPTCHA 接口只返回人机题目,不返回答案或可逆明文;WS refresh family 撤销、非法 WebDAV 路径、就绪检查、3D 目录过滤、公开模型 ID 和 WebDAV checksum/冲突副本已有回归覆盖。
- V1 回归测试新增显式 MFA recovery、用户会话列表/撤销、`/me/plans`、双管理员审批调账、资产归属与去重、WebDAV manifest 保留期及合法写入、过期 bucket 账本和渠道分组路由排序覆盖。
- Access token 现在绑定会话族;撤销设备会话后旧 bearer token 与 WebSocket 订阅会立即失效,管理端契约别名也已加入回归覆盖。
- 管理台列表已增加按页面业务维度、状态和日期范围筛选,系统概览增加近 7 日任务趋势和运行告警;接口级 403 会进入带角色、所需 scope 和 requestId 的权限恢复页。待浏览器验证筛选与分页保持、重新登录/切换管理员按钮清理会话且行为有区分。
- PostgreSQL dispatch worker 在本地任务映射缺失时会按 task_id 回载持久任务后再入队;待用双实例验证跨实例创建的任务不会长期停留在待重试状态。
- PostgreSQL 快照保存现在使用捕获时的不可变 revision 进行 fencing;待在并发 worker 下验证旧快照不能覆盖 direct lease/outbox 更新。
+28 -1
View File
@@ -5,4 +5,31 @@ description: Planned follow-up work
# TODO
No follow-up item is currently confirmed. New work should stay within the retained image, text, audio, canvas, asset, WebDAV, and internal-extension scope.
- Planned: unify `web/` and `admin/` on one locked TDesign component runtime while preserving the user-console custom visual system and the admin visual baseline. The single canonical long-task scope and copyable goal prompt are recorded in `docs/miragenflow-long-task-goal-prompt.md`; component migration details, shared-boundary rules, deletion/retention list, and acceptance gates remain in `docs/miragenflow-tdesign-unification-goal.md`. Implementation must remain phased until Ant Design, ProComponents, Radix controls, and `.ant-*` behavior dependencies are fully migrated and browser-verified.
## Long-task execution checklist
This is an index for `docs/miragenflow-long-task-goal-prompt.md`. Check an item only when code, command, or browser evidence exists; unchecked work must not be described as complete.
- [x] P0: confirm `tdesign-react@1.15.1`, the shared Provider, theme-variable bridge, and local Lineicons subset while keeping `web/` and `admin/` route, permission, state, and API boundaries separate; package entrypoints, source scans, typechecks, and production builds provide current evidence.
- [ ] P1: migrate ordinary controls, themed dropdowns, feedback, overlays, and forms; remove native `<select>` and verify keyboard, backdrop, Escape, focus return, and controlled fields.
- [ ] P2: complete real read/write, persistence, permission, error, and retry flows across all 22 admin entries, covering users, coins, channels, models, billing, tasks, settings, and audit.
- [ ] P3: regress the user homepage, authentication, AI creation, canvas, assets, tasks, account, and mobile layouts, including dynamic viewport height, theme surfaces, left-aligned titles, guest menu, and compact lists.
- [ ] P4: remove the user-side `web/` Ant Design, ProComponents, Radix controls, reset CSS, and remaining `.ant-*` dependencies only after references are clear, builds pass, and browser regression is complete; keep the admin TDesign baseline unchanged.
### Acceptance matrix
| Scope | Current status | Required evidence |
| --- | --- | --- |
| Eight core user routes | Public/protected route shells and desktop/390px theme smoke verified; authenticated page review pending | Authenticated desktop/390px, light/dark, refresh, and clean console |
| 22 admin pages | All business deep links load after local administrator login; authenticated interactions remain pending | Screenshots, write results, persistence after reopening, permission/error/retry paths |
| Five ECharts areas and audit dashboard | Implemented; populated/empty/error review pending | Charts, theme switch, narrow layout, category counts and ratios |
| OpenAI channel three-tab editor | Implemented; live compatible endpoint review pending | Draft retention, manual `/v1/models`, model selection, optional mapping, edit backfill |
| Component and dependency cleanup | In progress by phase | Typecheck/build/lint, static scans, and `git diff --check` |
### External dependencies or out-of-scope blockers
- [ ] Production email, SMS, and payment adapters: waiting for provider contracts; do not replace them with display-only mocks.
- [ ] Horizontal production scaling: incremental domain-table SQL, task row-level CAS, and remote WebDAV/message race verification remain outstanding.
The management-console shell and first P1 interactions are now implemented from `goal.md` and `admin-plan.md`: grouped navigation, recoverable permission pages, local admin credential reset, ordinary-user creation, modal-based channel/group/model/price creation, fixed-width Chinese status/capability rendering, authentication-message detail/retry, and user detail tabs. The independent versioned `/api/v1/admin/settings` resource now controls authentication enhancements and rate-limit values at runtime. The user-console visual foundation from `MiragenFlow-front` is now copied under `web/src/user-console/` and wired to real asset/catalog/routes; its static demo data and unsupported feature placeholders are intentionally not used as production data. Remaining work is full browser verification of all 22 admin pages and the migrated user shell, deeper domain-table/CAS migration before horizontal production scaling, plus connecting production email/SMS/provider adapters after their documentation is supplied. Task creation/reserve, settlement/release/reconciliation, and payment callbacks execute inside Store transaction boundaries with PostgreSQL snapshot atomicity; replacing the snapshot projection with incremental domain-table SQL and cross-instance worker claims/CAS remains required.
+28 -1
View File
@@ -5,4 +5,31 @@ description: 当前项目后续值得处理的事项
# TODO
当前没有已经确认的后续事项。新增工作应继续围绕图片、文本、音频、无限画布、资产库、WebDAV 和内部扩展能力展开。
- 规划:用户端 `web/` 与管理端 `admin/` 使用同一锁定版本的 TDesign 组件运行时,同时保留用户控制台自定义视觉和管理台现有视觉基线。唯一可复制的长期目标提示词位于 `docs/miragenflow-long-task-goal-prompt.md`;迁移方案、共享边界、删除/保留清单和验收门槛位于 `docs/miragenflow-tdesign-unification-goal.md`。在 Ant Design、ProComponents、Radix 控件和 `.ant-*` 行为依赖全部迁移并完成人工浏览器验收前,必须分阶段保留旧依赖。
## 本轮长期目标执行清单
以下清单是 `docs/miragenflow-long-task-goal-prompt.md` 的执行索引。复选框只有在有代码、命令或浏览器证据时才能勾选;未勾选项不得在版本说明中写成已完成。
- [x] P0:确认 `tdesign-react@1.15.1`、共享 Provider、主题变量桥接和本地 Lineicons 子集,保持 `web/` 与 `admin/` 的路由、权限、状态和 API 边界独立;共享包入口、源码扫描、类型检查和生产构建已留下当前证据。
- [ ] P1:迁移普通控件、主题化下拉、反馈、弹层和表单,清理原生 `<select>`,验证键盘、遮罩、ESC、焦点回收和受控字段。
- [ ] P2:逐页完成管理台 22 个入口的真实读取、写入、持久化、权限、错误和重试闭环,覆盖用户、金币、渠道、模型、计费、任务、设置和审计。
- [ ] P3:回归用户端首页、认证、AI 创作、画布、资产、任务、账户和移动端,确认动态视口、主题表面、标题左对齐、游客菜单和窄屏列表。
- [ ] P4:只有在用户端 `web/` 引用清零、构建通过和浏览器回归通过后,才删除用户端 Ant Design、ProComponents、Radix 普通控件、reset 样式及遗留 `.ant-*` 依赖;管理端继续保持既有 TDesign 基线。
### 验收矩阵
| 范围 | 当前状态 | 必须留下的证据 |
| --- | --- | --- |
| 用户端 8 个核心路由 | 公开/受保护路由壳层及桌面/390px主题烟测已通过,登录态页面仍待复核 | 登录态桌面/390px、浅色/深色、刷新、控制台无错误 |
| 管理台 22 个页面 | 本地管理员登录后全部业务深链可加载,登录态交互仍待复核 | 页面截图、写操作结果、持久化后再次打开、权限/错误/重试 |
| ECharts 五类页面与日志大板 | 已实现,待有数据/空数据/异常数据复核 | 图表截图、主题切换、窄屏无重叠、分类数量与占比 |
| OpenAI 渠道三 Tab | 已实现,待真实兼容端点复核 | Tab 草稿保留、主动 `/v1/models`、勾选模型、可选映射、编辑回填 |
| 组件与依赖清理 | 分阶段进行 | typecheck/build/lint、静态扫描、`git diff --check` |
### 外部依赖或非本轮阻塞
- [ ] 生产邮件、短信、支付适配器:等待供应商协议文档后接入,不能用展示用 mock 冒充完成。
- [ ] 横向生产扩容:仍需增量领域表 SQL、任务行级 CAS、远端 WebDAV/消息竞态的部署验证。
当前 V1 的 PostgreSQL/Redis 生产配置 smoke 与同源路由核验已完成。管理台一级/二级菜单、权限安全总览、弹窗/抽屉式 CRUD、中文状态、用户详情 Tab 和独立系统设置已落地;仍需逐页浏览器错误态验收。任务创建/预扣、结算/释放/未知对账和支付回调已统一进入 Store 事务边界,并通过 PostgreSQL 快照原子提交;横向生产扩容前仍需把快照投影替换为增量领域表 SQL,并补 WebDAV worker 远端状态事务化、任务状态行级 CAS 与数据库读取一致性。消息 worker 已具备跨实例 claim/CAS,但仍需在真实 PostgreSQL/Redis 多实例环境验证。organization/team nullable scope 已完成 schema/contract 预留,V1 仍不开放团队业务。后续工作是接入生产邮件/SMS/支付适配器,并在拿到供应商文档后确认 query/cancel 契约。新增产品能力继续围绕图片、文本、音频、无限画布、资产库、WebDAV 和内部扩展能力展开。
+1 -1
View File
@@ -5,7 +5,7 @@ description: Security reporting and responsible disclosure
# Report a Vulnerability
See [SECURITY.md](https://github.com/basketikun/infinite-canvas/blob/main/SECURITY.md). Do not post exploit details, private API keys, sensitive screenshots, or real user data in a public issue.
See [SECURITY.md](https://git.awaioi.com/awaioi/MiragenFlow/src/branch/main/SECURITY.md). Do not post exploit details, private API keys, sensitive screenshots, or real user data in a public issue.
Use GitHub private vulnerability reporting or a Security Advisory. If unavailable, email [1844025705@qq.com](mailto:1844025705@qq.com) with `[MiragenFlow security]` in the subject.
+1 -1
View File
@@ -5,7 +5,7 @@ description: 安全漏洞提交和负责任披露说明
# 漏洞提交
安全策略英文原文见 [SECURITY.md](https://github.com/basketikun/infinite-canvas/blob/main/SECURITY.md)。
安全策略英文原文见 [SECURITY.md](https://git.awaioi.com/awaioi/MiragenFlow/src/branch/main/SECURITY.md)。
请不要在公开 Issue 中直接发布漏洞细节、可利用代码、私密 API Key、截图中的敏感信息或真实用户数据。
@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
@@ -0,0 +1,8 @@
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"plugins": ["react", "typescript", "oxc"],
"rules": {
"react/rules-of-hooks": "error",
"react/only-export-components": ["warn", { "allowConstantExport": true }]
}
}
@@ -0,0 +1,32 @@
# React + TypeScript + Vite
This template provides a minimal setup to get React working in Vite with HMR and some Oxlint rules.
Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs)
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/)
## React Compiler
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
## Expanding the Oxlint configuration
If you are developing a production application, we recommend enabling type-aware lint rules by installing `oxlint-tsgolint` and editing `.oxlintrc.json`:
```json
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"plugins": ["react", "typescript", "oxc"],
"options": {
"typeAware": true
},
"rules": {
"react/rules-of-hooks": "error",
"react/only-export-components": ["warn", { "allowConstantExport": true }]
}
}
```
See the [Oxlint rules documentation](https://oxc.rs/docs/guide/usage/linter/rules) for the full list of rules and categories.
@@ -0,0 +1,125 @@
# Miragen Flow Frontend Component Plan
Status: `ready` implemented in the first foundation pass, `planned` API and ownership reserved.
## Foundations
- `ready` Design tokens: color, type, spacing, radius, shadow, motion, z-index
- `ready` Global reset, focus-visible, selection, scrollbar, responsive shell
- `planned` Theme provider and light theme
- `planned` Locale provider
- `planned` Permission boundary
- `planned` Feature flag boundary
## Primitives
- `ready` Button: primary, secondary, ghost, danger, loading, disabled, sizes
- `ready` IconButton: label, tooltip, selected, disabled, sizes
- `ready` TextInput, SearchInput, Field, Select, Switch
- `ready` Tabs, FilterChip, Badge
- `ready` Avatar
- `ready` Spinner, Skeleton, InlineAlert, EmptyState, Dialog
- `planned` Textarea, NumberInput, PasswordInput
- `planned` Checkbox, RadioGroup, SegmentedControl
- `planned` Slider, RangeSlider, Stepper
- `planned` MultiSelect, Combobox, TagInput
- `planned` Tooltip, Popover, DropdownMenu, ContextMenu
- `planned` Divider, Breadcrumbs, Pagination
- `planned` Progress, CircularProgress, Meter
- `planned` Drawer, ConfirmDialog, CommandPalette
- `planned` ToastViewport, UploadProgress, LoadingOverlay
- `planned` Table, DataGrid, VirtualList
## Layout And Navigation
- `ready` AppShell, AppHeader, AppSidebar, SidebarNavItem
- `ready` Responsive mobile navigation
- `planned` PageHeader, PageToolbar, StickyBar
- `planned` SplitPane, ResizablePanel, ScrollContainer
- `planned` InspectorLayout, EditorLayout, FullscreenLayout
- `planned` UserMenu, NotificationCenter, CreditPopover
## Discovery And Home
- `ready` HeroCarousel, BannerCard, ToolEntryCard
- `ready` CategoryTabs, FilterBar, SortControl
- `ready` ArtworkCard, ArtworkGrid, ArtworkMeta, ArtworkActions
- `planned` VideoPreview, MediaProtectionOverlay
- `planned` InfiniteScrollTrigger, LoginPrompt
- `planned` ArtworkDetailDrawer, ShareDialog, ReportDialog
## Models
- `planned` ModelCard, ModelCover, ModelStats, ModelTypeBadge
- `planned` ModelFilterPanel, ModelSearchToolbar
- `planned` ModelDetailHeader, ModelGallery, ModelVersionSelect
- `planned` ModelExampleGrid, ModelLicensePanel, ModelUseButton
- `planned` ModelComparison, ModelFavoriteButton
## AI Creation
- `planned` PromptEditor, NegativePromptInput, PromptPresetMenu
- `planned` PromptEnhanceButton, PromptHistory
- `planned` GenerationModeTabs, ModelPicker, LoRAPicker
- `planned` ImageUploader, ReferenceImageList, ReferenceWeightControl
- `planned` AspectRatioPicker, ResolutionPicker, BatchCountStepper
- `planned` SeedInput, AdvancedSettings, CreditCost
- `planned` GenerateButton, GenerationQueue, GenerationResultCard
- `planned` GenerationComparison, RetryAction, DownloadAction
## Canvas Editor
- `planned` CanvasStage, CanvasViewport, CanvasToolbar
- `planned` ZoomControls, HistoryControls, SelectionBox, TransformHandles
- `planned` LayerPanel, LayerItem, LayerGroup, LayerContextMenu
- `planned` PropertyPanel, AlignmentTools, DistributionTools
- `planned` TextControls, ColorPicker, GradientEditor
- `planned` ImageControls, MaskEditor, BrushControls, CropTool
- `planned` AssetDropzone, Minimap, GuideRuler
- `planned` ExportDialog, AutosaveIndicator, VersionHistory
## Rendering And 3D
- `planned` SceneViewport, SceneToolbar, CameraControls
- `planned` LightingPanel, MaterialPanel, EnvironmentPicker
- `planned` ObjectTree, ObjectInspector, TransformPanel
- `planned` RenderSettings, RenderQueue, RenderResultCard
- `planned` BeforeAfterViewer, AnglePresetPicker
- `planned` ModelUpload, TextureUpload, SceneLoadingState
## Upscale
- `planned` UpscaleUploader, ScalePicker, EnhancementSettings
- `planned` FaceEnhancementControl, DenoiseControl
- `planned` ComparisonSlider, UpscaleTaskCard, DownloadResult
## Assets
- `planned` AssetGrid, AssetCard, AssetList
- `planned` AssetFolderTree, AssetTypeFilter, AssetSearch
- `planned` AssetUploader, UploadQueue, AssetDetailDrawer
- `planned` AssetRenameDialog, MoveAssetDialog, DeleteAssetDialog
- `planned` BatchActionBar, StorageUsage
## Account And Commerce
- `planned` LoginDialog, AuthCodeInput, SocialLoginButtons
- `planned` ProfileHeader, ProfileTabs, ProfileEditor
- `planned` CreditBalance, CreditHistory, CreditPackageCard
- `planned` MembershipCard, BillingHistory, InvoiceForm
- `planned` NotificationItem, NotificationPreferences
- `planned` SettingsSection, ThemeSwitch, SecuritySettings
## Required States
Every interactive component must cover `default`, `hover`, `active`, `focus-visible`, `selected`, `disabled`, and `loading` where applicable. Data surfaces must additionally cover `loading`, `empty`, `error`, `success`, partial data, and retry behavior.
## Delivery Order
1. Foundations, primitives, feedback, shell, discovery.
2. Models and AI creation flow.
3. Task queue, results, assets, profile.
4. Canvas editor primitives and editing workflows.
5. Rendering, 3D, and upscale workflows.
6. Account, credits, membership, notifications, settings.
7. Accessibility audit, visual regression, performance budgets.
@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>miragenflow-front</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,35 @@
{
"name": "miragenflow-front",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "oxlint",
"test": "vitest run",
"preview": "vite preview"
},
"dependencies": {
"@tanstack/react-query": "^5.101.4",
"clsx": "^2.1.1",
"lucide-react": "^1.32.0",
"react": "^19.2.8",
"react-dom": "^19.2.8",
"react-router-dom": "^7.18.2",
"zustand": "^5.0.15"
},
"devDependencies": {
"@testing-library/jest-dom": "^7.0.1",
"@testing-library/react": "^16.3.2",
"@types/node": "^24.13.3",
"@types/react": "^19.2.17",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.4",
"jsdom": "^30.0.1",
"oxlint": "^1.75.0",
"typescript": "~6.0.2",
"vite": "^8.2.0",
"vitest": "^4.1.11"
}
}
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.3 KiB

@@ -0,0 +1,24 @@
<svg xmlns="http://www.w3.org/2000/svg">
<symbol id="bluesky-icon" viewBox="0 0 16 17">
<g clip-path="url(#bluesky-clip)"><path fill="#08060d" d="M7.75 7.735c-.693-1.348-2.58-3.86-4.334-5.097-1.68-1.187-2.32-.981-2.74-.79C.188 2.065.1 2.812.1 3.251s.241 3.602.398 4.13c.52 1.744 2.367 2.333 4.07 2.145-2.495.37-4.71 1.278-1.805 4.512 3.196 3.309 4.38-.71 4.987-2.746.608 2.036 1.307 5.91 4.93 2.746 2.72-2.746.747-4.143-1.747-4.512 1.702.189 3.55-.4 4.07-2.145.156-.528.397-3.691.397-4.13s-.088-1.186-.575-1.406c-.42-.19-1.06-.395-2.741.79-1.755 1.24-3.64 3.752-4.334 5.099"/></g>
<defs><clipPath id="bluesky-clip"><path fill="#fff" d="M.1.85h15.3v15.3H.1z"/></clipPath></defs>
</symbol>
<symbol id="discord-icon" viewBox="0 0 20 19">
<path fill="#08060d" d="M16.224 3.768a14.5 14.5 0 0 0-3.67-1.153c-.158.286-.343.67-.47.976a13.5 13.5 0 0 0-4.067 0c-.128-.306-.317-.69-.476-.976A14.4 14.4 0 0 0 3.868 3.77C1.546 7.28.916 10.703 1.231 14.077a14.7 14.7 0 0 0 4.5 2.306q.545-.748.965-1.587a9.5 9.5 0 0 1-1.518-.74q.191-.14.372-.293c2.927 1.369 6.107 1.369 8.999 0q.183.152.372.294-.723.437-1.52.74.418.838.963 1.588a14.6 14.6 0 0 0 4.504-2.308c.37-3.911-.63-7.302-2.644-10.309m-9.13 8.234c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.894 0 1.614.82 1.599 1.82.001 1-.705 1.82-1.6 1.82m5.91 0c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.893 0 1.614.82 1.599 1.82 0 1-.706 1.82-1.6 1.82"/>
</symbol>
<symbol id="documentation-icon" viewBox="0 0 21 20">
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="m15.5 13.333 1.533 1.322c.645.555.967.833.967 1.178s-.322.623-.967 1.179L15.5 18.333m-3.333-5-1.534 1.322c-.644.555-.966.833-.966 1.178s.322.623.966 1.179l1.534 1.321"/>
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M17.167 10.836v-4.32c0-1.41 0-2.117-.224-2.68-.359-.906-1.118-1.621-2.08-1.96-.599-.21-1.349-.21-2.848-.21-2.623 0-3.935 0-4.983.369-1.684.591-3.013 1.842-3.641 3.428C3 6.449 3 7.684 3 10.154v2.122c0 2.558 0 3.838.706 4.726q.306.383.713.671c.76.536 1.79.64 3.581.66"/>
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M3 10a2.78 2.78 0 0 1 2.778-2.778c.555 0 1.209.097 1.748-.047.48-.129.854-.503.982-.982.145-.54.048-1.194.048-1.749a2.78 2.78 0 0 1 2.777-2.777"/>
</symbol>
<symbol id="github-icon" viewBox="0 0 19 19">
<path fill="#08060d" fill-rule="evenodd" d="M9.356 1.85C5.05 1.85 1.57 5.356 1.57 9.694a7.84 7.84 0 0 0 5.324 7.44c.387.079.528-.168.528-.376 0-.182-.013-.805-.013-1.454-2.165.467-2.616-.935-2.616-.935-.349-.91-.864-1.143-.864-1.143-.71-.48.051-.48.051-.48.787.051 1.2.805 1.2.805.695 1.194 1.817.857 2.268.649.064-.507.27-.857.49-1.052-1.728-.182-3.545-.857-3.545-3.87 0-.857.31-1.558.8-2.104-.078-.195-.349-1 .077-2.078 0 0 .657-.208 2.14.805a7.5 7.5 0 0 1 1.946-.26c.657 0 1.328.092 1.946.26 1.483-1.013 2.14-.805 2.14-.805.426 1.078.155 1.883.078 2.078.502.546.799 1.247.799 2.104 0 3.013-1.818 3.675-3.558 3.87.284.247.528.714.528 1.454 0 1.052-.012 1.896-.012 2.156 0 .208.142.455.528.377a7.84 7.84 0 0 0 5.324-7.441c.013-4.338-3.48-7.844-7.773-7.844" clip-rule="evenodd"/>
</symbol>
<symbol id="social-icon" viewBox="0 0 20 20">
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M12.5 6.667a4.167 4.167 0 1 0-8.334 0 4.167 4.167 0 0 0 8.334 0"/>
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M2.5 16.667a5.833 5.833 0 0 1 8.75-5.053m3.837.474.513 1.035c.07.144.257.282.414.309l.93.155c.596.1.736.536.307.965l-.723.73a.64.64 0 0 0-.152.531l.207.903c.164.715-.213.991-.84.618l-.872-.52a.63.63 0 0 0-.577 0l-.872.52c-.624.373-1.003.094-.84-.618l.207-.903a.64.64 0 0 0-.152-.532l-.723-.729c-.426-.43-.289-.864.306-.964l.93-.156a.64.64 0 0 0 .412-.31l.513-1.034c.28-.562.735-.562 1.012 0"/>
</symbol>
<symbol id="x-icon" viewBox="0 0 19 19">
<path fill="#08060d" fill-rule="evenodd" d="M1.893 1.98c.052.072 1.245 1.769 2.653 3.77l2.892 4.114c.183.261.333.48.333.486s-.068.089-.152.183l-.522.593-.765.867-3.597 4.087c-.375.426-.734.834-.798.905a1 1 0 0 0-.118.148c0 .01.236.017.664.017h.663l.729-.83c.4-.457.796-.906.879-.999a692 692 0 0 0 1.794-2.038c.034-.037.301-.34.594-.675l.551-.624.345-.392a7 7 0 0 1 .34-.374c.006 0 .93 1.306 2.052 2.903l2.084 2.965.045.063h2.275c1.87 0 2.273-.003 2.266-.021-.008-.02-1.098-1.572-3.894-5.547-2.013-2.862-2.28-3.246-2.273-3.266.008-.019.282-.332 2.085-2.38l2-2.274 1.567-1.782c.022-.028-.016-.03-.65-.03h-.674l-.3.342a871 871 0 0 1-1.782 2.025c-.067.075-.405.458-.75.852a100 100 0 0 1-.803.91c-.148.172-.299.344-.99 1.127-.304.343-.32.358-.345.327-.015-.019-.904-1.282-1.976-2.808L6.365 1.85H1.8zm1.782.91 8.078 11.294c.772 1.08 1.413 1.973 1.425 1.984.016.017.241.02 1.05.017l1.03-.004-2.694-3.766L7.796 5.75 5.722 2.852l-1.039-.004-1.039-.004z" clip-rule="evenodd"/>
</symbol>
</svg>

After

Width:  |  Height:  |  Size: 4.9 KiB

@@ -0,0 +1,36 @@
import { useEffect, useState } from 'react'
import { AppShell, type AppRoute } from './components/layout/AppShell'
import { ComponentsPage } from './pages/ComponentsPage'
import { HomePage } from './pages/HomePage'
import { PlaceholderPage } from './pages/PlaceholderPage'
const routeTitles: Record<Exclude<AppRoute, 'home' | 'components'>, { title: string; description: string }> = {
models: { title: '模型广场', description: '模型筛选、模型卡片、详情画廊、版本选择和许可信息将在此组合。' },
create: { title: 'AI 创作', description: '提示词编辑器、模型选择、比例控制、生成参数和任务队列将在此组合。' },
canvas: { title: '自由画布', description: '画布舞台、图层、属性面板、工具栏和导出流程将在此组合。' },
assets: { title: '我的资产', description: '文件夹树、素材网格、上传队列、详情抽屉和批量操作将在此组合。' },
}
function readRoute(): AppRoute {
const value = window.location.hash.replace('#/', '') as AppRoute
return ['home', 'components', 'models', 'create', 'canvas', 'assets'].includes(value) ? value : 'home'
}
function App() {
const [route, setRoute] = useState<AppRoute>(readRoute)
useEffect(() => {
const onHashChange = () => setRoute(readRoute())
window.addEventListener('hashchange', onHashChange)
return () => window.removeEventListener('hashchange', onHashChange)
}, [])
const navigate = (next: AppRoute) => { window.location.hash = `/${next}`; setRoute(next) }
let page
if (route === 'home') page = <HomePage />
else if (route === 'components') page = <ComponentsPage />
else page = <PlaceholderPage {...routeTitles[route]} onGoComponents={() => navigate('components')} />
return <AppShell route={route} onRouteChange={navigate}>{page}</AppShell>
}
export default App
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

@@ -0,0 +1,7 @@
.card { position: relative; overflow: hidden; border: 1px solid var(--color-border-strong); border-radius: var(--radius-lg); background: linear-gradient(154deg,#333,#181818); break-inside: avoid; }
.media { position: relative; min-height: 110px; overflow: hidden; background: #242427; }.media img { display: block; width: 100%; height: auto; min-height: 110px; object-fit: cover; transition: transform .38s var(--ease-standard), filter .3s ease; }.card:hover .media img { transform: scale(1.025); filter: brightness(.7); }
.overlay { position: absolute; inset: 0; background: linear-gradient(180deg,rgba(0,0,0,.08),rgba(0,0,0,.82)); opacity: .72; }
.top { position: absolute; z-index: 2; top: 10px; left: 10px; right: 10px; display: flex; align-items: center; justify-content: space-between; }.top > .badge { opacity: 0; transform: translateY(-4px); transition: all var(--duration-normal); }.card:hover .top > .badge { opacity: 1; transform: none; }
.like { display: grid; place-items: center; width: 30px; height: 30px; border: 0; border-radius: 50%; background: rgba(16,16,20,.65); color: white; cursor: pointer; opacity: 0; transform: translateY(-4px); transition: all var(--duration-normal); }.card:hover .like { opacity: 1; transform: none; }.like svg { width: 15px; height: 15px; }
.play { position: absolute; z-index: 3; top: 10px; right: 10px; display: grid; place-items: center; width: 20px; height: 20px; border-radius: 50%; background: rgba(0,0,0,.56); color: white; transition: opacity var(--duration-normal); }.play svg { width: 10px; height: 10px; }.card:hover .play { opacity: 0; }
.bottom { position: absolute; z-index: 2; left: 10px; right: 10px; bottom: 10px; height: 32px; }.meta { position: absolute; inset: 0; display: flex; align-items: center; gap: 7px; color: #fff; font-size: 12px; transition: opacity var(--duration-fast); }.stats { display: flex; gap: 10px; margin-left: auto; color: #eee; }.stats span { display: flex; align-items: center; gap: 3px; }.stats svg { width: 13px; height: 13px; }.bottom > button { position: absolute; inset: 0; width: 100%; opacity: 0; box-shadow: none; transform: translateY(32px); transition: opacity var(--duration-normal), transform var(--duration-slow) var(--ease-spring); }.card:hover .meta { opacity: 0; }.card:hover .bottom > button { opacity: 1; transform: none; }
@@ -0,0 +1,16 @@
import { Copy, Heart, Play, Sparkles } from 'lucide-react'
import { useState } from 'react'
import { Button } from '../primitives/Button'
import { Badge } from '../primitives/Selection'
import { Avatar } from '../layout/Avatar'
import type { Artwork } from '../../data/artworks'
import styles from './ArtworkCard.module.css'
export function ArtworkCard({ artwork, onUse }: { artwork: Artwork; onUse?: (artwork: Artwork) => void }) {
const [liked, setLiked] = useState(false)
return <article className={styles.card}>
<div className={styles.media}><img src={artwork.image} alt={artwork.title} loading="lazy" />{artwork.video && <span className={styles.play}><Play fill="currentColor" /></span>}<div className={styles.overlay} /></div>
<div className={styles.top}><Badge tone="info">{artwork.category}</Badge><button type="button" className={styles.like} aria-label={liked ? '取消收藏' : '收藏'} onClick={() => setLiked(value => !value)}><Heart fill={liked ? 'currentColor' : 'none'} /></button></div>
<div className={styles.bottom}><div className={styles.meta}><Avatar size="xs" fallback={artwork.author.slice(0, 1)} alt={`${artwork.author}头像`} /><span>{artwork.author}</span><span className={styles.stats}><span><Heart />{artwork.likes}</span><span><Copy />{artwork.uses}</span></span></div><Button size="sm" variant="primary" leadingIcon={<Sparkles />} onClick={() => onUse?.(artwork)}>一键同款</Button></div>
</article>
}
@@ -0,0 +1,2 @@
.grid { display: grid; grid-template-columns: repeat(var(--column-count),minmax(0,1fr)); gap: 10px; align-items: start; }.column { display: flex; min-width: 0; flex-direction: column; gap: 10px; }
@media (max-width: 1439px) { .grid { gap: 8px; }.column { gap: 8px; } }
@@ -0,0 +1,16 @@
import { useEffect, useMemo, useState, type CSSProperties } from 'react'
import type { Artwork } from '../../data/artworks'
import { ArtworkCard } from './ArtworkCard'
import styles from './ArtworkGrid.module.css'
export function ArtworkGrid({ artworks, onUse }: { artworks: Artwork[]; onUse?: (artwork: Artwork) => void }) {
const getColumnCount = () => window.innerWidth >= 1440 ? 5 : window.innerWidth > 720 ? 4 : 2
const [columnCount, setColumnCount] = useState(getColumnCount)
useEffect(() => {
const onResize = () => setColumnCount(getColumnCount())
window.addEventListener('resize', onResize)
return () => window.removeEventListener('resize', onResize)
}, [])
const columns = useMemo(() => Array.from({ length: columnCount }, (_, index) => artworks.filter((_, itemIndex) => itemIndex % columnCount === index)), [artworks, columnCount])
return <div className={styles.grid} style={{ '--column-count': columnCount } as CSSProperties}>{columns.map((column, index) => <div className={styles.column} key={index}>{column.map(artwork => <ArtworkCard key={artwork.id} artwork={artwork} onUse={onUse} />)}</div>)}</div>
}
@@ -0,0 +1,4 @@
.layout { display: grid; grid-template-columns: minmax(380px,2fr) 260px minmax(260px,1.2fr); gap: 12px; height: 160px; }.hero,.side,.tools { min-width: 0; }.hero { position: relative; overflow: hidden; border-radius: var(--radius-lg); background: #2a2a30; }.track { display: flex; height: 100%; transition: transform .55s var(--ease-standard); }.track img { width: 100%; height: 100%; flex: 0 0 100%; object-fit: cover; }.arrows { position: absolute; z-index: 2; top: 64px; left: 12px; right: 12px; display: flex; justify-content: space-between; opacity: 0; transition: opacity var(--duration-normal); }.hero:hover .arrows { opacity: 1; }.dots { position: absolute; left: 0; right: 0; bottom: 12px; z-index: 2; display: flex; justify-content: center; gap: 5px; }.dots button { width: 6px; height: 6px; padding: 0; border: 0; border-radius: 999px; background: rgba(255,255,255,.48); cursor: pointer; transition: width var(--duration-normal); }.dots .activeDot { width: 20px; background: #fff; }
.side,.tools { display: flex; flex-direction: column; gap: 8px; }.side img { width: 100%; height: 76px; border: 1px solid var(--color-border); border-radius: var(--radius-lg); object-fit: cover; }.tool { display: flex; align-items: center; gap: 12px; height: 76px; padding: 8px 13px; border: 1px solid rgba(67,142,255,.45); border-radius: var(--radius-lg); background: linear-gradient(205deg,rgba(68,142,255,.20),rgba(112,206,255,.04) 66%,rgba(178,233,255,.18)); cursor: pointer; text-align: left; transition: transform var(--duration-normal), border-color var(--duration-normal); }.tool:hover { border-color: rgba(97,171,255,.75); transform: translateY(-1px); }.tool img { width: 44px; height: 44px; object-fit: contain; }.tool span { display: flex; min-width: 0; flex-direction: column; gap: 4px; }.tool strong { color: #fff; font-size: 14px; white-space: nowrap; }.tool small { overflow: hidden; color: #a9a8b3; font-size: 12px; text-overflow: ellipsis; white-space: nowrap; }
@media (max-width: 1040px) { .layout { grid-template-columns: minmax(360px,2fr) 220px; }.tools { display: none; } }
@media (max-width: 720px) { .layout { height: auto; grid-template-columns: 1fr; }.hero { height: 180px; }.side { display: grid; grid-template-columns: 1fr 1fr; }.tools { display: grid; grid-template-columns: 1fr 1fr; }.tool { min-width: 0; } }
@@ -0,0 +1,30 @@
import { ChevronLeft, ChevronRight } from 'lucide-react'
import { useEffect, useState } from 'react'
import { IconButton } from '../primitives/IconButton'
import styles from './HeroCarousel.module.css'
import heroOne from '../../assets/holopix/banner-main.webp'
import heroTwo from '../../assets/holopix/banner-main-2.webp'
import heroThree from '../../assets/holopix/banner-main-3.webp'
import sideOne from '../../assets/holopix/banner-side-1.webp'
import sideTwo from '../../assets/holopix/banner-side-2.webp'
import board from '../../assets/holopix/board.webp'
import explore from '../../assets/holopix/explore.webp'
export function HeroCarousel() {
const [index, setIndex] = useState(0)
const slides = [heroOne, heroTwo, heroThree]
const change = (next: number) => setIndex((next + slides.length) % slides.length)
useEffect(() => {
const timer = window.setInterval(() => setIndex(current => (current + 1) % slides.length), 6000)
return () => window.clearInterval(timer)
}, [slides.length])
return <section className={styles.layout}>
<div className={styles.hero}><div className={styles.track} style={{ transform: `translateX(-${index * 100}%)` }}>{slides.map((src, itemIndex) => <img key={src} src={src} alt={`首页宣传 ${itemIndex + 1}`} />)}</div><div className={styles.arrows}><IconButton label="上一张" icon={<ChevronLeft />} size="sm" onClick={() => change(index - 1)} /><IconButton label="下一张" icon={<ChevronRight />} size="sm" onClick={() => change(index + 1)} /></div><div className={styles.dots}>{slides.map((src, itemIndex) => <button type="button" key={src} className={itemIndex === index ? styles.activeDot : ''} aria-label={`第 ${itemIndex + 1} 张`} onClick={() => change(itemIndex)} />)}</div></div>
<div className={styles.side}><img src={sideOne} alt="模型活动" /><img src={sideTwo} alt="3D 工具" /></div>
<div className={styles.tools}><ToolEntry image={board} title="自由画布" description="自由创作,角色、UI、场景全覆盖" /><ToolEntry image={explore} title="一键成稿" description="文生图、图生图工作流" /></div>
</section>
}
function ToolEntry({ image, title, description }: { image: string; title: string; description: string }) {
return <button type="button" className={styles.tool}><img src={image} alt="" /><span><strong>{title}</strong><small>{description}</small></span></button>
}
@@ -0,0 +1,66 @@
.shell { height: 100vh; overflow: hidden; background: var(--color-bg); }
.header { position: fixed; inset: 0 0 auto; z-index: var(--z-header); height: var(--header-height); display: flex; align-items: center; gap: 16px; padding: 0 32px 0 20px; border-bottom: 1px solid var(--color-border); background: rgba(24,24,27,.94); backdrop-filter: blur(18px); }
.brand { width: 172px; flex: 0 0 auto; display: flex; align-items: center; gap: 9px; color: #fff; font-size: 17px; letter-spacing: -.3px; }
.brand em { background: var(--gradient-brand); background-clip: text; color: transparent; font-style: normal; }
.brandMark { position: relative; width: 20px; height: 20px; }
.brandMark i { position: absolute; top: 2px; width: 8px; height: 16px; border-radius: var(--radius-sm); background: var(--gradient-brand-strong); }
.brandMark i:first-child { left: 0; clip-path: polygon(0 20%,100% 0,100% 80%,0 100%); }
.brandMark i:last-child { right: 0; background: linear-gradient(180deg,#75d1ff,#2878ff); clip-path: polygon(0 0,100% 20%,100% 100%,0 80%); }
.headerSearch { width: min(460px, 42vw); display: flex; align-items: center; gap: 8px; }
.headerSearch > span { flex: 1; min-width: 0; }
.headerSearch button { flex: 0 0 auto; }
.headerSearch input { height: 36px; }
.headerActions { margin-left: auto; display: flex; align-items: center; gap: 12px; }
.headerAction { position: relative; display: inline-flex; align-items: center; }
.headerAction::after { content: ""; position: absolute; top: 100%; right: 0; width: 100%; height: 12px; }
.noticeButton { width: 36px; height: 36px; border-radius: 40px; border: 1px solid rgba(235, 235, 245, .08); background: rgba(235, 235, 245, .08); color: #fff; }
.noticeButton:hover { border-color: rgba(235, 235, 245, .08); background: rgba(235, 235, 245, .08); color: #fff; }
.loginButton { width: 168px; height: 36px; display: flex; align-items: center; justify-content: center; padding: 0; border: 0; background-size: contain; background-repeat: no-repeat; background-color: transparent; cursor: pointer; }
.loginButton img { display: block; width: 136px; height: 20px; }
.headerDivider { width: 1px; height: 12px; border-radius: 2px; background: rgba(235, 235, 245, .15); }
.chevron { width: 15px; color: var(--color-text-subtle); }
.avatarButton { width: 70px; height: 36px; display: flex; align-items: center; padding: 0; border: 0; background: transparent; cursor: pointer; }
.headerAvatar { width: 36px; height: 36px; margin: 0 6px; overflow: hidden; border: 1px solid #fff; border-radius: 32px; object-fit: cover; }
.avatarButton .chevron { width: 16px; height: 16px; margin-left: 6px; color: #fff; }
.headerPopover { position: absolute; z-index: var(--z-popover); top: calc(100% + 4px); right: 0; visibility: hidden; opacity: 0; pointer-events: none; transform: scale(.96); transform-origin: top right; transition: opacity .18s ease, transform .18s ease, visibility 0s linear .18s; }
.headerAction:hover > .headerPopover,.headerAction:focus-within > .headerPopover { visibility: visible; opacity: 1; pointer-events: auto; transform: translateY(0) scale(1); transition-delay: 0s; }
.noticePopover { width: 418px; height: 700px; overflow: hidden; border: 1px solid rgba(235, 235, 245, .08); border-radius: 24px; background: #2c2c2e; box-shadow: 0 0 16px 8px rgba(0, 0, 0, .32); }
.noticeHeader { box-sizing: content-box; height: 38px; display: flex; align-items: center; justify-content: space-between; padding: 12px 12px 12px 24px; border-bottom: 1px solid rgba(235, 235, 245, .08); color: #fff; font-size: 16px; font-weight: 500; }
.noticeHeader strong { font: inherit; }
.noticeHeader button { height: 38px; padding: 10px 24px; border: 1px solid rgba(235, 235, 245, .08); border-radius: 12px; background: rgba(235, 235, 245, .15); color: #fff; font-size: 13px; font-weight: 400; }
.noticeHeader button:disabled { border-color: transparent; background: transparent; color: rgba(235, 235, 245, .6); }
.noticeContent { height: calc(100% - 63px); display: flex; align-items: center; justify-content: center; padding: 8px 0 16px; color: rgba(235, 235, 245, .6); font-size: 14px; }
.rewardPopover { top: calc(100% + 12px); width: min(450px, 92vw); aspect-ratio: 450 / 507; }
.loginRewardPanel { display: block; width: 100%; height: 100%; object-fit: contain; }
.loginRewardHitArea { position: absolute; left: 5.333%; bottom: 5.917%; width: 89.333%; height: 12.229%; min-height: 40px; border: 0; border-radius: 12px; background: transparent; cursor: pointer; opacity: 0; }
.profilePopover { right: -32px; width: 286px; margin-top: 10px; padding: 8px; overflow: hidden; border: 1px solid rgba(235, 235, 245, .08); border-radius: 24px; background: #1c1c1e; box-shadow: 0 0 16px 8px rgba(0, 0, 0, .32); }
.visitorBody { display: flex; flex-direction: column; gap: 8px; background: url("../../assets/holopix/visitor-card-bg.png") center / cover no-repeat; }
.visitorInfo { height: 59px; display: flex; align-items: center; column-gap: 12px; padding: 8px 16px 8px 0; border-radius: 12px; }
.visitorInfo img { width: 40px; height: 40px; flex: 0 0 auto; border: 1px solid #fff; border-radius: 50%; object-fit: cover; }
.visitorInfo span { min-width: 0; overflow: hidden; color: #fff; font-size: 14px; line-height: 20px; text-overflow: ellipsis; white-space: nowrap; }
.visitorPanel { display: block; width: 268px; height: auto; cursor: pointer; }
.mobileMenu { display: none; }
.sidebar { position: fixed; z-index: 19; inset: var(--header-height) auto 0 0; width: var(--sidebar-width); display: flex; flex-direction: column; padding: 18px 10px 12px; border-right: 1px solid var(--color-border); background: var(--color-bg); }
.nav { display: flex; flex-direction: column; gap: 3px; }
.navLabel { margin: 4px 12px 7px; color: #5f5e67; font-size: 11px; letter-spacing: .08em; text-transform: uppercase; }
.navLabel:not(:first-child) { margin-top: 22px; }
.navItem { position: relative; display: flex; align-items: center; gap: 11px; width: 100%; height: 40px; padding: 0 12px; border: 0; border-radius: var(--radius-lg); background: transparent; color: var(--color-text-muted); cursor: pointer; text-align: left; transition: all var(--duration-normal); }
.navItem svg { width: 17px; height: 17px; color: #8c8995; }
.navItem:hover { background: var(--color-surface-raised); color: #fff; }
.navItem.active { background: var(--color-primary-soft); color: #fff; font-weight: 600; }
.navItem.active svg { color: #75d1ff; }
.navItem small { position: absolute; right: 7px; padding: 2px 5px; border: 1px solid #477bc5; border-radius: var(--radius-md); color: #8fc9ff; font-size: 9px; }
.sidebarBottom { margin-top: auto; }
.community { display: flex; align-items: center; gap: 8px; padding: 10px; border: 1px solid var(--color-border); border-radius: var(--radius-lg); background: var(--color-surface); color: #9e9ba7; }
.community > svg:first-child { width: 16px; color: #75d1ff; flex: 0 0 auto; }.community > svg:last-child { width: 13px; margin-left: auto; }
.community span { display: flex; flex-direction: column; min-width: 0; }.community strong { color: #e5e3e9; font-size: 11px; white-space: nowrap; }.community small { margin-top: 2px; font-size: 10px; white-space: nowrap; }
.sideLinks { display: flex; flex-direction: column; gap: 2px; margin-top: 14px; }.sideLinks button { display: flex; align-items: center; gap: 9px; padding: 5px 9px; border: 0; background: transparent; color: #777580; cursor: pointer; font-size: 11px; text-align: left; }.sideLinks button:hover { color: #fff; }.sideLinks svg { width: 14px; }
.legal { margin: 14px 2px 0; color: #4f4e57; font-size: 10px; line-height: 1.6; }
.main { height: 100vh; padding: calc(var(--header-height) + 18px) 20px 36px calc(var(--sidebar-width) + 20px); overflow-x: hidden; overflow-y: auto; overscroll-behavior: contain; }
@media (max-width: 900px) {
body { overflow: auto; }
.header { gap: 10px; padding: 0 12px; }.brand { width: auto; }.headerSearch { flex: 1; width: auto; }.headerSearch button { display: none; }.headerDivider { display: none; }.headerActions { gap: 6px; }.profilePopover { right: -12px; }.mobileMenu { display: inline-grid; }
.sidebar { transform: translateX(-100%); box-shadow: var(--shadow-popover); transition: transform var(--duration-slow) var(--ease-standard); }.sidebarOpen { transform: translateX(0); }
.main { padding-left: 16px; padding-right: 16px; }
}
@media (max-width: 560px) { .brand strong { display: none; }.noticeAction { display: none; }.main { padding-left: 12px; padding-right: 12px; }.rewardPopover,.profilePopover { position: fixed; top: var(--header-height); right: 8px; } }
@@ -0,0 +1,80 @@
import type { ReactNode } from 'react'
import { useState } from 'react'
import { Bell, ChevronDown, CircleUserRound, Folder, Grid2X2, Home, Layers3, LogOut, Menu, MessageCircle, Palette, Search, Settings2, Sparkles, WandSparkles } from 'lucide-react'
import { Button } from '../primitives/Button'
import { IconButton } from '../primitives/IconButton'
import { SearchInput } from '../primitives/FormControls'
import styles from './AppShell.module.css'
import avatarImage from '../../assets/holopix/avatar.png'
import loginRewardAnimation from '../../assets/holopix/login-reward.gif'
import loginRewardLabel from '../../assets/holopix/login-reward-label.svg'
import loginRewardPanel from '../../assets/holopix/login-reward-panel.webp'
import visitorPanel from '../../assets/holopix/visitor-panel.png'
export type AppRoute = 'home' | 'components' | 'models' | 'create' | 'canvas' | 'assets'
interface AppShellProps { route: AppRoute; onRouteChange: (route: AppRoute) => void; children: ReactNode }
const primaryNav: Array<{ id: AppRoute; label: string; icon: typeof Home; badge?: string }> = [
{ id: 'home', label: '首页', icon: Home },
{ id: 'models', label: '模型广场', icon: Grid2X2 },
{ id: 'create', label: 'AI 创作', icon: WandSparkles },
{ id: 'canvas', label: '自由画布', icon: Layers3, badge: '热门' },
{ id: 'assets', label: '我的资产', icon: Folder },
]
export function AppShell({ route, onRouteChange, children }: AppShellProps) {
const [sidebarOpen, setSidebarOpen] = useState(false)
const [search, setSearch] = useState('')
return <div className={styles.shell}>
<header className={styles.header}>
<div className={styles.brand}><span className={styles.brandMark}><i /><i /></span><strong>Miragen <em>Flow</em></strong></div>
<IconButton className={styles.mobileMenu} label="打开导航" icon={<Menu />} onClick={() => setSidebarOpen(value => !value)} />
<div className={styles.headerSearch}><SearchInput value={search} onChange={event => setSearch(event.target.value)} placeholder="搜索作品、模型或风格" /><Button size="sm" variant="primary" leadingIcon={<Search />}>找灵感</Button></div>
<div className={styles.headerActions}>
<div className={`${styles.headerAction} ${styles.noticeAction}`}>
<IconButton className={styles.noticeButton} label="通知" icon={<Bell />} aria-haspopup="dialog" />
<div className={`${styles.headerPopover} ${styles.noticePopover}`} role="dialog" aria-label="通知消息">
<div className={styles.noticeHeader}><strong>通知</strong><button type="button" disabled>全部已读</button></div>
<div className={styles.noticeContent}><span>暂无通知</span></div>
</div>
</div>
<div className={`${styles.headerAction} ${styles.loginAction}`}>
<button type="button" className={styles.loginButton} style={{ backgroundImage: `url(${loginRewardAnimation})` }} aria-label="登录,免费领算力" aria-haspopup="dialog"><img src={loginRewardLabel} alt="登录 免费领算力" /></button>
<div className={`${styles.headerPopover} ${styles.rewardPopover}`} role="dialog" aria-label="登录领取算力">
<img className={styles.loginRewardPanel} src={loginRewardPanel} alt="注册即送专属算力" />
<button type="button" className={styles.loginRewardHitArea} aria-label="立即登录领取" />
</div>
</div>
<span className={styles.headerDivider} />
<div className={`${styles.headerAction} ${styles.profileAction}`}>
<button type="button" className={styles.avatarButton} aria-label="用户菜单" aria-haspopup="dialog">
<img className={styles.headerAvatar} src={avatarImage} alt="用户头像" />
<ChevronDown className={styles.chevron} />
</button>
<div className={`${styles.headerPopover} ${styles.profilePopover}`} role="dialog" aria-label="用户菜单">
<div className={styles.visitorBody}>
<div className={styles.visitorInfo}><img src={avatarImage} alt="用户头像" /><span>游客 5ebe1365e9fdd2b7</span></div>
<img className={styles.visitorPanel} src={visitorPanel} alt="注册领取新手大礼包" />
</div>
</div>
</div>
</div>
</header>
<aside className={`${styles.sidebar} ${sidebarOpen ? styles.sidebarOpen : ''}`}>
<nav className={styles.nav} aria-label="主导航">
<p className={styles.navLabel}>工作台</p>
{primaryNav.map(item => { const Icon = item.icon; return <button key={item.id} type="button" className={`${styles.navItem} ${route === item.id ? styles.active : ''}`} onClick={() => { onRouteChange(item.id); setSidebarOpen(false) }}><Icon /><span>{item.label}</span>{item.badge && <small>{item.badge}</small>}</button> })}
<p className={styles.navLabel}>资源</p>
<button type="button" className={styles.navItem}><Palette /><span>模型定制</span></button>
<button type="button" className={styles.navItem}><Sparkles /><span>AI 学院</span></button>
</nav>
<div className={styles.sidebarBottom}><div className={styles.community}><MessageCircle /><span><strong>加入设计交流群</strong><small>与创作者一起交流</small></span><ChevronDown /></div><div className={styles.sideLinks}><button type="button" onClick={() => onRouteChange('components')}><Settings2 />组件中心</button><button type="button"><CircleUserRound />个人主页</button><button type="button"><LogOut />退出登录</button></div><p className={styles.legal}>©2026 Miragen Flow<br />用户协议 · 隐私协议</p></div>
</aside>
<main className={styles.main}>{children}</main>
</div>
}
@@ -0,0 +1,2 @@
.avatar { display: inline-grid; place-items: center; flex: 0 0 auto; overflow: hidden; border: 0; border-radius: 50%; outline: 0; box-shadow: none; object-fit: cover; background: linear-gradient(135deg,#2878ff,#75d1ff); color: white; font-size: 10px; font-weight: 700; }
.xs { width: 20px; height: 20px; }.sm { width: 28px; height: 28px; }.md { width: 34px; height: 34px; }.lg { width: 48px; height: 48px; font-size: 14px; }
@@ -0,0 +1,6 @@
import clsx from 'clsx'
import styles from './Avatar.module.css'
export function Avatar({ src, alt, size = 'sm', fallback = 'MF' }: { src?: string; alt: string; size?: 'xs' | 'sm' | 'md' | 'lg'; fallback?: string }) {
return src ? <img className={clsx(styles.avatar, styles[size])} src={src} alt={alt} /> : <span className={clsx(styles.avatar, styles[size], styles.fallback)} aria-label={alt}>{fallback}</span>
}
@@ -0,0 +1,37 @@
.button {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 7px;
border: 1px solid transparent;
border-radius: var(--radius-lg);
cursor: pointer;
font-weight: 600;
letter-spacing: 0;
white-space: nowrap;
transition: background var(--duration-normal), border-color var(--duration-normal), color var(--duration-normal), transform var(--duration-fast);
}
.button:active:not(:disabled) { transform: translateY(1px); }
.button:disabled { cursor: not-allowed; opacity: 0.45; }
.sm { height: 32px; padding: 0 12px; font-size: 12px; border-radius: var(--radius-lg); }
.md { height: 36px; padding: 0 16px; font-size: 13px; }
.lg { height: 40px; padding: 0 20px; font-size: 14px; }
.primary {
overflow: hidden;
border: 0;
background: linear-gradient(90deg, #3286ff, #63c8ff);
background-clip: padding-box;
color: #fff;
box-shadow: none;
}
.primary:hover:not(:disabled) { filter: brightness(1.08); }
.secondary { background: var(--color-surface-raised); border-color: var(--color-border); color: var(--color-text); }
.secondary:hover:not(:disabled) { background: var(--color-surface-hover); border-color: var(--color-border-strong); }
.ghost { background: transparent; color: var(--color-text-muted); }
.ghost:hover:not(:disabled) { background: var(--color-surface-raised); color: var(--color-text); }
.danger { background: rgba(255, 102, 122, 0.12); border-color: rgba(255, 102, 122, 0.28); color: #ff9caa; }
.danger:hover:not(:disabled) { background: rgba(255, 102, 122, 0.18); }
.fullWidth { width: 100%; }
.button svg { width: 16px; height: 16px; flex: 0 0 auto; }
.spinner { animation: spin 0.8s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }
@@ -0,0 +1,41 @@
import type { ButtonHTMLAttributes, ReactNode } from 'react'
import { LoaderCircle } from 'lucide-react'
import clsx from 'clsx'
import styles from './Button.module.css'
export type ButtonVariant = 'primary' | 'secondary' | 'ghost' | 'danger'
export type ButtonSize = 'sm' | 'md' | 'lg'
export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
variant?: ButtonVariant
size?: ButtonSize
leadingIcon?: ReactNode
trailingIcon?: ReactNode
loading?: boolean
fullWidth?: boolean
}
export function Button({
variant = 'secondary',
size = 'md',
leadingIcon,
trailingIcon,
loading = false,
fullWidth = false,
className,
children,
disabled,
...props
}: ButtonProps) {
return (
<button
className={clsx(styles.button, styles[variant], styles[size], fullWidth && styles.fullWidth, className)}
disabled={disabled || loading}
{...props}
>
{loading ? <LoaderCircle className={styles.spinner} aria-hidden="true" /> : leadingIcon}
<span>{children}</span>
{!loading && trailingIcon}
</button>
)
}
@@ -0,0 +1,21 @@
.spinner { width: 24px; height: 24px; display: inline-grid; place-items: center; }
.spinner > span { width: 20px; height: 20px; border: 2px solid rgba(117,209,255,.2); border-top-color: #75d1ff; border-radius: 50%; animation: spin .7s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }
.skeleton { display: block; width: 100%; border-radius: var(--radius-lg); background: linear-gradient(100deg,#242429 20%,#303038 36%,#242429 52%); background-size: 220% 100%; animation: shimmer 1.5s infinite; }
@keyframes shimmer { to { background-position-x: -220%; } }
.empty { min-height: 210px; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 8px; border: 1px dashed var(--color-border-strong); border-radius: var(--radius-xl); color: var(--color-text-subtle); text-align: center; }
.empty > svg { width: 28px; color: #5e5d67; }
.empty strong { color: var(--color-text); font-size: 14px; }
.empty span { font-size: 12px; }
.backdrop { position: fixed; inset: 0; z-index: var(--z-dialog); display: grid; place-items: center; padding: 24px; background: rgba(4,7,12,.7); backdrop-filter: blur(4px); }
.dialog { width: min(440px,100%); overflow: hidden; border: 1px solid var(--color-border-strong); border-radius: var(--radius-xl); background: #202025; box-shadow: var(--shadow-popover); animation: enter var(--duration-slow) var(--ease-standard); }
@keyframes enter { from { opacity: 0; transform: translateY(10px) scale(.98); } }
.dialog header { display: flex; align-items: flex-start; justify-content: space-between; padding: 18px 18px 14px; border-bottom: 1px solid var(--color-border); }
.dialog header > span { display: flex; flex-direction: column; gap: 3px; }
.dialog header strong { font-size: 16px; }
.dialog header small { color: var(--color-text-muted); }
.dialogBody { padding: 18px; color: var(--color-text-muted); }
.dialog footer { display: flex; justify-content: flex-end; gap: 8px; padding: 14px 18px; background: #1b1b1f; }
.alert { display: flex; align-items: center; gap: 9px; min-height: 40px; padding: 9px 12px; border: 1px solid rgba(40,120,255,.25); border-radius: var(--radius-lg); background: rgba(40,120,255,.1); color: #a9ddff; }
.alert[data-tone="danger"] { border-color: rgba(255,102,122,.25); background: rgba(255,102,122,.1); color: #ffadb8; }
.alert svg { width: 17px; flex: 0 0 auto; }
@@ -0,0 +1,27 @@
import type { ReactNode } from 'react'
import { AlertCircle, CheckCircle2, Inbox, X } from 'lucide-react'
import { Button } from './Button'
import { IconButton } from './IconButton'
import styles from './Feedback.module.css'
export function Spinner({ label = '加载中' }: { label?: string }) {
return <span className={styles.spinner} role="status"><span /> <span className="sr-only">{label}</span></span>
}
export function Skeleton({ height = 80 }: { height?: number }) {
return <span className={styles.skeleton} style={{ height }} aria-hidden="true" />
}
export function EmptyState({ title = '暂无内容', description = '新的内容会显示在这里', action }: { title?: string; description?: string; action?: ReactNode }) {
return <div className={styles.empty}><Inbox /><strong>{title}</strong><span>{description}</span>{action}</div>
}
interface DialogProps { open: boolean; title: string; description?: string; onClose: () => void; children?: ReactNode }
export function Dialog({ open, title, description, onClose, children }: DialogProps) {
if (!open) return null
return <div className={styles.backdrop} role="presentation" onMouseDown={onClose}><section className={styles.dialog} role="dialog" aria-modal="true" aria-labelledby="dialog-title" onMouseDown={event => event.stopPropagation()}><header><span><strong id="dialog-title">{title}</strong>{description && <small>{description}</small>}</span><IconButton label="关闭" icon={<X />} onClick={onClose} /></header><div className={styles.dialogBody}>{children}</div><footer><Button variant="ghost" onClick={onClose}>取消</Button><Button variant="primary" leadingIcon={<CheckCircle2 />}>确认</Button></footer></section></div>
}
export function InlineAlert({ tone = 'info', children }: { tone?: 'info' | 'danger'; children: ReactNode }) {
return <div className={styles.alert} data-tone={tone}>{tone === 'danger' ? <AlertCircle /> : <CheckCircle2 />}{children}</div>
}
@@ -0,0 +1,22 @@
.field { display: flex; flex-direction: column; gap: 7px; color: var(--color-text-muted); }
.label { color: var(--color-text); font-size: 13px; font-weight: 600; }
.hint { color: var(--color-text-subtle); font-size: 12px; }
.error { color: #ff8c9c; }
.inputWrap { height: 38px; display: flex; align-items: center; gap: 9px; padding: 0 12px; border: 1px solid var(--color-border); border-radius: var(--radius-lg); background: var(--color-surface-raised); transition: border-color var(--duration-normal), box-shadow var(--duration-normal); }
.inputWrap:focus-within { border-color: var(--color-primary-border); box-shadow: 0 0 0 3px rgba(40,120,255,.12); }
.inputWrap > svg { width: 17px; color: var(--color-text-subtle); }
.input { width: 100%; min-width: 0; padding: 0; border: 0; outline: 0; background: transparent; color: var(--color-text); }
.input::placeholder { color: var(--color-text-subtle); }
.invalid { border-color: rgba(255,102,122,.6); }
.select { height: 38px; padding: 0 34px 0 12px; border: 1px solid var(--color-border); border-radius: var(--radius-lg); background: var(--color-surface-raised); color: var(--color-text); outline: 0; }
.select:focus-visible { border-color: var(--color-primary-border); }
.switchLabel { display: inline-flex; align-items: center; gap: 10px; color: var(--color-text-muted); cursor: pointer; }
.switch { position: relative; width: 38px; height: 22px; }
.switch input { position: absolute; opacity: 0; pointer-events: none; }
.switchTrack { position: absolute; inset: 0; border-radius: 999px; background: #3c3c43; transition: background var(--duration-normal); }
.switchThumb { position: absolute; left: 3px; top: 3px; width: 16px; height: 16px; display: grid; place-items: center; border-radius: 50%; background: #fff; color: var(--color-primary); transition: transform var(--duration-normal) var(--ease-spring); }
.switchThumb svg { width: 10px; opacity: 0; }
.switch input:checked + .switchTrack { background: var(--color-primary); }
.switch input:checked + .switchTrack .switchThumb { transform: translateX(16px); }
.switch input:checked + .switchTrack .switchThumb svg { opacity: 1; }
@@ -0,0 +1,37 @@
import type { InputHTMLAttributes, ReactNode, SelectHTMLAttributes } from 'react'
import { Check, Search } from 'lucide-react'
import clsx from 'clsx'
import styles from './FormControls.module.css'
interface FieldProps {
label?: string
hint?: string
error?: string
children: ReactNode
}
export function Field({ label, hint, error, children }: FieldProps) {
return <label className={styles.field}>{label && <span className={styles.label}>{label}</span>}{children}{(hint || error) && <span className={clsx(styles.hint, error && styles.error)}>{error ?? hint}</span>}</label>
}
interface TextInputProps extends InputHTMLAttributes<HTMLInputElement> {
invalid?: boolean
icon?: ReactNode
}
export function TextInput({ invalid, icon, className, ...props }: TextInputProps) {
return <span className={clsx(styles.inputWrap, invalid && styles.invalid, className)}>{icon}<input className={styles.input} {...props} /></span>
}
export function SearchInput(props: InputHTMLAttributes<HTMLInputElement>) {
return <TextInput icon={<Search aria-hidden="true" />} {...props} />
}
export function Select({ className, children, ...props }: SelectHTMLAttributes<HTMLSelectElement>) {
return <select className={clsx(styles.select, className)} {...props}>{children}</select>
}
interface SwitchProps extends Omit<InputHTMLAttributes<HTMLInputElement>, 'type'> { label?: string }
export function Switch({ label, className, ...props }: SwitchProps) {
return <label className={clsx(styles.switchLabel, className)}><span className={styles.switch}><input type="checkbox" {...props} /><span className={styles.switchTrack}><span className={styles.switchThumb}><Check /></span></span></span>{label && <span>{label}</span>}</label>
}
@@ -0,0 +1,17 @@
.button {
display: inline-grid;
place-items: center;
padding: 0;
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
background: var(--color-surface-raised);
color: var(--color-text-muted);
cursor: pointer;
transition: all var(--duration-normal);
}
.button:hover { border-color: var(--color-border-strong); background: var(--color-surface-hover); color: #fff; }
.button:disabled { opacity: 0.4; cursor: not-allowed; }
.button svg { width: 17px; height: 17px; }
.sm { width: 32px; height: 32px; }
.md { width: 36px; height: 36px; }
.selected { background: var(--color-primary-soft); border-color: var(--color-primary-border); color: #86c7ff; }
@@ -0,0 +1,25 @@
import type { ButtonHTMLAttributes, ReactNode } from 'react'
import clsx from 'clsx'
import styles from './IconButton.module.css'
interface IconButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
label: string
icon: ReactNode
selected?: boolean
size?: 'sm' | 'md'
}
export function IconButton({ label, icon, selected, size = 'md', className, ...props }: IconButtonProps) {
return (
<button
type="button"
className={clsx(styles.button, styles[size], selected && styles.selected, className)}
aria-label={label}
aria-pressed={selected}
title={label}
{...props}
>
{icon}
</button>
)
}
@@ -0,0 +1,16 @@
.tabs { min-width: 0; display: flex; align-items: flex-end; gap: 28px; border-bottom: 1px solid var(--color-border); overflow-x: auto; scrollbar-width: none; }
.tabs::-webkit-scrollbar { display: none; }
.tab { position: relative; height: 44px; flex: 0 0 auto; padding: 0 0 8px; border: 0; background: transparent; color: #8d8a96; cursor: pointer; font-family: var(--font-brand); font-size: 16px; font-weight: 500; transform: scale(.95); transform-origin: left bottom; transition: color var(--duration-normal), transform var(--duration-slow) var(--ease-spring); }
.tab:hover { color: #c9c3d8; }
.tab:disabled { opacity: .4; cursor: not-allowed; }
.activeTab { background: var(--gradient-brand); background-clip: text; color: transparent; font-weight: 600; transform: scale(1); }
.activeTab::after { content: ""; position: absolute; left: 0; right: 0; bottom: 0; height: 2px; border-radius: 2px; background: var(--gradient-brand-strong); }
.chip { height: 32px; flex: 0 0 auto; padding: 0 15px; border: 1px solid var(--color-border); border-radius: var(--radius-lg); background: #1a1920; color: var(--color-text-muted); cursor: pointer; font-size: 13px; transition: all var(--duration-normal); }
.chip:hover { border-color: var(--color-border-strong); color: #fff; }
.selectedChip { border-color: var(--color-primary-border); background: var(--color-primary-soft); color: #fff; font-weight: 600; }
.badge { display: inline-flex; align-items: center; min-height: 22px; padding: 2px 8px; border: 1px solid var(--color-border); border-radius: var(--radius-md); font-size: 11px; font-weight: 600; }
.neutral { color: var(--color-text-muted); background: var(--color-surface-raised); }
.info { color: #9edbff; background: rgba(40,120,255,.12); border-color: rgba(40,120,255,.28); }
.success { color: #75e2b8; background: rgba(54,201,143,.12); border-color: rgba(54,201,143,.28); }
.warning { color: #ffd786; background: rgba(241,185,71,.12); border-color: rgba(241,185,71,.28); }
.danger { color: #ff9caa; background: rgba(255,102,122,.12); border-color: rgba(255,102,122,.28); }
@@ -0,0 +1,20 @@
import type { ReactNode } from 'react'
import clsx from 'clsx'
import styles from './Selection.module.css'
export interface TabItem { id: string; label: string; disabled?: boolean }
interface TabsProps { items: TabItem[]; value: string; onChange: (id: string) => void; ariaLabel?: string }
export function Tabs({ items, value, onChange, ariaLabel = '内容分类' }: TabsProps) {
return <div className={styles.tabs} role="tablist" aria-label={ariaLabel}>{items.map(item => <button key={item.id} type="button" role="tab" aria-selected={item.id === value} className={clsx(styles.tab, item.id === value && styles.activeTab)} disabled={item.disabled} onClick={() => onChange(item.id)}>{item.label}</button>)}</div>
}
interface ChipProps { selected?: boolean; children: ReactNode; onClick?: () => void }
export function FilterChip({ selected, children, onClick }: ChipProps) {
return <button type="button" className={clsx(styles.chip, selected && styles.selectedChip)} aria-pressed={selected} onClick={onClick}>{children}</button>
}
interface BadgeProps { tone?: 'neutral' | 'info' | 'success' | 'warning' | 'danger'; children: ReactNode }
export function Badge({ tone = 'neutral', children }: BadgeProps) {
return <span className={clsx(styles.badge, styles[tone])}>{children}</span>
}
@@ -0,0 +1,22 @@
import '@testing-library/jest-dom/vitest'
import { fireEvent, render, screen } from '@testing-library/react'
import { describe, expect, it, vi } from 'vitest'
import { Button } from './Button'
import { Tabs } from './Selection'
describe('Button', () => {
it('prevents interaction while loading', () => {
render(<Button loading>生成</Button>)
expect(screen.getByRole('button', { name: '生成' })).toBeDisabled()
})
})
describe('Tabs', () => {
it('reports the selected tab and emits changes', () => {
const onChange = vi.fn()
render(<Tabs items={[{ id: 'one', label: '作品灵感' }, { id: 'two', label: '画布模板' }]} value="one" onChange={onChange} />)
expect(screen.getByRole('tab', { name: '作品灵感' })).toHaveAttribute('aria-selected', 'true')
fireEvent.click(screen.getByRole('tab', { name: '画布模板' }))
expect(onChange).toHaveBeenCalledWith('two')
})
})
@@ -0,0 +1,10 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './styles/global.css'
import App from './App.tsx'
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>,
)
@@ -0,0 +1,3 @@
.page { max-width: 1320px; margin: 0 auto; }.pageHeader { display: flex; align-items: flex-end; justify-content: space-between; gap: 20px; margin: 4px 0 16px; }.pageHeader small { color: #69c4ff; font-size: 10px; font-weight: 700; letter-spacing: .14em; }.pageHeader h1 { margin: 5px 0 3px; font-family: var(--font-brand); font-size: 24px; line-height: 1.3; }.pageHeader p { margin: 0; color: var(--color-text-muted); font-size: 13px; }.sections { display: grid; gap: 14px; margin-top: 14px; }.showcase { overflow: hidden; border: 1px solid var(--color-border); border-radius: var(--radius-xl); background: var(--color-surface); }.showcase > header { display: flex; flex-direction: column; gap: 3px; padding: 14px 16px; border-bottom: 1px solid var(--color-border); }.showcase > header strong { font-size: 14px; }.showcase > header span { color: var(--color-text-subtle); font-size: 12px; }.demo { padding: 18px; }.row { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; }.formGrid { display: grid; grid-template-columns: repeat(4,minmax(0,1fr)); gap: 14px; }.statusStack { display: grid; gap: 12px; }.muted { color: var(--color-text-muted); }.dialogButton { margin-top: 14px; }.businessGrid { display: grid; grid-template-columns: repeat(3,minmax(0,1fr)); gap: 12px; margin-top: 14px; }.businessItem { min-height: 150px; padding: 16px; border: 1px solid var(--color-border); border-radius: var(--radius-xl); background: linear-gradient(205deg,rgba(68,142,255,.12),rgba(36,36,41,.8) 60%); }.businessItem strong { font-size: 15px; }.businessItem p { min-height: 42px; color: var(--color-text-muted); font-size: 12px; line-height: 1.7; }
@media (max-width: 980px) { .formGrid { grid-template-columns: repeat(2,minmax(0,1fr)); }.businessGrid { grid-template-columns: repeat(2,minmax(0,1fr)); } }
@media (max-width: 620px) { .pageHeader { align-items: flex-start; flex-direction: column; }.formGrid,.businessGrid { grid-template-columns: 1fr; } }
@@ -0,0 +1,36 @@
import { Bell, Download, Plus, Save, Trash2, WandSparkles } from 'lucide-react'
import { useState } from 'react'
import { Button } from '../components/primitives/Button'
import { Dialog, EmptyState, InlineAlert, Skeleton, Spinner } from '../components/primitives/Feedback'
import { Field, SearchInput, Select, Switch, TextInput } from '../components/primitives/FormControls'
import { IconButton } from '../components/primitives/IconButton'
import { Badge, FilterChip, Tabs } from '../components/primitives/Selection'
import styles from './ComponentsPage.module.css'
const componentTabs = [{ id: 'primitives', label: '基础组件' }, { id: 'feedback', label: '反馈组件' }, { id: 'business', label: '业务组件' }]
export function ComponentsPage() {
const [tab, setTab] = useState('primitives')
const [dialogOpen, setDialogOpen] = useState(false)
const [chip, setChip] = useState('全部')
const [enabled, setEnabled] = useState(true)
return <div className={styles.page}>
<header className={styles.pageHeader}><span><small>DESIGN SYSTEM</small><h1>Miragen Flow 组件中心</h1><p>用户端组件、交互状态与视觉约束的集中验收页面。</p></span><Button variant="primary" leadingIcon={<Plus />}>新建组件</Button></header>
<Tabs items={componentTabs} value={tab} onChange={setTab} ariaLabel="组件分类" />
{tab === 'primitives' && <div className={styles.sections}>
<Showcase title="Button" description="主要命令、次要命令、低强调和危险操作。"><div className={styles.row}><Button variant="primary" leadingIcon={<WandSparkles />}>开始生成</Button><Button variant="secondary" leadingIcon={<Save />}>保存</Button><Button variant="ghost" leadingIcon={<Download />}>下载</Button><Button variant="danger" leadingIcon={<Trash2 />}>删除</Button><Button loading>处理中</Button><Button disabled>不可用</Button></div></Showcase>
<Showcase title="IconButton & Badge" description="图标命令必须带可访问名称,状态标签使用固定语义色。"><div className={styles.row}><IconButton label="通知" icon={<Bell />} /><IconButton label="已选择" icon={<Save />} selected /><IconButton label="不可用" icon={<Trash2 />} disabled /><Badge>草稿</Badge><Badge tone="info">生成中</Badge><Badge tone="success">已完成</Badge><Badge tone="warning">等待中</Badge><Badge tone="danger">失败</Badge></div></Showcase>
<Showcase title="Input & Select" description="覆盖提示、错误、禁用与搜索输入。"><div className={styles.formGrid}><Field label="作品名称" hint="最多 40 个字符"><TextInput placeholder="输入作品名称" /></Field><Field label="搜索"><SearchInput placeholder="搜索模型或风格" /></Field><Field label="模型类型"><Select defaultValue="general"><option value="general">通用模型</option><option value="style">风格模型</option></Select></Field><Field label="错误状态" error="请输入有效内容"><TextInput invalid defaultValue="无效内容" /></Field></div></Showcase>
<Showcase title="Selection" description="筛选项、切换项与开关。"><div className={styles.row}>{['全部','游戏界面','角色','场景'].map(item => <FilterChip key={item} selected={chip === item} onClick={() => setChip(item)}>{item}</FilterChip>)}<Switch label="启用高清修复" checked={enabled} onChange={event => setEnabled(event.target.checked)} /></div></Showcase>
</div>}
{tab === 'feedback' && <div className={styles.sections}>
<Showcase title="Status" description="加载、骨架和语义提示。"><div className={styles.statusStack}><InlineAlert>模型参数已经自动保存。</InlineAlert><InlineAlert tone="danger">上传失败,请检查文件格式。</InlineAlert><div className={styles.row}><Spinner /><span className={styles.muted}>正在生成预览</span></div><Skeleton height={88} /></div></Showcase>
<Showcase title="Empty State & Dialog" description="空内容提示与模态任务确认。"><EmptyState title="还没有生成任务" description="完成第一次创作后,任务会出现在这里" action={<Button variant="primary">开始创作</Button>} /><Button className={styles.dialogButton} onClick={() => setDialogOpen(true)}>打开对话框</Button></Showcase>
</div>}
{tab === 'business' && <div className={styles.businessGrid}><BusinessItem title="内容发现" items="HeroCarousel · ArtworkCard · FilterBar · MasonryGrid" /><BusinessItem title="AI 创作" items="PromptEditor · ModelPicker · RatioPicker · GenerationQueue" /><BusinessItem title="自由画布" items="CanvasStage · LayerPanel · PropertyPanel · ExportDialog" /><BusinessItem title="资产管理" items="AssetCard · FolderTree · UploadQueue · BatchActionBar" /><BusinessItem title="3D 渲染" items="SceneViewport · LightingPanel · MaterialPanel · RenderQueue" /><BusinessItem title="账户系统" items="LoginDialog · CreditBalance · ProfileHeader · SettingsSection" /></div>}
<Dialog open={dialogOpen} title="确认生成任务" description="预计消耗 2 点算力" onClose={() => setDialogOpen(false)}>当前模型为 Miragen General V2,生成比例 16:9,共生成 4 张。</Dialog>
</div>
}
function Showcase({ title, description, children }: { title: string; description: string; children: React.ReactNode }) { return <section className={styles.showcase}><header><strong>{title}</strong><span>{description}</span></header><div className={styles.demo}>{children}</div></section> }
function BusinessItem({ title, items }: { title: string; items: string }) { return <article className={styles.businessItem}><strong>{title}</strong><p>{items}</p><Badge tone="info">已规划</Badge></article> }
@@ -0,0 +1,2 @@
.page { width: 100%; min-width: 0; }.filterBar { display: flex; align-items: center; gap: 12px; min-height: 62px; }.chips { display: flex; min-width: 0; flex: 1; align-items: center; gap: 8px; overflow-x: auto; scrollbar-width: none; }.chips::-webkit-scrollbar { display: none; }.filterBar > button { flex: 0 0 auto; }.placeholder { min-height: 360px; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 8px; border: 1px dashed var(--color-border-strong); border-radius: var(--radius-xl); color: var(--color-text-muted); }.placeholder strong { color: var(--color-text); font-size: 18px; }.placeholder span { font-size: 13px; }
@@ -0,0 +1,39 @@
import { ChevronDown, SlidersHorizontal } from 'lucide-react'
import { useMemo, useState } from 'react'
import { ArtworkGrid } from '../components/content/ArtworkGrid'
import { HeroCarousel } from '../components/content/HeroCarousel'
import { Button } from '../components/primitives/Button'
import { IconButton } from '../components/primitives/IconButton'
import { Dialog } from '../components/primitives/Feedback'
import { FilterChip, Tabs } from '../components/primitives/Selection'
import { artworks, type Artwork } from '../data/artworks'
import styles from './HomePage.module.css'
const tabs = [
{ id: 'inspiration', label: '作品灵感' },
{ id: 'canvas', label: '画布模板' },
{ id: 'style', label: '风格模型' },
{ id: 'render', label: '万能渲染模型' },
{ id: 'academy', label: 'AI 学院' },
]
const categories = ['猜你喜欢', '小游戏', '游戏界面', '买量视频', '二次元', '装饰风格', '角色演绎', '场景展示', '循环动画', '特效', '插画', 'ICON']
export function HomePage() {
const [tab, setTab] = useState('inspiration')
const [category, setCategory] = useState('猜你喜欢')
const [sort, setSort] = useState<'最热' | '最新'>('最热')
const [selectedArtwork, setSelectedArtwork] = useState<Artwork | null>(null)
const visibleArtworks = useMemo(() => {
const filtered = category === '猜你喜欢' ? artworks : artworks.filter(item => item.category === category)
return sort === '最热' ? [...filtered].sort((a, b) => b.likes - a.likes) : [...filtered].reverse()
}, [category, sort])
return <div className={styles.page}>
<HeroCarousel />
<Tabs items={tabs} value={tab} onChange={setTab} />
<div className={styles.filterBar}><div className={styles.chips}>{categories.map(item => <FilterChip key={item} selected={item === category} onClick={() => setCategory(item)}>{item}</FilterChip>)}<IconButton label="更多筛选" icon={<SlidersHorizontal />} /></div><Button variant="secondary" size="sm" trailingIcon={<ChevronDown />} onClick={() => setSort(value => value === '最热' ? '最新' : '最热')}>{sort}</Button></div>
{tab === 'inspiration' ? <ArtworkGrid artworks={visibleArtworks} onUse={setSelectedArtwork} /> : <section className={styles.placeholder}><strong>{tabs.find(item => item.id === tab)?.label}</strong><span>该模块的组件结构已纳入后续实现队列。</span></section>}
<Dialog open={Boolean(selectedArtwork)} title="创建同款作品" description={selectedArtwork?.title} onClose={() => setSelectedArtwork(null)}><p>将使用当前作品的模型、尺寸与风格参数创建新的生成任务。</p></Dialog>
</div>
}
@@ -0,0 +1 @@
.page { min-height: calc(100vh - 112px); display: flex; flex-direction: column; align-items: center; justify-content: center; text-align: center; }.page > svg { width: 36px; height: 36px; margin-bottom: 13px; color: #75d1ff; }.page small { color: #69c4ff; font-weight: 700; letter-spacing: .14em; }.page h1 { margin: 8px 0; font-family: var(--font-brand); font-size: 26px; }.page p { max-width: 480px; margin: 0 0 22px; color: var(--color-text-muted); }
@@ -0,0 +1,7 @@
import { ArrowRight, Construction } from 'lucide-react'
import { Button } from '../components/primitives/Button'
import styles from './PlaceholderPage.module.css'
export function PlaceholderPage({ title, description, onGoComponents }: { title: string; description: string; onGoComponents: () => void }) {
return <section className={styles.page}><Construction /><small>MODULE SCAFFOLD</small><h1>{title}</h1><p>{description}</p><Button variant="primary" trailingIcon={<ArrowRight />} onClick={onGoComponents}>查看组件规划</Button></section>
}
@@ -0,0 +1,42 @@
@import "./tokens.css";
* { box-sizing: border-box; }
html, body, #root { width: 100%; min-width: 320px; min-height: 100%; margin: 0; }
html { background: var(--color-bg); }
body {
min-height: 100vh;
overflow: hidden;
background: var(--color-bg);
color: var(--color-text);
font-family: var(--font-sans);
font-size: 14px;
line-height: 1.5;
letter-spacing: 0;
-webkit-font-smoothing: antialiased;
}
button, input, textarea, select { font: inherit; }
button { color: inherit; }
a { color: inherit; text-decoration: none; }
img { max-width: 100%; }
::selection { background: rgba(40, 120, 255, 0.4); color: #fff; }
:focus-visible { outline: 2px solid #63c8ff; outline-offset: 2px; }
::-webkit-scrollbar { width: 8px; height: 8px; }
::-webkit-scrollbar-thumb { background: #3c3c43; border-radius: 999px; }
::-webkit-scrollbar-track { background: transparent; }
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
@@ -0,0 +1,54 @@
@font-face {
font-family: "Alimama FangYuanTi VF";
src: url("../assets/holopix/alimama.woff2") format("woff2");
font-display: swap;
}
:root {
color-scheme: dark;
--font-sans: "PingFang SC", "Microsoft YaHei", system-ui, sans-serif;
--font-brand: "Alimama FangYuanTi VF", var(--font-sans);
--color-bg: #18181b;
--color-surface: #1d1d21;
--color-surface-raised: #242429;
--color-surface-hover: #2a2a30;
--color-border: rgba(235, 235, 245, 0.08);
--color-border-strong: #34343a;
--color-text: #ece9f2;
--color-text-muted: #a7a2b3;
--color-text-subtle: #777580;
--color-primary: #2878ff;
--color-primary-hover: #3d8bff;
--color-primary-soft: #15294d;
--color-primary-border: #2d62b7;
--gradient-brand: linear-gradient(90deg, #55a7ff, #75d1ff 52%, #c1eeff);
--gradient-brand-strong: linear-gradient(135deg, #2878ff, #63c8ff);
--color-success: #36c98f;
--color-warning: #f1b947;
--color-danger: #ff667a;
--color-info: #55a7ff;
--space-1: 4px;
--space-2: 8px;
--space-3: 12px;
--space-4: 16px;
--space-5: 20px;
--space-6: 24px;
--space-8: 32px;
--radius-sm: 2px;
--radius-md: 3px;
--radius-lg: 4px;
--radius-xl: 4px;
--shadow-popover: 0 16px 40px rgba(0, 0, 0, 0.36);
--shadow-primary: 0 8px 24px rgba(40, 120, 255, 0.26);
--duration-fast: 120ms;
--duration-normal: 180ms;
--duration-slow: 250ms;
--ease-standard: cubic-bezier(0.2, 0.7, 0.2, 1);
--ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
--header-height: 56px;
--sidebar-width: 172px;
--z-header: 20;
--z-popover: 40;
--z-dialog: 60;
--z-toast: 80;
}
@@ -0,0 +1,26 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "es2023",
"lib": ["ES2023", "DOM"],
"module": "esnext",
"types": ["vite/client"],
"allowArbitraryExtensions": true,
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"]
}
@@ -0,0 +1,7 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}
@@ -0,0 +1,23 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "es2023",
"lib": ["ES2023"],
"types": ["node"],
"skipLibCheck": true,
/* Bundler mode */
"module": "nodenext",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true
},
"include": ["vite.config.ts"]
}
@@ -0,0 +1,10 @@
import { defineConfig } from 'vitest/config'
import react from '@vitejs/plugin-react'
// https://vite.dev/config/
export default defineConfig({
plugins: [react()],
test: {
environment: 'jsdom',
},
})
+7
View File
@@ -0,0 +1,7 @@
# 设计参考
`MiragenFlow-front/` 是一期视觉参考快照,不是运行项目、工作区或生产静态资源。生产入口只有 `web/`、`admin/` 和 `server/`;该快照中的 hash 路由、占位页和静态演示数据不参与构建。
其中的图片和字体仅用于设计核验,未作为生产业务数据或用户资产发布。
已接入的生产视觉组件位于 `web/src/user-console/`;该目录只迁移壳层、作品灵感、模型广场和基础 primitives,业务数据继续由 `web/src/services` 与 Zustand stores 提供。
+3 -1
View File
@@ -1,6 +1,8 @@
services:
docs:
image: ghcr.io/basketikun/infinite-canvas-docs:latest
build:
context: ..
dockerfile: docs/Dockerfile
ports:
- "3001:3000"
restart: unless-stopped
+6
View File
@@ -33,6 +33,12 @@
- [Pending Tests](/docs/progress/pending-test)
- [TODO](/docs/progress/todo)
## Long-term Goals
- [Current long-task goal and copyable prompt](/docs/miragenflow-long-task-goal-prompt.md)
- [TDesign unification execution baseline](/docs/miragenflow-tdesign-unification-goal.md)
- [Admin business goal](/goal.md)
## Notes
- Canvas projects and My Assets are primarily stored in the browser. WebDAV can be configured for cross-device synchronization.
+221
View File
@@ -0,0 +1,221 @@
# MiragenFlow 架构分析
## 1. 系统定位
MiragenFlow 是一个本地优先的 AI 创作工作台,并在服务端提供统一的任务网关、计费、渠道路由和管理能力。
- 用户端负责画布编排、图片/文本/音频创作、任务查看、资产管理和 WebDAV 同步。
- 管理台负责用户、渠道、模型产品、套餐、计费、审批、任务、存储、指标和审计。
- 服务端负责认证、目录、任务、余额、队列、供应商调用、对象落地、事件和后台作业。
- `packages/contracts` 维护浏览器和服务端共享的会话、任务、事件、上传对象等 TypeScript 合约。
当前系统不是“浏览器直接持有供应商 Key”的纯前端工具:计费任务默认通过 `/api/v1` 服务端网关执行。另一方面,画布项目和“我的资产”仍主要保存在浏览器本地,WebDAV 是可选同步层,不是平台自带云端项目库。
## 2. 运行时边界
```text
用户端 web/ ─┐
├─ shared contracts ─> server HTTP /api/v1
管理台 admin/ ┘ │
├─ Auth / CSRF / MFA / Scope
├─ Store + PersistenceRepository
├─ Task Dispatch Outbox + Queue
├─ Task Worker + Provider Adapter
├─ Staging / Object / Asset
├─ Task WebSocket Events
└─ Message / WebDAV / GC / Reconcile Workers
```
服务端启动时加载配置、迁移和持久化状态,恢复任务租约、消息 outbox、WebDAV 作业和排队任务,再启动 HTTP、任务 WebSocket 及周期性恢复/GC 作业。证据入口:`server/src/index.ts:13`。
## 3. 用户端架构
### 3.1 页面和启动
- `web/src/main.tsx:1` 组合 `AppProviders` 与 `RouterProvider`。
- `web/src/router.tsx:29` 集中注册公开页面、认证页面和受保护工作台路由。
- `AuthGuard` 在认证 store 完成 hydrate 前保持加载态,未登录则跳到登录页并保留 redirect。
- `ClientRootInit` 启动时并行恢复会话并加载公开模型目录。
### 3.2 状态与本地持久化
- `useAuthStore` 管理内存 access token、用户、余额、MFA challenge 和 refresh 恢复。
- `useCatalogStore` 缓存 `/api/v1/catalog/models` 的公开模型目录。
- `useCanvasStore` 通过 localForage 持久化项目,约 400ms 防抖写入。
- `useAssetStore` 持久化文本、图片和音频资产,并在删除时清理无引用媒体。
- `local-data-namespace` 以用户 ID 或 anonymous 区分本地命名空间,并用 epoch 防止切换竞态。
因此账号登录和服务端任务是云端边界,画布及本地资产不是。清理站点数据仍可能丢失未同步的本地内容。
### 3.3 API 与任务体验
`web/src/services/api/platform.ts:81` 是统一请求边界:
- 同源 `/api/v1` 请求带 `credentials: include`。
- access token 只放内存并以 Bearer 发送。
- Cookie 会话存在时,写请求附带 CSRF token。
- 401 时只刷新一次,失败后触发统一会话过期处理。
任务创建链路位于同文件 `runPlatformTask`:创建任务后优先订阅 `/api/v1/ws/tasks`;WebSocket 失败时以 250ms 轮询任务详情。任务页另以 5 秒可见刷新维护列表。
### 3.4 上传、对象和 WebDAV
- 小于等于 50MB 的图片可走 multipart;其他对象走预签名/complete 链路。
- 任务结果通过带认证的 `/api/v1/objects/*` 读取。
- `app-sync.ts` 在画布/资产 hydration 后合并远端 manifest,补齐媒体并上传变化。
- WebDAV 同步是用户主动配置的跨设备同步,不是多人协作或实时云存储。
## 4. 管理台架构
### 4.1 入口、状态和路由
- `admin/src/main.tsx:3` 组合 Redux `Provider`、BrowserRouter 和应用入口。
- Redux 只维护 `global` 与 `user` 两个 slice;业务列表不进入大型全局 store。
- `admin/src/router/index.ts:36` 为路由声明 `requiredScope`。
- 用户安全、财务、审计等页面分别需要 `admin:security`、`admin:finance`、`admin:audit`;其他页面默认 `admin:read`。
- `PermissionGate` 读取角色和 scope,`super_admin` 直接通过,其余不足权限时进入 `/403`。
### 4.2 请求与业务页
`admin/src/services/platform.ts:65` 的 `adminRequest` 统一处理:
- Bearer access token、管理员 CSRF Cookie、`credentials: include`。
- 非 GET 请求自动生成 Idempotency-Key。
- 401 只触发一次并发去重的 refresh。
- 错误对象保留 status、code、details 和 requestId。
当前大多数业务路由懒加载 `admin/src/pages/Business/index.tsx`。该页面根据 section 映射读取 `/api/v1/admin/*`,写操作统一处理 `If-Match`、幂等、刷新列表及 409/422/503。
管理台的“实时指标”并非 WebSocket 客户端:页面每 10 秒 GET `/api/v1/admin/ws/metrics`,服务端即时聚合订阅数、队列和事件积压。审计大板和详细日志都读取 `/api/v1/admin/audit-logs`。
## 5. HTTP、安全与权限
HTTP 入口在 `server/src/app/http.ts:1174`:
- 每个请求分配 requestId,并统一处理 CORS、body limit、健康和就绪检查。
- `/api/v1` 写请求先经过 CSRF 检查;纯 Bearer 且没有 refresh Cookie 的请求不强制双重提交。认证引导端点可以恢复缺少 CSRF Cookie 的旧 refresh 会话并补发缺失 Cookie,普通业务写请求仍受双提交保护。
- 用户 token 需要校验 JWT、session family、撤销状态和账号状态。
- 管理 token 使用独立 audience、角色和 scopes。
- 管理资源 scope 由 `adminRequiredScope` 统一映射,不依赖前端菜单作为安全边界。
- 非读取管理请求必须带合法 Idempotency-Key,并以 body fingerprint 防止同键异体重放。
密码使用 scrypt;JWT 使用 HS256 并包含 aud、scope、jti、sid、roles/scopes 等声明。登录策略还支持 CAPTCHA、TOTP 和 recovery code。
生产配置会强制检查 JWT secret、管理员密码、渠道加密密钥、PostgreSQL 和 Redis 配置;本地开发允许 memory/file persistence 与 memory queue。
## 6. 任务主链
### 6.1 创建与预留
`server/src/app/http.ts:751` 的任务创建先完成:
1. 校验公开模型、能力、输入引用、mask、数量、分辨率和参数。
2. 解析产品、价格、渠道组和套餐权益快照。
3. 计算预计金币并检查并发限制。
4. 在账本中把 available 移到 reserved。
5. 原子写入 task、幂等记录、task event、dispatch outbox 和队列信息。
6. 返回 `202` 与公开任务投影。
### 6.2 队列和租约
`server/src/infra/queue.ts` 定义统一队列接口。开发可使用内存队列;生产 Redis 适配器使用 sorted set、hash 和 Lua 原子 claim/renew/nack,并维护 dead-letter 集合。
PostgreSQL 模式使用 task dispatch outbox:事务提交后再由 dispatcher 以 `SKIP LOCKED` claim 并投递唯一 taskId,避免“数据库已提交但队列未写入”的双写问题。
### 6.3 Worker、重试和未知结果
`server/src/jobs/task-worker.ts:400`:
- Worker claim durable lease 后把 queued 改为 running,并周期性续租。
- 路由快照生成按渠道优先级、重试预算和断路器状态排列的 attempt plan。
- 每个 attempt 带平台幂等键,调用 provider adapter 后分类为 success、retryable、permanent 或 unknown。
- 可重试错误切换渠道;预算耗尽才失败并释放剩余预留。
- 网络超时、租约丢失或结果不确定不会直接重复请求,而是进入 unknown/reconciliation,防止供应商已完成但平台重复扣费。
- 续租失败会 fence 后续副作用,避免两个 Worker 同时结算同一任务。
### 6.4 输出与结算
成功输出先写 staging/object,再创建用户 asset 和 output 记录;结算按实际成功输出和价格快照扣除 reserved,释放多余预留。结算失败会进入 unknown,等待一致性恢复。
## 7. 供应商适配
`server/src/adapters/provider.ts` 是供应商边界:
- 平台显示模型 ID 与供应商请求模型 ID 分开解析,模型映射只在出站时生效。
- OpenAI 兼容图片适配器支持 `/images/generations`、`/images/edits`、多参考图、mask、参数和 idempotency header。
- 出站 URL 在生产要求 HTTPS 与公网 DNS;请求固定解析地址、禁用重定向,并限制超时和响应字节数。
- transport/timeout 归类为 unknown,而不是轻率判为 failed。
- admin 模型探测会明确请求 `/v1/models` 并返回 requestAttempted 与分类诊断。
设计文档中的 `@图片X` 文本重写、固定 1K/2K/4K 模型后缀和比例矩阵属于产品设计要求;当前源码已确认参考图数组、mask、分辨率模型映射和 OpenAI 兼容调用,但本次没有在服务端调用链中确认独立的 `@图片X -> 第X张图` 文本重写器。
## 8. 一致性、持久化与数据
### 8.1 Store 与事务
Store 聚合用户、会话、模型、渠道、任务、attempt、事件、余额 bucket、ledger、订单、审批、审计和 WebDAV 状态。
- 内存事务保存 snapshot,失败时回滚,提交后才发布缓冲事件。
- 文件适配器使用临时文件 + rename 原子替换,并以 0600 权限写入。
- PostgreSQL Repository 使用 revision/advisory lock、乐观冲突重试和领域行投影。
- task lease、dispatch outbox、message outbox 和 WebDAV job 都有独立 durable claim/renew/complete 接口。
### 8.2 余额与审批
账本使用 bucket slice 记录 reserve、settle、release、refund 和 adjustment。人工充值、退款及高风险调账先创建审批单;默认禁止申请人自批,执行时可要求管理员 TOTP,并把账本、订单、审批状态和审计放入同一事务。
支付 webhook 校验签名、金额、币种和唯一 eventId;订单、payment event、余额 bucket 和 ledger 原子提交。非生产 mock 支付与真实 adapter 路径明确分开。
### 8.3 审计
审计 helper 会脱敏凭证、provider 内部标识和路由快照,并用 previousHash/hash 形成链式校验。管理读审计列表本身也会写入 `audit.read`,因此审计读取是可追踪操作。
## 9. 事件、指标与后台作业
### 9.1 用户任务事件
`server/src/app/ws.ts` 只接受 `/api/v1/ws/tasks`:
- 校验同源与用户会话;生产拒绝 query access_token。
- 每连接最多 8 个任务订阅,20 秒 heartbeat。
- 订阅时先按 cursor 回放,再接收实时事件。
- 以 eventId 和每任务 sequence 去重。
- HTTP 事件与 WS 事件都经过 `publicTaskEvent` 白名单投影,隐藏 channel/provider 等敏感字段。
### 9.2 后台作业
服务端还运行消息 outbox、WebDAV sync/retention、staging GC、租约恢复、reservation 恢复和 unknown reconciliation。WebDAV 适配器校验公网 HTTPS、清理路径穿越,并使用 ETag、租约、指数退避和冲突副本。
## 10. 部署形态
| 环境 | 持久化 | 队列 | 外部依赖 |
| --- | --- | --- | --- |
| 本地开发 | memory/file | memory | 可使用本地 fixture,但不会自动创建展示渠道 |
| 生产 | PostgreSQL | Redis | 公网 HTTPS provider、真实支付/消息/WebDAV 配置 |
已确认的生产硬要求来自 `server/src/config.ts:76`。`system-diagrams.md` 提到可选 S3,但当前源码中实际确认的是本地 staging/object 处理;不能把外部 S3 视为已经实现。
## 11. 已确认事实与未确认边界
### 已确认
- 用户端、管理台、共享合约和服务端是四个明确代码边界。
- 任务采用余额预留、dispatch outbox、队列 lease、attempt、重试/断路器和 unknown 对账。
- 用户任务事件有 WS + cursor replay + HTTP fallback。
- 管理台实时指标为 HTTP 轮询。
- 画布与本地资产以 localForage/IndexedDB 为主,WebDAV 可选同步。
- 生产要求 PostgreSQL 与 Redis。
### 未确认或不应过度承诺
- 没有确认外部 S3 对象存储实现;当前应按本地 staging/object 理解。
- 没有确认设计文档中的 `@图片X` 服务端重写器已经落地。
- 没有确认真正的 3D/视频生成;现有范围以图片、文本、音频和 2D DOM/SVG 画布为主。
- AGENTS.md 提到的 Agent 对话 `threadId/turnId/itemId` 协议未在当前 `server/` 与 `packages/contracts/` 中找到实现。
- 多实例扩展仍依赖 PostgreSQL/Redis、租约 fencing 和外部服务的生产验证,不能以本地 fixture 代替。
## 12. 图示产物
- `docs/miragenflow-architecture.html`:全平台组件、边界、请求和异步主链。
- `docs/miragenflow-task-lifecycle.html`:任务状态主线、失败重试、unknown 保护和取消出口。
- 对应 JSON 规格位于同目录,可继续用 Archify 验证和生成。

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