{ const next = newData.map((item) => String(item.id)); setChannelOrderDraft(next); updateDialogField('channelIds', next.join(', ')); }} columns={[{ colKey: 'order', title: '顺序', width: 80, cell: ({ row }: { row: Row }) => String(channelOrderRows.findIndex((item) => item.id === row.id) + 1) }, { colKey: 'label', title: '渠道', width: 220, ellipsis: true }, { colKey: 'provider', title: '供应商', width: 180, ellipsis: true }, { colKey: 'models', title: '可用模型', width: 260, ellipsis: true }]} tableLayout='fixed' empty='暂无渠道成员' />当前回退顺序:{channelOrderRows.length ? channelOrderRows.map((item) => `${item.order}. ${item.label}`).join(' → ') : '暂无渠道'}
;
+ const dialogBody = dialog?.kind === 'resolutions' ? resolutionEditor : dialog?.kind === 'create' && section.write === 'channel' ? channelFields : dialog?.kind === 'create' && section.write === 'channelGroup' ? channelGroupFields : dialog?.kind === 'create' && section.write === 'modelProduct' ? {section.fields?.map((field) => dialogField(field.key, field.label, field.type))}{resolutionEditor}
: dialog?.kind === 'create' ? {section.fields?.map((field) => dialogField(field.key, field.label, field.type))}
: isBalanceDialog ? balanceDialogBody : dialog?.kind === 'channelTest' ? channelTestBody : dialog?.kind === 'channelHealthTest' ? channelHealthTestBody : dialog?.kind === 'rights' ? {dialogField('modelIds', '可用模型 ID(逗号分隔)')}{dialogField('groupIds', '可用渠道组 ID(逗号分隔)')}
: dialog?.kind === 'price' ? {dialogField('basePrice', '基础金币价格', 'number')}
: dialog?.kind === 'groupOrder' ? channelOrderBody : dialog?.kind === 'groupEdit' ? channelGroupFields : dialog?.kind === 'channelEdit' ? channelFields : dialog?.kind === 'productEdit' ? {dialogField('name', '产品名称')}{dialogField('basePrice', '基础价格', 'number')}{dialogField('channelGroupId', '渠道组 ID')}{dialogField('enabled', '上架状态', 'switch')}
: dialog?.kind === 'userEdit' ? {dialogField('status', '账户状态')}{dialogField('mfaRequired', '要求该用户绑定多因素认证(MFA)', 'switch')}
: dialog?.kind === 'paymentProviderEdit' ? {dialogField('enabled', '启用适配器', 'switch')}{dialogField('secret', '新签名密钥', 'password')}
: dialog?.kind === 'messageProviderEdit' ? {dialogField('endpoint', 'HTTPS 接口地址')}{dialogField('priority', '优先级', 'number')}{dialogField('enabled', '启用供应商', 'switch')}{dialogField('secret', '新访问凭证', 'password')}
: dialog?.kind === 'messageTemplateEdit' ? {dialogField('content', '模板内容', 'textarea')}{dialogField('enabled', '启用模板', 'switch')}
: dialog?.kind === 'planEdit' ? {dialogField('code', '套餐编码')}{dialogField('icon', '图标标识')}{dialogField('name', '套餐名称')}{dialogField('description', '基本介绍', 'textarea')}{dialogField('includedBalance', '包含金币', 'number')}{dialogField('purchaseAmount', '购买价格', 'number')}{dialogField('durationDays', '时长天数', 'number')}{dialogField('maxConcurrent', '并发上限', 'number')}{dialogField('queuePriority', '队列优先级', 'number')}{dialogField('retentionDays', '保留天数', 'number')}{dialogField('maxRetentionExtensionDays', '最大延长天数', 'number')}{dialogField('published', '上架状态', 'switch')}
: dialog?.kind === 'approve' ? {admin.mfaRequired ? dialogField('mfaCode', '管理员两步验证码', 'password') :
确认后将执行该审批,操作会写入审计日志。
}
: dialog?.kind === 'reconcile' ? reconcileBody : dialog?.kind === 'reject' || dialog?.kind === 'refund' ? {dialogField('reason', dialog.kind === 'reject' ? '驳回原因' : '退款原因', 'textarea')}
: null;
+ const settingSwitch = (name: string, label: string) => {label} setSettingsDraft((current) => ({ ...current, [name]: String(value) }))} />
;
+ const settingNumber = (name: string, label: string, min?: number, max?: number) => {label} setSettingsDraft((current) => ({ ...current, [name]: String(value ?? '') }))} />
;
+ const settingsPanel = key === 'settings' ?
+ {loading && !rows[0] ? : rows[0] ? <>
+
+ {settingSwitch('emailRegistrationVerificationRequired', '注册需要邮箱验证')}{settingSwitch('phoneRegistrationVerificationRequired', '注册需要手机号验证')}
+ {settingSwitch('captchaRequired', '普通用户启用人机验证')}{settingNumber('rateLimitWindowMs', '限流窗口(毫秒)', 1_000, 3_600_000)}{settingNumber('rateLimitMax', '窗口最大请求数', 1, 10_000)}
+ {settingSwitch('userMfaRequired', '要求普通用户绑定 MFA')}{settingSwitch('userVerificationRequired', '启用普通用户身份验证')}
+ {settingSwitch('adminMfaRequired', '管理员登录要求 MFA')}{settingSwitch('adminCaptchaRequired', '管理员登录启用人机验证')}
+ {settingNumber('queueMaxConcurrent', '队列最大并发', 1, 1_000)}{settingNumber('auditRetentionDays', '审计保留天数', 1, 3_650)}{settingNumber('auditMaxEntries', '审计最大记录数', 100, 1_000_000)}
+
+ 修改会立即应用,并记录到审计日志。 void saveSettings()}>保存设置
+ > : 暂无系统设置
}
+ : null;
+ const dashboardStats = dashboard ? [
+ { title: '用户总数', value: finiteNumber(dashboard.users) },
+ { title: '活跃用户', value: finiteNumber(dashboard.activeUsers) },
+ { title: '任务总数', value: finiteNumber(dashboard.tasks) },
+ { title: '成功率', value: percentageNumber(dashboard.successRate), suffix: '%', decimalPlaces: 1 },
+ { title: '今日收入', value: finiteNumber(dashboard.todayReceivedAmount ?? dashboard.rechargeTotal), suffix: '元' },
+ { title: '渠道成功率', value: percentageNumber(dashboard.channelSuccessRate), suffix: '%', decimalPlaces: 1 },
+ { title: '消息失败', value: finiteNumber(dashboard.messageFailureCount) },
+ { title: '支付异常', value: finiteNumber(dashboard.paymentRejectedCount) },
+ ] : [];
+
+ if (hiddenSection) return ;
+
+ return
+
{section.title}
{section.description}
{isAuditDashboard ? navigate('/audit-logs')}>查看详细日志 : null}{key === 'channel-health' && can('admin:write') ? void probeAllChannels()}>测试全部渠道 : null}{key === 'storage' && can('admin:write') ? } onClick={runStorageGc}>立即清理 : null}{section.fields && section.write && canConfigure && key !== 'settings' ? {createActionLabels[section.write] || '新增配置'} : null} void load()}>刷新
+ {error ?
void load()}>重试} className={Style.alert} /> : null}
+ {key === 'channel-groups' ? : null}
+ {dashboard ? <>{dashboardStats.map(({ title, value, suffix, decimalPlaces }) =>
{value === undefined ? 暂无数据
: })}
{Array.isArray(dashboard.alerts) && dashboard.alerts.length ? {dashboard.alerts.map((item: Row) => {typeof item.message === 'string' ? localizeTechnicalText(item.message) : formatFieldValue('message', item.message, 'dashboard')}
)} } className={Style.alert} /> : null}> : key === 'dashboard/base' && !error ? {loading ? 正在加载系统概览,请稍候…
: 暂无系统概览数据,请点击刷新重试。
} : null}
+ {key === 'pricing' ? : null}
+ {key === 'channel-health' ? : null}
+ {key === 'tasks' ? : null}
+ {key === 'ws-metrics' ? : null}
+ {isAuditDashboard ? : null}
+ {key === 'billing' && can('admin:finance') ?
+
+
+ : null}
+ {key === 'billing' || key === 'recharges' || key === 'payments' ? : null}
+ {settingsPanel}
+ {summaryFields && key !== 'settings' ? {loading ? : summaryRow ? {summaryFields.map((name) =>
{fieldLabel(name)}
)}
: 暂无配置
} : null}
+ {section.endpoint !== 'dashboard' && !isAuditDashboard && !summaryFields ? {key === 'approvals' ?
{[['', '全部'], ['pending', '待处理'], ['executed', '已执行'], ['rejected', '已驳回'], ['expired', '已过期']].map(([value, label]) => { setStatusFilter(value); setPage(1); }}>{label})}
: null}
{ setQuery(value); setPage(1); }} placeholder='搜索当前列表' />{key === 'users' ? <>
} bordered={false} className={Style.card}>{loading ? : void openAuditDetail(row) : undefined} pagination={filteredRows.length > pageSize ? { current: page, pageSize, total: filteredRows.length, totalContent: false, onChange: ({ current }) => setPage(current) } : undefined} empty={filteredRows.length && !pagedRows.length ? '没有符合筛选条件的记录' : '暂无数据'} />} : null}
+
+
+ {detailLoading ? 正在加载详情,请稍候…
: detailError ? detailRetryRef.current?.()}>重试} /> : detail ? key === 'users' ? <>
+ {userDetailTabs.map((tab) => setUserDetailTab(tab.key)}>{tab.label})}
+ {userDetailTab === 'account' ? <>
+ {detail.id && can('admin:finance') ? 金币调整{iconAction('detail-balance-add', '增加金币', AddIcon, () => openDialog('balanceAdd', detail), 'success')}{iconAction('detail-balance-deduct', '扣减金币', MinusCircleIcon, () => openDialog('balanceDeduct', detail), 'danger')}
: null}
+ {Object.entries(detail).filter(([name]) => !hiddenFields.has(name) && !detailCollections.has(name) && Boolean(fieldLabels[name])).map(([name, value]) =>
{fieldLabel(name)}
)}
+ > : tab.key === userDetailTab)?.label || '详情'}记录`} />}
+ > : {Object.entries(detail).filter(([name]) => !hiddenFields.has(name) && Boolean(fieldLabels[name])).map(([name, value]) =>
{fieldLabel(name)}
)}
: null}
+
+ {attemptsLoading ? 正在加载调用记录,请稍候…
: attemptsError ? attemptsRetryRef.current?.()}>重试} /> : attempts ? Object.keys(row)))].filter((name) => !hiddenFields.has(name) && Boolean(fieldLabels[name])).map((name) => ({ colKey: name, title: fieldLabel(name), width: 190, ellipsis: true, cell: ({ row }: { row: Row }) => tableCell(name, row, 'tasks') }))} empty='暂无调用记录' /> : null}
+ ;
+}
diff --git a/admin/src/pages/Dashboard/Base/chart.ts b/admin/src/pages/Dashboard/Base/chart.ts
deleted file mode 100644
index f8caa22..0000000
--- a/admin/src/pages/Dashboard/Base/chart.ts
+++ /dev/null
@@ -1,261 +0,0 @@
-import type { EChartOption } from 'echarts';
-import { getChartDataSet, ONE_WEEK_LIST } from 'utils/chart';
-
-export const getLineChartOptions = (dateTime: Array = []): EChartOption => {
- const [timeArray, inArray, outArray] = getChartDataSet(dateTime);
- return {
- tooltip: {
- trigger: 'item',
- },
- grid: {
- left: '0',
- right: '20px',
- top: '5px',
- bottom: '36px',
- containLabel: true,
- },
- legend: {
- left: 'center',
- bottom: '0',
- orient: 'horizontal', // legend 横向布局。
- data: ['本月', '上月'],
- textStyle: {
- fontSize: 12,
- },
- },
- xAxis: {
- type: 'category',
- data: timeArray,
- boundaryGap: false,
- axisLine: {
- lineStyle: {
- color: '#E3E6EB',
- width: 1,
- },
- },
- },
- yAxis: {
- type: 'value',
- },
- series: [
- {
- name: '本月',
- data: outArray,
- type: 'line',
- smooth: false,
- showSymbol: true,
- symbol: 'circle',
- symbolSize: 8,
- itemStyle: {
- borderWidth: 1,
- },
- areaStyle: {
- color: '#0053D92F',
- },
- },
- {
- name: '上月',
- data: inArray,
- type: 'line',
- smooth: false,
- showSymbol: true,
- symbol: 'circle',
- symbolSize: 8,
- itemStyle: {
- borderWidth: 1,
- },
- },
- ],
- };
-};
-
-export const getPieChartOptions = (radius = 42): EChartOption => ({
- tooltip: {
- trigger: 'item',
- },
- grid: {
- top: '0',
- right: '0',
- },
- legend: {
- itemWidth: 12,
- itemHeight: 4,
- textStyle: {
- fontSize: 12,
- },
- left: 'center',
- bottom: '0',
- orient: 'horizontal', // legend 横向布局。
- },
- series: [
- {
- name: '销售渠道',
- type: 'pie',
- radius: ['48%', '60%'],
- avoidLabelOverlap: false,
- silent: true,
- itemStyle: {
- borderWidth: 1,
- },
- label: {
- show: true,
- position: 'center',
- formatter: ['{value|{d}%}', '{name|{b}渠道占比}'].join('\n'),
- rich: {
- value: {
- fontSize: 28,
- fontWeight: 'normal',
- lineHeight: 46,
- },
- name: {
- color: '#909399',
- fontSize: 12,
- lineHeight: 14,
- },
- },
- },
- labelLine: {
- show: false,
- },
- data: [
- { value: 1048, name: '线上' },
- { value: radius * 7, name: '门店' },
- ],
- },
- ],
-});
-
-export const getBarChartOptions = (dateTime: Array = []): EChartOption => {
- const [timeArray, inArray, outArray] = getChartDataSet(dateTime);
- return {
- tooltip: {
- trigger: 'item',
- },
- xAxis: {
- type: 'category',
- data: timeArray,
- axisLine: {
- lineStyle: {
- width: 1,
- },
- },
- },
- yAxis: {
- type: 'value',
- },
- grid: {
- top: '5%',
- left: '25px',
- right: 0,
- bottom: '60px',
- },
- legend: {
- icon: 'rect',
- itemWidth: 12,
- itemHeight: 4,
- itemGap: 48,
- textStyle: {
- fontSize: 12,
- color: 'rgba(0, 0, 0, 0.6)',
- },
- left: 'center',
- bottom: '0',
- orient: 'horizontal',
- data: ['本月', '上月'],
- },
- series: [
- {
- name: '本月',
- data: outArray,
- type: 'bar',
- },
- {
- name: '上月',
- data: inArray,
- type: 'bar',
- },
- ],
- };
-};
-
-// PieChartIcon Data
-export const MICRO_CHART_OPTIONS_LINE: EChartOption = {
- xAxis: {
- type: 'category',
- show: false,
- data: ONE_WEEK_LIST,
- },
- yAxis: {
- show: false,
- type: 'value',
- },
- grid: {
- top: 0,
- left: 0,
- right: 0,
- bottom: 0,
- tooltip: {
- show: false,
- },
- },
- color: ['#fff'],
- series: [
- {
- data: [150, 230, 224, 218, 135, 147, 260],
- type: 'line',
- showSymbol: false,
- },
- ],
-};
-
-// BarChartIcon Data
-export const MICRO_CHART_OPTIONS_BAR: EChartOption = {
- xAxis: {
- type: 'category',
- show: false,
- data: ONE_WEEK_LIST,
- },
- yAxis: {
- show: false,
- type: 'value',
- },
- grid: {
- top: 0,
- left: 0,
- right: 0,
- bottom: 0,
- tooltip: {
- show: false,
- },
- },
- series: [
- {
- data: [
- 100,
- 130,
- 184,
- 218,
- {
- value: 135,
- itemStyle: {
- opacity: 0.2,
- },
- },
- {
- value: 118,
- itemStyle: {
- opacity: 0.2,
- },
- },
- {
- value: 60,
- itemStyle: {
- opacity: 0.2,
- },
- },
- ],
- type: 'bar',
- barWidth: 9,
- },
- ],
-};
diff --git a/admin/src/pages/Dashboard/Base/components/MiddleChart.module.less b/admin/src/pages/Dashboard/Base/components/MiddleChart.module.less
deleted file mode 100644
index 0c8afcd..0000000
--- a/admin/src/pages/Dashboard/Base/components/MiddleChart.module.less
+++ /dev/null
@@ -1,3 +0,0 @@
-.middleChartPanel {
- margin-top: 16px;
-}
diff --git a/admin/src/pages/Dashboard/Base/components/MiddleChart.tsx b/admin/src/pages/Dashboard/Base/components/MiddleChart.tsx
deleted file mode 100644
index c3d4c17..0000000
--- a/admin/src/pages/Dashboard/Base/components/MiddleChart.tsx
+++ /dev/null
@@ -1,47 +0,0 @@
-import React, { useState } from 'react';
-import { Col, Row, Card } from 'tdesign-react';
-import ReactEcharts from 'echarts-for-react';
-import useDynamicChart from 'hooks/useDynamicChart';
-import LastWeekDatePicker from 'components/DatePicker';
-import { getLineChartOptions, getPieChartOptions } from '../chart';
-import Style from './MiddleChart.module.less';
-
-const lineOptions = getLineChartOptions();
-const pieOptions = getPieChartOptions();
-
-const MiddleChart = () => {
- const [customOptions, setCustomOptions] = useState(lineOptions);
-
- const onTimeChange = (value: Array) => {
- const options = getLineChartOptions(value);
- setCustomOptions(options);
- };
-
- const dynamicLineChartOption = useDynamicChart(customOptions, {
- placeholderColor: ['legend.textStyle.color', 'xAxis.axisLabel.color', 'yAxis.axisLabel.color'],
- borderColor: ['series.0.itemStyle.borderColor', 'series.1.itemStyle.borderColor'],
- });
-
- const dynamicPieChartOption = useDynamicChart(pieOptions, {
- placeholderColor: ['legend.textStyle.color'],
- containerColor: ['series.0.itemStyle.borderColor'],
- textColor: ['label.color', 'label.color'],
- });
-
- return (
-
-
-
-
-
-
-
-
-
-
-
-
- );
-};
-
-export default React.memo(MiddleChart);
diff --git a/admin/src/pages/Dashboard/Base/components/Overview.module.less b/admin/src/pages/Dashboard/Base/components/Overview.module.less
deleted file mode 100644
index 745fe90..0000000
--- a/admin/src/pages/Dashboard/Base/components/Overview.module.less
+++ /dev/null
@@ -1,5 +0,0 @@
-.overviewPanel {
- background-color: var(--td-bg-color-container);
- border-radius: 3px;
- margin-top: 16px;
-}
diff --git a/admin/src/pages/Dashboard/Base/components/Overview.tsx b/admin/src/pages/Dashboard/Base/components/Overview.tsx
deleted file mode 100644
index 1195048..0000000
--- a/admin/src/pages/Dashboard/Base/components/Overview.tsx
+++ /dev/null
@@ -1,69 +0,0 @@
-import React, { useState } from 'react';
-import { Button, Col, Row, Card } from 'tdesign-react';
-import ReactEcharts from 'echarts-for-react';
-import useDynamicChart from 'hooks/useDynamicChart';
-import Board, { ETrend } from 'components/Board';
-import LastWeekDatePicker from 'components/DatePicker';
-import { getBarChartOptions } from '../chart';
-import Style from './Overview.module.less';
-
-const options = getBarChartOptions();
-const Overview = (): React.ReactElement => {
- const [customOptions, setCustomOptions] = useState(options);
- const onTimeChange = (value: any) => {
- // eslint-disable-next-line no-shadow
- const options = getBarChartOptions(value);
- setCustomOptions(options);
- };
-
- const dynamicChartOption = useDynamicChart(customOptions, {
- placeholderColor: ['legend.textStyle.color', 'xAxis.axisLabel.color', 'yAxis.axisLabel.color'],
- });
-
- return (
-
-
-
-
-
-
-
-
- 导出数据} bordered={false}>
-
-
-
-
-
-
-
-
-
-
-
-
- );
-};
-
-export default React.memo(Overview);
diff --git a/admin/src/pages/Dashboard/Base/components/RankList.module.less b/admin/src/pages/Dashboard/Base/components/RankList.module.less
deleted file mode 100644
index c214369..0000000
--- a/admin/src/pages/Dashboard/Base/components/RankList.module.less
+++ /dev/null
@@ -1,19 +0,0 @@
-.rankListPanel {
- margin-top: 16px;
-}
-
-.rankIndex {
- display: inline-flex;
- width: 24px;
- height: 24px;
- border-radius: 50%;
- color: #fff;
- font-size: 14px;
- background-color: var(--td-gray-color-5);
- align-items: center;
- justify-content: center;
- font-weight: 700;
-}
-.rankIndexTop {
- background: var(--td-brand-color);
-}
diff --git a/admin/src/pages/Dashboard/Base/components/RankList.tsx b/admin/src/pages/Dashboard/Base/components/RankList.tsx
deleted file mode 100644
index e04de55..0000000
--- a/admin/src/pages/Dashboard/Base/components/RankList.tsx
+++ /dev/null
@@ -1,133 +0,0 @@
-import React from 'react';
-import { Col, Radio, Row, Table, Button, Card } from 'tdesign-react';
-import { TdPrimaryTableProps } from 'tdesign-react/es/table';
-import classnames from 'classnames';
-import { TrendIcon, ETrend } from 'components/Board';
-import { PURCHASE_TREND_LIST, SALE_TREND_LIST } from '../constant';
-import Style from './RankList.module.less';
-
-const DateRadioGroup = (
-
- 本周
- 近三个月
-
-);
-
-const SALE_COLUMNS: TdPrimaryTableProps['columns'] = [
- {
- align: 'center',
- colKey: 'index',
- title: '排名',
- width: 80,
- fixed: 'left',
- cell: ({ rowIndex }) => (
- {rowIndex + 1}
- ),
- },
- {
- align: 'left',
- ellipsis: true,
- colKey: 'productName',
- title: '客户名称',
- width: 200,
- },
- {
- align: 'center',
- colKey: 'growUp',
- width: 100,
- title: '较上周',
- cell: ({ row }) => ,
- },
- {
- align: 'center',
- colKey: 'count',
- title: '订单量',
- width: 100,
- },
- {
- align: 'center',
- colKey: 'date',
- width: 140,
- title: '合同签订日期',
- },
- {
- align: 'center',
- colKey: 'operation',
- fixed: 'right',
- title: '操作',
- width: 80,
- cell: ({ row }) => (
-
- ),
- },
-];
-
-const PURCHASE_COLUMNS: TdPrimaryTableProps['columns'] = [
- {
- align: 'center',
- colKey: 'index',
- title: '排名',
- width: 80,
- fixed: 'left',
- cell: ({ rowIndex }) => (
- {rowIndex + 1}
- ),
- },
- {
- align: 'left',
- ellipsis: true,
- colKey: 'productName',
- title: '供应商名称',
- width: 200,
- },
- {
- align: 'center',
- colKey: 'growUp',
- width: 100,
- title: '较上周',
- cell: ({ row }) => ,
- },
- {
- align: 'center',
- colKey: 'count',
- title: '订单量',
- width: 100,
- },
- {
- align: 'center',
- colKey: 'date',
- width: 140,
- title: '合同签订日期',
- },
- {
- align: 'center',
- colKey: 'operation',
- title: '操作',
- fixed: 'right',
- width: 80,
- cell: ({ row }) => (
-
- ),
- },
-];
-
-const RankList = () => (
-
-
-
-
-
-
-
-
-
-
-
-
-);
-
-export default React.memo(RankList);
diff --git a/admin/src/pages/Dashboard/Base/components/TopPanel.module.less b/admin/src/pages/Dashboard/Base/components/TopPanel.module.less
deleted file mode 100644
index 69a3cd8..0000000
--- a/admin/src/pages/Dashboard/Base/components/TopPanel.module.less
+++ /dev/null
@@ -1,35 +0,0 @@
-.iconWrap {
- display: inline-flex;
- align-items: center;
- justify-content: center;
- width: 56px;
- height: 56px;
- background: var(--td-brand-color-1);
- border-radius: 50%;
- position: absolute;
- top: 32px;
- right: 32px;
-}
-
-.svgIcon {
- font-size: 24px;
- color: var(--td-brand-color);
-}
-
-.paneLineChart {
- width: 103px;
- height: 56px;
- overflow: hidden;
- position: absolute;
- top: 55px;
- right: 32px;
-}
-.paneBarChart {
- width: 103px;
- height: 36px;
- -webkit-tap-highlight-color: transparent;
- user-select: none;
- position: absolute;
- top: 55px;
- right: 32px;
-}
diff --git a/admin/src/pages/Dashboard/Base/components/TopPanel.tsx b/admin/src/pages/Dashboard/Base/components/TopPanel.tsx
deleted file mode 100644
index 3ead175..0000000
--- a/admin/src/pages/Dashboard/Base/components/TopPanel.tsx
+++ /dev/null
@@ -1,92 +0,0 @@
-import React, { memo } from 'react';
-import { Col, Row } from 'tdesign-react';
-import { UsergroupIcon, FileIcon } from 'tdesign-icons-react';
-import ReactEcharts from 'echarts-for-react';
-import Board, { ETrend, IBoardProps } from 'components/Board';
-import useDynamicChart from 'hooks/useDynamicChart';
-import { MICRO_CHART_OPTIONS_BAR, MICRO_CHART_OPTIONS_LINE } from '../chart';
-import Style from './TopPanel.module.less';
-
-const BarChartIcon = memo(() => {
- const dynamicChartOption = useDynamicChart(MICRO_CHART_OPTIONS_BAR);
- return (
-
-
-
- );
-});
-
-const PieChartIcon = memo(() => (
-
-
-
-));
-
-const PANE_LIST: Array = [
- {
- title: '总收入',
- count: '¥ 28,425.00',
- trend: ETrend.up,
- trendNum: '20.5%',
- Icon: ,
- },
- {
- title: '总退款',
- count: '¥ 768.00',
- trend: ETrend.down,
- trendNum: '20.5%',
- Icon: ,
- },
- {
- title: '活跃用户(个)',
- count: '1126',
- trend: ETrend.down,
- trendNum: '20.5%',
- Icon: (
-
-
-
- ),
- },
- {
- title: '产生订单(个)',
- count: '527',
- trend: ETrend.down,
- trendNum: '20.5%',
- Icon: (
-
-
-
- ),
- },
-];
-
-const TopPanel = () => (
-
- {PANE_LIST.map((item, index) => (
-
-
-
- ))}
-
-);
-
-export default React.memo(TopPanel);
diff --git a/admin/src/pages/Dashboard/Base/constant.ts b/admin/src/pages/Dashboard/Base/constant.ts
deleted file mode 100644
index cd7dc6a..0000000
--- a/admin/src/pages/Dashboard/Base/constant.ts
+++ /dev/null
@@ -1,77 +0,0 @@
-export const SALE_TREND_LIST = [
- {
- growUp: 1,
- productName: '国家电网有限公司',
- count: 7059,
- date: '2021-09-01',
- },
- {
- growUp: -1,
- productName: '深圳燃气集团股份有限公司',
- count: 6437,
- date: '2021-09-01',
- },
- {
- growUp: 4,
- productName: '国家烟草专卖局',
- count: 4221,
- date: '2021-09-01',
- },
- {
- growUp: 3,
- productName: '中国电信集团有限公司',
- count: 3317,
- date: '2021-09-01',
- },
- {
- growUp: -3,
- productName: '中国移动通信集团有限公司',
- count: 3015,
- date: '2021-09-01',
- },
- {
- growUp: -3,
- productName: '新余市办公用户采购项目',
- count: 2015,
- date: '2021-09-12',
- },
-];
-
-export const PURCHASE_TREND_LIST = [
- {
- growUp: 1,
- productName: '腾讯科技(深圳)有限公司',
- count: 3015,
- date: '2021-09-01',
- },
- {
- growUp: -1,
- productName: '大润发有限公司',
- count: 2015,
- date: '2021-09-01',
- },
- {
- growUp: 6,
- productName: '四川海底捞股份有限公司',
- count: 1815,
- date: '2021-09-11',
- },
- {
- growUp: -3,
- productName: '索尼(中国)有限公司',
- count: 1015,
- date: '2021-09-21',
- },
- {
- growUp: -4,
- productName: '松下电器(中国)有限公司',
- count: 445,
- date: '2021-09-19',
- },
- {
- growUp: -3,
- productName: '新余市办公用户采购项目',
- count: 2015,
- date: '2021-09-12',
- },
-];
diff --git a/admin/src/pages/Dashboard/Base/index.tsx b/admin/src/pages/Dashboard/Base/index.tsx
deleted file mode 100644
index 653ee98..0000000
--- a/admin/src/pages/Dashboard/Base/index.tsx
+++ /dev/null
@@ -1,16 +0,0 @@
-import React, { memo } from 'react';
-import TopPanel from './components/TopPanel';
-import MiddleChart from './components/MiddleChart';
-import RankList from './components/RankList';
-import Overview from './components/Overview';
-
-const DashBoard = () => (
-
-
-
-
-
-
-);
-
-export default memo(DashBoard);
diff --git a/admin/src/pages/Dashboard/Detail/chart.ts b/admin/src/pages/Dashboard/Detail/chart.ts
deleted file mode 100644
index 68b5790..0000000
--- a/admin/src/pages/Dashboard/Detail/chart.ts
+++ /dev/null
@@ -1,203 +0,0 @@
-import type { EChartOption } from 'echarts';
-
-import { ONE_WEEK_LIST, getChartDataSet, getTimeArray, getRandomInt } from 'utils/chart';
-
-// line chart options
-export const getLineChartOptions = (dateTime: Array = []): EChartOption => {
- let dateArray: Array = ONE_WEEK_LIST;
- if (dateTime.length > 0) {
- const dividedNum = 7;
- dateArray = getTimeArray(dateTime, dividedNum, 'YYYY-MM-DD');
- }
-
- return {
- grid: {
- top: '5%',
- right: '10px',
- left: '30px',
- bottom: '60px',
- },
- legend: {
- left: 'center',
- bottom: '0',
- orient: 'horizontal', // legend 横向布局。
- data: ['杯子', '茶叶', '蜂蜜', '面粉'],
- },
- xAxis: {
- type: 'category',
- data: dateArray,
- boundaryGap: false,
- axisLabel: {
- color: 'rgba(0, 0, 0, 0.4)',
- },
- axisLine: {
- lineStyle: {
- color: '#E3E6EB',
- width: 1,
- },
- },
- },
- yAxis: {
- type: 'value',
- axisLabel: {
- color: 'rgba(0, 0, 0, 0.4)',
- },
- },
- tooltip: {
- trigger: 'item',
- },
- series: [
- {
- showSymbol: true,
- symbol: 'circle',
- symbolSize: 8,
- name: '杯子',
- stack: '总量',
- data: [
- getRandomInt(),
- getRandomInt(),
- getRandomInt(),
- getRandomInt(),
- getRandomInt(),
- getRandomInt(),
- getRandomInt(),
- ],
- type: 'line',
- itemStyle: {
- borderColor: '#ffffff',
- borderWidth: 1,
- },
- },
- {
- showSymbol: true,
- symbol: 'circle',
- symbolSize: 8,
- name: '茶叶',
- stack: '总量',
- data: [
- getRandomInt(),
- getRandomInt(),
- getRandomInt(),
- getRandomInt(),
- getRandomInt(),
- getRandomInt(),
- getRandomInt(),
- ],
- type: 'line',
- itemStyle: {
- borderColor: '#ffffff',
- borderWidth: 1,
- },
- },
- {
- showSymbol: true,
- symbol: 'circle',
- symbolSize: 8,
- name: '蜂蜜',
- stack: '总量',
- data: [
- getRandomInt(),
- getRandomInt(),
- getRandomInt(),
- getRandomInt(),
- getRandomInt(),
- getRandomInt(),
- getRandomInt(),
- ],
- type: 'line',
- itemStyle: {
- borderColor: '#ffffff',
- borderWidth: 1,
- },
- },
- {
- showSymbol: true,
- symbol: 'circle',
- symbolSize: 8,
- name: '面粉',
- stack: '总量',
- data: [
- getRandomInt(),
- getRandomInt(),
- getRandomInt(),
- getRandomInt(),
- getRandomInt(),
- getRandomInt(),
- getRandomInt(),
- ],
- type: 'line',
- itemStyle: {
- borderColor: '#ffffff',
- borderWidth: 1,
- },
- },
- ],
- };
-};
-
-export const getScatterChartOptions = (dateTime: Array = []): EChartOption => {
- const [timeArray, inArray, outArray] = getChartDataSet(dateTime);
-
- return {
- xAxis: {
- data: timeArray,
- axisLabel: {
- color: 'rgba(0, 0, 0, 0.4)',
- },
- splitLine: { show: false },
- axisLine: {
- lineStyle: {
- color: '#E3E6EB',
- width: 1,
- },
- },
- },
- yAxis: {
- type: 'value',
- // splitLine: { show: false},
- axisLabel: {
- color: 'rgba(0, 0, 0, 0.4)',
- },
- nameTextStyle: {
- padding: [0, 0, 0, 60],
- },
- axisTick: {
- show: false,
- },
- axisLine: {
- show: false,
- },
- },
- tooltip: {
- trigger: 'item',
- },
- grid: {
- top: '5px',
- left: '25px',
- right: '5px',
- bottom: '60px',
- },
- legend: {
- left: 'center',
- bottom: '0',
- orient: 'horizontal', // legend 横向布局。
- data: ['按摩仪', '咖啡机'],
- itemHeight: 8,
- itemWidth: 8,
- },
- series: [
- {
- name: '按摩仪',
- symbolSize: 10,
- data: outArray,
- type: 'scatter',
- },
- {
- name: '咖啡机',
- symbolSize: 10,
- data: inArray,
- type: 'scatter',
- },
- ],
- };
-};
diff --git a/admin/src/pages/Dashboard/Detail/components/MonthPurchase.tsx b/admin/src/pages/Dashboard/Detail/components/MonthPurchase.tsx
deleted file mode 100644
index c00afd4..0000000
--- a/admin/src/pages/Dashboard/Detail/components/MonthPurchase.tsx
+++ /dev/null
@@ -1,25 +0,0 @@
-import React from 'react';
-import { Col, Row, Card } from 'tdesign-react';
-import Board from 'components/Board';
-import { PANE_LIST } from '../constant';
-
-const MonthPurchase = () => (
-
-
- {PANE_LIST.map((item) => (
-
-
-
- ))}
-
-
-);
-
-export default React.memo(MonthPurchase);
diff --git a/admin/src/pages/Dashboard/Detail/components/PurchaseThrend.module.less b/admin/src/pages/Dashboard/Detail/components/PurchaseThrend.module.less
deleted file mode 100644
index 834f579..0000000
--- a/admin/src/pages/Dashboard/Detail/components/PurchaseThrend.module.less
+++ /dev/null
@@ -1,56 +0,0 @@
-.purchaseTrendPanel {
- margin-top: 16px;
-}
-
-.productTrendPanel {
- display: flex;
- flex-direction: column;
- border-radius: 3px;
- overflow: hidden;
- cursor: pointer;
- color: var(--td-text-color-secondary);
- background: var(--td-bg-color-container);
- padding: 24px 32px;
-}
-
-.productLogo {
- width: 56px;
- height: 56px;
- border-radius: 50%;
- display: flex;
- justify-content: center;
- align-items: center;
- background: var(--td-brand-color-1);
- font-size: 32px;
- color: var(--td-brand-color);
-}
-
-.productName {
- margin: 24px 0 8px;
- font-size: 16px;
- font-weight: 400;
- color: var(--td-text-color-primary);
-}
-
-.productDesc {
- font-size: 12px;
- line-height: 20px;
- overflow: hidden;
- text-overflow: ellipsis;
- display: -webkit-box;
- -webkit-line-clamp: 2;
- -webkit-box-orient: vertical;
- margin-bottom: 24px;
- height: 40px;
-}
-
-.iconWrap {
- position: relative;
- .lightBtn {
- position: absolute;
- left: 22px;
- background-color: var(--td-brand-color-2);
- --ripple-color: var(--td-brand-color-2);
- color: var(--td-brand-color);
- }
-}
diff --git a/admin/src/pages/Dashboard/Detail/components/PurchaseTrend.tsx b/admin/src/pages/Dashboard/Detail/components/PurchaseTrend.tsx
deleted file mode 100644
index 36726a0..0000000
--- a/admin/src/pages/Dashboard/Detail/components/PurchaseTrend.tsx
+++ /dev/null
@@ -1,101 +0,0 @@
-import React, { useState } from 'react';
-import { Col, Dropdown, Row, Tag, Card, Avatar } from 'tdesign-react';
-import { Icon, AddIcon } from 'tdesign-icons-react';
-import ReactEcharts from 'echarts-for-react';
-import LastWeekDatePicker from 'components/DatePicker';
-import useDynamicChart from 'hooks/useDynamicChart';
-import { PRODUCT_LIST } from '../constant';
-import { getLineChartOptions } from '../chart';
-import Style from './PurchaseThrend.module.less';
-
-interface IProductTrendProps {
- description: string;
- isSetup: boolean;
- name: string;
- type: string;
- icon: string;
-}
-
-const ProductTrend = ({ type, isSetup, description, name, icon }: IProductTrendProps) => (
-
-
-
-
-
- {isSetup ? '已启用' : '已停用'}
-
-
-
{name}
-
{description}
-
-
- {type}
- }>
-
- {},
- },
- {
- content: '删除',
- value: 'delete',
- onClick: () => {},
- },
- ]}
- >
-
-
-
-
-);
-
-const options = getLineChartOptions();
-
-const PurchaseTrend = () => {
- const [customOptions, setCustomOptions] = useState(options);
-
- const onTimeChange = (value: Array) => {
- const options = getLineChartOptions(value);
- setCustomOptions(options);
- };
-
- const dynamicChartOptions = useDynamicChart(customOptions, {
- placeholderColor: ['legend.textStyle.color', 'xAxis.axisLabel.color', 'yAxis.axisLabel.color'],
- });
-
- return (
-
-
-
-
-
-
-
-
- {PRODUCT_LIST.map((item, index) => (
-
-
-
- ))}
-
-
-
- );
-};
-
-export default React.memo(PurchaseTrend);
diff --git a/admin/src/pages/Dashboard/Detail/components/Satisfaction.module.less b/admin/src/pages/Dashboard/Detail/components/Satisfaction.module.less
deleted file mode 100644
index 6cf0dd7..0000000
--- a/admin/src/pages/Dashboard/Detail/components/Satisfaction.module.less
+++ /dev/null
@@ -1,14 +0,0 @@
-.satisfactionPanel {
- margin-top: 16px;
-}
-
-.operation {
- display: flex;
-}
-
-.exportBtn {
- margin-left: 8px;
-}
-
-
-
diff --git a/admin/src/pages/Dashboard/Detail/components/Satisfaction.tsx b/admin/src/pages/Dashboard/Detail/components/Satisfaction.tsx
deleted file mode 100644
index 15e15eb..0000000
--- a/admin/src/pages/Dashboard/Detail/components/Satisfaction.tsx
+++ /dev/null
@@ -1,44 +0,0 @@
-import React, { useState } from 'react';
-import { Button, Card } from 'tdesign-react';
-import ReactEcharts from 'echarts-for-react';
-import LastWeekDatePicker from 'components/DatePicker';
-import useDynamicChart from 'hooks/useDynamicChart';
-import { getScatterChartOptions } from '../chart';
-import Style from './Satisfaction.module.less';
-
-const Satisfaction = () => {
- const options = getScatterChartOptions();
- const [customOptions, setCustomOptions] = useState(options);
- const onTimeChange = (value: Array) => {
- const options = getScatterChartOptions(value);
- setCustomOptions(options);
- };
-
- const dynamicChartOption = useDynamicChart(customOptions, {
- placeholderColor: ['legend.textStyle.color', 'xAxis.axisLabel.color', 'yAxis.axisLabel.color'],
- });
-
- return (
-
-
- {LastWeekDatePicker(onTimeChange)}
-
-
- }
- bordered={false}
- >
-
-
-
- );
-};
-
-export default React.memo(Satisfaction);
diff --git a/admin/src/pages/Dashboard/Detail/constant.ts b/admin/src/pages/Dashboard/Detail/constant.ts
deleted file mode 100644
index 4381a4a..0000000
--- a/admin/src/pages/Dashboard/Detail/constant.ts
+++ /dev/null
@@ -1,59 +0,0 @@
-import { IBoardProps, ETrend } from 'components/Board';
-
-export const PANE_LIST: Array = [
- {
- title: '总申请数(次)',
- count: '1126',
- trendNum: '10%',
- trend: ETrend.up,
- },
- {
- title: '供应商数量(个)',
- count: '13',
- trendNum: '13%',
- trend: ETrend.down,
- },
- {
- title: '采购商品品类(类)',
- count: '4',
- trendNum: '10%',
- trend: ETrend.up,
- },
- {
- title: '申请人数量(人)',
- count: '90',
- trendNum: '44%',
- trend: ETrend.down,
- },
- {
- title: '申请完成率(%)',
- count: '80.5',
- trendNum: '70%',
- trend: ETrend.up,
- },
- {
- title: '到货及时率(%)',
- count: '78',
- trendNum: '16%',
- trend: ETrend.up,
- },
-];
-
-export const PRODUCT_LIST = [
- {
- description: 'SSL证书又叫服务器证书,腾讯云为您提供证书的一站式服务,包括免费、付费证书的申请、管理及部',
- index: 1,
- isSetup: true,
- name: 'SSL证书',
- type: 'D',
- icon: 'user',
- },
- {
- description: 'SSL证书又叫服务器证书,腾讯云为您提供证书的一站式服务,包括免费、付费证书的申请、管理及部',
- index: 1,
- isSetup: true,
- name: 'SSL证书',
- type: 'C',
- icon: 'calendar',
- },
-];
diff --git a/admin/src/pages/Dashboard/Detail/index.tsx b/admin/src/pages/Dashboard/Detail/index.tsx
deleted file mode 100644
index 19c893f..0000000
--- a/admin/src/pages/Dashboard/Detail/index.tsx
+++ /dev/null
@@ -1,12 +0,0 @@
-import React, { memo } from 'react';
-import MonthPurchase from './components/MonthPurchase';
-import PurchaseTrend from './components/PurchaseTrend';
-import PurchaseSatisfaction from './components/Satisfaction';
-
-export default memo(() => (
-
-));
diff --git a/admin/src/pages/Detail/Advanced/components/Base.module.less b/admin/src/pages/Detail/Advanced/components/Base.module.less
deleted file mode 100644
index 6876837..0000000
--- a/admin/src/pages/Detail/Advanced/components/Base.module.less
+++ /dev/null
@@ -1,41 +0,0 @@
-.infoBox {
- column-count: 2;
- .infoBoxItem {
- padding: 12px 0;
- display: flex;
- color: var(--td-text-color-primary);
- h1 {
- font-weight: 400;
- font-size: 14px;
- color: var(--td-text-color-secondary);
- text-align: left;
- line-height: 22px;
- margin: 0;
- }
- span {
- margin-left: 24px;
- font-size: 14px;
- line-height: 1.5;
- overflow: hidden;
- white-space: nowrap;
- text-overflow: ellipsis;
- }
- i {
- display: inline-block;
- width: 8px;
- height: 8px;
- border-radius: 50%;
- background: var(--td-success-color-5);
- margin-right: 5px;
- }
- .inProgress {
- color: var(--td-success-color-5);
- }
- .pdf {
- color: var(--td-brand-color);
- &:hover {
- cursor: pointer;
- }
- }
- }
-}
diff --git a/admin/src/pages/Detail/Advanced/components/Base.tsx b/admin/src/pages/Detail/Advanced/components/Base.tsx
deleted file mode 100644
index 488d9c1..0000000
--- a/admin/src/pages/Detail/Advanced/components/Base.tsx
+++ /dev/null
@@ -1,28 +0,0 @@
-import React from 'react';
-import { Card } from 'tdesign-react';
-import classnames from 'classnames';
-import { dataInfo } from '../consts';
-import Style from './Base.module.less';
-
-const Base = () => (
-
-
- {dataInfo.map((item) => (
-
-
{item.name}
-
- {item.type === 'status' && }
- {item.value}
-
-
- ))}
-
-
-);
-
-export default React.memo(Base);
diff --git a/admin/src/pages/Detail/Advanced/components/Detail.tsx b/admin/src/pages/Detail/Advanced/components/Detail.tsx
deleted file mode 100644
index 71dc894..0000000
--- a/admin/src/pages/Detail/Advanced/components/Detail.tsx
+++ /dev/null
@@ -1,110 +0,0 @@
-import React from 'react';
-import { Button, Table, Tag, Card } from 'tdesign-react';
-import { dataBuyList, total } from '../consts';
-import Style from '../index.module.less';
-
-const Detail = () => (
-
-
- {row.name}
-
- {row.tag}
-
- >
- );
- },
- },
- {
- align: 'left',
- width: 200,
- ellipsis: true,
- colKey: 'code',
- title: '产品编号',
- },
- {
- align: 'left',
- width: 100,
- ellipsis: true,
- colKey: 'amount',
- title: '采购数量',
- cell({ row }) {
- return row.amount;
- },
- },
- {
- align: 'left',
- width: 200,
- ellipsis: true,
- colKey: 'department',
- title: '申请部门',
- },
- {
- align: 'left',
- width: 500,
- ellipsis: true,
- colKey: 'createtime',
- title: '创建时间',
- },
- {
- align: 'left',
- fixed: 'right',
- width: 300,
- colKey: 'op',
- title: '操作',
- cell() {
- return (
- <>
-
-
- >
- );
- },
- },
- ]}
- rowKey='id'
- verticalAlign='top'
- hover
- pagination={{
- defaultPageSize: 10,
- total,
- defaultCurrent: 1,
- showJumper: true,
- onChange(pageInfo) {
- console.log(pageInfo, 'onChange pageInfo');
- },
- onCurrentChange(current, pageInfo) {
- console.log(current, 'onCurrentChange current');
- console.log(pageInfo, 'onCurrentChange pageInfo');
- },
- onPageSizeChange(size, pageInfo) {
- console.log(size, 'onPageSizeChange size');
- console.log(pageInfo, 'onPageSizeChange pageInfo');
- },
- }}
- />
-
-);
-
-export default React.memo(Detail);
diff --git a/admin/src/pages/Detail/Advanced/components/Product.tsx b/admin/src/pages/Detail/Advanced/components/Product.tsx
deleted file mode 100644
index 4370f62..0000000
--- a/admin/src/pages/Detail/Advanced/components/Product.tsx
+++ /dev/null
@@ -1,41 +0,0 @@
-import React from 'react';
-import { Col, Row, Card } from 'tdesign-react';
-import ProductCard from './ProductCard';
-import Style from '../index.module.less';
-
-const Product = () => (
-
-
-
-);
-
-export default React.memo(Product);
diff --git a/admin/src/pages/Detail/Advanced/components/ProductCard.module.less b/admin/src/pages/Detail/Advanced/components/ProductCard.module.less
deleted file mode 100644
index 49c512b..0000000
--- a/admin/src/pages/Detail/Advanced/components/ProductCard.module.less
+++ /dev/null
@@ -1,108 +0,0 @@
-.productionCard {
- position: relative;
- background-color: var(--td-bg-color-container);
- border: 1px solid var(--td-component-border);
- border-radius: 3px;
- padding: 20px 32px 24px;
- height: 212px;
- box-sizing: content-box;
-}
-
-.productionTitle {
- margin-bottom: 25px;
- position: relative;
-}
-
-.title {
- font-weight: 500;
- font-size: 24px;
- color: var(--td-text-color-primary);
-}
-
-.productionIcon {
- position: absolute;
- top: 0px;
- right: 0px;
- background: var(--td-brand-color-1);
- color: var(--td-brand-color);
- font-size: 56px;
- padding: 14px;
- border-radius: 100%;
-}
-
-.tag {
- margin-right: 4px;
- margin-top: 8px;
- margin-left: unset;
- border: unset;
-}
-
-.item {
- position: relative;
- padding-top: 8px;
- padding-bottom: 8px;
-}
-
-.info {
- display: inline-block;
- width: 60%;
- text-align: left;
- font-size: 14px;
- color: var(--td-text-color-placeholder);
-}
-
-.icon {
- position: absolute;
- bottom: 8px;
- right: 0;
- color: rgba(0, 0, 0, 0.26);
-}
-
-.footer {
- position: absolute;
- width: 100%;
- bottom: 0px;
- left: 0;
- .percent {
- position: absolute;
- bottom: 15px;
- right: 32px;
- color: var(--td-text-color-placeholder);
- }
- .progress {
- margin: 0;
- }
-}
-
-.productionAdd {
- position: relative;
- background-color: var(--td-bg-color-container);
- border: 1px dashed var(--td-component-border);
- border-radius: 3px;
- padding: 20px 32px 24px;
- height: 212px;
- box-sizing: content-box;
- display: flex;
- justify-content: center;
- align-items: center;
-}
-
-.productionAddBtn {
- font-size: 14px;
- color: var(--td-text-color-secondary);
- margin: 0 auto;
- text-align: center;
- line-height: 22px;
- display: flex;
- flex-direction: column;
- align-items: center;
- cursor: pointer;
- .productionAddIcon {
- background: var(--td-brand-color-1);
- color: var(--td-brand-color);
- font-size: 33px;
- padding: 8px;
- border-radius: 100%;
- margin-bottom: 12px;
- }
-}
diff --git a/admin/src/pages/Detail/Advanced/components/ProductCard.tsx b/admin/src/pages/Detail/Advanced/components/ProductCard.tsx
deleted file mode 100644
index e9963bf..0000000
--- a/admin/src/pages/Detail/Advanced/components/ProductCard.tsx
+++ /dev/null
@@ -1,55 +0,0 @@
-import React from 'react';
-import { Progress, Tag } from 'tdesign-react';
-import { AddIcon, ChevronRightIcon, Icon } from 'tdesign-icons-react';
-import Styles from './ProductCard.module.less';
-
-interface IProps {
- isAdd?: boolean;
- title?: string;
- tags?: string[];
- desc?: string;
- progress?: number;
- percent?: string;
- Icon?: string;
- color?: string;
- trackColor?: string;
-}
-
-const ProductCard = (props: IProps) => (
-
- {props.isAdd ? (
-
- ) : (
-
-
-
-
{props.title}
-
- {props?.tags?.map((tag, index) => (
-
- {tag}
-
- ))}
-
-
-
- {props.desc}
-
-
-
-
- )}
-
-);
-
-export default React.memo(ProductCard);
diff --git a/admin/src/pages/Detail/Advanced/components/Progress.tsx b/admin/src/pages/Detail/Advanced/components/Progress.tsx
deleted file mode 100644
index 084201c..0000000
--- a/admin/src/pages/Detail/Advanced/components/Progress.tsx
+++ /dev/null
@@ -1,18 +0,0 @@
-import React from 'react';
-import { Steps, Card } from 'tdesign-react';
-import { dataStep, stepCurrent } from '../consts';
-import Style from '../index.module.less';
-
-const { StepItem } = Steps;
-
-const Progress = () => (
-
-
- {dataStep.map((item) => (
-
- ))}
-
-
-);
-
-export default React.memo(Progress);
diff --git a/admin/src/pages/Detail/Advanced/consts.ts b/admin/src/pages/Detail/Advanced/consts.ts
deleted file mode 100644
index 4b90042..0000000
--- a/admin/src/pages/Detail/Advanced/consts.ts
+++ /dev/null
@@ -1,54 +0,0 @@
-// Mock Data of 基本信息
-interface InfoItem {
- id: number;
- name: string;
- value: string;
- type?: string;
-}
-export const dataInfo: InfoItem[] = [
- { id: 1, name: '合同名称', value: '总部办公用品采购项目' },
- { id: 2, name: '合同状态', value: '履行中', type: 'status' },
- { id: 3, name: '合同编号', value: 'BH00010' },
- { id: 4, name: '合同类型', value: '主合同' },
- { id: 5, name: '合同收付类型', value: '付款' },
- { id: 6, name: '合同金额', value: '5,000,000元' },
- { id: 7, name: '甲方', value: '腾讯科技(深圳)有限公司' },
- { id: 8, name: '乙方', value: '欧尚' },
- { id: 9, name: '合同签订日期', value: '2020-12-20' },
- { id: 10, name: '合同生效日期', value: '2021-01-20' },
- { id: 11, name: '合同结束日期', value: '2022-12-20' },
- { id: 12, name: '合同附件', value: '总部办公用品采购项目合同.pdf', type: 'link' },
- { id: 13, name: '备注', value: '--' },
- { id: 14, name: '创建时间', value: '2020-12-22 10:00:00' },
-];
-
-// Mock Data of 变更记录
-interface IStepItem {
- id: number;
- name: string;
- detail?: string;
-}
-export const dataStep: IStepItem[] = [
- { id: 1, name: '申请提交', detail: '已于12月21日提交' },
- { id: 2, name: '电子发票', detail: '预计1~3个工作日' },
- { id: 3, name: '发票已邮寄', detail: '电子发票开出后7个工作日内联系' },
- { id: 4, name: '完成', detail: '' },
-];
-export const stepCurrent = 2;
-
-// Mock Data of 产品采购明细
-export const total = 36;
-const listTables: any = [];
-for (let i = 0; i < total; i++) {
- listTables.push({
- id: i,
- number: 'S20201228115950963',
- name: 'Macbook ',
- tag: '电子产品',
- code: 'p_tmp_60a637cd0d ',
- amount: 52,
- department: '财务部',
- createtime: '2021-12-30 10:43:56',
- });
-}
-export const dataBuyList = listTables;
diff --git a/admin/src/pages/Detail/Advanced/index.module.less b/admin/src/pages/Detail/Advanced/index.module.less
deleted file mode 100644
index 090de63..0000000
--- a/admin/src/pages/Detail/Advanced/index.module.less
+++ /dev/null
@@ -1,3 +0,0 @@
-.cardBox {
- margin-top: 15px;
-}
diff --git a/admin/src/pages/Detail/Advanced/index.tsx b/admin/src/pages/Detail/Advanced/index.tsx
deleted file mode 100644
index 5a8ed4b..0000000
--- a/admin/src/pages/Detail/Advanced/index.tsx
+++ /dev/null
@@ -1,14 +0,0 @@
-import React, { memo } from 'react';
-import Base from './components/Base';
-import ProgressComp from './components/Progress';
-import Product from './components/Product';
-import Detail from './components/Detail';
-
-export default memo(() => (
-
-));
diff --git a/admin/src/pages/Detail/Base/consts.ts b/admin/src/pages/Detail/Base/consts.ts
deleted file mode 100644
index f652f7e..0000000
--- a/admin/src/pages/Detail/Base/consts.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-export const dataInfo = [
- { id: 1, name: '合同名称', value: '总部办公用品采购项目' },
- { id: 2, name: '合同状态', value: '履行中', type: 'status' },
- { id: 3, name: '合同编号', value: 'BH00010' },
- { id: 4, name: '合同类型', value: '主合同' },
- { id: 5, name: '合同收付类型', value: '付款' },
- { id: 6, name: '合同金额', value: '5,000,000元' },
- { id: 7, name: '甲方', value: '腾讯科技(深圳)有限公司' },
- { id: 8, name: '乙方', value: '欧尚' },
- { id: 9, name: '合同签订日期', value: '2020-12-20' },
- { id: 10, name: '合同生效日期', value: '2021-01-20' },
- { id: 11, name: '合同结束日期', value: '2022-12-20' },
- { id: 12, name: '合同附件', value: '总部办公用品采购项目合同.pdf', type: 'link' },
- { id: 13, name: '备注', value: '--' },
- { id: 14, name: '创建时间', value: '2020-12-22 10:00:00' },
-];
-
-export const dataStep = [
- { id: 1, name: '上传合同附件', detail: '这里是提示文字' },
- { id: 2, name: '修改合同金额', detail: '这里是提示文字' },
- { id: 3, name: '新建合同', detail: '2020-12-01 15:00:00 管理员-李川操作' },
-];
diff --git a/admin/src/pages/Detail/Base/index.module.less b/admin/src/pages/Detail/Base/index.module.less
deleted file mode 100644
index aaeb89a..0000000
--- a/admin/src/pages/Detail/Base/index.module.less
+++ /dev/null
@@ -1,57 +0,0 @@
-@Wrap: infoBox;
-
-.@{Wrap} {
- column-count: 2;
- &Item {
- padding: 12px 0;
- display: flex;
- color: var(--td-text-color-primary);
- h1 {
- font-weight: 400;
- font-size: 14px;
- color: var(--td-text-color-secondary);
- text-align: left;
- line-height: 22px;
- margin: 0;
- }
- span {
- margin-left: 24px;
- font-size: 14px;
- line-height: 1.5;
- overflow: hidden;
- white-space: nowrap;
- text-overflow: ellipsis;
- }
- i {
- display: inline-block;
- width: 8px;
- height: 8px;
- border-radius: 50%;
- background: var(--td-success-color-5);
- margin-right: 5px;
- }
- .inProgress {
- color: var(--td-success-color-5);
- }
- .pdf {
- color: var(--td-brand-color);
- &:hover {
- cursor: pointer;
- }
- }
- }
-}
-
-.logBox {
- margin-top: 15px;
-}
-
-@media (min-width: 992px) {
- .@{Wrap} {
- &Item {
- h1 {
- width: 200px;
- }
- }
- }
-}
diff --git a/admin/src/pages/Detail/Base/index.tsx b/admin/src/pages/Detail/Base/index.tsx
deleted file mode 100644
index df945d7..0000000
--- a/admin/src/pages/Detail/Base/index.tsx
+++ /dev/null
@@ -1,39 +0,0 @@
-import React, { memo } from 'react';
-import { Steps, Card } from 'tdesign-react';
-import classnames from 'classnames';
-import { dataInfo, dataStep } from './consts';
-import Style from './index.module.less';
-
-const { StepItem } = Steps;
-
-export default memo(() => (
-
-
-
- {dataInfo.map((item) => (
-
-
{item.name}
-
- {item.type === 'status' && }
- {item.value}
-
-
- ))}
-
-
-
-
-
- {dataStep.map((item) => (
-
- ))}
-
-
-
-
-));
diff --git a/admin/src/pages/Detail/Deploy/chart.ts b/admin/src/pages/Detail/Deploy/chart.ts
deleted file mode 100644
index 908cc82..0000000
--- a/admin/src/pages/Detail/Deploy/chart.ts
+++ /dev/null
@@ -1,158 +0,0 @@
-import type { EChartOption } from 'echarts';
-import { getTimeArray, getRandomInt } from 'utils/chart';
-
-export function getLineOptions(dateTime: any = []): EChartOption {
- let dateArray: Array = ['00:00', '02:00', '04:00', '06:00'];
- if (dateTime.length > 0) {
- const divideNum = 7;
- dateArray = getTimeArray(dateTime, divideNum);
- }
-
- return {
- tooltip: {
- trigger: 'item',
- },
- grid: {
- top: '10px',
- left: '0',
- right: '20px',
- bottom: '36px',
- containLabel: true,
- },
- xAxis: {
- type: 'category',
- data: dateArray,
- boundaryGap: false,
- axisLine: {
- lineStyle: {
- color: '#E3E6EB',
- width: 1,
- },
- },
- },
- yAxis: {
- type: 'value',
- },
- legend: {
- data: ['本月', '上月'],
- icon: 'circle',
- bottom: '0',
- itemGap: 48,
- itemHeight: 8,
- itemWidth: 8,
- },
- series: [
- {
- name: '上月',
- data: [
- getRandomInt(),
- getRandomInt(),
- getRandomInt(),
- getRandomInt(),
- getRandomInt(),
- getRandomInt(),
- getRandomInt(),
- ],
- type: 'line',
- smooth: true,
- showSymbol: true,
- symbol: 'circle',
- symbolSize: 8,
- areaStyle: {
- color: '#0053D92F',
- },
- },
- {
- name: '本月',
- data: [
- getRandomInt(),
- getRandomInt(),
- getRandomInt(),
- getRandomInt(),
- getRandomInt(),
- getRandomInt(),
- getRandomInt(),
- ],
- type: 'line',
- smooth: true,
- showSymbol: true,
- symbol: 'circle',
- symbolSize: 8,
- },
- ],
- };
-}
-
-export const lastYearList: Array = [100, 120, 140, 160, 180, 200, 210];
-
-export function getBarOptions(isMonth = false): EChartOption {
- let lastYearListCopy = lastYearList.concat([]);
- let thisYearListCopy = lastYearList.concat([]);
-
- if (isMonth) {
- lastYearListCopy = lastYearListCopy.reverse();
- thisYearListCopy = thisYearListCopy.reverse();
- }
-
- return {
- tooltip: {
- trigger: 'item',
- },
- grid: {
- top: '10px',
- left: '0',
- right: '0',
- bottom: '36px',
- containLabel: true,
- },
- xAxis: [
- {
- type: 'category',
- data: ['周一', '周二', '周三', '周四', '周五', '周六', '周日'],
- axisTick: {
- alignWithLabel: true,
- },
- axisLine: {
- lineStyle: {
- color: '#E3E6EB',
- width: 1,
- },
- },
- },
- ],
- yAxis: [
- {
- type: 'value',
- axisLabel: {
- color: 'rgba(0, 0, 0, 0.4)',
- },
- },
- ],
- legend: {
- data: ['去年', '今年'],
- bottom: '0',
- icon: 'rect',
- itemGap: 48,
- itemHeight: 4,
- itemWidth: 12,
- // itemStyle: {},
- },
- series: [
- {
- name: '去年',
- type: 'bar',
- barWidth: '30%',
- data: lastYearListCopy,
- itemStyle: {
- color: '#BCC4D0',
- },
- },
- {
- name: '今年',
- type: 'bar',
- barWidth: '30%',
- data: thisYearListCopy,
- },
- ],
- };
-}
diff --git a/admin/src/pages/Detail/Deploy/components/BottomTable.module.less b/admin/src/pages/Detail/Deploy/components/BottomTable.module.less
deleted file mode 100644
index ad01ace..0000000
--- a/admin/src/pages/Detail/Deploy/components/BottomTable.module.less
+++ /dev/null
@@ -1,10 +0,0 @@
-.operationLink {
- color: var(--td-brand-color);
- text-decoration: none;
- margin-right: 24px;
- cursor: pointer;
- transition: color 0.2s cubic-bezier(0.38, 0, 0.24, 1);
- &:last-child {
- margin-right: 0;
- }
-}
diff --git a/admin/src/pages/Detail/Deploy/components/BottomTable.tsx b/admin/src/pages/Detail/Deploy/components/BottomTable.tsx
deleted file mode 100644
index c384a73..0000000
--- a/admin/src/pages/Detail/Deploy/components/BottomTable.tsx
+++ /dev/null
@@ -1,72 +0,0 @@
-import React, { useEffect, useState } from 'react';
-import { Table, Card } from 'tdesign-react';
-import { TableSort, TdPrimaryTableProps } from 'tdesign-react/es/table';
-import request from 'utils/request';
-import { TABLE_COLUMNS } from '../constant';
-import ManagementPopup from './ManagementPopup';
-import Style from './BottomTable.module.less';
-
-const BottomTable = () => {
- const [sort, setSort] = useState({ sortBy: 'name', descending: true });
- const [visible, setVisible] = useState(false);
- const [{ tableData }, setTableData] = useState({ tableData: [] });
- const pagination = {
- pageSize: 10,
- total: tableData.length,
- pageSizeOptions: [],
- };
-
- useEffect(() => {
- request.get('/api/get-project-list').then((res: any) => {
- if (res.code === 0) {
- const { list = [] } = res.data;
- setTableData({ tableData: list });
- }
- });
- }, []);
-
- const removeRow = (rowIndex: number) => {
- console.log(' rowIndex = ', rowIndex);
- console.log(' tableData = ', tableData);
-
- tableData.splice(rowIndex, 1);
- setTableData({ tableData });
- };
-
- const getTableColumns = (columns: TdPrimaryTableProps['columns']): TdPrimaryTableProps['columns'] => {
- if (columns) {
- columns[4].cell = (context) => {
- const { rowIndex } = context;
- return (
- <>
- setVisible(!visible)}>
- 管理
-
- removeRow(rowIndex)}>
- 删除
-
- >
- );
- };
- }
- return columns;
- };
-
- return (
- <>
-
- setSort(newSort)}
- />
-
- {visible && }
- >
- );
-};
-
-export default React.memo(BottomTable);
diff --git a/admin/src/pages/Detail/Deploy/components/DynamicLineChart.tsx b/admin/src/pages/Detail/Deploy/components/DynamicLineChart.tsx
deleted file mode 100644
index 3406e37..0000000
--- a/admin/src/pages/Detail/Deploy/components/DynamicLineChart.tsx
+++ /dev/null
@@ -1,30 +0,0 @@
-import React, { useEffect, useState } from 'react';
-import { EChartOption } from 'echarts';
-import ReactEcharts from 'echarts-for-react';
-import useDynamicChart from 'hooks/useDynamicChart';
-import { getLineOptions } from '../chart';
-
-const DynamicLineChart = () => {
- const [lineOptions, setLineOptions] = useState(getLineOptions());
- const dynamicLineChartOptions = useDynamicChart(lineOptions, {
- placeholderColor: ['legend.textStyle.color', 'xAxis.axisLabel.color', 'yAxis.axisLabel.color'],
- });
-
- useEffect(() => {
- const timer = setInterval(() => setLineOptions(getLineOptions()), 3000);
- return () => {
- clearInterval(timer);
- };
- }, []);
-
- return (
-
- );
-};
-
-export default React.memo(DynamicLineChart);
diff --git a/admin/src/pages/Detail/Deploy/components/ManagementPopup.module.less b/admin/src/pages/Detail/Deploy/components/ManagementPopup.module.less
deleted file mode 100644
index 4e97bae..0000000
--- a/admin/src/pages/Detail/Deploy/components/ManagementPopup.module.less
+++ /dev/null
@@ -1,30 +0,0 @@
-.popupBox {
- max-height: 620px;
-}
-
-.popupItem {
- padding: 12px 0;
- display: flex;
- height: 46px;
- align-items: center;
- box-sizing: border-box;
- > h1 {
- margin: 0;
- width: 84px;
- font-size: 14px;
- color: var(--td-text-color-secondary);
- text-align: left;
- line-height: 22px;
- }
- > p {
- margin: 0 0 0 24px;
- }
-}
-
-.popupItem_blue {
- color: var(--td-brand-color-8);
-}
-
-.popupItem_green {
- color: var(--td-success-color-5);
-}
diff --git a/admin/src/pages/Detail/Deploy/components/ManagementPopup.tsx b/admin/src/pages/Detail/Deploy/components/ManagementPopup.tsx
deleted file mode 100644
index 6cbbaad..0000000
--- a/admin/src/pages/Detail/Deploy/components/ManagementPopup.tsx
+++ /dev/null
@@ -1,44 +0,0 @@
-import React, { useState } from 'react';
-import { Dialog } from 'tdesign-react';
-import classnames from 'classnames';
-import { BASE_INFO_DATA } from '../constant';
-import Style from './ManagementPopup.module.less';
-
-interface IProps {
- visible: boolean;
-}
-
-const ManagementPopup = ({ visible }: IProps): React.ReactElement => {
- const [isShow, setVisible] = useState(visible);
- const handleConfirm = () => setVisible(!isShow);
-
- return (
-
- );
-};
-
-export default React.memo(ManagementPopup);
diff --git a/admin/src/pages/Detail/Deploy/components/TopChart.module.less b/admin/src/pages/Detail/Deploy/components/TopChart.module.less
deleted file mode 100644
index bb16ba3..0000000
--- a/admin/src/pages/Detail/Deploy/components/TopChart.module.less
+++ /dev/null
@@ -1,8 +0,0 @@
-.panel {
- margin-bottom: 16px;
-}
-.deployPanelLeft {
- border-radius: 3px;
- min-width: 230px;
- width: 100%;
-}
diff --git a/admin/src/pages/Detail/Deploy/components/TopChart.tsx b/admin/src/pages/Detail/Deploy/components/TopChart.tsx
deleted file mode 100644
index 1e0f4cd..0000000
--- a/admin/src/pages/Detail/Deploy/components/TopChart.tsx
+++ /dev/null
@@ -1,51 +0,0 @@
-import React, { useState } from 'react';
-import { Col, Radio, Row, Card } from 'tdesign-react';
-import { EChartOption } from 'echarts';
-import ReactEcharts from 'echarts-for-react';
-import { getBarOptions } from '../chart';
-import useDynamicChart from 'hooks/useDynamicChart';
-import DynamicLineChart from './DynamicLineChart';
-import Style from './TopChart.module.less';
-
-const TopChart = () => {
- const [barOptions, setBarOptions] = useState(getBarOptions());
-
- const tabChange = (isMonth: boolean) => {
- setBarOptions(getBarOptions(isMonth));
- };
- const dynamicBarChartOptions = useDynamicChart(barOptions, {
- placeholderColor: ['legend.textStyle.color', 'xAxis.0.axisLabel.color', 'yAxis.0.axisLabel.color'],
- });
- return (
-
-
-
-
-
-
-
-
-
- tabChange(val === 'month')}>
- 本周
- 本月
-
- }
- bordered={false}
- >
-
-
-
-
- );
-};
-
-export default React.memo(TopChart);
diff --git a/admin/src/pages/Detail/Deploy/constant.ts b/admin/src/pages/Detail/Deploy/constant.ts
deleted file mode 100644
index 3d3299c..0000000
--- a/admin/src/pages/Detail/Deploy/constant.ts
+++ /dev/null
@@ -1,197 +0,0 @@
-import type { TdPrimaryTableProps, SorterFun, TableRowData } from 'tdesign-react/es/table';
-
-export interface TableModel {
- name: string;
- adminName: string;
- updateTime: string;
- telephone: string | number;
- op?: undefined | string | number;
-}
-
-const nameSorter: SorterFun = (a, b) => {
- const result = a.name.charCodeAt(0) - b.name.charCodeAt(0);
- if (result > 0) return 1;
- if (result < 0) return -1;
- return 0;
-};
-
-export const TABLE_COLUMNS: TdPrimaryTableProps['columns'] = [
- {
- width: '448',
- ellipsis: true,
- colKey: 'name',
- title: '项目名称',
- sortType: 'all',
- sorter: nameSorter,
- },
- {
- width: '224',
- ellipsis: true,
- colKey: 'adminName',
- title: '管理员',
- },
- {
- width: '224',
- ellipsis: true,
- colKey: 'telephone',
- title: '联系方式',
- },
- {
- width: '224',
- className: 'test',
- ellipsis: true,
- colKey: 'updateTime',
- title: '创建时间',
- sortType: 'all',
- sorter: true,
- },
- {
- align: 'left',
- width: '200',
- className: 'test2',
- ellipsis: true,
- colKey: 'op',
- fixed: 'right',
- title: '操作',
- },
-];
-
-export const POPUP_DATA = {
- formData: {
- name: '',
- warning: '',
- success: '',
- failB: '',
- warningB: '',
- loading: '',
- add: '',
- help: '',
- },
- options: [
- {
- label: '资源初始化后',
- value: 'beijing',
- },
- {
- label: '上海',
- value: 'shanghai',
- },
- ],
- options1: [
- {
- label: '资源初始化后',
- value: 'guangzhou',
- },
- {
- label: '深圳',
- value: 'shenzhen',
- },
- {
- label: '东莞',
- value: 'dongguang',
- },
- ],
- options2: [
- {
- label: '资源初始化后',
- value: 'nanjing',
- },
- {
- label: '苏州',
- value: '苏州',
- },
- {
- label: '无锡',
- value: 'wuxi',
- },
- ],
- tSelectOptions: [
- {
- label: 'Sanzhang',
- value: '1',
- },
- {
- label: 'ls',
- value: '2',
- },
- {
- label: 'James',
- value: '3',
- },
- ],
-};
-
-export const BASE_INFO_DATA = [
- {
- name: '集群名',
- value: 'helloworld',
- },
- {
- name: '集群ID',
- value: 'cls - 2ntelvxw',
- type: {
- key: 'color',
- value: 'blue',
- },
- },
- {
- name: '状态',
- value: '运行中',
- type: {
- key: 'color',
- value: 'green',
- },
- },
- {
- name: 'K8S版本',
- value: '1.7.8',
- },
- {
- name: '配置',
- value: '6.73 核 10.30 GB',
- },
- {
- name: '所在地域',
- value: '广州',
- },
- {
- name: '新增资源所属项目',
- value: '默认项目',
- },
- {
- name: '节点数量',
- value: '4 个',
- },
- {
- name: '节点网络',
- value: 'vpc - 5frmkm1x',
- type: {
- key: 'color',
- value: 'blue',
- },
- },
- {
- name: '容器网络',
- value: '172.16.0.0 / 16',
- },
- {
- name: '集群凭证',
- value: '显示凭证',
- type: {
- key: 'color',
- value: 'blue',
- },
- },
- {
- name: '创建/更新',
- value: '2018-05-31 22:11:44 2018-05-31 22:11:44',
- type: {
- key: 'contractAnnex',
- value: 'pdf',
- },
- },
- {
- name: '描述',
- value: 'istio_test',
- },
-];
diff --git a/admin/src/pages/Detail/Deploy/index.tsx b/admin/src/pages/Detail/Deploy/index.tsx
deleted file mode 100644
index 71de10d..0000000
--- a/admin/src/pages/Detail/Deploy/index.tsx
+++ /dev/null
@@ -1,12 +0,0 @@
-import React, { memo } from 'react';
-import TopChart from './components/TopChart';
-import BottomTable from './components/BottomTable';
-
-const Deploy = () => (
-
-
-
-
-);
-
-export default memo(Deploy);
diff --git a/admin/src/pages/Detail/Secondary/consts.ts b/admin/src/pages/Detail/Secondary/consts.ts
deleted file mode 100644
index 01a3a3d..0000000
--- a/admin/src/pages/Detail/Secondary/consts.ts
+++ /dev/null
@@ -1,60 +0,0 @@
-export type TStatus = 1 | 2; // 1: 未读 2:已读
-export interface IItem {
- id: number;
- content: string;
- tag: string;
- createTime: string;
- status: TStatus;
- type: number;
-}
-
-export const dataItemList: IItem[] = [
- {
- id: 1,
- content: '腾讯大厦一楼改造施工项目 已通过审核!',
- tag: '合同动态',
- createTime: '2021-01-01 08:00',
- status: 1,
- type: 1,
- },
- {
- id: 2,
- content: '三季度生产原材料采购项目 开票成功!',
- tag: '票务动态',
- createTime: '2021-01-01 08:00',
- status: 2,
- type: 2,
- },
- {
- id: 3,
- content: '2021-01-01 10:00的【国家电网线下签约】会议即将开始,请提前10分钟前往 会议室1 进行签到!',
- tag: '会议通知',
- createTime: '2021-01-01 08:00',
- status: 1,
- type: 3,
- },
- {
- id: 4,
- content: '一季度生产原材料采购项目 开票成功!',
- tag: '票务动态',
- createTime: '2021-01-01 08:00',
- status: 1,
- type: 2,
- },
- {
- id: 5,
- content: '二季度生产原材料采购项目 开票成功!',
- tag: '票务动态',
- createTime: '2021-01-01 08:00',
- status: 1,
- type: 2,
- },
- {
- id: 6,
- content: '三季度生产原材料采购项目 开票成功!',
- tag: '票务动态',
- createTime: '2021-01-01 08:00',
- status: 1,
- type: 2,
- },
-];
diff --git a/admin/src/pages/Detail/Secondary/index.module.less b/admin/src/pages/Detail/Secondary/index.module.less
deleted file mode 100644
index 97e7703..0000000
--- a/admin/src/pages/Detail/Secondary/index.module.less
+++ /dev/null
@@ -1,54 +0,0 @@
-.secondaryNotification {
- background-color: var(--td-bg-color-container);
- border-radius: 3px;
- padding: 24px 32px;
- .msgList {
- height: 70vh;
- .listItem {
- cursor: pointer;
- padding: 13px 24px 13px 0;
- &:hover {
- background-color: var(--td-bg-color-container-hover);
- }
- &:hover .createTime {
- display: none;
- }
- .action {
- display: none;
- }
- &:hover .action {
- display: block;
- }
- &:after {
- content: "";
- position: absolute;
- left: 0;
- bottom: 0;
- width: 100%;
- height: 1px;
- background: var(--td-border-level-1-color);
- }
- }
- .content {
- margin: 0;
- font-size: 14px;
- color: var(--td-text-color-placeholder);
- text-align: left;
- line-height: 22px;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- }
- .tag {
- margin-right: 8px;
- }
- .noData {
- text-align: center;
- height: 28px;
- line-height: 28px;
- }
- .unread {
- color: var(--td-brand-color-8);
- }
- }
-}
diff --git a/admin/src/pages/Detail/Secondary/index.tsx b/admin/src/pages/Detail/Secondary/index.tsx
deleted file mode 100644
index 72a48a4..0000000
--- a/admin/src/pages/Detail/Secondary/index.tsx
+++ /dev/null
@@ -1,113 +0,0 @@
-import React, { memo, useState } from 'react';
-import { Tabs, List, Tag, Row, Col, Popup } from 'tdesign-react';
-import { AddRectangleIcon, DeleteIcon, ChatIcon } from 'tdesign-icons-react';
-import classnames from 'classnames';
-import { dataItemList, IItem, TStatus } from './consts';
-import Style from './index.module.less';
-
-const { TabPanel } = Tabs;
-const { ListItem } = List;
-
-const typeMap: {
- [key: number]: 'default' | 'primary' | 'warning' | 'danger' | 'success';
-} = {
- 1: 'danger',
- 2: 'primary',
- 3: 'warning',
-};
-
-interface IMsgListProps {
- list: IItem[];
- onDelete: Function;
- onUpdate: Function;
-}
-
-const MsgList = memo((props: IMsgListProps) => {
- const { list, onUpdate, onDelete } = props;
- return (
-
- {list?.map((item) => (
-
- {item.createTime}
-
-
-
- {item.status === 1 && (
-
- onUpdate(item, 2)} />
-
- )}
- {item.status === 2 && (
-
- onUpdate(item, 1)} />
-
- )}
-
-
-
- onDelete(item)} />
-
-
-
-
-
- }
- >
-
-
- {item.tag}
-
- {item.content}
-
-
- ))}
- {list.length === 0 && 暂无数据
}
-
- );
-});
-
-export default memo(() => {
- const [list, setList] = useState(dataItemList);
-
- const deleteItem = (item: IItem) => {
- setList((value) => value.filter((val) => val.id !== item.id));
- };
-
- const updateStatus = (item: IItem, status: TStatus) => {
- setList((value) => {
- value.forEach((val) => {
- if (val.id === item.id) {
- val.status = status;
- }
- return val;
- });
- return [...value];
- });
- };
-
- return (
-
-
-
-
-
-
-
- item.status === 1)} onDelete={deleteItem} onUpdate={updateStatus} />
-
-
- item.status === 2)} onDelete={deleteItem} onUpdate={updateStatus} />
-
-
-
-
- );
-});
diff --git a/admin/src/pages/Form/Base/index.module.less b/admin/src/pages/Form/Base/index.module.less
deleted file mode 100644
index 6114ae9..0000000
--- a/admin/src/pages/Form/Base/index.module.less
+++ /dev/null
@@ -1,23 +0,0 @@
-.formContainer {
- width: 676px;
- margin: 0 auto;
- display: flex;
- align-items: center;
- justify-content: center;
- background-color: var(--td-bg-color-container);
- padding: 30px 0 134px;
-}
-
-.titleText {
- font-size: 20px;
- margin: 64px 0 32px;
- color: var(--td-text-color-primary);
-}
-
-.dateCol {
- :global {
- .t-form__controls-content {
- display: inline;
- }
- }
-}
diff --git a/admin/src/pages/Form/Base/index.tsx b/admin/src/pages/Form/Base/index.tsx
deleted file mode 100644
index f7ec65d..0000000
--- a/admin/src/pages/Form/Base/index.tsx
+++ /dev/null
@@ -1,194 +0,0 @@
-import React, { memo, useRef } from 'react';
-import {
- Form,
- Row,
- Col,
- Input,
- Radio,
- Button,
- DatePicker,
- Select,
- Textarea,
- Avatar,
- Upload,
- MessagePlugin,
-} from 'tdesign-react';
-import classnames from 'classnames';
-import { SubmitContext, FormInstanceFunctions } from 'tdesign-react/es/form/type';
-import CommonStyle from 'styles/common.module.less';
-import Style from './index.module.less';
-
-const { FormItem } = Form;
-const { Option } = Select;
-const { Group } = Avatar;
-
-const INITIAL_DATA = {
- name: '',
- type: '',
- payment: '',
- partyA: '',
- partyB: '',
- signDate: '',
- effectiveDate: '',
- endDate: '',
- remark: '',
- notary: '',
- file: [],
-};
-
-export default memo(() => {
- const formRef = useRef();
-
- const onSubmit = (e: SubmitContext) => {
- if (e.validateResult === true) {
- console.log('form 值', formRef.current?.getFieldsValue?.(true));
- MessagePlugin.info('提交成功');
- }
- };
-
- const handleFail = ({ file }: { file: any }) => {
- console.error(`文件 ${file.name} 上传失败`);
- };
-
- return (
-
- );
-});
diff --git a/admin/src/pages/Form/Step/components/StepFour.tsx b/admin/src/pages/Form/Step/components/StepFour.tsx
deleted file mode 100644
index 93d4277..0000000
--- a/admin/src/pages/Form/Step/components/StepFour.tsx
+++ /dev/null
@@ -1,44 +0,0 @@
-import React, { memo } from 'react';
-import { useNavigate } from 'react-router-dom';
-import { CheckCircleFilledIcon } from 'tdesign-icons-react';
-import { Button } from 'tdesign-react';
-import Style from './index.module.less';
-
-export default memo((props: { callback: Function }) => {
- const navigate = useNavigate();
- const { callback } = props;
-
- const onClickAgain = () => {
- callback('first');
- };
-
- const onCheck = () => {
- const url = '/detail/advanced';
- navigate(url);
- };
-
- return (
-
-
-
-
完成开票申请
-
预计1~3个工作日会将电子发票发至邮箱,发票邮寄请耐心等待
-
-
-
-
-
-
- );
-});
diff --git a/admin/src/pages/Form/Step/components/StepOne.tsx b/admin/src/pages/Form/Step/components/StepOne.tsx
deleted file mode 100644
index 2c1e61e..0000000
--- a/admin/src/pages/Form/Step/components/StepOne.tsx
+++ /dev/null
@@ -1,85 +0,0 @@
-import React, { memo } from 'react';
-import { Alert, Button, Form, Select } from 'tdesign-react';
-import Style from '../index.module.less';
-
-const { FormItem } = Form;
-const { Option } = Select;
-
-const message = [
- '1、申请开票后,电子发票在1~3个工作日内开具;增值税专用发票(纸质)如资质审核通过,将在电子发票开具后10个工作日内为您寄出;',
- '2、开票金额为您实际支付金额;',
- '3、如有疑问请直接联系:13300001111。',
-];
-
-const names = [
- {
- label: '合同A',
- value: 'A',
- },
- {
- label: '合同B',
- value: 'B',
- },
- {
- label: '合同C',
- value: 'C',
- },
-];
-
-const types = [
- {
- label: '类型A',
- value: 'A',
- },
- {
- label: '类型B',
- value: 'B',
- },
- {
- label: '类型C',
- value: 'C',
- },
-];
-
-export default memo((props: { callback: Function }) => {
- const { callback } = props;
- const next = () => {
- callback('next');
- };
-
- return (
- <>
-
-
- >
- );
-});
diff --git a/admin/src/pages/Form/Step/components/StepThree.tsx b/admin/src/pages/Form/Step/components/StepThree.tsx
deleted file mode 100644
index f7572f0..0000000
--- a/admin/src/pages/Form/Step/components/StepThree.tsx
+++ /dev/null
@@ -1,91 +0,0 @@
-import React, { memo } from 'react';
-import { Button, Form, Select, Input, Textarea } from 'tdesign-react';
-
-const { FormItem } = Form;
-const { Option } = Select;
-
-const addressOptions = [
- {
- label: '广东省深圳市南山区',
- value: '0',
- },
- {
- label: '北京市海淀区',
- value: '1',
- },
- {
- label: '四川省成都市高新区',
- value: '2',
- },
- {
- label: '广东省广州市天河区',
- value: '3',
- },
- {
- label: '陕西省西安市高新区',
- value: '4',
- },
-];
-
-export default memo((props: { current: number; callback: Function; steps: any[] }) => {
- const { current, callback, steps } = props;
-
- const next = () => {
- callback('next');
- };
-
- const prev = () => {
- callback('back');
- };
-
- return (
-
- );
-});
diff --git a/admin/src/pages/Form/Step/components/StepTwo.tsx b/admin/src/pages/Form/Step/components/StepTwo.tsx
deleted file mode 100644
index b5f35ba..0000000
--- a/admin/src/pages/Form/Step/components/StepTwo.tsx
+++ /dev/null
@@ -1,66 +0,0 @@
-import React, { memo } from 'react';
-import { Button, Form, Input } from 'tdesign-react';
-
-const { FormItem } = Form;
-
-export default memo((props: { current: number; callback: Function; steps: any[] }) => {
- const { current, callback, steps = [] } = props;
-
- const next = () => {
- callback('next');
- };
-
- const prev = () => {
- callback('back');
- };
-
- return (
-
- );
-});
diff --git a/admin/src/pages/Form/Step/components/index.module.less b/admin/src/pages/Form/Step/components/index.module.less
deleted file mode 100644
index f4bbc9e..0000000
--- a/admin/src/pages/Form/Step/components/index.module.less
+++ /dev/null
@@ -1,15 +0,0 @@
-.stepFourWrapper {
- text-align: center;
-}
-
-.icon {
- font-size: 52px;
- color: green;
-}
-
-.title {
- margin-top: 20px;
- font-size: 20px;
- // font-weight: 500;
- color: var(--td-text-color-secondary);
-}
\ No newline at end of file
diff --git a/admin/src/pages/Form/Step/components/index.ts b/admin/src/pages/Form/Step/components/index.ts
deleted file mode 100644
index b890016..0000000
--- a/admin/src/pages/Form/Step/components/index.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-export { default as StepOne } from './StepOne';
-export { default as StepTwo } from './StepTwo';
-export { default as StepThree } from './StepThree';
-export { default as StepFour } from './StepFour';
diff --git a/admin/src/pages/Form/Step/index.module.less b/admin/src/pages/Form/Step/index.module.less
deleted file mode 100644
index f7bca06..0000000
--- a/admin/src/pages/Form/Step/index.module.less
+++ /dev/null
@@ -1,3 +0,0 @@
-.alertBox {
- padding: 24px 0;
-}
\ No newline at end of file
diff --git a/admin/src/pages/Form/Step/index.tsx b/admin/src/pages/Form/Step/index.tsx
deleted file mode 100644
index f444a8b..0000000
--- a/admin/src/pages/Form/Step/index.tsx
+++ /dev/null
@@ -1,79 +0,0 @@
-import React, { memo } from 'react';
-import classnames from 'classnames';
-import { Steps } from 'tdesign-react';
-import { StepOne, StepTwo, StepThree, StepFour } from './components';
-import CommonStyle from 'styles/common.module.less';
-
-const { StepItem: Step } = Steps;
-interface IStep {
- title: string;
- content: string;
- component: any;
-}
-
-const steps: IStep[] = [
- {
- title: '申请提交',
- content: '申请提交已于12月21日提交',
- component: StepOne,
- },
- {
- title: '电子发票',
- content: '预计1~3个工作日',
- component: StepTwo,
- },
- {
- title: '发票已邮寄',
- content: '电子发票开出后7个工作日内联系',
- component: StepThree,
- },
- {
- title: '完成',
- content: '',
- component: StepFour,
- },
-];
-
-export default memo(() => {
- const [current, setCurrent] = React.useState(0);
- const Comp = steps[current].component;
-
- const next = () => {
- setCurrent(current + 1);
- };
-
- const prev = () => {
- setCurrent(current - 1);
- };
-
- const handleSteps = (value: string) => {
- switch (value) {
- case 'back':
- prev();
- break;
- case 'next':
- next();
- break;
- case 'first':
- setCurrent(0);
- break;
- default:
- break;
- }
- };
-
- return (
-
- <>
-
- {steps.map((item) => (
-
- ))}
-
-
-
-
- >
-
- );
-});
diff --git a/admin/src/pages/List/Base/index.module.less b/admin/src/pages/List/Base/index.module.less
deleted file mode 100644
index f1f4b90..0000000
--- a/admin/src/pages/List/Base/index.module.less
+++ /dev/null
@@ -1,4 +0,0 @@
-.toolBar {
- color: #666;
- margin-bottom: 20px;
-}
diff --git a/admin/src/pages/List/Base/index.tsx b/admin/src/pages/List/Base/index.tsx
deleted file mode 100644
index 346bd36..0000000
--- a/admin/src/pages/List/Base/index.tsx
+++ /dev/null
@@ -1,211 +0,0 @@
-import React, { useState, memo, useEffect } from 'react';
-import { Table, Tag, Row, Col, Button, Input } from 'tdesign-react';
-import { SearchIcon } from 'tdesign-icons-react';
-import classnames from 'classnames';
-import { useAppDispatch, useAppSelector } from 'modules/store';
-import { selectListBase, getList, clearPageState } from 'modules/list/base';
-import CommonStyle from 'styles/common.module.less';
-import style from './index.module.less';
-import { TrendIcon, ETrend } from 'components/Board';
-
-export const PaymentTypeMap: {
- [key: number]: React.ReactElement;
-} = {
- 0: ,
- 1: ,
-};
-
-export const StatusMap: {
- [key: number]: React.ReactElement;
-} = {
- 1: (
-
- 待审核
-
- ),
- 2: (
-
- 待履行
-
- ),
- 3: (
-
- 履行中
-
- ),
- 4: (
-
- 已完成
-
- ),
- 5: (
-
- 审核失败
-
- ),
-};
-
-export const ContractTypeMap: {
- [key: number]: string;
-} = {
- 0: '审核失败',
- 1: '待审核',
- 2: '待履行',
-};
-
-export default memo(() => {
- const dispatch = useAppDispatch();
- const pageState = useAppSelector(selectListBase);
- const [selectedRowKeys, setSelectedRowKeys] = useState<(string | number)[]>([1, 2]);
-
- const { loading, contractList, current, pageSize, total } = pageState;
-
- useEffect(() => {
- dispatch(
- getList({
- pageSize: pageState.pageSize,
- current: pageState.current,
- }),
- );
- return () => {
- console.log('clear');
- dispatch(clearPageState());
- };
- }, []);
-
- function onSelectChange(value: (string | number)[]) {
- setSelectedRowKeys(value);
- }
- return (
-
-
-
-
-
-
-
-
-
-
-
- 已选 {selectedRowKeys?.length || 0} 项
-
-
-
-
- } placeholder='请输入你需要搜索的型号' />
-
-
-
-
-
-
- >
- );
- },
- },
- ]}
- loading={loading}
- data={contractList}
- rowKey='index'
- selectedRowKeys={selectedRowKeys}
- verticalAlign='top'
- hover
- onSelectChange={onSelectChange}
- pagination={{
- pageSize,
- total,
- current,
- showJumper: true,
- onCurrentChange(current, pageInfo) {
- dispatch(
- getList({
- pageSize: pageInfo.pageSize,
- current: pageInfo.current,
- }),
- );
- },
- onPageSizeChange(size) {
- dispatch(
- getList({
- pageSize: size,
- current: 1,
- }),
- );
- },
- }}
- />
-
- );
-});
diff --git a/admin/src/pages/List/Card/components/ProductCard.module.less b/admin/src/pages/List/Card/components/ProductCard.module.less
deleted file mode 100644
index d0c8fa8..0000000
--- a/admin/src/pages/List/Card/components/ProductCard.module.less
+++ /dev/null
@@ -1,28 +0,0 @@
-.panel {
- cursor: pointer;
-}
-
-.name {
- margin-bottom: 8px;
- font-size: 16px;
- font-weight: 400;
- color: var(--td-text-color-primary);
-}
-
-.description {
- color: var(--td-text-color-secondary);
- font-size: 12px;
- line-height: 20px;
- overflow: hidden;
- text-overflow: ellipsis;
- display: -webkit-box;
- -webkit-line-clamp: 2;
- -webkit-box-orient: vertical;
- height: 40px;
-}
-
-.footer {
- display: flex;
- justify-content: space-between;
- align-items: center;
-}
diff --git a/admin/src/pages/List/Card/components/ProductCard.tsx b/admin/src/pages/List/Card/components/ProductCard.tsx
deleted file mode 100644
index becb452..0000000
--- a/admin/src/pages/List/Card/components/ProductCard.tsx
+++ /dev/null
@@ -1,68 +0,0 @@
-import React from 'react';
-import { Card, Avatar, Tag, Dropdown, Button } from 'tdesign-react';
-import { UserAvatarIcon, CalendarIcon, LaptopIcon, ShopIcon, ServiceIcon, Icon } from 'tdesign-icons-react';
-import { IProduct } from 'services/product';
-import Style from './ProductCard.module.less';
-
-const { Group: AvatarGroup } = Avatar;
-const icons = [UserAvatarIcon, CalendarIcon, LaptopIcon, ShopIcon, ServiceIcon];
-
-const CardIcon = React.memo(() => {
- const random = Math.floor(Math.random() * icons.length);
- const Icon = icons[random];
- return ;
-});
-
-const ProductCard = ({ product }: { product: IProduct }) => {
- const disabled = !product.isSetup;
- return (
-
- 已停用
-
- ) : (
- 已启用
- )
- }
- avatar={
-
-
-
- }
- footer={
-
-
- {String.fromCharCode(64 + product.type || 0)}
- +
-
-
-
-
-
- }
- >
- {product?.name}
- {product?.description}
-
- );
-};
-
-export default React.memo(ProductCard);
diff --git a/admin/src/pages/List/Card/index.module.less b/admin/src/pages/List/Card/index.module.less
deleted file mode 100644
index 02194e6..0000000
--- a/admin/src/pages/List/Card/index.module.less
+++ /dev/null
@@ -1,21 +0,0 @@
-.toolBar {
- display: flex;
- justify-content: space-between;
- .search {
- width: 342px;
- }
-}
-.cardList {
- margin-top: 14px;
- margin-bottom: 24px;
-}
-
-.pagination {
- padding: 16px;
-}
-
-.loading {
- display: flex;
- justify-content: center;
- margin: 20px 0;
-}
diff --git a/admin/src/pages/List/Card/index.tsx b/admin/src/pages/List/Card/index.tsx
deleted file mode 100644
index c275fea..0000000
--- a/admin/src/pages/List/Card/index.tsx
+++ /dev/null
@@ -1,84 +0,0 @@
-import React, { useEffect } from 'react';
-import { Row, Col, Button, Input, Pagination, Loading } from 'tdesign-react';
-import { SearchIcon } from 'tdesign-icons-react';
-import { useAppDispatch, useAppSelector } from 'modules/store';
-import { selectListCard, clearPageState, getList, switchPageLoading } from 'modules/list/card';
-import ProductCard from './components/ProductCard';
-import { PageInfo } from 'tdesign-react/es/pagination/type';
-import Style from './index.module.less';
-
-const CardList = () => {
- const dispatch = useAppDispatch();
- const pageState = useAppSelector(selectListCard);
-
- const pageInit = async () => {
- await dispatch(
- getList({
- pageSize: pageState.pageSize,
- current: 1,
- }),
- );
- await dispatch(switchPageLoading(false));
- };
-
- useEffect(() => {
- pageInit();
- return () => {
- clearPageState();
- };
- }, []);
-
- const onChange = async ({ current, pageSize }: PageInfo) => {
- await dispatch(
- getList({
- pageSize,
- current,
- }),
- );
- };
-
- const handlePageSizeChange = async (pageSize: number, { current }: PageInfo) => {
- await dispatch(
- getList({
- pageSize,
- current,
- }),
- );
- };
-
- return (
-
-
-
- } placeholder='请输入你需要搜索的内容' />
-
- {pageState.pageLoading ? (
-
-
-
- ) : (
- <>
-
-
- {pageState?.productList?.map((product, index) => (
-
-
-
- ))}
-
-
-
- >
- )}
-
- );
-};
-
-export default React.memo(CardList);
diff --git a/admin/src/pages/List/Select/components/SearchForm.tsx b/admin/src/pages/List/Select/components/SearchForm.tsx
deleted file mode 100644
index 08eeef6..0000000
--- a/admin/src/pages/List/Select/components/SearchForm.tsx
+++ /dev/null
@@ -1,78 +0,0 @@
-import React, { useRef, memo } from 'react';
-import { Row, Col, Form, Input, Button, MessagePlugin, Select } from 'tdesign-react';
-import { CONTRACT_STATUS_OPTIONS, CONTRACT_TYPE_OPTIONS } from '../consts';
-import { FormInstanceFunctions, SubmitContext } from 'tdesign-react/es/form/type';
-
-const { FormItem } = Form;
-
-export type FormValueType = {
- name?: string;
- status?: string;
- number?: string;
- time?: string;
- type?: string;
-};
-
-export type SearchFormProps = {
- onCancel: () => void;
- onSubmit: (values: FormValueType) => Promise;
-};
-
-const SearchForm: React.FC = (props) => {
- const formRef = useRef();
- const onSubmit = (e: SubmitContext) => {
- if (e.validateResult === true) {
- MessagePlugin.info('提交成功');
- }
- const queryValue = formRef?.current?.getFieldsValue?.(true);
- console.log('form 数据', queryValue);
- };
-
- const onReset = () => {
- props.onCancel();
- MessagePlugin.info('重置成功');
- };
-
- return (
-
-
-
- );
-};
-
-export default memo(SearchForm);
diff --git a/admin/src/pages/List/Select/consts.ts b/admin/src/pages/List/Select/consts.ts
deleted file mode 100644
index feb2fe3..0000000
--- a/admin/src/pages/List/Select/consts.ts
+++ /dev/null
@@ -1,34 +0,0 @@
-interface IOption {
- value: number | string;
- label: string;
-}
-
-// 合同状态枚举
-export const CONTRACT_STATUS = {
- FAIL: 0,
- AUDIT_PENDING: 1,
- EXEC_PENDING: 2,
- EXECUTING: 3,
- FINISH: 4,
-};
-
-export const CONTRACT_STATUS_OPTIONS: Array = [
- { value: CONTRACT_STATUS.FAIL, label: '审核失败' },
- { value: CONTRACT_STATUS.AUDIT_PENDING, label: '待审核' },
- { value: CONTRACT_STATUS.EXEC_PENDING, label: '待履行' },
- { value: CONTRACT_STATUS.EXECUTING, label: '审核成功' },
- { value: CONTRACT_STATUS.FINISH, label: '已完成' },
-];
-
-// 合同类型枚举
-export const CONTRACT_TYPES = {
- MAIN: 0,
- SUB: 1,
- SUPPLEMENT: 2,
-};
-
-export const CONTRACT_TYPE_OPTIONS: Array = [
- { value: CONTRACT_TYPES.MAIN, label: '主合同' },
- { value: CONTRACT_TYPES.SUB, label: '子合同' },
- { value: CONTRACT_TYPES.SUPPLEMENT, label: '补充合同' },
-];
diff --git a/admin/src/pages/List/Select/index.module.less b/admin/src/pages/List/Select/index.module.less
deleted file mode 100644
index 94fef82..0000000
--- a/admin/src/pages/List/Select/index.module.less
+++ /dev/null
@@ -1,12 +0,0 @@
-.list-common-table-query {
- margin-bottom: 30px;
-}
-
-.table-container {
- margin-top: 30px;
-}
-&-btn {
- margin-right: 10px;
- color: #29a4fb;
- cursor: pointer;
-}
diff --git a/admin/src/pages/List/Select/index.tsx b/admin/src/pages/List/Select/index.tsx
deleted file mode 100644
index a705fa6..0000000
--- a/admin/src/pages/List/Select/index.tsx
+++ /dev/null
@@ -1,179 +0,0 @@
-import React, { useState, memo, useEffect } from 'react';
-import { Table, Dialog, Button, Row } from 'tdesign-react';
-import { useAppDispatch, useAppSelector } from 'modules/store';
-import { selectListSelect, getList, clearPageState } from 'modules/list/select';
-import SearchForm from './components/SearchForm';
-import { StatusMap, ContractTypeMap, PaymentTypeMap } from '../Base';
-
-import './index.module.less';
-import classnames from 'classnames';
-import CommonStyle from '../../../styles/common.module.less';
-
-export const SelectTable = () => {
- const dispatch = useAppDispatch();
- const pageState = useAppSelector(selectListSelect);
- const [selectedRowKeys, setSelectedRowKeys] = useState<(string | number)[]>([0, 1]);
- const [visible, setVisible] = useState(false);
- const { loading, contractList, current, pageSize, total } = pageState;
-
- useEffect(() => {
- dispatch(
- getList({
- pageSize: pageState.pageSize,
- current: pageState.current,
- }),
- );
- return () => {
- dispatch(clearPageState());
- };
- }, []);
-
- function onSelectChange(value: (string | number)[]) {
- setSelectedRowKeys(value);
- }
-
- function rehandleClickOp(record: any) {
- console.log(record);
- }
-
- function handleClickDelete(record: any) {
- console.log(record);
- setVisible(true);
- }
-
- function handleClose() {
- setVisible(false);
- }
-
- return (
- <>
-
- {
- console.log(value);
- }}
- onCancel={() => {}}
- />
-
-
-
-
- >
- );
- },
- },
- ]}
- rowKey='index'
- selectedRowKeys={selectedRowKeys}
- hover
- onSelectChange={onSelectChange}
- pagination={{
- pageSize,
- total,
- current,
- showJumper: true,
- onCurrentChange(current, pageInfo) {
- dispatch(
- getList({
- pageSize: pageInfo.pageSize,
- current: pageInfo.current,
- }),
- );
- },
- onPageSizeChange(size) {
- dispatch(
- getList({
- pageSize: size,
- current: 1,
- }),
- );
- },
- }}
- />
-
- >
- );
-};
-
-const selectPage: React.FC = () => (
-
-
-
-);
-
-export default memo(selectPage);
diff --git a/admin/src/pages/List/Tree/consts.ts b/admin/src/pages/List/Tree/consts.ts
deleted file mode 100644
index 7add260..0000000
--- a/admin/src/pages/List/Tree/consts.ts
+++ /dev/null
@@ -1,86 +0,0 @@
-export const treeList = [
- {
- label: '深圳总部',
- value: 0,
- children: [
- {
- label: '总办',
- value: '0-0',
- },
- {
- label: '市场部',
- value: '0-1',
- children: [
- {
- label: '采购1组',
- value: '0-1-0',
- },
- {
- label: '采购2组',
- value: '0-1-1',
- },
- ],
- },
- {
- label: '技术部',
- value: '0-2',
- },
- ],
- },
- {
- label: '北京总部',
- value: 1,
- children: [
- {
- label: '总办',
- value: '1-0',
- },
- {
- label: '市场部',
- value: '1-1',
- children: [
- {
- label: '采购1组',
- value: '1-1-0',
- },
- {
- label: '采购2组',
- value: '1-1-1',
- },
- ],
- },
- ],
- },
- {
- label: '上海总部',
- value: 2,
- children: [
- {
- label: '市场部',
- value: '2-0',
- },
- {
- label: '财务部',
- value: '2-1',
- children: [
- {
- label: '财务1组',
- value: '2-1-0',
- },
- {
- label: '财务2组',
- value: '2-1-1',
- },
- ],
- },
- ],
- },
- {
- label: '湖南',
- value: 3,
- },
- {
- label: '湖北',
- value: 4,
- },
-];
diff --git a/admin/src/pages/List/Tree/index.module.less b/admin/src/pages/List/Tree/index.module.less
deleted file mode 100644
index 671c323..0000000
--- a/admin/src/pages/List/Tree/index.module.less
+++ /dev/null
@@ -1,18 +0,0 @@
-.content {
- display: flex;
-}
-
-.treeContent {
- width: 200px;
- padding: 30px 32px;
- border-right: 1px solid var(--td-border-level-1-color);
-}
-
-.search {
- width: 150px;
-}
-
-.tableContent {
- padding: 30px 32px;
- width: calc(100% - 200px);
-}
diff --git a/admin/src/pages/List/Tree/index.tsx b/admin/src/pages/List/Tree/index.tsx
deleted file mode 100644
index e92b90f..0000000
--- a/admin/src/pages/List/Tree/index.tsx
+++ /dev/null
@@ -1,22 +0,0 @@
-import React from 'react';
-import { Input, Tree } from 'tdesign-react';
-import { SearchIcon } from 'tdesign-icons-react';
-import classnames from 'classnames';
-import { SelectTable } from '../Select';
-import { treeList } from './consts';
-import CommonStyle from 'styles/common.module.less';
-import Style from './index.module.less';
-
-const TreeTable: React.FC = () => (
-
-
- } placeholder='请输入关键词' />
-
-
-
-
-
-
-);
-
-export default TreeTable;
diff --git a/admin/src/pages/Login/components/Header/index.module.less b/admin/src/pages/Login/components/Header/index.module.less
index be10d78..26c12bd 100644
--- a/admin/src/pages/Login/components/Header/index.module.less
+++ b/admin/src/pages/Login/components/Header/index.module.less
@@ -8,9 +8,30 @@
color: var(--td-text-color-primary);
}
+@media screen and (max-width: 700px) {
+ .loginHeader {
+ padding: 0 16px;
+ backdrop-filter: none;
+ }
+}
+
.logo {
width: 188px;
height: 64px;
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ justify-content: center;
+ gap: 2px;
+ font-size: 18px;
+ line-height: 1.1;
+ text-align: left;
+ color: var(--td-text-color-primary);
+ small {
+ font-size: 11px;
+ font-weight: 400;
+ color: var(--td-text-color-secondary);
+ }
}
.operationsContainer {
@@ -23,13 +44,13 @@
.operationsButton {
margin-left: 16px;
+ width: 40px;
+ height: 40px;
}
.icon {
- height: 20px;
- width: 20px;
- padding: 6px;
- box-sizing: content-box;
+ height: 24px !important;
+ width: 24px !important;
&:hover {
cursor: pointer;
diff --git a/admin/src/pages/Login/components/Header/index.tsx b/admin/src/pages/Login/components/Header/index.tsx
index 1a17f9d..91f60e2 100644
--- a/admin/src/pages/Login/components/Header/index.tsx
+++ b/admin/src/pages/Login/components/Header/index.tsx
@@ -1,61 +1,32 @@
import React from 'react';
-import { Button } from 'tdesign-react';
-import { LogoGithubIcon, HelpCircleIcon, SettingIcon } from 'tdesign-icons-react';
+import { MiragenFlowButton } from '@miragenflow/ui';
+import { SettingIcon } from 'components/LineIcons';
import { useAppDispatch } from 'modules/store';
import { toggleSetting } from 'modules/global';
-import LogoFullIcon from 'assets/svg/assets-logo-full.svg?component';
import Style from './index.module.less';
export default function Header() {
const dispatch = useAppDispatch();
- const navToGitHub = () => {
- window.open('https://github.com/tencent/tdesign-react-starter');
- };
-
- const navToHelper = () => {
- window.open('http://tdesign.tencent.com/starter/docs/react/get-started');
- };
-
const toggleSettingPanel = () => {
dispatch(toggleSetting());
};
return (
-
-
-
-
-
-
-
-
-
-
+
+ 元境幻生MiragenFlow 管理台
+
+
+
+
+
+
);
}
diff --git a/admin/src/pages/Login/components/Login/index.module.less b/admin/src/pages/Login/components/Login/index.module.less
index 5a69af9..cfdd4a3 100644
--- a/admin/src/pages/Login/components/Login/index.module.less
+++ b/admin/src/pages/Login/components/Login/index.module.less
@@ -1,6 +1,12 @@
.itemContainer {
- width: 400px;
+ width: 100%;
+ max-width: 400px;
margin-top: 48px;
+
+ :global(.t-input__prefix > svg) {
+ width: 22px;
+ height: 22px;
+ }
}
.checkContainer {
diff --git a/admin/src/pages/Login/components/Login/index.tsx b/admin/src/pages/Login/components/Login/index.tsx
index 9527e15..852f58c 100644
--- a/admin/src/pages/Login/components/Login/index.tsx
+++ b/admin/src/pages/Login/components/Login/index.tsx
@@ -1,148 +1,55 @@
+/* eslint-disable no-void */
import React, { useRef, useState } from 'react';
+import { Form, type FormInstanceFunctions, type SubmitContext } from '@miragenflow/ui/form';
+import { MessagePlugin } from '@miragenflow/ui/message';
+import { Input, MiragenFlowButton } from '@miragenflow/ui';
+import { LockOnIcon, UserIcon } from 'components/LineIcons';
import { useNavigate } from 'react-router-dom';
-import {
- Button,
- Checkbox,
- Form,
- type FormInstanceFunctions,
- Input,
- MessagePlugin,
- QRCode,
- type SubmitContext,
-} from 'tdesign-react';
-import { BrowseIcon, BrowseOffIcon, LockOnIcon, RefreshIcon, UserIcon } from 'tdesign-icons-react';
-import classnames from 'classnames';
-import { useAppDispatch } from 'modules/store';
-import { login } from 'modules/user';
-import useCountdown from '../../hooks/useCountDown';
-
+import { useAppDispatch, useAppSelector } from 'modules/store';
+import { clearMfaChallenge, getUserInfo, login } from 'modules/user';
+import { AdminRequestError, adminCaptcha, formatAdminError } from 'services/platform';
import Style from './index.module.less';
const { FormItem } = Form;
-export type ELoginType = 'password' | 'phone' | 'qrcode';
-
export default function Login() {
- const [loginType, changeLoginType] = useState('password');
- const [showPsw, toggleShowPsw] = useState(false);
- const { countdown, setupCountdown } = useCountdown(60);
const formRef = useRef();
const navigate = useNavigate();
const dispatch = useAppDispatch();
+ const userInfo = useAppSelector((state) => state.user.userInfo as { mfaChallengeId?: string; mfaRequired?: boolean });
+ const mfaVisible = userInfo.mfaRequired === true && Boolean(userInfo.mfaChallengeId);
+ const [captcha, setCaptcha] = useState<{ challengeId: string; question: string }>();
+ const [captchaLoading, setCaptchaLoading] = useState(false);
+ const [captchaVisible, setCaptchaVisible] = useState(false);
+ const loadCaptcha = async (target = '') => { setCaptchaLoading(true); try { setCaptcha(await adminCaptcha(target.trim().toLowerCase())); } catch { setCaptcha(undefined); } finally { setCaptchaLoading(false); } };
+ React.useEffect(() => { dispatch(clearMfaChallenge()); setCaptchaVisible(false); }, [dispatch]);
- const onSubmit = async (e: SubmitContext) => {
- if (e.validateResult === true) {
- try {
- const formValue = formRef.current?.getFieldsValue?.(true) || {};
- await dispatch(login(formValue));
-
- MessagePlugin.success('登录成功');
-
- navigate('/dashboard/base');
- } catch (e) {
- console.log(e);
- MessagePlugin.error('登录失败');
+ const onSubmit = async (event: SubmitContext) => {
+ if (event.validateResult !== true) return;
+ try {
+ const values = formRef.current?.getFieldsValue?.(true) || {};
+ const result = await dispatch(login({ ...values, challengeId: mfaVisible ? userInfo.mfaChallengeId : undefined, captchaChallengeId: captcha?.challengeId })).unwrap();
+ if (!result.accessToken) {
+ MessagePlugin.info('请输入身份验证器动态码;也可以使用一次性恢复码。');
+ return;
}
+ await dispatch(getUserInfo()).unwrap();
+ MessagePlugin.success('登录成功');
+ navigate('/dashboard/base');
+ } catch (error) {
+ const rawMessage = error instanceof Error ? error.message : '';
+ const requiresCaptcha = /验证码|人机/.test(rawMessage) || (error instanceof AdminRequestError && ['VALIDATION_FAILED', 'RATE_LIMITED'].includes(error.code || '') && !mfaVisible);
+ const message = formatAdminError(error, '管理员登录失败,请检查账号信息后重试。');
+ if (requiresCaptcha) { setCaptchaVisible(true); const values = formRef.current?.getFieldsValue?.(true) || {}; void loadCaptcha(String(values.account || '')); }
+ MessagePlugin.error(message);
}
};
- const switchType = (val: ELoginType) => {
- formRef.current?.reset?.();
- changeLoginType(val);
- };
-
- return (
-
- );
+ return ;
}
diff --git a/admin/src/pages/Login/components/Register/index.module.less b/admin/src/pages/Login/components/Register/index.module.less
deleted file mode 100644
index f3e5380..0000000
--- a/admin/src/pages/Login/components/Register/index.module.less
+++ /dev/null
@@ -1,36 +0,0 @@
-.itemContainer {
- width: 400px;
- margin-top: 48px;
-}
-
-.verificationBtn {
- flex-shrink: 0;
- width: 102px;
- height: 40px;
- margin-left: 11px;
-}
-
-.switchContainer {
- margin-top: 24px;
-}
-
-.switchTip {
- font-size: 14px;
- color: var(--td-brand-color);
- cursor: pointer;
- display: inline-flex;
- align-items: center;
- margin-right: 14px;
-}
-
-.checkContainer {
- font-size: 14px;
- color: var(--td-text-color-secondary);
- :global {
- .tip {
- float: right;
- font-size: 14px;
- color: var(--td-brand-color);
- }
- }
-}
diff --git a/admin/src/pages/Login/components/Register/index.tsx b/admin/src/pages/Login/components/Register/index.tsx
deleted file mode 100644
index c656bea..0000000
--- a/admin/src/pages/Login/components/Register/index.tsx
+++ /dev/null
@@ -1,107 +0,0 @@
-import React, { useState, useRef } from 'react';
-import classnames from 'classnames';
-import { Form, MessagePlugin, Input, Checkbox, Button, FormInstanceFunctions, SubmitContext } from 'tdesign-react';
-import { LockOnIcon, UserIcon, MailIcon, BrowseOffIcon, BrowseIcon } from 'tdesign-icons-react';
-import useCountdown from '../../hooks/useCountDown';
-
-import Style from './index.module.less';
-
-const { FormItem } = Form;
-
-export type ERegisterType = 'phone' | 'email';
-
-export default function Register() {
- const [registerType, changeRegisterType] = useState('phone');
- const [showPsw, toggleShowPsw] = useState(false);
- const { countdown, setupCountdown } = useCountdown(60);
- const formRef = useRef();
-
- const onSubmit = (e: SubmitContext) => {
- if (e.validateResult === true) {
- const { checked } = formRef.current?.getFieldsValue?.(['checked']) as { checked: boolean };
- if (!checked) {
- MessagePlugin.error('请同意 TDesign 服务协议和 TDesign 隐私声明');
- return;
- }
- MessagePlugin.success('注册成功');
- }
- };
-
- const switchType = (val: ERegisterType) => {
- formRef.current?.reset?.();
- changeRegisterType(val);
- };
-
- return (
-
- );
-}
diff --git a/admin/src/pages/Login/index.module.less b/admin/src/pages/Login/index.module.less
index 9c65ea5..fb71a16 100644
--- a/admin/src/pages/Login/index.module.less
+++ b/admin/src/pages/Login/index.module.less
@@ -1,26 +1,57 @@
.loginWrapper {
height: 100vh;
+ min-height: 100dvh;
display: flex;
flex-direction: column;
background-size: cover;
- background-position: 100%;
+ background-position: center;
+ background-image: url('../../assets/image/admin-login-background.webp');
+ overflow-x: hidden;
+ overflow-y: auto;
position: relative;
+ &::before,
+ &::after {
+ content: '';
+ position: absolute;
+ pointer-events: none;
+ z-index: 0;
+ }
+ // The supplied artwork already provides the visual structure; keep the
+ // legacy geometric decorations from competing with its focal subject.
+ &::before,
+ &::after { display: none; }
&.dark {
background-color: var(--td-bg-color-page);
- background-image: url('assets/image/assets-login-bg-black.png');
+ background-image: linear-gradient(rgba(14, 27, 52, 0.72), rgba(14, 27, 52, 0.72)), url('../../assets/image/admin-login-background.webp');
+ &::before { border-color: rgba(111, 146, 255, 0.22); background: rgba(111, 146, 255, 0.08); }
+ &::after { border-color: rgba(102, 212, 174, 0.18); }
}
&.light {
- background-color: white;
- background-image: url('assets/image/assets-login-bg-white.png');
+ background-color: #f7f9fc;
+ background-image: url('../../assets/image/admin-login-background.webp');
+ &::before { border-color: rgba(54, 91, 220, 0.14); background: rgba(54, 91, 220, 0.045); }
+ &::after { border-color: rgba(35, 161, 125, 0.12); }
}
+ &::before { width: 58vw; height: 58vw; right: -20vw; top: -25vw; border: 1px solid; border-radius: 34%; transform: rotate(24deg); }
+ &::after { width: 34vw; height: 34vw; right: 12vw; bottom: -21vw; border: 1px solid; border-radius: 50%; }
}
-.loginContainer {
+.loginContent {
position: absolute;
top: 22%;
left: 5%;
- min-height: 500px;
+ width: min(400px, calc(100vw - 10%));
+ min-height: 0;
line-height: 22px;
+ z-index: 1;
+ /* 仅负责定位;登录页主体直接落在背景图左侧,不创建卡片层。 */
+ background: none;
+ border: 0;
+ box-shadow: none;
+}
+
+.titleContainer {
+ text-align: left;
}
.title {
@@ -33,6 +64,7 @@
.subTitle {
margin-top: 16px;
+ text-align: left;
}
.tip {
@@ -58,6 +90,34 @@
left: 5%;
bottom: 64px;
color: var(--td-text-color-secondary);
+ z-index: 1;
+}
+
+@media screen and (max-width: 700px) {
+ .loginWrapper {
+ height: 100dvh;
+ min-height: 100dvh;
+ overflow-x: hidden;
+ overflow-y: auto;
+ // Keep the desktop left-side composition; the artwork remains a full-page
+ // background and the form stays directly on its quiet left half.
+ background-position: left center;
+ background-size: auto 100%;
+ background-attachment: initial;
+ }
+
+ .title {
+ font-size: 30px;
+ line-height: 38px;
+ }
+
+ .copyright {
+ right: auto;
+ bottom: 64px;
+ left: 5%;
+ text-align: left;
+ }
+
}
@media screen and (max-height: 762px) {
diff --git a/admin/src/pages/Login/index.tsx b/admin/src/pages/Login/index.tsx
index 9df335d..015395e 100644
--- a/admin/src/pages/Login/index.tsx
+++ b/admin/src/pages/Login/index.tsx
@@ -1,7 +1,6 @@
-import React, { memo, useState } from 'react';
+import React, { memo } from 'react';
import classNames from 'classnames';
import Login from './components/Login';
-import Register from './components/Register';
import LoginHeader from './components/Header';
import { useAppSelector } from 'modules/store';
import { selectGlobal } from 'modules/global';
@@ -9,35 +8,23 @@ import { selectGlobal } from 'modules/global';
import Style from './index.module.less';
export default memo(() => {
- const [type, setType] = useState('login');
const globalState = useAppSelector(selectGlobal);
const { theme } = globalState;
- const handleSwitchLoginType = () => {
- setType(type === 'register' ? 'login' : 'register');
- };
-
return (
-
+
登录到
元境幻生管理台
-
-
- {type === 'register' ? '已有账号?' : '没有账号吗?'}
-
-
- {type === 'register' ? '登录' : '注册新账号'}
-
-
+
- {type === 'login' ? : }
-
+
+
);
diff --git a/admin/src/pages/Result/500/index.tsx b/admin/src/pages/Result/500/index.tsx
deleted file mode 100644
index 881b089..0000000
--- a/admin/src/pages/Result/500/index.tsx
+++ /dev/null
@@ -1,6 +0,0 @@
-import React from 'react';
-import ErrorPage from 'components/ErrorPage';
-
-const ServerError = () => ;
-
-export default React.memo(ServerError);
diff --git a/admin/src/pages/Result/BrowserIncompatible/index.module.less b/admin/src/pages/Result/BrowserIncompatible/index.module.less
deleted file mode 100644
index c5babe7..0000000
--- a/admin/src/pages/Result/BrowserIncompatible/index.module.less
+++ /dev/null
@@ -1,69 +0,0 @@
-.Content {
- display: flex;
- flex-direction: column;
- justify-content: center;
- align-items: center;
- height: 75vh;
- padding: 24px;
- min-height: 400px;
- color: var(--td-brand-color);
-
- .title {
- font-weight: 500;
- font-size: 20px;
- line-height: 28px;
- margin-top: 8px;
- color: var(--td-text-color-primary);
- }
- .description {
- margin: 8px 0 32px;
- font-size: 14px;
- line-height: 22px;
- color: var(--td-text-color-secondary);
- }
-
- .rightButton {
- margin-left: 8px;
- }
-
- .resultSlotContainer {
- position: relative;
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: space-between;
- color: var(--td-text-color-secondary);
-
- .recommendContainer {
- position: absolute;
- display: flex;
- align-items: center;
- justify-content: space-between;
- top: 175px;
- padding: 24px 48px;
- width: 640px;
- background: var(--td-bg-color-container);
- box-shadow: 0 1px 2px var(--td-shadow-1);
- border-radius: 3px;
-
- .recommendBrowser {
- display: flex;
-
- img {
- width: 36.67px;
- height: 36.67px;
- }
- }
- .recommendBrowser > div {
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: space-between;
- height: 70px;
- }
- .recommendBrowser > div + div {
- margin-left: 40px;
- }
- }
- }
-}
diff --git a/admin/src/pages/Result/BrowserIncompatible/index.tsx b/admin/src/pages/Result/BrowserIncompatible/index.tsx
deleted file mode 100644
index 363e980..0000000
--- a/admin/src/pages/Result/BrowserIncompatible/index.tsx
+++ /dev/null
@@ -1,32 +0,0 @@
-import React, { memo } from 'react';
-import { Button } from 'tdesign-react';
-
-import BrowserIncompatibleIcon from 'assets/svg/assets-result-browser-incompatible.svg?component';
-import style from './index.module.less';
-
-const BrowserIncompatible = () => (
-
-
-
浏览器版本低
-
抱歉,您正在使用的浏览器版本过低,无法打开当前网页。
-
-
-
-
-
元境幻生管理台推荐以下主流浏览器
-
-
-

-
Chrome
-
-
-

-
QQ Browser
-
-
-
-
-
-);
-
-export default memo(BrowserIncompatible);
diff --git a/admin/src/pages/Result/Fail/index.module.less b/admin/src/pages/Result/Fail/index.module.less
deleted file mode 100644
index 2f76aed..0000000
--- a/admin/src/pages/Result/Fail/index.module.less
+++ /dev/null
@@ -1,30 +0,0 @@
-.Content {
- display: flex;
- flex-direction: column;
- justify-content: center;
- align-items: center;
- height: 75vh;
-
- .icon {
- font-size: 72px;
- color: var(--td-text-color-secondary);
- }
-
- .title {
- font-weight: 500;
- font-size: 20px;
- line-height: 28px;
- margin-top: 28px;
- color: var(--td-text-color-primary);
- }
- .description {
- margin: 8px 0 32px;
- font-size: 14px;
- line-height: 22px;
- color: var(--td-text-color-secondary);
- }
-
- .rightButton {
- margin-left: 8px;
- }
-}
diff --git a/admin/src/pages/Result/Fail/index.tsx b/admin/src/pages/Result/Fail/index.tsx
deleted file mode 100644
index f9666b2..0000000
--- a/admin/src/pages/Result/Fail/index.tsx
+++ /dev/null
@@ -1,21 +0,0 @@
-import React, { memo } from 'react';
-import { Button } from 'tdesign-react';
-import { ErrorCircleIcon } from 'tdesign-icons-react';
-
-import style from './index.module.less';
-
-const Fail = () => (
-
-
-
创建失败
-
抱歉,您的项目创建失败,企业微信联系检查创建者权限,或返回修改。
-
-
-
-
-
-);
-
-export default memo(Fail);
diff --git a/admin/src/pages/Result/Maintenance/index.module.less b/admin/src/pages/Result/Maintenance/index.module.less
deleted file mode 100644
index 1d9da3e..0000000
--- a/admin/src/pages/Result/Maintenance/index.module.less
+++ /dev/null
@@ -1,37 +0,0 @@
-.Content {
- display: flex;
- flex-direction: column;
- justify-content: center;
- align-items: center;
- height: 75vh;
- padding: 24px;
- min-height: 400px;
- color: var(--td-brand-color);
-
- .title {
- font-weight: 500;
- font-size: 20px;
- line-height: 28px;
- margin-top: 8px;
- color: var(--td-text-color-primary);
- }
- .description {
- margin: 8px 0 32px;
- font-size: 14px;
- line-height: 22px;
- color: var(--td-text-color-secondary);
- }
-
- .rightButton {
- margin-left: 8px;
- }
-
- .resultSlotContainer {
- position: relative;
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: space-between;
- color: var(--td-text-color-secondary);
- }
-}
diff --git a/admin/src/pages/Result/Maintenance/index.tsx b/admin/src/pages/Result/Maintenance/index.tsx
deleted file mode 100644
index cb95dd6..0000000
--- a/admin/src/pages/Result/Maintenance/index.tsx
+++ /dev/null
@@ -1,19 +0,0 @@
-import React, { memo } from 'react';
-import { Button } from 'tdesign-react';
-
-import MaintenanceIcon from 'assets/svg/assets-result-maintenance.svg?component';
-import style from './index.module.less';
-
-const BrowserIncompatible = () => (
-
-
-
系统维护中
-
系统维护中,请稍后再试。
-
-
-
-
-
-);
-
-export default memo(BrowserIncompatible);
diff --git a/admin/src/pages/Result/NetworkError/index.tsx b/admin/src/pages/Result/NetworkError/index.tsx
deleted file mode 100644
index 601f0a7..0000000
--- a/admin/src/pages/Result/NetworkError/index.tsx
+++ /dev/null
@@ -1,21 +0,0 @@
-import React, { memo } from 'react';
-import { Button } from 'tdesign-react';
-
-import NetworkErrorIcon from 'assets/svg/assets-result-network-error.svg?component';
-import style from '../index.module.less';
-
-const NetworkError = () => (
-
-
-
网络异常
-
网络异常,请稍后再试
-
-
-
-
-
-);
-
-export default memo(NetworkError);
diff --git a/admin/src/pages/Result/Success/index.module.less b/admin/src/pages/Result/Success/index.module.less
deleted file mode 100644
index e595065..0000000
--- a/admin/src/pages/Result/Success/index.module.less
+++ /dev/null
@@ -1,30 +0,0 @@
-.Content {
- display: flex;
- flex-direction: column;
- justify-content: center;
- align-items: center;
- height: 75vh;
-
- .icon {
- font-size: 72px;
- color: var(--td-success-color);
- }
-
- .title {
- font-weight: 500;
- font-size: 20px;
- line-height: 28px;
- margin-top: 28px;
- color: var(--td-text-color-primary);
- }
- .description {
- margin: 8px 0 32px;
- font-size: 14px;
- line-height: 22px;
- color: var(--td-text-color-secondary);
- }
-
- .rightButton {
- margin-left: 8px;
- }
-}
diff --git a/admin/src/pages/Result/Success/index.tsx b/admin/src/pages/Result/Success/index.tsx
deleted file mode 100644
index 354baa8..0000000
--- a/admin/src/pages/Result/Success/index.tsx
+++ /dev/null
@@ -1,21 +0,0 @@
-import React, { memo } from 'react';
-import { Button } from 'tdesign-react';
-import { CheckCircleIcon } from 'tdesign-icons-react';
-
-import style from './index.module.less';
-
-const Success = () => (
-
-
-
项目已创建成功
-
可以联系负责人分发应用
-
-
-
-
-
-);
-
-export default memo(Success);
diff --git a/admin/src/pages/User/chart.ts b/admin/src/pages/User/chart.ts
deleted file mode 100644
index a96e692..0000000
--- a/admin/src/pages/User/chart.ts
+++ /dev/null
@@ -1,49 +0,0 @@
-export const visitData = {
- tooltip: {
- trigger: 'axis',
- },
- legend: {
- data: ['杯子', '茶叶', '蜂蜜', '面粉'],
- },
- grid: {
- left: '3%',
- right: '4%',
- bottom: '3%',
- containLabel: true,
- },
- toolbox: {
- feature: {
- saveAsImage: {},
- },
- },
- xAxis: {
- type: 'category',
- boundaryGap: false,
- data: ['周一', '周二', '周三', '周四', '周五', '周六', '周日'],
- },
- yAxis: {
- type: 'value',
- },
- series: [
- {
- name: '杯子',
- type: 'line',
- data: [21, 99, 56, 66, 55, 7, 83],
- },
- {
- name: '茶叶',
- type: 'line',
- data: [84, 30, 70, 14, 19, 75, 73],
- },
- {
- name: '蜂蜜',
- type: 'line',
- data: [57, 3, 25, 13, 49, 80, 11],
- },
- {
- name: '面粉',
- type: 'line',
- data: [8, 85, 2, 77, 10, 65, 90],
- },
- ],
-};
diff --git a/admin/src/pages/User/consts.ts b/admin/src/pages/User/consts.ts
deleted file mode 100644
index f450f45..0000000
--- a/admin/src/pages/User/consts.ts
+++ /dev/null
@@ -1,27 +0,0 @@
-// 团队成员数据
-export const TEAMS = [
- {
- id: 1,
- name: 'Lovellzhong 钟某某',
- position: '直客销售 港澳拓展组员工',
- avatar: 'https://tdesign.gtimg.com/mobile/demos/avatar1.png',
- },
- {
- id: 2,
- name: 'Jiajingwang 彭某某',
- position: '前端开发 前台研发组员工',
- avatar: 'https://tdesign.gtimg.com/mobile/demos/avatar2.png',
- },
- {
- id: 3,
- name: 'Cruisezhang 林某某',
- position: '技术产品 产品组员工',
- avatar: 'https://tdesign.gtimg.com/mobile/demos/avatar3.png',
- },
- {
- id: 4,
- name: 'Lovellzhang 商某某',
- position: '产品运营 港澳拓展组员工',
- avatar: 'https://tdesign.gtimg.com/mobile/demos/avatar4.png',
- },
-];
diff --git a/admin/src/pages/User/index.module.less b/admin/src/pages/User/index.module.less
deleted file mode 100644
index 12711a0..0000000
--- a/admin/src/pages/User/index.module.less
+++ /dev/null
@@ -1,97 +0,0 @@
-// 欢迎语
-.welcome {
- .name {
- font-size: 20px;
- color: var(--td-text-color-primary);
- }
-
- .regular {
- margin-right: 15px;
- font-size: 14px;
- }
-
- .logo {
- width: 180px;
- }
-}
-
-// 基本信息
-.userinfo {
- margin-top: 16px;
- .label {
- white-space: nowrap;
- text-overflow: ellipsis;
- overflow: hidden;
- line-height: 24px;
- margin: 20px 0 6px;
- font-size: 14px;
- color: var(--td-text-color-placeholder);
- }
-
- .value {
- white-space: nowrap;
- text-overflow: ellipsis;
- overflow: hidden;
- line-height: 40px;
- font-size: 14px;
- color: var(--td-text-color-primary);
- }
-}
-
-// 统计数据
-.statistics {
- margin-top: 16px;
- padding-top: 20px;
-}
-
-// 职位信息
-.postmsg {
- background: var(--td-brand-color);
- color: #fff;
- .avatar {
- width: 90px;
- height: 90px;
- font-size: 45px;
- position: relative;
- display: inline-flex;
- background: var(--td-brand-color-2);
- color: var(--td-text-color-brand);
- overflow: hidden;
- white-space: nowrap;
- vertical-align: middle;
- justify-content: center;
- align-items: center;
- box-sizing: content-box;
- border-radius: 50%;
- }
-
- .name {
- line-height: 37px;
- font-size: 20px;
- margin-top: 36px;
- }
-
- .position {
- line-height: 24px;
- font-size: 14px;
- margin-top: 8px;
- }
-}
-
-// 团队
-.teams {
- margin-top: 16px;
- :global {
- .t-list-item {
- padding: 15px 0;
- }
- }
-}
-
-// 服务产品
-.product {
- margin-top: 16px;
- .logo {
- width: 48px;
- }
-}
diff --git a/admin/src/pages/User/index.tsx b/admin/src/pages/User/index.tsx
deleted file mode 100644
index 21bcaad..0000000
--- a/admin/src/pages/User/index.tsx
+++ /dev/null
@@ -1,151 +0,0 @@
-import React, { memo } from 'react';
-import { Row, Col, Button, List, Card } from 'tdesign-react';
-import { IconFont } from 'tdesign-icons-react';
-import { BrowserRouterProps } from 'react-router-dom';
-import ReactEcharts from 'echarts-for-react';
-import { TEAMS } from './consts';
-import { visitData } from './chart';
-import ProductA from 'assets/svg/assets-product-1.svg?component';
-import ProductB from 'assets/svg/assets-product-2.svg?component';
-import ProductC from 'assets/svg/assets-product-3.svg?component';
-import ProductD from 'assets/svg/assets-product-4.svg?component';
-import useDynamicChart from 'hooks/useDynamicChart';
-
-import styles from './index.module.less';
-
-const { ListItem, ListItemMeta } = List;
-
-const User: React.FC = () => {
- const chartData = useDynamicChart(visitData, {
- placeholderColor: ['legend.textStyle.color', 'xAxis.axisLabel.color', 'yAxis.axisLabel.color'],
- });
- return (
-
-
-
-
-
-
- Hi,Image 下午好,今天是你加入鹅厂的第 100 天~
-
-
-
-
-
-
-
-
-
- }
- header
- >
-
-
- 手机
- +86 13923734567
-
-
- 座机
- 734567
-
-
- 办公室邮箱
- Account@qq.com
-
-
- 座位
- T32F 012
-
-
-
-
- 管理主体
- 腾讯集团
-
-
- 直属上级
- Michael Wang
-
-
- 职称
- 高级 UI 设计师
-
-
- 入职时间
- 2021-07-01
-
-
-
-
- 所属团队
- 腾讯/腾讯公司/某事业群/某产品部/某运营中心/商户服务组
-
-
-
-
-
-
-
-
-
-
- T
-
- My Account
- XXG 港澳业务拓展组员工 直客销售
-
-
-
-
- }
- >
-
- {TEAMS.map((item) => (
-
-
-
- ))}
-
-
-
-
-
- }
- >
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
-};
-
-export default memo(User);
diff --git a/admin/src/router/index.ts b/admin/src/router/index.ts
index a0a6481..1c0f65f 100644
--- a/admin/src/router/index.ts
+++ b/admin/src/router/index.ts
@@ -1,55 +1,101 @@
import React, { lazy } from 'react';
-import { BrowserRouterProps } from 'react-router-dom';
-import dashboard from './modules/dashboard';
-import list from './modules/list';
-import form from './modules/form';
-import detail from './modules/detail';
-import result from './modules/result';
-import user from './modules/user';
-import login from './modules/login';
-import otherRoutes from './modules/others';
+import type { BrowserRouterProps } from 'react-router-dom';
+import {
+ AiIcon,
+ BillIcon,
+ CheckCircleIcon,
+ ChartIcon,
+ ChartLineIcon,
+ CloudIcon,
+ CreditcardIcon,
+ DashboardIcon,
+ Dashboard1Icon,
+ DiscountIcon,
+ FileSearchIcon,
+ FolderIcon,
+ FolderOpenIcon,
+ GiftIcon,
+ HeartIcon,
+ LayersIcon,
+ LockOnIcon,
+ MailIcon,
+ NotificationIcon,
+ MoneyIcon,
+ OrderIcon,
+ ServerIcon,
+ SystemSettingIcon,
+ TaskIcon,
+ TaskSettingIcon,
+ UndertakeTransactionIcon,
+ UserIcon,
+ UserListIcon,
+ WalletIcon,
+ TemplateIcon,
+} from 'components/LineIcons';
export interface IRouter {
+ /** Internal path; Vite's `/admin/` base exposes it as `/admin/...`. */
path: string;
+ /** Visual groups keep absolute business paths without changing the URL. */
+ absolute?: boolean;
redirect?: string;
Component?: React.FC | (() => any);
- /**
- * 当前路由是否全屏显示
- */
isFullPage?: boolean;
- /**
- * meta未赋值 路由不显示到菜单中
- */
meta?: {
title?: string;
Icon?: React.FC;
- /**
- * 侧边栏隐藏该路由
- */
hidden?: boolean;
- /**
- * 单层路由
- */
single?: boolean;
+ requiredScope?: string;
};
children?: IRouter[];
}
+const BusinessPage = lazy(() => import('pages/Business'));
+const page = (path: string, title: string, Icon: React.FC, requiredScope = 'admin:read', hidden = false): IRouter => ({
+ path,
+ absolute: true,
+ Component: BusinessPage,
+ meta: { title, Icon, requiredScope, hidden },
+});
+const group = (path: string, title: string, Icon: React.FC, children: IRouter[]): IRouter => ({ path, meta: { title, Icon }, children });
+
const routes: IRouter[] = [
- {
- path: '/login',
- Component: lazy(() => import('pages/Login')),
- isFullPage: true,
- meta: {
- hidden: true,
- },
- },
- {
- path: '/',
- redirect: '/dashboard/base',
- },
+ { path: '/login', Component: lazy(() => import('pages/Login')), isFullPage: true, meta: { hidden: true } },
+ { path: '/', redirect: '/dashboard/base' },
+ { path: '/403', Component: lazy(() => import('pages/Result/403')), meta: { hidden: true } },
+ { path: '/404', Component: lazy(() => import('pages/Result/404')), meta: { hidden: true } },
+ group('/overview', '总览', Dashboard1Icon, [page('/dashboard/base', '系统概览', DashboardIcon)]),
+ group('/identity', '用户与认证', UserListIcon, [
+ page('/users', '用户管理', UserIcon, 'admin:security'),
+ page('/auth-messages', '认证消息', MailIcon, 'admin:security'),
+ page('/message-providers', '消息供应商', NotificationIcon, 'admin:security', true),
+ page('/message-templates', '消息模板', TemplateIcon, 'admin:security', true),
+ page('/security/mfa', '管理员安全', LockOnIcon, 'admin:security'),
+ ]),
+ group('/models', '渠道与模型', CloudIcon, [
+ page('/channels', '渠道管理', ServerIcon),
+ page('/channel-health', '渠道健康', HeartIcon),
+ page('/channel-groups', '渠道分组', LayersIcon),
+ page('/model-products', '模型产品', AiIcon),
+ page('/pricing', '价格规则', DiscountIcon),
+ ]),
+ group('/commerce', '计费与套餐', WalletIcon, [
+ page('/billing/units', '金币单位与换算', MoneyIcon, 'admin:finance'),
+ page('/billing', '余额与账本', BillIcon, 'admin:finance'),
+ page('/plans', '套餐管理', GiftIcon),
+ page('/recharges', '充值订单', OrderIcon, 'admin:finance'),
+ page('/payments', '支付事件', UndertakeTransactionIcon, 'admin:finance'),
+ page('/payment-providers', '支付适配器', CreditcardIcon, 'admin:finance'),
+ page('/approvals', '审批中心', CheckCircleIcon, 'admin:finance'),
+ ]),
+ group('/operations', '任务与运行', TaskSettingIcon, [page('/tasks', '任务中心', TaskIcon), page('/ws-metrics', '实时指标', ChartLineIcon)]),
+ group('/system', '存储与系统', FolderOpenIcon, [page('/storage', '存储策略', FolderIcon), page('/settings', '系统设置', SystemSettingIcon, 'admin:security')]),
+ group('/audit-group', '审计', FileSearchIcon, [
+ page('/audit', '日志大板', ChartIcon, 'admin:audit'),
+ page('/audit-logs', '详细日志', FileSearchIcon, 'admin:audit'),
+ ]),
+ { path: '*', Component: lazy(() => import('pages/Result/404')) },
];
-const allRoutes = [...routes, ...dashboard, ...list, ...form, ...detail, ...result, ...user, ...login, ...otherRoutes];
-
-export default allRoutes;
+export default routes;
diff --git a/admin/src/router/modules/dashboard.ts b/admin/src/router/modules/dashboard.ts
deleted file mode 100644
index 3def893..0000000
--- a/admin/src/router/modules/dashboard.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-import { lazy } from 'react';
-import { DashboardIcon } from 'tdesign-icons-react';
-import { IRouter } from '../index';
-
-const dashboard: IRouter[] = [
- {
- path: '/dashboard',
- meta: {
- title: '统计报表',
- Icon: DashboardIcon,
- },
- children: [
- {
- path: 'base',
- Component: lazy(() => import('pages/Dashboard/Base')),
- meta: {
- title: '概览仪表盘',
- },
- },
- {
- path: 'detail',
- Component: lazy(() => import('pages/Dashboard/Detail')),
- meta: {
- title: '统计报表',
- },
- },
- ],
- },
-];
-
-export default dashboard;
diff --git a/admin/src/router/modules/detail.ts b/admin/src/router/modules/detail.ts
deleted file mode 100644
index 0747bdc..0000000
--- a/admin/src/router/modules/detail.ts
+++ /dev/null
@@ -1,39 +0,0 @@
-import { lazy } from 'react';
-import { LayersIcon } from 'tdesign-icons-react';
-import { IRouter } from '../index';
-
-const result: IRouter[] = [
- {
- path: '/detail',
- meta: {
- title: '详情页',
- Icon: LayersIcon,
- },
- children: [
- {
- path: 'base',
- Component: lazy(() => import('pages/Detail/Base')),
- meta: {
- title: '基础详情页',
- },
- },
- {
- path: 'advanced',
- Component: lazy(() => import('pages/Detail/Advanced')),
- meta: { title: '多卡片详情页' },
- },
- {
- path: 'deploy',
- Component: lazy(() => import('pages/Detail/Deploy')),
- meta: { title: '数据详情页' },
- },
- {
- path: 'secondary',
- Component: lazy(() => import('pages/Detail/Secondary')),
- meta: { title: '二级详情页' },
- },
- ],
- },
-];
-
-export default result;
diff --git a/admin/src/router/modules/form.ts b/admin/src/router/modules/form.ts
deleted file mode 100644
index ecb15bd..0000000
--- a/admin/src/router/modules/form.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-import { lazy } from 'react';
-import { QueueIcon } from 'tdesign-icons-react';
-import { IRouter } from '../index';
-
-const result: IRouter[] = [
- {
- path: '/form',
- meta: {
- title: '表单类',
- Icon: QueueIcon,
- },
- children: [
- {
- path: 'base',
- Component: lazy(() => import('pages/Form/Base')),
- meta: {
- title: '基础表单页',
- },
- },
- {
- path: 'step',
- Component: lazy(() => import('pages/Form/Step')),
- meta: { title: '分步表单页' },
- },
- ],
- },
-];
-
-export default result;
diff --git a/admin/src/router/modules/list.ts b/admin/src/router/modules/list.ts
deleted file mode 100644
index d2de15d..0000000
--- a/admin/src/router/modules/list.ts
+++ /dev/null
@@ -1,41 +0,0 @@
-import { lazy } from 'react';
-import { ViewModuleIcon } from 'tdesign-icons-react';
-import { IRouter } from '../index';
-
-const result: IRouter[] = [
- {
- path: '/list',
- meta: {
- title: '列表页',
- Icon: ViewModuleIcon,
- },
- children: [
- {
- path: 'base',
- Component: lazy(() => import('pages/List/Base')),
- meta: {
- title: '基础列表页',
- },
- },
- {
- path: 'card',
- Component: lazy(() => import('pages/List/Card')),
- meta: {
- title: '卡片列表页',
- },
- },
- {
- path: 'select',
- Component: lazy(() => import('pages/List/Select')),
- meta: { title: '筛选列表页' },
- },
- {
- path: 'tree',
- Component: lazy(() => import('pages/List/Tree')),
- meta: { title: '树状筛选列表页' },
- },
- ],
- },
-];
-
-export default result;
diff --git a/admin/src/router/modules/login.ts b/admin/src/router/modules/login.ts
deleted file mode 100644
index 767cc55..0000000
--- a/admin/src/router/modules/login.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-import { lazy } from 'react';
-import { LogoutIcon } from 'tdesign-icons-react';
-import { IRouter } from '../index';
-
-const result: IRouter[] = [
- {
- path: '/login',
- meta: {
- title: '登录页',
- Icon: LogoutIcon,
- },
- children: [
- {
- path: 'index',
- Component: lazy(() => import('pages/Login')),
- isFullPage: true,
- meta: {
- title: '登录中心',
- },
- },
- ],
- },
-];
-
-export default result;
diff --git a/admin/src/router/modules/others.ts b/admin/src/router/modules/others.ts
deleted file mode 100644
index ecf8398..0000000
--- a/admin/src/router/modules/others.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-import { lazy } from 'react';
-import { IRouter } from '../index';
-
-const otherRoutes: IRouter[] = [
- {
- path: '/403',
- Component: lazy(() => import('pages/Result/403')),
- },
- {
- path: '/500',
- Component: lazy(() => import('pages/Result/500')),
- },
- {
- path: '*',
- Component: lazy(() => import('pages/Result/404')),
- },
-];
-
-export default otherRoutes;
diff --git a/admin/src/router/modules/result.ts b/admin/src/router/modules/result.ts
deleted file mode 100644
index 6c2a813..0000000
--- a/admin/src/router/modules/result.ts
+++ /dev/null
@@ -1,73 +0,0 @@
-import { lazy } from 'react';
-import { CheckCircleIcon } from 'tdesign-icons-react';
-import { IRouter } from '../index';
-
-const result: IRouter[] = [
- {
- path: '/result',
- meta: {
- title: '结果页',
- Icon: CheckCircleIcon,
- },
- children: [
- {
- path: 'success',
- Component: lazy(() => import('pages/Result/Success')),
- meta: {
- title: '成功页',
- },
- },
- {
- path: 'fail',
- Component: lazy(() => import('pages/Result/Fail')),
- meta: {
- title: '失败页',
- },
- },
- {
- path: 'network-error',
- Component: lazy(() => import('pages/Result/NetworkError')),
- meta: {
- title: '网络异常',
- },
- },
- {
- path: '403',
- Component: lazy(() => import('pages/Result/403')),
- meta: {
- title: '无权限',
- },
- },
- {
- path: '404',
- Component: lazy(() => import('pages/Result/404')),
- meta: {
- title: '访问页面不存在页',
- },
- },
- {
- path: '500',
- Component: lazy(() => import('pages/Result/500')),
- meta: {
- title: '服务器出错页',
- },
- },
- {
- path: 'browser-incompatible',
- Component: lazy(() => import('pages/Result/BrowserIncompatible')),
- meta: {
- title: '浏览器不兼容页',
- },
- },
- {
- path: 'maintenance',
- Component: lazy(() => import('pages/Result/Maintenance')),
- meta: {
- title: '系统维护页',
- },
- },
- ],
- },
-];
-
-export default result;
diff --git a/admin/src/router/modules/user.ts b/admin/src/router/modules/user.ts
deleted file mode 100644
index 95f6588..0000000
--- a/admin/src/router/modules/user.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-import { lazy } from 'react';
-import { UserCircleIcon } from 'tdesign-icons-react';
-import { IRouter } from '../index';
-
-const result: IRouter[] = [
- {
- path: '/user',
- meta: {
- title: '个人页',
- Icon: UserCircleIcon,
- },
- children: [
- {
- path: 'index',
- Component: lazy(() => import('pages/User')),
- meta: {
- title: '个人中心',
- },
- },
- ],
- },
-];
-
-export default result;
diff --git a/admin/src/services/contract.ts b/admin/src/services/contract.ts
deleted file mode 100644
index eccb97d..0000000
--- a/admin/src/services/contract.ts
+++ /dev/null
@@ -1,35 +0,0 @@
-import request from 'utils/request';
-
-export interface IContract {
- adminName: string;
- amount: string;
- contractType: number;
- index: number;
- name: string;
- no: string;
- paymentType: 1 | 2;
- status: number;
- updateTime: string;
-}
-
-interface IResult {
- list: IContract[];
-}
-
-interface IParams {
- pageSize: number;
- current: number;
-}
-
-export const getContractList = async (params: IParams) => {
- const result = await request.get('/api/get-list');
-
- // 模拟接口分页
- let list = result?.data?.list || [];
- const total = list.length;
- list = list.splice(params.pageSize * (params.current - 1), params.pageSize);
- return {
- list,
- total,
- };
-};
diff --git a/admin/src/services/platform.ts b/admin/src/services/platform.ts
new file mode 100644
index 0000000..6434f2a
--- /dev/null
+++ b/admin/src/services/platform.ts
@@ -0,0 +1,154 @@
+type ApiPayload = { ok: true; data: T; requestId: string } | { ok: false; error: { message?: string; code?: string; details?: unknown; requestId?: string } };
+
+export class AdminRequestError extends Error {
+ status: number;
+
+ code?: string;
+
+ details?: unknown;
+
+ requestId?: string;
+
+ constructor(message: string, status: number, code?: string, details?: unknown, requestId?: string) {
+ super(message);
+ this.name = 'AdminRequestError';
+ this.status = status;
+ this.code = code;
+ this.details = details;
+ this.requestId = requestId;
+ }
+}
+
+const adminErrorLabels: Record = {
+ UNAUTHORIZED: '身份验证失败,请重新登录。',
+ FORBIDDEN: '当前账号没有执行此操作的权限。',
+ VALIDATION_FAILED: '提交内容不符合要求,请检查字段。',
+ RATE_LIMITED: '操作过于频繁,请稍后重试。',
+ NOT_FOUND: '请求的数据不存在。',
+ CONFLICT: '数据已发生变化,请刷新后重试。',
+ IDEMPOTENCY_CONFLICT: '重复操作的内容不一致,请刷新页面后重试。',
+ VERSION_CONFLICT: '数据版本已变化,请刷新后重试。',
+ INSUFFICIENT_BALANCE: '账户金币不足。',
+ CAPABILITY_NOT_ENABLED: '当前能力尚未开放。',
+ MODEL_GROUP_UNAVAILABLE: '当前模型渠道暂不可用,请检查渠道健康。',
+ VERIFICATION_REQUIRED: '该账户需要先完成身份验证。',
+ VERIFICATION_EXPIRED: '验证信息已失效,请重新发起。',
+ SESSION_REUSED: '当前会话存在安全风险,请重新登录。',
+ OBJECT_NOT_FOUND: '关联文件不存在或已经过期。',
+ OBJECT_OWNERSHIP_DENIED: '当前账号无权访问关联文件。',
+ FILE_REJECTED: '文件未通过格式或安全检查。',
+ STORAGE_UNAVAILABLE: '存储服务暂时不可用。',
+ APPROVAL_REQUIRED: '该操作需要审批后才能生效。',
+ ADMIN_MFA_REQUIRED: '需要有效的管理员两步验证码。',
+ REQUEST_TIMEOUT: '请求超时,请检查网络或供应商状态后重试。',
+ REQUEST_ABORTED: '请求已取消。',
+ INTERNAL_ERROR: '后台服务异常,请稍后重试。',
+};
+
+export function formatAdminError(error: unknown, fallback = '操作失败。') {
+ if (error instanceof AdminRequestError) {
+ if (error.code && adminErrorLabels[error.code]) return adminErrorLabels[error.code];
+ if (error.status >= 500) return '后台服务暂时不可用,请稍后重试。';
+ if (typeof error.message === 'string' && /[\u3400-\u9fff]/.test(error.message)) return error.message;
+ return fallback;
+ }
+ const message = error instanceof Error ? error.message : '';
+ if (error instanceof Error && error.name === 'AbortError') return '请求已取消。';
+ if (/failed to fetch|network|timeout|timed out|连接超时/i.test(message)) return '网络连接失败,请检查地址和网络后重试。';
+ return fallback;
+}
+
+let accessToken: string | null = null;
+let refreshPromise: Promise | null = null;
+
+// Provider probes/model discovery can involve an upstream request. Keep the
+// admin UI from waiting forever when that upstream never responds.
+export const ADMIN_REQUEST_TIMEOUT_MS = 120_000;
+
+export function setAdminAccessToken(token: string | null) { accessToken = token; }
+export function getAdminAccessToken() { return accessToken; }
+
+function csrfToken() {
+ const cookies = document.cookie.split(';').map((value) => value.trim()).filter(Boolean);
+ // The server appends `_admin` to the configured CSRF cookie name. Prefer
+ // the default name, then accept a custom name without exposing the
+ // HttpOnly refresh cookie to JavaScript.
+ const item = cookies.find((value) => value.startsWith('miragenflow_csrf_admin='))
+ || cookies.find((value) => {
+ const name = value.slice(0, value.indexOf('='));
+ return name.endsWith('_admin') && !name.startsWith('miragenflow_refresh');
+ });
+ if (!item) return undefined;
+ const separator = item.indexOf('=');
+ return separator >= 0 ? decodeURIComponent(item.slice(separator + 1)) : undefined;
+}
+
+export async function adminRequest(path: string, init: RequestInit = {}, retry = true): Promise {
+ const headers = new Headers(init.headers);
+ const method = (init.method || 'GET').toUpperCase();
+ if (!['GET', 'HEAD', 'OPTIONS'].includes(method) && !path.includes('/auth/')) {
+ if (!headers.has('idempotency-key')) headers.set('idempotency-key', crypto.randomUUID());
+ }
+ headers.set('accept', 'application/json');
+ if (typeof init.body === 'string' && !headers.has('content-type')) headers.set('content-type', 'application/json');
+ if (accessToken) headers.set('authorization', `Bearer ${accessToken}`);
+ const csrf = csrfToken();
+ if (csrf) headers.set('x-csrf-token', csrf);
+ const controller = new AbortController();
+ let timedOut = false;
+ const timeoutId = window.setTimeout(() => {
+ timedOut = true;
+ controller.abort();
+ }, ADMIN_REQUEST_TIMEOUT_MS);
+ const abortFromCaller = () => controller.abort();
+ if (init.signal) {
+ if (init.signal.aborted) abortFromCaller();
+ else init.signal.addEventListener('abort', abortFromCaller, { once: true });
+ }
+
+ let response: Response;
+ let text: string;
+ try {
+ response = await fetch(path, { ...init, headers, credentials: 'include', signal: controller.signal });
+ text = await response.text();
+ } catch (cause) {
+ if (timedOut) throw new AdminRequestError('请求超时。', 408, 'REQUEST_TIMEOUT');
+ if (init.signal?.aborted) throw new AdminRequestError('请求已取消。', 499, 'REQUEST_ABORTED');
+ throw cause;
+ } finally {
+ window.clearTimeout(timeoutId);
+ init.signal?.removeEventListener('abort', abortFromCaller);
+ }
+ let payload: ApiPayload | undefined;
+ if (text) { try { payload = JSON.parse(text) as ApiPayload; } catch { payload = undefined; } }
+ const canRefreshSession = !path.includes('/auth/');
+ if (response.status === 401 && retry && canRefreshSession) {
+ if (init.signal?.aborted) throw new AdminRequestError('请求已取消。', 499, 'REQUEST_ABORTED');
+ // The refresh helper is declared below to keep the public request API first.
+ // eslint-disable-next-line no-use-before-define
+ const token = await refreshAdminSession();
+ if (init.signal?.aborted) throw new AdminRequestError('请求已取消。', 499, 'REQUEST_ABORTED');
+ if (token) return adminRequest(path, { ...init, headers }, false);
+ }
+ if (!response.ok || !payload?.ok) {
+ const error = payload && !payload.ok ? payload.error : undefined;
+ throw new AdminRequestError(error?.message || `HTTP ${response.status}`, response.status, error?.code, error?.details, error?.requestId);
+ }
+ return payload.data;
+}
+
+export function adminCaptcha(target = '') {
+ return adminRequest<{ challengeId: string; question: string; expiresAt: string }>('/api/v1/auth/challenge', { method: 'POST', body: JSON.stringify({ action: 'admin-login', target, deviceId: 'admin-browser' }) });
+}
+
+export async function refreshAdminSession() {
+ if (!refreshPromise) {
+ refreshPromise = adminRequest<{ accessToken: string }>('/api/v1/admin/auth/refresh', { method: 'POST' }, false).then((result) => { accessToken = result.accessToken; return accessToken; }).catch(() => { accessToken = null; return null; }).finally(() => { refreshPromise = null; });
+ }
+ return refreshPromise;
+}
+
+export async function adminLogout() {
+ await adminRequest('/api/v1/admin/auth/logout', { method: 'POST' }, false).catch(() => undefined);
+ accessToken = null;
+}
diff --git a/admin/src/services/product.ts b/admin/src/services/product.ts
deleted file mode 100644
index 4b4c4c1..0000000
--- a/admin/src/services/product.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-import request from 'utils/request';
-
-export interface IProduct {
- banner: string;
- description: string;
- index: number;
- isSetup: boolean;
- name: string;
- type: number;
-}
-
-interface IResult {
- list: IProduct[];
-}
-
-interface IParams {
- pageSize: number;
- current: number;
-}
-
-export const getProductList = async (params: IParams) => {
- const result = await request.get('api/get-card-list');
-
- // 模拟接口分页
- let list = result?.data?.list || [];
- const total = list.length;
- list = list.splice(params.pageSize * (params.current - 1), params.pageSize);
- return {
- list,
- total,
- };
-};
diff --git a/admin/src/styles/index.less b/admin/src/styles/index.less
index 08df376..865e864 100644
--- a/admin/src/styles/index.less
+++ b/admin/src/styles/index.less
@@ -15,3 +15,21 @@ body,
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace;
}
+
+// Localized Lineicons are rendered as plain SVG nodes instead of TDesign's .t-icon.
+.t-button > svg {
+ width: 22px;
+ height: 22px;
+ flex: 0 0 auto;
+}
+
+.t-button > svg + .t-button__text:not(:empty) {
+ margin-left: 8px;
+}
+
+.t-input__prefix > svg {
+ width: 20px;
+ height: 20px;
+ flex: 0 0 auto;
+ color: var(--td-text-color-placeholder);
+}
diff --git a/admin/src/utils/chart.ts b/admin/src/utils/chart.ts
deleted file mode 100644
index c8949b6..0000000
--- a/admin/src/utils/chart.ts
+++ /dev/null
@@ -1,38 +0,0 @@
-// 获取 chart 的 mock 数据
-import dayjs, { Dayjs } from 'dayjs';
-
-const RECENT_7_DAYS: [Dayjs, Dayjs] = [dayjs().subtract(7, 'day'), dayjs().subtract(1, 'day')];
-export const ONE_WEEK_LIST = ['周一', '周二', '周三', '周四', '周五', '周六', '周日'];
-
-export const getRandomInt = (num = 100): number => {
- const resultNum = Number((Math.random() * num).toFixed(0));
- return resultNum <= 1 ? 1 : resultNum;
-};
-
-type ChartValue = number | string;
-
-export function getTimeArray(dateTime: string[] = [], divideNum = 10, format = 'MM-DD'): string[] {
- const timeArray = [];
- if (dateTime.length === 0) {
- dateTime.push(...RECENT_7_DAYS.map((item) => item.format(format)));
- }
- for (let i = 0; i < divideNum; i++) {
- const dateAbsTime: number = (new Date(dateTime[1]).getTime() - new Date(dateTime[0]).getTime()) / divideNum;
- const timeNode: number = new Date(dateTime[0]).getTime() + dateAbsTime * i;
- timeArray.push(dayjs(timeNode).format(format));
- }
-
- return timeArray;
-}
-
-export const getChartDataSet = (dateTime: Array = [], divideNum = 10): ChartValue[][] => {
- const timeArray = getTimeArray(dateTime, divideNum);
- const inArray = [];
- const outArray = [];
- for (let index = 0; index < divideNum; index++) {
- inArray.push(getRandomInt().toString());
- outArray.push(getRandomInt().toString());
- }
-
- return [timeArray, inArray, outArray];
-};
diff --git a/admin/src/utils/request.ts b/admin/src/utils/request.ts
deleted file mode 100644
index 6088250..0000000
--- a/admin/src/utils/request.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-import axios from 'axios';
-import proxy from '../configs/host';
-
-const env = import.meta.env.MODE || 'development';
-const API_HOST = proxy[env].API;
-
-const SUCCESS_CODE = 0;
-const TIMEOUT = 5000;
-
-export const instance = axios.create({
- baseURL: API_HOST,
- timeout: TIMEOUT,
- withCredentials: true,
-});
-
-instance.interceptors.response.use(
- // eslint-disable-next-line consistent-return
- (response) => {
- if (response.status === 200) {
- const { data } = response;
- if (data.code === SUCCESS_CODE) {
- return data;
- }
- return Promise.reject(data);
- }
- return Promise.reject(response?.data);
- },
- (e) => Promise.reject(e),
-);
-
-export default instance;
diff --git a/admin/tsconfig.json b/admin/tsconfig.json
index 417e08f..f2a03bc 100644
--- a/admin/tsconfig.json
+++ b/admin/tsconfig.json
@@ -12,6 +12,10 @@
"noFallthroughCasesInSwitch": true,
"module": "esnext",
"moduleResolution": "node",
+ "paths": {
+ "@miragenflow/contracts": ["../../packages/contracts/src/index.ts"],
+ "@miragenflow/ui/*": ["../../packages/ui/src/*"]
+ },
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
diff --git a/admin/vite.config.js b/admin/vite.config.js
index eb8931c..0b7755c 100644
--- a/admin/vite.config.js
+++ b/admin/vite.config.js
@@ -1,6 +1,5 @@
import path from 'path';
import { loadEnv } from 'vite';
-import { viteMockServe } from 'vite-plugin-mock';
import react from '@vitejs/plugin-react';
import svgr from '@honkhonk/vite-plugin-svgr';
@@ -14,7 +13,9 @@ export default (params) => {
return {
base: baseUrl,
resolve: {
+ dedupe: ['react', 'react-dom', 'tdesign-react'],
alias: {
+ 'tdesign-react': path.resolve(__dirname, './node_modules/tdesign-react'),
assets: path.resolve(__dirname, './src/assets'),
components: path.resolve(__dirname, './src/components'),
configs: path.resolve(__dirname, './src/configs'),
@@ -30,6 +31,12 @@ export default (params) => {
},
},
+ optimizeDeps: {
+ // TDesign Alert uses react-transition-group's ESM entry, which imports
+ // CommonJS prop-types. Pre-bundle both modules for Vite 2 dev parity.
+ include: ['prop-types', 'react-transition-group'],
+ },
+
css: {
preprocessorOptions: {
less: {
@@ -43,11 +50,6 @@ export default (params) => {
plugins: [
svgr(),
react(),
- mode === 'mock' &&
- viteMockServe({
- mockPath: './mock',
- localEnabled: true,
- }),
],
build: {
@@ -61,8 +63,9 @@ export default (params) => {
'/api': {
// 用于开发环境下的转发请求
// 更多请参考:https://vitejs.dev/config/#server-proxy
- target: 'https://service-exndqyuk-1257786608.gz.apigw.tencentcs.com',
+ target: `http://127.0.0.1:${process.env.MIRAGENFLOW_SERVER_PORT || '3100'}`,
changeOrigin: true,
+ ws: true,
},
},
},
diff --git a/develop.md b/develop.md
new file mode 100644
index 0000000..3dcd7ce
--- /dev/null
+++ b/develop.md
@@ -0,0 +1,1005 @@
+# MiragenFlow 一期开发规划
+
+> 本文是产品、前台、后台和服务端网关的一期设计基线。实现按本文分阶段落地,未完成部分必须在验收记录中明确,不得用演示数据冒充完成。
+>
+> 参考产品:`https://holopix.cn/`。参考重点是它的功能理念和工作流组织方式,不复制其品牌、文案、素材或内部实现。
+
+## 0. 一期目标与已知约束
+
+### 0.1 一期目标
+
+把 MiragenFlow 从“浏览器直连模型的本地画布工具”升级为一个具备完整商业化基础链路的 AI 游戏美术创作产品:
+
+```text
+注册/登录
+ -> 获取公开模型产品目录
+ -> 选择模型档位、分辨率和创作参数
+ -> 提交图片/文本/音频/图片工具任务
+ -> 服务端鉴权、校验、预扣余额
+ -> 按模型产品绑定的渠道分组顺序路由
+ -> 单渠道失败自动切换后续渠道
+ -> 返回任务进度和结果
+ -> 成功扣除实际消耗,失败释放预扣余额
+ -> 结果进入画布、资产库和历史记录
+```
+
+### 0.2 本文直接采用的设计原则
+
+1. **前台用户只看到产品能力,不看到供应商内部信息。** 用户看到“基础模型 / 高级模型 / 旗舰模型”和后台配置的分辨率、数量、价格,不直接看到供应商 API Key、渠道地址或原始 provider model ID。
+2. **模型产品和供应商渠道解耦。** 用户选择的是 `modelProduct`,后台把它绑定到一个有顺序的 `channelGroup`;组内可以放多个供应商账号和多个内部模型 ID。
+3. **组内按顺序故障切换。** 第一个渠道超时、限流、网关错误或被管理员禁用时,自动尝试下一个渠道;不会因为单个供应商故障直接中断用户任务。
+4. **不做静默降档。** 一个旗舰模型组全部不可用时,任务明确失败并建议用户切换到其他旗舰模型;除非管理员显式配置,不自动把旗舰请求降级为高级或基础模型。
+5. **余额制计费。** 充值增加余额,任务按公开价格和当前余额单位扣减;所有扣款、预扣、释放、退款均进入不可变账本。
+6. **现有 OpenAI 图片协议优先迁移。** 无限画布项目已有的图片生成、图片编辑、响应解析和错误归一化逻辑迁移到服务端网关/供应商适配层,前台不再保存外部 API Key 并直连供应商。
+7. **WebDAV 继续保留。** 它用于用户保存画布、资产和生成文件;一期不把 WebDAV 误写成平台自带云盘,也不强制把全部大图永久存放在平台服务器。
+8. **视频和动画先不开发。** Holopix 的视频、首尾帧、图生视频、文生视频和动画能力作为参考差距记录,但不进入当前一期开发清单。音频生成保留。
+9. **V1 认证增强可配置。** 不接第三方登录;用户邮箱/手机号验证、图形验证码或等价人机校验、登录风控和两步验证都保留为可配置能力,普通用户默认关闭,后台开启后才生效。管理员保持独立认证域,邮箱+密码为默认登录链路;管理员 MFA、管理员人机验证码等增强策略也由后台开关控制,默认关闭,开启/关闭均记录审计。
+10. **V1 统一使用可配置余额单位。** 用户界面显示“金币”或后台配置的文字/图标单位,不出现“积分”概念。充值货币通过后台发布的换算比例转成余额单位,账本使用最小整数单位记录。
+11. **V1 强制异步任务和 WebSocket。** 生图、音频、反推、拆分、多角度等耗时操作统一进入队列;WebSocket 是长任务进度的主通道,轮询只作为断线或兼容降级。
+12. **V1 存储为浏览器本地 + WebDAV。** 平台对象存储只实现接口和适配器边界,不作为一期默认持久化目标;服务端临时 staging 文件必须隔离于静态目录并按 TTL 清理。
+13. **真正的图生 3D/文生 3D 延后。** 在没有确认可稳定生成、导出和预览的模型前,V1 不开放 3D 生成入口;只保留能力矩阵、adapter 接口和后台开关。
+
+### 0.3 当前技术基线
+
+- 前台:Vite、React、TypeScript、React Router、TDesign、Tailwind、Zustand。
+- 画布:现有 DOM/SVG 无限画布、节点 registry、图片工具和 ZIP/WebDAV 数据能力继续复用。
+- 后台:现有 TDesign React Starter 继续作为管理台壳,替换模板 mock 页面和示例业务。
+- 本地/容器同源:Nginx 将 `/api/*` 和 `/api/v1/ws/*` 反代到 `server:3100`;开发环境由 Vite 代理到同一服务。生产密钥仅通过运行环境注入。
+- 服务端建议基线:Node.js + TypeScript、HTTP API、WebSocket 网关、PostgreSQL、Redis、任务队列、隔离的本地 staging、邮件/SMS/支付适配器。对象存储 adapter 预留但 V1 默认关闭。具体框架可以在技术评审时确定,但领域边界和接口契约先按本文执行。
+- 外部模型规范:一期统一使用 OpenAI 兼容图片接口作为规范化入口;其他供应商通过服务端 adapter 转换,不向前台暴露差异。
+- 用户端模板:`docs/design-reference/MiragenFlow-front/` 只作为设计和组件输入;不得让运行代码从该参考目录跨层引用。
+
+## 1. 前台产品范围
+
+前台用户只负责创作、查看自己的任务和余额、管理自己的资产/存储。渠道、供应商账号、价格表、路由优先级和健康状态全部由后台控制。
+
+### 1.1 首页和工作台入口
+
+- 保留网站首页,展示元境幻生定位、游戏美术/艺术资产/多视图工作流;3D 生成仅作为后续能力说明,不提供 V1 入口。
+- 首页入口至少包括:开始创作、打开画布、查看资产、登录/注册、余额状态。
+- 登录前可以浏览首页和公开能力说明;未登录提交生成任务时跳转登录/注册。
+- 首页不展示供应商名、内部模型 ID、API Key 配置和渠道状态。
+
+### 1.2 用户注册、登录和账户中心
+
+一期用户端必须支持:
+
+- 邮箱注册和邮箱验证码验证。
+- 手机号注册和短信验证码验证;是否强制手机号由后台策略控制。
+- V1 不接入 Google、Apple、微信等第三方 OAuth/社交登录。
+- 密码登录、验证码登录(是否开放由后台策略控制)。
+- Access Token + Refresh Token 会话。
+- 用户两步验证默认关闭;后台开启后才要求用户完成 MFA,启用/关闭必须记录操作者、原因和生效时间。
+- 退出登录、刷新会话、修改密码、忘记密码、重新发送验证邮件/短信。
+- 账户状态:正常、待验证、冻结、注销中。
+- 基本账户页:昵称、邮箱/手机号脱敏、余额、消费记录、任务历史、登录设备。
+- 风险控制:验证码发送频率限制、图形验证码/人机校验、登录失败次数限制、IP/设备限流、异常会话注销;普通用户默认不展示验证挑战,开关开启后才执行。
+
+认证安全基线固定如下:
+
+- 生产环境使用同源会话;短期 Access Token 只保存在前端内存,Refresh Token 只通过 `HttpOnly`、`Secure`、`SameSite=Lax/Strict` Cookie 传输,不写入 `localStorage` 或画布导出文件。
+- Access Token 必须包含明确的 `aud`、`sub`、`scope` 和过期时间;Refresh Token 采用轮换、哈希存储和复用检测,检测到旧 Token 重放时撤销同一设备会话链。
+- Cookie 会话启用 CSRF 防护;跨域开发请求必须经过本地反向代理,不允许前端直接把凭证交给第三方域名。
+- 邮箱和手机号先做规范化再建立唯一索引;验证码发送、消费和失败次数使用原子更新,验证码仅保存哈希。未完成验证的账户可以浏览公开页面,但不能调用会产生费用的任务接口。
+- 密码使用成熟密码哈希算法(优先 Argon2id),记录算法版本;重置 Token 单次消费并在修改密码后撤销其他会话。账户注销通过 `POST /api/v1/me/close` 进入 `注销中`,由异步清理任务完成匿名化/删除。
+- 前端在刷新会话时只允许一个刷新请求,其余请求等待同一结果;刷新失败后清空内存身份并回到登录页。
+
+前台不再使用当前“浏览器本地空 user store”作为商业认证。`useUserStore` 改为服务端会话状态;登录成功后所有需要计费的 API 都必须携带用户身份。
+
+### 1.3 公开模型产品目录
+
+用户看到的是后台发布的产品目录,而不是渠道配置。
+
+建议公开结构:
+
+| 用户看到的选项 | 后台控制内容 |
+| --- | --- |
+| 基础模型 | 公开模型 ID、绑定的渠道分组、内部模型 ID、默认参数、单次价格、可用任务类型 |
+| 高级模型 | 公开模型 ID、绑定的渠道分组、内部模型 ID、质量策略、单次价格、并发限制 |
+| 旗舰模型 | 公开模型 ID、绑定的渠道分组、内部模型 ID、最高质量策略、单次价格、失败提示文案 |
+| 分辨率 | 低/中/高/超高或后台自定义名称、实际像素尺寸、价格倍率、模型支持范围 |
+| 生成数量 | 后台设置最大数量和阶梯价格 |
+| 功能模式 | 文生图、图生图、局部编辑、反推提示词、多角度、抠图、拆分等 V1 能力开关;3D capability 仅保留禁用的预留开关 |
+
+#### 模型 ID 可见性决策
+
+一期固定采用“**显示公开模型 ID,不显示 provider 信息**”方案:
+
+- 前台请求只携带公开的 `modelProductId`、`resolutionPresetId`、业务参数和幂等键。
+- 前台最多显示后台配置的稳定公共模型 ID,例如 `basic-image-v1`;该 ID 与 provider 无关。
+- 后台保存多个渠道的 `providerModelId`,用于调用、审计和故障排查;同一公开模型 ID 可以映射多个 provider 的不同内部 ID。
+- 前台、用户导出文件和公开 API 均不得返回 provider 名称、Base URL、API Key、渠道顺序或 provider model ID。
+- 管理员可以查看精确内部 ID;客服排障使用任务 ID 和 attempt ID,不要求用户知道供应商信息。
+
+如果后续确认必须让专业用户看到模型信息,应只增加“公开别名/能力说明”,不要暴露渠道地址、Key、供应商账号或未经抽象的内部 ID。
+
+### 1.4 无限画布工作台
+
+保留并升级当前 `/canvas/:id`:
+
+- 图片、文本、音频、生成配置、分组节点。
+- 平移、缩放、框选、拖拽、调整尺寸、连线、分组、复制、删除、撤销/重做、小地图。
+- 画布背景、节点标题、节点 JSON/信息查看、批量图片组。
+- 画布项目创建、重命名、导入、导出、删除;项目级复制作为后续补充,当前不假设已实现。
+- 画布内直接插入资产、上传图片/音频、粘贴剪贴板图片/文本。
+- 任务节点展示排队、运行、成功、失败、取消、部分成功状态。
+- 生成结果可以继续作为下一个节点的提示词、参考图或编辑输入。
+
+一期要把当前“节点生成直接调用浏览器 API”改成“节点生成调用平台任务 API”,但保留当前节点数据结构和工作流表达方式,避免一次性重写画布交互。
+
+### 1.5 游戏资产和艺术资产工作流
+
+参考 Holopix 的功能理念,一期前台应提供以下产品能力入口。每项能力都应能从普通工具页或画布节点触发,并生成可回到画布/资产库的结果。
+
+#### 通用生成和编辑
+
+- 文生图、图生图、参考图编辑。
+- 风格转换、构图参考、风格参考、主体参考。
+- 万能渲染:统一承载构图、风格、参考和质量参数,而不是暴露供应商 API 差异。
+- 局部细化、局部替换、擦除/重绘。
+- 智能扩图/扩展画布。
+- 相似图裂变/变体生成。
+- 一键成稿、丰富细节、重绘放大。
+- 反推提示词:从图片生成结构化、可再次执行的提示词结果。
+
+#### 游戏资产专项
+
+- 游戏角色、场景、道具、武器、ICON、UI 素材生成。
+- 透明背景和前景抠图。
+- Sprite Sheet/规则网格切分。
+- 全能拆分:自动识别角色、道具、ICON、背景和可分离区域,输出透明子图、边界框、命名和关联组。
+- 图层拆补:从一张合成图中恢复或重建局部图层。
+- 线稿提取、材质/颜色参考和调色板生成。
+- 像素图转换和像素风格变体。
+- 表情生成或角色表情变体。
+- 统一尺寸、命名、透明边缘和导出规格校验。
+
+#### 多视图和 3D 化(V1 仅保留 2D 能力)
+
+- 正视图、斜侧图、侧视图、俯视图、仰视图、背视图。
+- 一键三视图、一键六视图,结果作为关联视图组。
+- 自由视角:输入参考图后,使用水平角、俯仰角、相机距离、镜头范围控制视角。
+- V1 不开放真正的单图/多图转 3D、文生 3D、图生 3D、3D 预览和模型导出;这些入口由 feature flag 关闭。
+- V1 只实现多视图/自由视角的 2D 参考图任务,结果为关联图片组。
+- 服务端保留 3D adapter、GLB/GLTF、OBJ、纹理和缩略图字段,待确认有稳定模型并通过生成、预览、导出验收后再启用。
+
+当前画布可以复用为 2D 编排层;后续真正的 3D 节点和自由摄像机需要新增 Three.js/WebGL 视口,不能把当前“角度 prompt 重绘”包装成真实 3D。
+
+### 1.6 音频能力
+
+音频按既定范围保留:
+
+- 文本生成语音。
+- voice、格式、速度、instructions 参数。
+- 音频任务进度、取消、失败重试、下载和画布节点引用。
+- 音频文件进入统一媒体结果和 WebDAV 同步链路。
+
+视频、首尾帧视频、图生视频、文生视频和动画时间轴不进入一期。
+
+所有前台能力通过目录能力矩阵发布:每个 `modelProduct` 返回 `capabilities[]`、参数 schema、输入类型/大小、是否异步、结果类型和失败码映射。前台只渲染当前产品声明的控件;当能力未配置或版本失效时显示“暂不可用”,不自行猜测 provider 参数。图片、音频和 V1 多视图结果统一关联 `taskId`、`outputId`、资产/画布引用和保留期;3D 结果字段只做接口预留。
+
+### 1.7 资产、历史和存储
+
+- 我的资产:图片、文本,后续可扩展音频、3D、Sprite、序列帧和模型。
+- 搜索、标签、类型筛选、预览、编辑、复制、下载、删除、画布插入。
+- 作品/任务历史:按项目、任务类型、模型产品、时间、状态筛选。
+- 生成结果版本:保留输入参数、公开模型产品、分辨率、任务 ID 和输出关联,不暴露供应商凭证。
+- 本地:画布、节点、资产索引和已保存媒体默认写入浏览器 IndexedDB/localForage,按用户命名空间隔离;认证 Refresh Token、API Key 和 WebDAV 密码不得进入本地业务存储。
+- WebDAV:用户配置并同步画布、资产、图片、音频及后续 3D/序列文件。同步使用每用户命名空间、manifest 版本、校验和和软删除标记;同一资源发生双向修改时保留冲突副本并提示用户选择,不静默覆盖。
+- WebDAV 凭证在服务端加密保存或使用 KMS 引用,前台只能看到脱敏连接状态;Base URL 做 HTTPS/allowlist/私网阻断和路径规范化,连接失败进入可重试的离线队列,不阻塞生成任务主链路。
+- 平台侧 V1 只保存任务元数据、账本和必要的临时 staging 结果;staging 位于静态目录之外,使用随机对象键、所有权校验、TTL、软删除和 GC 状态。对象存储 adapter 预留但默认关闭,后续接入不改变资产/任务接口。
+- 后台可视化配置任务结果、原图和 WebDAV manifest 的保留时长、归档策略和最大延长次数;用户可以在上限内手动延长,超过上限需要后台权限或套餐策略。归档优先写入 WebDAV,失败时保留待处理状态,不静默丢失。
+- 用户可以在权限和保留策略允许的范围内主动删除任务结果、原图和自己的 WebDAV manifest;删除先写软删除/审计事件,再由 GC 清理平台 staging,WebDAV 删除失败进入重试队列并显示状态。
+
+### 1.8 余额和消费展示
+
+- 右上角或账户中心显示统一余额单位,V1 默认展示“金币”文字和金币图标;前台不出现“积分”字样。
+- 余额单位由后台配置名称、短代码、图标 key、符号、精度和显示小数位;后续可以切换为其他文字单位或图标单位,但同一生效版本只能有一个用户可用单位。
+- 充值金额通过后台发布的换算比例转为金币,例如支付货币 `amount` -> `balanceUnitAmount`;换算比例、舍入方式、最低充值和生效版本都进入订单快照。
+- 提交任务前显示预计消耗。
+- 批量生成显示按数量和分辨率计算的预计消耗。
+- 任务详情显示预扣、实际扣除、释放/退款状态。
+- 余额不足时阻止提交并跳转充值,不创建不可执行任务。
+- 用户可以查看充值订单、消费账单和退款/释放记录,但不能修改账本。
+- 用户可以购买后台发布的套餐;套餐可包含赠送金币、购买价格、有效时长、并发限制、队列优先级和可用渠道组。套餐余额和普通充值余额都进入同一账本,但来源和过期策略必须分开记录。
+
+### 1.9 用户侧错误和可用性提示
+
+用户不能看到供应商内部故障细节,但必须得到可执行提示:
+
+- 当前模型不可用,请切换到其他旗舰模型。
+- 余额不足,请充值后重试。
+- 当前分辨率不支持该模型,请降低分辨率或切换模型。
+- 请求参数无效,请修改提示词/参考图。
+- 任务已取消,预扣余额已释放。
+- 任务部分成功,仅对成功结果扣费。
+- 系统正在排队,显示任务编号和预计状态,不重复提交。
+
+内部日志可以保留 provider、channel、attempt、HTTP 状态和响应摘要,但前台只显示公共错误码、任务 ID 和建议动作。
+
+## 2. 后台管理范围
+
+后台仅管理员可用,地址保留 `/admin/`。TDesign 模板中的 Dashboard、List、Form、Detail、User、Result 页面可以继续作为视觉和路由壳,但所有示例 mock、Tencent 文案、模板用户数据和模板接口必须替换为真实业务域。
+
+### 2.1 管理员认证和权限
+
+- 管理员登录、退出、刷新会话。
+- 管理员使用独立认证域和独立会话 Cookie;管理员 Access Token 的 `aud` 不得被用户端 API 接受,`/admin/` 静态路径本身不视为权限边界,所有 `/api/v1/admin/*` 请求都必须经过服务端鉴权。
+- 管理员角色:至少 `super_admin`、`operator`、`finance`、`support`、`auditor`。
+- 权限粒度:用户、认证消息、渠道、模型产品、任务、余额/订单、系统配置、审计日志;默认拒绝,按角色授予最小权限,并对用户、任务、账单等对象做范围校验。
+- 高风险操作二次确认:禁用渠道、调整余额、退款、删除用户、修改价格和切换路由优先级。
+- 管理员操作写入审计日志,记录操作者、对象、前后值、IP、请求 ID 和时间。
+
+管理员安全基线:
+
+- 高风险操作必须重新认证;MFA、IP 白名单和设备策略由后台开关控制,默认关闭增强项。管理员登录、权限变更、密钥查看/轮换和高风险操作均写入不可变审计记录;管理员未开启增强策略时只需邮箱和密码。
+- 人工调账和支付退款采用“申请 -> 审批 -> 执行”状态机,申请人和审批人不能是同一账号;没有审批记录不得执行。
+- 管理员列表和配置写入使用 `Idempotency-Key`、资源版本号/`If-Match` 做幂等和乐观锁,版本冲突返回 `409`,不能静默覆盖其他管理员的变更。
+- `support` 默认不能查看完整提示词、原图、WebDAV 凭证或渠道密钥;临时 break-glass 授权需要审批、过期时间、脱敏展示和独立审计。
+
+### 2.2 用户管理
+
+管理员可以:
+
+- 按用户 ID、邮箱、手机号、状态、注册时间、余额和最近活动搜索。
+- 查看用户基本资料、验证状态、会话、登录设备、任务历史、消费和充值记录。
+- 冻结/解冻账户、触发密码重置、重新发送验证消息。
+- 手动增加/扣减余额,但必须通过“人工账本调整”记录原因和审批人。
+- 设置用户等级、限流、并发数、每日额度和可见模型产品。
+- 查看用户是否配置 WebDAV,但不能明文查看用户 WebDAV 密码。
+
+### 2.3 注册、认证、邮件和短信管理
+
+后台配置并监控:
+
+- 邮件供应商:SMTP/第三方邮件 API、发件人、模板、域名状态。
+- 短信供应商:供应商、签名、模板、区域和发送策略。
+- 验证模板:注册验证、登录验证码、密码重置、异常登录提醒、余额/订单通知。
+- 消息发送日志:待发送、发送中、成功、失败、重试次数、供应商响应摘要。
+- 验证码策略:有效期、发送冷却、每日上限、失败次数、IP/设备/目标限制、图形验证码或人机挑战开关。防刷策略为一期必做项,不得只依赖前端按钮禁用。
+- 两步验证策略:用户默认关闭,管理员保持独立登录流程,MFA 默认关闭并由后台安全开关启用。后台提供启用/关闭、重置、恢复码和生效范围配置,策略变更必须二次确认并写审计。
+- Provider 故障切换:邮件/SMS 也使用 outbox + 重试,不阻塞用户注册主事务。
+
+验证码必须只保存哈希或不可逆摘要,不保存明文;敏感信息不得写入普通日志。
+验证码的目标(邮箱/手机号)需规范化后参与限流键;消费操作必须使用一次性原子更新(未过期、未使用、尝试次数未超限),并记录发送 IP、设备和 provider message ID,重复消费统一返回验证失败而不改变账户状态。
+
+### 2.4 渠道管理
+
+渠道是供应商账号或接口实例,不是用户看到的模型产品。
+
+单个渠道最少包含:
+
+- `channelId`、名称、供应商、环境、区域。
+- Base URL、API 格式、认证方式、加密后的 API Key/Secret。
+- 支持的能力:image/text/audio/3D/utility。
+- 可用 provider model ID 映射。
+- 超时、最大并发、速率限制、单次重试次数。
+- 启用/禁用状态、管理员备注、维护窗口。
+- 健康状态、连续失败次数、熔断状态、最近成功/失败时间。
+- 成本估算、供应商计费单位和内部成本备注。
+
+渠道安全要求:Base URL 只允许 HTTPS 和后台配置的域名/IP allowlist,阻断 loopback、link-local、RFC1918、Unix socket 和重定向到私网;API Key/Secret 使用 KMS 或部署密钥加密并带版本号,轮换时保留旧版本到在途 attempt 结束,前台、审计日志和错误响应均不得出现明文凭证。
+
+后台操作:新增、编辑、测试、启用、禁用、轮换凭证、查看脱敏健康信息、查看调用统计和失败原因。
+
+### 2.5 渠道分组和路由顺序
+
+分组是用户请求所映射的内部路由池,例如:
+
+```text
+旗舰图片组
+ 1. Provider-A 账号 1 -> providerModelId=MODEL_A
+ 2. Provider-B 账号 2 -> providerModelId=MODEL_B
+ 3. Provider-C 账号 1 -> providerModelId=MODEL_C
+ 4. Provider-D 账号 4 -> providerModelId=MODEL_D
+```
+
+分组配置:
+
+- `groupId`、内部名称、能力、用途和维护说明。
+- 成员渠道及明确的 priority 顺序。
+- 是否允许自动故障切换。
+- 组级超时、重试上限、并发和熔断策略。
+- 组级公共错误文案和推荐替代模型产品。
+- 绑定的公开模型产品和分辨率策略。
+- 配置发布版本、修改人和生效时间;每次发布生成不可变版本,支持回滚到上一版本。
+- 失败分类白名单、每渠道 retry budget、退避初始值/上限/抖动、熔断阈值、半开探活间隔和任务 lease 超时。
+
+默认路由规则:
+
+1. 创建任务时把 `modelProduct`、分辨率、价格、渠道组、绑定关系和路由策略的发布版本全部写入任务快照;任务排队期间后台改价、改 priority 或下架产品,不改变该任务的计费和路由依据。
+2. 按快照中的 priority 从小到大选择启用且未熔断的渠道;渠道启停只影响尚未创建快照的新任务,已创建任务仅在安全策略允许时跳过明确禁用的渠道。
+3. 每次调用都创建一个 `attempt`,状态至少为 `pending -> sent -> succeeded/failed/unknown/canceled`;写入 worker lease、provider request ID、平台幂等键和超时原因。队列使用可见性超时、心跳和死信队列,worker 崩溃后由恢复任务接管。
+4. 单渠道发生可重试错误时,按组级 retry budget、指数退避和抖动策略重试;达到该渠道预算后切到下一渠道。供应商已接受请求但客户端超时的 `unknown` 结果先按 provider request ID 查询/对账,不直接重复提交。
+5. 熔断计数、半开探活和并发占用使用 Redis 原子操作;熔断状态包含 `closed/open/half_open`、版本和过期时间,只有探活成功才能恢复。管理员手动启停或探活结果都写健康事件。
+6. 401/403/模型不存在按“渠道凭证/模型配置错误”处理:隔离该渠道并告警;若请求本身被策略拒绝,不切换渠道。用户参数错误、内容审核拒绝、图片格式无效等不可重试错误直接结束任务。
+7. 组内全部渠道不可用时返回公共错误 `MODEL_GROUP_UNAVAILABLE`,提示用户切换同档位其他模型;除非后台明确设置 `fallbackGroupId`,否则不从旗舰组静默降级到高级组或基础模型。
+8. 渠道切换对用户透明,任务 ID 不变;后台任务详情展示所有 attempt。用户取消与 worker/供应商完成竞态时,以服务端先提交的终态为准,迟到结果只做对账,不重复入账或覆盖已取消状态。
+
+### 2.6 模型产品和分辨率配置
+
+管理员要管理“用户看到的模型”而不是只管理渠道:
+
+- 公开模型产品:基础、高级、旗舰,以及抠图、拆分、线稿等 V1 专项产品;3D 产品只保留后台草稿/契约,不在 V1 上架。
+- 公共名称、公开模型 ID、描述、图标、能力标签、排序、上架状态。
+- 绑定的 `channelGroupId`。
+- 可选分辨率预设:低、中、高、超高/4K,实际宽高、比例、模型限制。
+- 默认质量、最大生成数量、参考图数量、支持 mask 与否。
+- 每种“模型产品 + 分辨率 + 功能”的公开价格/余额消耗。
+- 替代模型建议:同档位优先,其次由后台选择允许的跨档位推荐。
+- 发布版本和生效时间;修改价格或路由需写审计日志。
+
+套餐和队列策略由后台单独配置,不把 provider 信息暴露给用户:
+
+- 套餐字段:`planId`、套餐名称、公共编码、图标、基本介绍、套餐时长、购买价格、包含金币、有效期、上架状态、排序、发布版本。
+- 套餐权益:允许使用的公开模型产品,以及后台内部的 `allowedChannelGroupIds`、并发上限、队列优先级、单文件大小、结果保留上限和是否允许延长/归档;渠道组 ID 只存于后台权益快照,不返回用户端。
+- V1 不限制每日任务数;并发数、队列优先级和套餐可用范围必须限制,并在任务创建时写入权益快照。
+- 用户端只展示套餐名称、价格、金币数量、时长、公开模型/能力范围和权益说明;不显示 provider 分组、账号或内部渠道顺序。
+- 套餐可停用但不删除历史版本;已购买权益按购买时版本结算,新增任务按当前发布版本校验。
+
+前台目录 API 只返回这些公开字段,不返回 `channelId`、`providerModelId`、Base URL、Key、内部成本和渠道顺序。
+
+### 2.7 任务中心和接口调用监控
+
+后台可以查看:
+
+- 任务 ID、用户、公开模型产品、功能、分辨率、数量、状态、余额状态。
+- `queued/running/succeeded/partial/failed/canceled/refunded` 状态变化。
+- 每次 provider attempt、渠道、开始/结束时间、HTTP 状态、错误分类和重试结果。
+- 平均耗时、成功率、P50/P95 延迟、渠道错误率、模型组可用率。
+- 取消任务、重试任务、标记异常、查看脱敏请求/响应摘要。
+- 查看 WebSocket 连接数、心跳、断线重连、事件积压和消费延迟;任务事件以 `taskId + eventId + sequence` 去重。
+- 不允许管理员在任务页面直接查看用户 API Key、完整提示词附件或敏感凭证,除非有明确权限和审计。
+
+### 2.8 余额、账本、充值和支付预留
+
+计费方式统一为余额制:
+
+- 充值成功:按订单快照中的换算比例增加统一余额单位(V1 默认金币)。
+- 任务提交:预扣预计余额。
+- 任务成功:按实际成功输出结算。
+- 任务取消、全渠道失败或系统错误:释放预扣或产生退款账本记录。
+- 批量任务部分成功:只扣成功结果,失败项释放预扣。
+- 同一任务的 provider 重试不向用户重复收费。
+
+金额必须使用整数最小单位或定点 Decimal,禁止使用浮点数直接计算余额。余额不可直接覆盖,只能追加账本流水:
+
+```text
+balance_accounts
+balance_ledger
+ -> reserve 预扣
+ -> settle 成功结算
+ -> release 释放预扣
+ -> refund 退款
+ -> manual_adj 管理员人工调整
+recharge_orders
+payment_events
+```
+
+支付接口只做适配层预留:
+
+- `createRechargeOrder(userId, amount, provider)`。
+- `queryPayment(orderId)`。
+- `handlePaymentWebhook(provider, signature, payload)`。
+- `refundPayment(orderId, amount, reason)`。
+- 支付回调签名校验、幂等键、订单状态机和重复通知处理。
+
+余额单位和换算配置必须可视化管理:
+
+- 余额单位配置:名称、短代码、图标 key、符号、精度、显示小数位、是否启用。
+- 换算规则配置:支付货币、余额单位、兑换比例、舍入模式、最低/最高充值、赠送规则、版本和生效时间。
+- 价格和套餐金额以余额最小单位保存;充值订单同时保存支付金额、余额到账金额和规则版本,避免后续改比例影响历史订单。
+
+一期充值只提供管理员人工充值和 mock provider;不能用“收到前端 success 参数”直接加余额。真实支付供应商在后续接入时只实现 adapter,不改余额账本规则。支付 API 和 webhook 契约必须先预留,等待用户提供第三方支付开发文档。
+
+套餐后台页面必须支持:新增/编辑/复制/上架/下架、名称、公共编码、图标、介绍、套餐时长、包含金币、购买价格、可用模型产品/渠道组、并发上限、队列优先级、结果保留上限和版本回滚。套餐购买订单与普通充值订单分开记录,但到账仍写入统一余额账本。
+
+### 2.9 系统配置和审计
+
+- 邮件/SMS 供应商、验证码策略。
+- 模型产品、渠道组、渠道健康阈值、价格和限流。
+- 余额单位、金币图标/文字、换算比例、舍入和充值上下限。
+- 套餐名称、时长、购买价格、包含金币、上架状态、介绍、可用模型/渠道组、并发和队列优先级。
+- 余额/订单/支付开关。
+- 本地 staging、WebDAV、对象存储 adapter 开关、任务/原图/manifest 保留期、归档和用户延长上限。
+- 全部敏感配置加密或通过部署环境注入。
+- 审计日志采用 append-only 存储,至少保存链式摘要/防篡改校验、保留期、脱敏策略和导出权限;审计日志自身的读取/导出也必须产生审计记录。
+- 审计日志支持按管理员、对象、操作类型、请求 ID 查询和导出;任务、支付、消息和 provider 请求贯穿同一 `requestId/correlationId`,便于追踪一次调用的全部状态。
+
+## 3. 服务端调用和路由逻辑
+
+### 3.1 前台提交任务
+
+前台统一调用平台 API,不再调用供应商地址:
+
+```text
+POST /api/v1/tasks/image
+Authorization: Bearer ACCESS_TOKEN
+Idempotency-Key: CLIENT_GENERATED_KEY
+{
+ "modelProductId": "flagship-image",
+ "resolutionPresetId": "high",
+ "prompt": "...",
+ "references": [...],
+ "mask": ...,
+ "count": 4,
+ "canvasProjectId": "..."
+}
+```
+
+服务端顺序:
+
+1. 校验 Access Token、账户状态、产品是否对该用户可见。
+2. 校验提示词、参考图、分辨率、数量和功能参数。
+3. 校验引用文件、mask 和画布项目属于当前用户,检查 MIME、大小、像素、数量、病毒/恶意内容扫描状态和引用有效期;不接受任意外部 URL 直接进入 provider。
+4. 读取模型产品、分辨率价格、绑定分组;不信任前端传来的价格或 provider model ID,并把目录、价格、分辨率、渠道组和路由策略版本复制到任务快照。
+5. 解析用户当前有效套餐,检查公开模型/内部渠道组权益、余额、并发和队列优先级;每日任务数 V1 不设上限。
+6. 使用幂等键创建任务并预扣余额;重复请求返回原任务,不重复预扣。
+7. 将任务放入队列,返回任务 ID、状态和预计消耗。
+
+### 3.2 队列和 provider attempt
+
+```text
+generation_task
+ -> resolve modelProduct
+ -> resolve channelGroup
+ -> ordered channels
+ -> provider attempt #1
+ success -> normalize result -> store output -> settle
+ retryable failure -> attempt #2
+ -> provider attempt #2 ...
+ -> all unavailable -> fail public error -> release reserve
+```
+
+任务应支持:
+
+- HTTP 查询 `GET /api/v1/tasks/:id`。
+- WebSocket 是 V1 长任务进度主通道:连接使用用户会话鉴权、心跳、订阅/取消订阅和 `taskId` 权限校验;事件带递增 `sequence`、`taskId`、`attemptId` 和 `eventId`,客户端通过 cursor/last sequence 断线重放,服务端以任务历史快照为权威并按 eventId 去重。HTTP 轮询仅作为断线或不支持 WS 客户端的降级方案,SSE 不作为一期主通道。
+- `POST /api/v1/tasks/:id/cancel` 取消排队或可取消的运行任务;若 provider 支持取消则发送取消请求,否则等待未知结果对账,不能因迟到响应重复结算。
+- 任务完成后返回结果元数据、缩略图/签名 URL、画布关联信息和资产保存入口;结果下载必须校验用户所有权和资源 scope。
+- 任务结果存储采用短期签名对象 URL,URL 绑定 `userId/taskId/outputId`、用途和过期时间,可主动撤销;保存到资产或 WebDAV 后创建持久化引用,清理任务回收未保存结果。
+
+服务端与 provider 使用统一 adapter 契约:
+
+- 输入至少包含 `taskType`、规范化参数、引用对象、超时、取消信号和平台幂等键;adapter 负责把它转换成供应商请求,不把密钥、Base URL 或原始响应传回前台。
+- 输出统一为 `queued/running/succeeded/partial/failed/unknown/canceled`、标准化 `outputs[]`、成本/用量摘要、provider request ID 和可审计错误分类。V1 的 image、text、audio、2D utility 可以有各自参数;3D adapter 只保留契约,启用后也必须遵守同一任务/attempt/结果生命周期。
+- adapter 必须限制出站超时、响应大小和 MIME,屏蔽供应商提示词/凭证泄露;管理员配置的 Base URL 经过 HTTPS、域名/IP allowlist 和私网地址阻断,禁止 SSRF。
+
+### 3.3 错误分类
+
+| 错误类别 | 示例 | 行为 |
+| --- | --- | --- |
+| 用户参数错误 | 尺寸非法、参考图格式错误、提示词缺失 | 直接失败,不切换渠道,不扣费。 |
+| 内容/策略拒绝 | provider 返回内容审核拒绝 | 直接失败,禁止切换渠道;该输出不计费,未使用的预扣全部 `release`,记录公共拒绝码。 |
+| 渠道暂时不可用 | 超时、网络错误、429、502、503、供应商维护 | 记录 attempt,按顺序切换后续渠道。 |
+| 渠道凭证/配置错误 | 401、403、模型不存在 | 标记渠道健康异常,切换后续渠道;管理员收到告警。 |
+| 系统错误 | 队列、存储、账本事务失败 | 任务进入系统失败,释放预扣并记录告警。 |
+| 全组不可用 | 所有渠道耗尽或全部熔断 | 返回 `MODEL_GROUP_UNAVAILABLE`,建议切换其他公开模型。 |
+
+用户消息只使用公共错误码和可执行建议;provider 原文只在后台脱敏日志中保存。
+
+`401/403` 只有在确认属于渠道凭证/权限配置错误时才隔离渠道;若是用户内容、租户权限或产品策略拒绝,按用户参数错误处理,不把同一请求扩散到其他供应商。所有 attempt 的原始响应先脱敏再写入日志,禁止保存完整 API Key、Cookie、Authorization、原图和未授权的 WebDAV 凭证。
+
+### 3.4 余额结算规则
+
+- 预扣发生在任务入队前,余额不足不入队。
+- 预扣金额以公开模型产品、分辨率、数量和功能参数计算,provider 切换不改变用户价格。
+- 成功输出逐项结算;一张失败图不应让整批成功图重复或多扣。
+- provider 重试属于平台成本,不向用户重复生成多条扣费记录。
+- 任务超时、取消、全渠道失败必须产生 release/refund 账本记录。
+- 所有账本写入和任务状态变化必须具备事务边界和幂等约束。
+
+账本不变量和状态转移:
+
+- 账户永远满足 `available + reserved = ledger_confirmed_balance`,`available >= 0`、`reserved >= 0`;所有金额使用同一币种和整数最小单位/Decimal,流水金额正负方向固定并在 API 中明确。
+- V1 账本以余额单位最小整数(默认金币)记账;支付货币只出现在充值订单和换算规则中。任务价格、套餐到账和人工调账都必须保存单位版本,禁止在账本层混用支付货币与余额单位。
+- `reserve` 只允许从 `available` 转入 `reserved`,`settle` 从 `reserved` 转为实际消费,`release` 把未使用部分转回 `available`;`refund` 仅表示已结算消费的补回,不等同于预扣释放,也不直接代表支付渠道退款。
+- 预扣按后台定义的消费顺序(默认优先使用最早到期的套餐/赠送批次)分配到 `balance_buckets`,释放时按原批次返还;任务快照记录 bucket allocation,避免套餐过期或换算比例变化造成错扣。
+- 每个任务/输出最多一条生效的 reserve、settle、release 或 refund 关系;数据库唯一约束和 `Idempotency-Key` 共同防止重复记账。任务状态和账本状态在同一事务中提交,或由带版本的 outbox/reconciler 可证明地补偿。
+- 预扣带 `expiresAt`;超时由 reconciler 扫描并根据 attempt/任务最终状态执行 release 或进入人工对账,不允许静默丢失冻结余额。余额更新使用行锁或乐观版本 CAS,冲突重试后仍失败则任务不入队。
+- 批量任务按输出保存 `unitPriceSnapshot` 和 `chargedAmount`;成功输出逐项 settle,失败/重复/未知输出不重复扣费。provider 成本与用户价格分离,provider 重试只记录平台成本。
+
+支付订单与回调状态至少为 `created -> pending -> paid/failed/expired/refunding/refunded`。`payment_events` 以供应商事件 ID 唯一,校验签名、订单号、金额、币种和用户后再入账;乱序/重复通知按状态机幂等处理,另有定时对账和拒付/退款补偿任务。
+
+## 4. 最小领域数据模型
+
+以下是一期需要在服务端落地的领域,不要求一次采用完全相同的表名,但关系和约束必须保留。
+
+### 4.1 用户和认证
+
+- `users`:用户主表、状态、昵称、注册时间、最后活动。
+- `user_identities`:邮箱/手机号、验证状态、唯一索引、脱敏展示字段。
+- `password_credentials`:密码哈希、算法版本、修改时间。
+- `verification_codes`:用途、目标、哈希、过期时间、尝试次数、发送来源。
+- `sessions`:用户/管理员会话域、refresh token 哈希、设备、IP、audience、过期、轮换链和撤销时间。
+- `user_mfa_factors`:用户/管理员身份、因子类型、密钥/恢复码摘要、启用状态和最近使用时间;密钥材料加密或只保存不可逆摘要。
+- `captcha_challenges`:目标、IP/设备指纹、挑战类型、哈希结果、过期时间、消费状态和失败次数。
+- `admin_roles` / `admin_permissions` / `admin_user_roles`:后台 RBAC。
+
+### 4.2 模型和渠道
+
+- `model_products`:用户看到的基础/高级/旗舰及专项产品。
+- `model_product_resolutions`:公开分辨率、实际像素、倍率、能力限制。
+- `channel_groups`:分组策略、优先级、熔断和公共错误配置、发布版本和生效时间。
+- `provider_channels`:供应商账号、加密凭证、Base URL、状态和限流。
+- `channel_bindings`:组成员顺序、provider model ID、参数映射。
+- `channel_health`:成功率、连续失败、熔断开始/结束、最近探活。
+- `pricing_rules`:模型产品/分辨率/功能/数量的公开价格和生效版本。
+- `membership_plans`:套餐名称、公共编码、图标、介绍、时长、购买价格、包含金币、上架状态、可用模型/渠道组、并发、队列优先级、保留上限和版本。
+- `plan_purchases`:用户、套餐版本、支付金额、到账金币、有效期、权益快照和状态。
+
+### 4.3 任务和结果
+
+- `generation_tasks`:用户、公开模型、功能、参数摘要、状态、幂等键、预扣金额、余额单位版本、产品/分辨率/价格/渠道组/路由/套餐权益快照版本、状态版本、lease、队列优先级和结果保留期。
+- `generation_attempts`:任务、渠道、provider model、开始/结束、错误分类、重试序号、attempt 状态、worker lease、平台幂等键、provider request ID、unknown 对账信息。
+- `generation_outputs`:结果文件、缩略图、MIME、尺寸、成功/失败、单位价格快照、实际扣费、资源 scope、签名 URL 过期和保留期。
+- `upload_objects`:用户、对象 key、MIME、大小、校验和、扫描状态、来源、过期时间和软删除时间;所有参考图/mask/资产内容先经过该对象层。
+- `canvas_projects` / `canvas_nodes` / `assets`:如果一期把项目和资产同步到云端,则与用户隔离;若暂时保持浏览器本地,也必须保存任务和资产关联 ID。
+
+### 4.4 余额和支付
+
+- `balance_accounts`:用户余额、冻结/预扣余额、版本号。
+- `balance_buckets`:充值/套餐到账批次、剩余余额、有效期、消费顺序;过期批次由 GC/账本任务处理,不能直接覆盖账户总额。
+- `balance_ledger`:不可变流水、类型、金额、余额单位版本、来源(充值/套餐/任务/调账)、关联任务/订单、有效期、幂等键。
+- `balance_units`:单位名称、短代码、图标 key、符号、精度、显示小数位和状态。
+- `balance_conversion_rules`:支付货币、余额单位、兑换比例、舍入、上下限、赠送规则、版本和生效时间。
+- `recharge_orders`:用户、金额、支付渠道、状态、过期时间。
+- `payment_events`:供应商事件 ID、签名校验结果、原始摘要、处理状态。
+- `refund_records`:退款金额、原因、原订单、支付状态。
+
+### 4.5 消息和审计
+
+- `message_providers`:邮件/SMS 供应商和加密配置。
+- `message_templates`:注册、验证、重置、通知模板和版本。
+- `message_outbox`:异步发送状态、重试和 provider message ID。
+- `audit_logs`:管理员、操作、对象、前后值摘要、IP、请求 ID。
+- `storage_policies`:任务结果、原图、WebDAV manifest 的保留期、归档策略、用户延长上限和 GC 规则。
+
+### 4.6 存储和同步
+
+- `webdav_connections`:用户、加密凭证引用、规范化 Base URL、根目录命名空间、最近测试和轮换时间;读取接口只返回脱敏信息。
+- `sync_manifests`:用户、资源类型、版本、校验和、更新时间、冲突状态和最后同步游标。
+- `canvas_projects`、`assets` 和 `upload_objects` 都必须带 `ownerId`;对象访问、下载、任务引用和 WebDAV 同步均做所有权校验,禁止通过可猜测 ID 跨用户读取。
+
+### 4.7 团队扩展预留(V1 不启用)
+
+- `organizations` / `teams`:未来的工作空间、所有者、套餐/余额归属和状态;V1 不创建团队业务记录。
+- `organization_members`:未来的成员、角色、邀请状态和加入/退出时间;V1 不开放邀请和共享操作。
+- `resource_scopes`:未来为画布、资产、任务和对象增加 `ownerType/ownerId` 范围字段;V1 固定为用户所有权,预留字段不能改变当前的越权校验。
+- 未来启用团队时,必须先定义共享资产、团队余额、成员审计和对象级权限,再开放 `/organizations/*` API。
+
+## 5. API 边界规划
+
+### 5.1 用户公开/认证 API
+
+```text
+POST /api/v1/auth/register
+POST /api/v1/auth/challenge
+GET /api/v1/auth/policy
+POST /api/v1/auth/verify-email
+POST /api/v1/auth/verify-sms
+POST /api/v1/auth/login
+POST /api/v1/auth/refresh
+POST /api/v1/auth/logout
+POST /api/v1/auth/password/forgot
+POST /api/v1/auth/password/reset
+POST /api/v1/auth/password/change
+POST /api/v1/auth/verification/resend
+POST /api/v1/auth/mfa/setup
+POST /api/v1/auth/mfa/verify
+POST /api/v1/auth/mfa/disable
+POST /api/v1/auth/mfa/recovery
+POST /api/v1/me/close
+GET /api/v1/me
+GET /api/v1/me/plans
+GET /api/v1/me/entitlements
+GET /api/v1/me/sessions
+DELETE /api/v1/me/sessions/:id
+```
+
+### 5.2 用户目录、任务、资产和余额 API
+
+```text
+GET /api/v1/catalog/models
+GET /api/v1/catalog/models/:id
+GET /api/v1/catalog/balance-unit
+GET /api/v1/catalog/plans
+GET /api/v1/catalog/plans/:id
+POST /api/v1/tasks/image
+POST /api/v1/tasks/text
+POST /api/v1/tasks/audio
+POST /api/v1/tasks/image/reverse-prompt
+POST /api/v1/tasks/image/multi-angle
+POST /api/v1/tasks/image/matting
+POST /api/v1/tasks/image/split
+POST /api/v1/tasks/image/upscale
+POST /api/v1/tasks/image/expand
+POST /api/v1/tasks/3d
+GET /api/v1/tasks
+GET /api/v1/tasks/:id
+GET /api/v1/tasks/:id/events # 事件历史/cursor 重放,不是 SSE
+WS /api/v1/ws/tasks
+POST /api/v1/tasks/:id/cancel
+GET /api/v1/balance
+GET /api/v1/balance/ledger
+POST /api/v1/recharge/orders
+GET /api/v1/recharge/orders/:id
+POST /api/v1/recharge/plan-orders
+GET /api/v1/recharge/plan-orders/:id
+GET /api/v1/assets
+POST /api/v1/assets
+PATCH /api/v1/assets/:id
+DELETE /api/v1/assets/:id
+POST /api/v1/uploads/presign
+POST /api/v1/uploads/:id/complete
+GET /api/v1/uploads/:id/download
+GET /api/v1/me/webdav
+PUT /api/v1/me/webdav
+POST /api/v1/me/webdav/test
+POST /api/v1/me/webdav/sync
+GET /api/v1/me/webdav/sync-status
+```
+
+图片专项 API 可以先统一映射到一个 `taskType`,不要求每个功能都复制一套 provider 调用代码。裁剪等纯本地操作可以继续在前台执行,但凡涉及模型或扣费的能力必须经过任务网关。
+`POST /api/v1/tasks/3d` 仅作为禁用状态的契约预留;V1 目录不发布 3D capability,调用时返回统一的 `CAPABILITY_NOT_ENABLED`,不能把 2D 多角度结果伪装成 3D 文件。
+
+一期固定以浏览器本地画布/资产 + WebDAV 为用户内容的持久化方式,服务端只保存任务、余额、结果引用和必要的资产关联;云端 `canvas/projects` CRUD 不进入 V1,仅保留未来扩展契约。
+
+上传接口只返回平台对象 ID,不接受任意外部 URL;完成上传时服务端重新校验大小、MIME、校验和、扫描状态和用户所有权。WebDAV 接口保存加密凭证引用,Base URL 必须通过 HTTPS、域名/IP allowlist 和私网地址阻断,远端路径固定在每个用户的命名空间内,禁止 `..`、绝对路径和跨用户 manifest。
+
+资产写入至少包含 `kind`、`title`、`tags`、`sourceTaskId`、`sourceOutputId` 和 `objectId`/文本内容;服务端生成下载授权并校验软删除状态。任务结果保存到资产与账本结算必须有明确的关联 ID,重复保存请求返回同一资产而不是创建副本。
+
+### 5.3 管理员 API
+
+```text
+POST /api/v1/admin/auth/login
+POST /api/v1/admin/auth/refresh
+POST /api/v1/admin/auth/logout
+POST /api/v1/admin/auth/mfa/setup
+POST /api/v1/admin/auth/mfa/verify
+POST /api/v1/admin/auth/mfa/disable
+
+GET/PATCH /api/v1/admin/users
+POST /api/v1/admin/users/:id/freeze
+POST /api/v1/admin/users/:id/balance-adjustments
+
+GET/POST/PATCH /api/v1/admin/message-providers
+GET/POST/PATCH /api/v1/admin/message-templates
+GET /api/v1/admin/message-outbox
+
+GET/POST/PATCH /api/v1/admin/channels
+POST /api/v1/admin/channels/:id/test
+POST /api/v1/admin/channels/:id/rotate-secret
+GET/POST/PATCH /api/v1/admin/channel-groups
+PATCH /api/v1/admin/channel-groups/:id/order
+GET /api/v1/admin/channel-health
+
+GET/POST/PATCH /api/v1/admin/model-products
+GET/POST/PATCH /api/v1/admin/model-products/:id/resolutions
+GET/POST/PATCH /api/v1/admin/pricing-rules
+GET/POST/PATCH /api/v1/admin/billing/units
+GET/POST/PATCH /api/v1/admin/billing/conversion-rules
+GET/POST/PATCH /api/v1/admin/membership-plans
+POST /api/v1/admin/membership-plans/:id/publish
+POST /api/v1/admin/membership-plans/:id/rollback
+
+GET /api/v1/admin/tasks
+GET /api/v1/admin/tasks/:id/attempts
+POST /api/v1/admin/tasks/:id/retry
+POST /api/v1/admin/tasks/:id/cancel
+
+GET /api/v1/admin/balance/ledger
+GET/POST/PATCH /api/v1/admin/recharge-orders
+GET/POST/PATCH /api/v1/admin/payment-providers
+GET /api/v1/admin/payment-events
+GET/POST/PATCH /api/v1/admin/storage-policies
+GET /api/v1/admin/ws/metrics
+
+GET /api/v1/admin/audit-logs
+```
+
+所有管理员写操作都需要权限检查、请求 ID、审计日志和幂等处理。
+
+## 6. 后台页面规划
+
+### 6.1 总览 Dashboard
+
+- 注册用户、活跃用户、余额总额、充值金额、生成任务数。
+- 各模型产品成功率、P95 延迟、失败率和消耗。
+- 渠道组健康度、熔断渠道、当前告警。
+- 邮件/SMS 成功率、支付回调异常、队列积压。
+
+### 6.2 用户与认证
+
+- 用户列表、用户详情、身份验证状态、登录设备和会话。
+- 冻结/解冻、重置密码、验证消息重发。
+- 用户模型可见范围、额度、并发和余额调整。
+- 注册策略、验证码策略、邮件/SMS provider、模板和发送日志。
+
+### 6.3 模型与渠道
+
+- 模型产品列表:基础/高级/旗舰/专项能力。
+- 分辨率和数量配置、价格、上架/下架、替代模型建议。
+- 渠道列表:凭证脱敏、测试、启停、健康、成本、限流。
+- 渠道分组:成员顺序、绑定模型 ID、熔断策略、公共不可用提示。
+- 变更预览:修改路由或价格前显示影响范围。
+
+### 6.4 任务、余额与支付
+
+- 任务列表、任务详情、attempt 时间线、结果和扣费状态。
+- 余额单位/金币图标、换算比例、舍入和充值上下限。
+- 套餐管理:名称、编码、时长、包含金币、购买价格、介绍、上架状态、可用模型/渠道组、并发、队列优先级、保留上限和版本发布。
+- 余额账户、账本、人工调整审批。
+- 充值订单、支付 provider 配置、回调事件、退款记录。
+- 任务队列和重试监控。
+
+### 6.5 系统、存储与审计
+
+- 对象存储、临时结果保留期、WebDAV 策略。
+- 结果/原图/manifest 归档、用户延长次数和 GC 执行状态。
+- WebSocket 连接、心跳、事件积压和断线重连指标。
+- 任务、媒体和日志清理策略。
+- 审计日志、管理员操作记录、密钥轮换记录。
+- 系统错误、provider 告警和消息 outbox。
+
+## 7. 现有代码迁移规划
+
+### 7.1 前台代码复用
+
+保留并逐步改造:
+
+- `MiragenFlow-front/src/data/artworks.ts`:不进入 V1 运行时;首页不保留演示作品数据,公开模型目录和工作流状态必须来自真实 API。
+- `MiragenFlow-front/src/App.tsx`、`src/main.tsx`:不直接复制;重写到 `web/src/app/`,改用项目现有 BrowserRouter、认证守卫、错误边界和统一 providers,不能把 hash 路由作为 V1 公网路由。
+- 各页面和组件的 `*.module.css`:随对应组件迁移到目标目录;公共 token 合并到 `web/src/styles/`,不复制一份互相覆盖的全局 CSS。
+- `MiragenFlow-front/src/components/primitives/*`:迁移到 `web/src/components/ui/`,保留按钮、输入、反馈、选择器的视觉规范;不把组件验收页作为生产导航。
+- `MiragenFlow-front/src/components/layout/*`:迁移到 `web/src/components/layout/`,接入真实登录、余额、通知、账户和权限状态。
+- `MiragenFlow-front/src/components/content/*` 与 `src/pages/HomePage.tsx`:视觉原则已吸收至 `web/src/pages/home/`;首页内容、模型目录和任务入口使用真实 API,不复制演示作品流或占位页。
+- `MiragenFlow-front/src/styles/*`:合并到 `web/src/styles/`,以现有画布主题、TDesign/Tailwind tokens 为最终来源,避免两套全局变量并存。
+- `MiragenFlow-front/src/assets/holopix/*`:迁移到 `web/src/assets/reference/`;只允许作为前台公开素材或视觉参考,禁止在其中放配置、用户上传文件、响应数据或密钥。
+- `MiragenFlow-front/src/pages/ComponentsPage.tsx`:迁移为受保护的开发验收入口或移出生产构建,不保留“组件中心”作为普通用户功能。
+- `MiragenFlow-front/src/pages/PlaceholderPage.tsx`:只作为迁移过渡,不进入一期生产路由;真实页面完成后删除。
+- `MiragenFlow-front/src/components/primitives/primitives.test.tsx`:迁移到 `web/src/components/ui/__tests__/` 或按现有测试结构重写;测试 fixture 不进入生产 bundle。
+- `MiragenFlow-front/public/favicon.svg`、`public/icons.svg`:只在完成版权、命名和内容核验后迁移到 `web/public/`;不得把不明 SVG 中的外链、脚本或用户数据带入静态目录。
+- `MiragenFlow-front/index.html`:重建为 `web/index.html`,只保留元境幻生标题、favicon 和公开 meta,不复制模板的 `/src/main.tsx` 引用或演示标题。
+- `MiragenFlow-front/package.json`、`package-lock.json`、`vite.config.ts`、`tsconfig*.json`、`.oxlintrc.json`:不直接覆盖现有 `web/` 构建配置;按依赖和脚本逐项评估后合并,锁文件只保留 web 自身的一份。
+- `MiragenFlow-front/docs/COMPONENTS.md`、`README.md`:迁移到 `docs/design-reference/` 作为设计输入记录,不能当作产品文档或运行时依赖。
+- `MiragenFlow-front/dist/`、`node_modules/`、临时截图和构建缓存:不作为源码迁移,不提交仓库;即使目录已存在,也不得复制其中 hashed JS/CSS、图片或字体到生产资源。
+- `web/src/pages/canvas/project.tsx`:保留节点交互、画布编排、结果回写和本地编辑工具。
+- `web/src/components/canvas/*`:保留节点 UI、裁剪、蒙版、切分、角度和批量结果 UI。
+- `web/src/lib/canvas/canvas-node-generation.ts`:改为组装平台任务请求和引用资源。
+- `web/src/lib/canvas/canvas-export.ts`、`web/src/services/webdav-sync.ts`:保留用户导出和 WebDAV 能力。
+- `web/src/stores/use-asset-store.ts`、`useCanvasStore`:先支持本地恢复,再增加用户/云端任务关联。
+
+### 7.2 必须移出用户端的能力
+
+- `web/src/stores/use-config-store.ts` 中的 Base URL、API Key、供应商渠道管理。
+- `web/src/components/layout/channel-editor-drawer.tsx` 的用户渠道编辑。
+- `web/src/components/layout/model-script-editor.tsx` 的用户自定义供应商脚本。
+- `web/src/services/api/image.ts`、`audio.ts` 中直接携带外部 Key 的浏览器请求。
+
+这些逻辑迁移到服务端 adapter/gateway。前台配置面板改为公开模型档位、分辨率、数量、语言、主题、WebDAV 和账户设置。
+
+### 7.3 后台模板改造
+
+- 保留 TDesign Layout、菜单、主题、表格、表单和结果页组件。
+- 删除模板示例业务的 mock 数据、Tencent/tdesign 示例链接、合同/商品示例命名。
+- 将页面替换为用户、认证消息、渠道、渠道组、模型产品、任务、余额、充值、支付、审计。
+- 统一通过 `/api/v1/admin/*` 调用服务端,不再在页面里写静态演示数据。
+
+### 7.4 同一公网入口
+
+最终对外建议保持一个 origin:
+
+```text
+/ 前台静态资源
+/admin/ 后台静态资源
+/api/ 业务 API、认证、任务、余额
+/api/v1/ws/ WebSocket 长任务事件(V1 主通道)
+```
+
+开发环境可以继续保留两个 Vite 进程;生产环境由反向代理统一转发,前台和后台不能依赖不同公网端口。
+
+### 7.5 模板接入原则
+
+- `MiragenFlow-front` 是一次性模板输入目录,不作为运行时 package、workspace 或静态资源根目录;迁移完成后删除临时目录,或仅保留到 `docs/design-reference/` 供设计追溯。
+- `web/`、`admin/` 和 `server/` 只能通过 `packages/contracts/` 共享 API 类型、错误码和事件 schema;前台不得 import server 文件,server 不得 import web/admin 页面。
+- 浏览器端只能保存公开目录、主题、语言和本地画布/资产数据;Access Token 在内存,Refresh Token 使用 HttpOnly Cookie,provider 凭证和 WebDAV 密码只在服务端密文/KMS 中保存。
+- `.env.example` 只允许公开地址、功能开关和非敏感默认值;真实 `.env`、密钥、支付签名、短信/邮件凭证和 provider Key 不进入 git、构建产物、`public/`、日志或图示文档。
+- 用户上传、任务 staging、WebDAV 下载和导出文件必须使用服务层授权,不得放进 `web/public/`、`admin/public/` 或可被 Vite 静态访问的目录。
+- CI/提交前检查必须拒绝 `*.env`、`*.pem`、`*.key`、上传目录、staging 目录、数据库快照和疑似 Authorization/API Key 内容;构建产物扫描通过后才能发布前台或后台静态资源。
+
+## 8. 一期验收标准
+
+### 8.1 用户链路
+
+- 普通用户默认使用邮箱或手机号加密码直接注册并登录;后台开启验证策略后,才需要邮箱/短信验证、人机校验或 MFA。
+- 用户可以登录、刷新、退出、重置密码,冻结账户不能提交任务。
+- 登录后能看到公开模型产品、分辨率、数量、预计余额消耗。
+- 前台网络请求中不出现 provider API Key、Base URL、provider model ID 或渠道顺序。
+- 用户可以提交图片、文本、音频和已纳入的图片工具任务。
+- 任务支持排队、进度、取消、结果、失败重试和历史查询。
+- 长任务通过 WebSocket 接收心跳、进度、attempt 和终态事件;断线后可使用 cursor 重放,轮询只作为降级。
+
+### 8.2 路由和计费
+
+- 一个分组有四个渠道时,第一渠道失败会自动继续第二、第三、第四渠道。
+- 单渠道重试不会创建新的用户扣费。
+- 全组不可用时任务给出公共错误和同档位切换建议。
+- 余额不足不能入队;任务成功只扣实际成功结果。
+- 全失败、取消和系统错误会释放预扣余额。
+- 重复幂等请求不会重复创建任务或扣款。
+- 支付回调重复到达不会重复增加余额。
+- 金币单位、图标/文字、换算比例和套餐版本修改不影响历史订单和已创建任务快照。
+- 任务创建后修改模型价格或渠道 priority,不改变该任务的快照、路由和用户价格。
+- provider 已接受但网关超时会进入 `unknown` 对账流程,不直接重复生成;worker 崩溃可由 lease 恢复且不重复结算。
+- reserve/settle/release/refund 任一重复事件都保持账本不变量,过期预扣能被 reconciler 找回。
+
+### 8.3 管理后台
+
+- 管理员能配置渠道、渠道组、priority、内部模型 ID、模型产品、分辨率和价格。
+- 管理员能配置金币单位、换算比例、套餐名称/时长/价格/金币/介绍/上架状态、可用模型或渠道组、并发和队列优先级。
+- 管理员能看到健康、失败 attempt、任务状态、余额账本、充值订单和消息 outbox。
+- 管理员写操作有权限校验和审计记录。
+- 不同角色无法读取越权用户对象、完整提示词、WebDAV 凭证或渠道密钥;高风险操作需要重新认证/审批。
+- 禁用一个渠道后,路由自动跳过;恢复后按策略重新参与。
+- 后台不展示明文 Key,日志不记录完整敏感请求和响应。
+
+### 8.4 前台能力
+
+- 画布现有节点、连线、导入导出、WebDAV 和本地图片工具不因网关迁移而回归。
+- 基础/高级/旗舰和分辨率选项由后台目录动态控制。
+- 游戏资产、艺术资产、参考图、反推提示词、多角度、局部编辑、抠图、拆分、放大具备统一任务状态和结果回写;3D 入口在 V1 不发布,调用预留契约返回 `CAPABILITY_NOT_ENABLED`。
+- 参考图/mask 只能引用当前用户已上传且通过扫描的对象;结果签名 URL 按用户/任务/输出授权并可过期或撤销。
+- 视频/动画入口不存在,避免把未开发功能暴露给用户。
+- 3D 生成入口不存在;调用预留 3D 契约返回 `CAPABILITY_NOT_ENABLED`。
+- V1 不包含模型训练和内容治理;内容治理需求进入 V2 规划。
+
+## 9. 当前阶段明确不开发
+
+- 视频生成、首尾帧、图生视频、文生视频。
+- 动画时间轴、骨骼、动效编辑器和视频序列导出。
+- Local Agent、Codex、MCP、Skill、远程 Prompt 和远程插件市场。
+- 面向用户的供应商 API Key、Base URL、原始 model ID 和自定义调用脚本。
+- 多人实时协作、团队空间和公开模型市场;V1 不实现,但保留 organization/team、成员邀请、workspace owner、共享资产、团队余额和对象范围字段/接口预留。
+- 内容治理不进入 V1;敏感词、图片审核、版权标记、投诉处理和人工复核统一列入 V2。
+- V2 内容治理预留:`moderation_policies`、`moderation_decisions`、`copyright_marks`、`complaints`、`manual_reviews`;V1 只保留 provider 拒绝结果和基础文件扫描,不建设治理后台流程。
+
+版本边界:
+
+| 版本 | 纳入范围 |
+| --- | --- |
+| V1 | 认证/防刷/两步验证、金币余额和换算、人工/mock 充值、套餐、异步队列、WebSocket、本地 + WebDAV、图片/音频/2D 多视图工具、渠道路由和后台管理 |
+| V2 | 内容治理、敏感词/图片审核、版权标记、投诉和人工复核 |
+| 后续 | 真正图生 3D/文生 3D、团队协作、模型训练、对象存储正式启用、公开模型市场、视频/动画 |
+
+## 10. 一期交付顺序和依赖
+
+按以下顺序拆分开发,前一阶段的契约和数据约束作为后一阶段的输入:
+
+1. **基础工程与安全底座**:服务端项目、数据库迁移、配置/密钥管理、统一错误码、requestId、用户/管理员认证、RBAC、审计和限流。
+2. **余额与消息底座**:余额账户和账本状态机、预扣/结算/释放、充值订单与支付 adapter 接口、邮件/SMS outbox、验证码策略和对账任务。
+3. **模型目录与渠道路由**:渠道、渠道组、模型产品、分辨率/价格发布版本、健康探活、熔断、attempt 状态机、队列 lease、未知结果对账。
+4. **任务网关**:上传对象、图片/文本/音频 adapter、统一任务 API、WebSocket 长任务进度、HTTP 轮询降级、取消/重试、签名结果 URL和输出持久化。
+5. **前台账户和目录改造**:登录注册、账户/余额/账单、公开模型目录、配置面板替换为产品选项,移除浏览器 provider Key/脚本入口。
+6. **画布接入平台任务**:保留节点和交互,改写生成上下文、引用对象、进度、结果回写、取消和错误提示;保留本地导入导出与 WebDAV。
+7. **游戏/艺术资产工具**:反推提示词、多角度、局部编辑、抠图、规则切分/全能拆分、放大、扩图等统一接入任务能力矩阵。
+8. **套餐与后台业务页面**:完成 TDesign 后台的用户、认证、渠道、模型、套餐、任务、余额、支付、存储、审计页面;3D 仅保留禁用的 adapter 契约和 feature flag,不纳入 V1 交付。
+9. **联调与验收**:按第 8 节逐项验收,覆盖故障切换、重复事件、余额不变量、权限越权、WebDAV 冲突和前台无 provider 凭证。
+
+视频、动画时间轴、团队协作实现、公开模型市场、模型训练和 V2 内容治理不阻塞前述一期主链路。
+
+## 11. 目录结构和模板接入树
+
+一期统一采用以下目录边界。目录名称先固定,后续实现不得把服务端密钥、用户文件或 provider 请求代码放入前台/后台静态目录。
+
+### 11.1 当前模板输入快照
+
+当前提供的模板目录为 `/Users/qiu/Desktop/MiragenFlow/docs/design-reference/MiragenFlow-front`,其用途是用户端视觉和交互基础,不是新的独立产品目录:
+
+- 技术栈:Vite 8、React 19、TypeScript、React Router 7、Zustand、TanStack Query、Lucide React、CSS Modules。
+- 当前入口:`src/main.tsx` -> `src/App.tsx` -> `AppShell`;使用 hash 路由,已有 `home`、`components`、`models`、`create`、`canvas`、`assets` 六个演示路由。
+- 已有视觉基础:`src/components/primitives/` 基础组件、`src/components/layout/` 壳层、`src/components/content/` 首页内容、`src/styles/` tokens/global、`src/assets/holopix/` 公开参考素材;其中 `alimama.woff2` 被 tokens 引用,迁移前必须完成字体版权和静态资源核验。
+- `ComponentsPage` 是设计系统验收页,`PlaceholderPage` 是模块占位页;二者不能作为 V1 用户业务页面或默认导航。
+- 当前模板没有真实认证、模型目录、余额、任务 API、WebSocket、WebDAV 服务端或 provider 调用;接入时必须迁移到本项目 `web/` 并替换 mock/占位逻辑。
+- `dist/` 和 `node_modules/` 属于构建产物/依赖缓存,禁止迁入源码、Docker 镜像或生产静态目录。
+
+```text
+MiragenFlow/
+├── web/ # 用户端,唯一面向普通用户的前台应用
+│ ├── public/ # 仅公开 favicon、静态图标和无敏感信息资源
+│ └── src/
+│ ├── app/ # AppProviders、路由、权限守卫、错误边界
+│ ├── pages/ # home、auth、workspace、canvas、tools、tasks、assets、account、settings
+│ ├── components/
+│ │ ├── ui/ # MiragenFlow-front primitives 迁移后的基础组件
+│ │ ├── layout/ # Header、Sidebar、UserMenu、NotificationCenter
+│ │ ├── home/ # Hero、作品卡片、发现和筛选
+│ │ ├── generation/ # Prompt、ModelPicker、Queue、Result
+│ │ ├── canvas/ # 画布节点、工具栏、属性面板和导出
+│ │ ├── assets/ # 资产卡片、上传队列、详情抽屉
+│ │ ├── account/ # 余额、套餐、账单、会话
+│ │ └── storage/ # WebDAV、同步、归档和保留状态
+│ ├── services/
+│ │ ├── api/ # 只调用 /api/v1,不保存 provider 地址或 Key
+│ │ ├── realtime/ # WebSocket 连接、心跳、cursor、任务事件去重
+│ │ └── storage/ # localforage/IndexedDB、ZIP、WebDAV 客户端
+│ ├── stores/ # auth、catalog、balance、tasks、canvas、assets、settings
+│ ├── hooks/ # 跨页面复用的 UI 副作用和任务订阅
+│ ├── lib/ # 纯函数、校验、错误码、格式化和画布工具
+│ ├── styles/ # tokens、global、主题和第三方覆盖
+│ └── types/ # 前台类型,公共 API 类型从 packages/contracts 引入
+├── admin/ # TDesign 管理台,不承载用户端页面
+│ ├── public/ # 仅公开静态资源
+│ └── src/
+│ ├── pages/ # dashboard、users、auth-messages、channels、groups、products、plans、tasks、billing、storage、audit
+│ ├── router/ # /admin 路由和权限元数据
+│ ├── services/ # 只调用 /api/v1/admin/*
+│ ├── stores/ # admin session、RBAC、filters、dashboard metrics
+│ ├── components/ # 管理台私有表格、表单、审批和时间线
+│ └── styles/ # 管理台主题和 TDesign token
+├── server/ # 一期新增的服务端网关和任务系统
+│ ├── src/
+│ │ ├── app/ # HTTP/WS bootstrap、middleware、error boundary
+│ │ ├── modules/ # auth、admin-auth、users、catalog、billing、plans、channels、tasks、providers、messages、storage、webdav、audit
+│ │ ├── adapters/ # provider、payment、email、sms、staging、future object storage
+│ │ ├── infra/ # PostgreSQL、Redis、queue、WS broker、KMS、observability
+│ │ ├── jobs/ # retry、unknown reconciliation、outbox、GC、archive、payment reconciliation
+│ │ └── shared/ # domain errors、idempotency、requestId、security helpers
+│ ├── migrations/ # 数据库迁移,禁止把密钥或生产数据提交进来
+│ └── tests/ # 单元、集成、路由故障、账本和权限测试
+├── packages/
+│ └── contracts/ # API DTO、WS event schema、错误码、能力矩阵和版本常量
+├── docs/ # 用户文档、开发文档和图示
+├── scripts/ # dev/build/迁移/校验脚本
+├── assets/ # 仓库级公开素材,不放用户上传或服务端响应
+├── docs/design-reference/ # 临时模板输入和视觉参考,不参与运行时构建
+└── .env.example # 只放公开配置示例,不放任何真实凭证
+```
+
+模板迁移映射固定如下:
+
+| 临时模板路径 | 目标路径 | 处理方式 |
+| --- | --- | --- |
+| `src/components/primitives/` | `web/src/components/ui/` | 合并基础组件,保留交互状态和可访问性 |
+| `src/components/primitives/primitives.test.tsx` | `web/src/components/ui/__tests__/` | 迁移或重写测试,不进入生产 bundle |
+| `src/components/layout/` | `web/src/components/layout/` | 接入真实会话、余额、通知和权限 |
+| `src/components/content/` | `web/src/pages/home/` | 仅吸收视觉原则,首页使用真实 API |
+| `src/data/artworks.ts` | 不迁移 | 删除演示数据,生产改接公开 API |
+| `src/App.tsx`、`src/main.tsx` | `web/src/app/` | 重写入口、providers、BrowserRouter 和守卫,不复制 hash 路由 |
+| `src/pages/HomePage.tsx` | `web/src/pages/home/index.tsx` | 改为真实首页和公开目录入口 |
+| `src/pages/*.module.css` | 对应 `web/src/pages/*` | 随页面迁移,合并 tokens |
+| `src/pages/ComponentsPage.tsx` | `web/src/pages/dev/components/` | 仅开发验收使用,不进普通用户导航 |
+| `src/pages/PlaceholderPage.tsx` | 不迁移 | 真实页面完成后删除 |
+| `src/assets/holopix/` | `web/src/assets/reference/` | 只放公开设计素材,不放业务数据 |
+| `public/favicon.svg`、`public/icons.svg` | `web/public/` | 版权、脚本和外链核验后迁移 |
+| `index.html` | `web/index.html` | 重建标题、favicon 和公开 meta |
+| `package.json`、`package-lock.json`、Vite/TS/Oxlint 配置 | 不直接复制 | 逐项合并到 web 构建配置 |
+| `README.md`、`docs/COMPONENTS.md` | `docs/design-reference/` | 仅作设计输入记录 |
+| `dist/`、`node_modules/` | 不迁移 | 构建产物和依赖缓存禁止作为源码 |
+
+## 12. 系统图示
+
+页面结构、ASCII(SSI)图示、用户侧逻辑、管理员侧逻辑、系统请求时序、整体架构和数据关系统一维护在 [system-diagrams.md](system-diagrams.md)。
+
+该文档覆盖当前已有页面和一期规划页面,并明确标记 `current`、`phase 1` 与“待确认”范围。新增页面、API、任务状态或账本状态时,必须同步更新图示文档,避免页面规划与调用链脱节。
+
+## 13. 已确认决策和实现参数
+
+本期范围已经收敛,不再把以下内容列为待确认功能:
+
+- 不接第三方登录;用户邮箱/短信验证、验证码防刷和用户两步验证作为可配置增强能力,普通用户默认关闭;管理员 MFA/人机验证码同样默认关闭,并支持审计启用/关闭。
+- V1 统一余额单位默认称为“金币”,支持后台配置文字/图标、换算比例、舍入和版本;不使用“积分”作为用户界面概念。
+- 充值只做管理员人工充值、mock provider 和支付接口预留;真实支付等用户提供第三方文档后接入。
+- 所有长任务使用异步队列,WebSocket 为进度主通道,HTTP 轮询为降级;V1 不做 SSE 主通道。
+- V1 使用浏览器本地 + WebDAV,平台对象存储只保留 adapter 接口且默认关闭;保留、归档、用户延长次数由后台可视化配置。
+- V1 不开放真正图生 3D/文生 3D、模型训练或内容治理;3D 仅保留契约和 feature flag,内容治理列入 V2。
+- V1 每日任务数不设上限,但必须限制并发、队列优先级、套餐权益、单文件大小和结果保留策略。
+- V1 不做团队协作,但预留 organization/team、成员、workspace owner、共享资产、团队余额和对象授权字段/接口。
+
+仍需在实现阶段确定的只是参数和供应商细节,不改变上述范围:
+
+1. 图形验证码、人机挑战和风控供应商的具体选型、阈值和地区策略。
+2. 管理员 IP 白名单、硬件密钥等增强策略是否在默认 MFA 之外启用。
+3. 真实支付 provider、回调字段、签名算法和对账周期,等待第三方支付文档。
+4. 任务结果、原图和 WebDAV manifest 的默认 TTL 数值、归档目录格式、用户延长的具体上限。
+5. 退款/拒付 provider 流程的字段映射;平台内部 release、refund 和人工调账状态机不改变。
diff --git a/docker-compose.local.yml b/docker-compose.local.yml
index 05283b7..af6bbd0 100644
--- a/docker-compose.local.yml
+++ b/docker-compose.local.yml
@@ -1,9 +1,42 @@
services:
+ server:
+ image: miragenflow-server:local
+ build:
+ context: .
+ dockerfile: server/Dockerfile
+ environment:
+ NODE_ENV: development
+ MIRAGENFLOW_HOST: 0.0.0.0
+ MIRAGENFLOW_PORT: 3100
+ MIRAGENFLOW_COOKIE_SECURE: ${MIRAGENFLOW_COOKIE_SECURE:-false}
+ MIRAGENFLOW_ACCESS_TOKEN_SECRET: ${MIRAGENFLOW_ACCESS_TOKEN_SECRET:-local-compose-change-me}
+ MIRAGENFLOW_ADMIN_EMAIL: ${MIRAGENFLOW_ADMIN_EMAIL:-admin@admin.com}
+ MIRAGENFLOW_ADMIN_PASSWORD: ${MIRAGENFLOW_ADMIN_PASSWORD:-admin}
+ MIRAGENFLOW_ADMIN_MFA_REQUIRED: ${MIRAGENFLOW_ADMIN_MFA_REQUIRED:-false}
+ MIRAGENFLOW_ADMIN_MFA_SECRET: ${MIRAGENFLOW_ADMIN_MFA_SECRET:-local-compose-admin-mfa-secret}
+ MIRAGENFLOW_ADMIN_SELF_APPROVAL: ${MIRAGENFLOW_ADMIN_SELF_APPROVAL:-true}
+ MIRAGENFLOW_USER_MFA_REQUIRED: ${MIRAGENFLOW_USER_MFA_REQUIRED:-false}
+ MIRAGENFLOW_USER_VERIFICATION_REQUIRED: ${MIRAGENFLOW_USER_VERIFICATION_REQUIRED:-false}
+ MIRAGENFLOW_EMAIL_REGISTRATION_VERIFICATION_REQUIRED: ${MIRAGENFLOW_EMAIL_REGISTRATION_VERIFICATION_REQUIRED:-false}
+ MIRAGENFLOW_PHONE_REGISTRATION_VERIFICATION_REQUIRED: ${MIRAGENFLOW_PHONE_REGISTRATION_VERIFICATION_REQUIRED:-false}
+ MIRAGENFLOW_CAPTCHA_REQUIRED: ${MIRAGENFLOW_CAPTCHA_REQUIRED:-false}
+ MIRAGENFLOW_ALLOW_LOCAL_PROVIDER_URLS: ${MIRAGENFLOW_ALLOW_LOCAL_PROVIDER_URLS:-true}
+ MIRAGENFLOW_STORE_FILE: /var/lib/miragenflow/store.json
+ MIRAGENFLOW_STAGING_DIR: /var/lib/miragenflow/staging
+ expose:
+ - "3100"
+ volumes:
+ - miragenflow-server-local:/var/lib/miragenflow
app:
image: miragenflow:local
build:
context: .
dockerfile: Dockerfile
ports:
- - "3000:3000"
+ - "127.0.0.1:3000:3000"
+ depends_on:
+ - server
restart: unless-stopped
+
+volumes:
+ miragenflow-server-local:
diff --git a/docker-compose.yml b/docker-compose.yml
index 79c0521..3b1c4c7 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -1,11 +1,83 @@
services:
+ postgres:
+ image: postgres:16-alpine
+ environment:
+ POSTGRES_DB: miragenflow
+ POSTGRES_USER: miragenflow
+ POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?POSTGRES_PASSWORD must be set}
+ volumes:
+ - miragenflow-postgres:/var/lib/postgresql/data
+ healthcheck:
+ test: ["CMD-SHELL", "pg_isready -U miragenflow -d miragenflow"]
+ interval: 5s
+ timeout: 3s
+ retries: 10
+ redis:
+ image: redis:7-alpine
+ command: ["redis-server", "--appendonly", "yes"]
+ volumes:
+ - miragenflow-redis:/data
+ healthcheck:
+ test: ["CMD", "redis-cli", "ping"]
+ interval: 5s
+ timeout: 3s
+ retries: 10
+ server:
+ build:
+ context: .
+ dockerfile: server/Dockerfile
+ environment:
+ NODE_ENV: production
+ MIRAGENFLOW_HOST: 0.0.0.0
+ MIRAGENFLOW_PORT: 3100
+ # The bundled nginx listener is HTTP. Set this to true only when the
+ # public reverse proxy terminates TLS before forwarding to the app.
+ MIRAGENFLOW_COOKIE_SECURE: ${MIRAGENFLOW_COOKIE_SECURE:-false}
+ MIRAGENFLOW_ACCESS_TOKEN_SECRET: ${MIRAGENFLOW_ACCESS_TOKEN_SECRET:?MIRAGENFLOW_ACCESS_TOKEN_SECRET must be set}
+ MIRAGENFLOW_ADMIN_EMAIL: ${MIRAGENFLOW_ADMIN_EMAIL:?MIRAGENFLOW_ADMIN_EMAIL must be set}
+ MIRAGENFLOW_ADMIN_PASSWORD: ${MIRAGENFLOW_ADMIN_PASSWORD:?MIRAGENFLOW_ADMIN_PASSWORD must be set}
+ MIRAGENFLOW_ADMIN_MFA_SECRET: ${MIRAGENFLOW_ADMIN_MFA_SECRET:?MIRAGENFLOW_ADMIN_MFA_SECRET must be set}
+ MIRAGENFLOW_ADMIN_MFA_REQUIRED: ${MIRAGENFLOW_ADMIN_MFA_REQUIRED:-false}
+ MIRAGENFLOW_ADMIN_CAPTCHA_REQUIRED: ${MIRAGENFLOW_ADMIN_CAPTCHA_REQUIRED:-false}
+ MIRAGENFLOW_USER_MFA_REQUIRED: ${MIRAGENFLOW_USER_MFA_REQUIRED:-false}
+ MIRAGENFLOW_USER_VERIFICATION_REQUIRED: ${MIRAGENFLOW_USER_VERIFICATION_REQUIRED:-false}
+ MIRAGENFLOW_EMAIL_REGISTRATION_VERIFICATION_REQUIRED: ${MIRAGENFLOW_EMAIL_REGISTRATION_VERIFICATION_REQUIRED:-false}
+ MIRAGENFLOW_PHONE_REGISTRATION_VERIFICATION_REQUIRED: ${MIRAGENFLOW_PHONE_REGISTRATION_VERIFICATION_REQUIRED:-false}
+ MIRAGENFLOW_CAPTCHA_REQUIRED: ${MIRAGENFLOW_CAPTCHA_REQUIRED:-false}
+ MIRAGENFLOW_CHANNEL_ENCRYPTION_KEY: ${MIRAGENFLOW_CHANNEL_ENCRYPTION_KEY:?MIRAGENFLOW_CHANNEL_ENCRYPTION_KEY must be set}
+ DATABASE_URL: postgres://miragenflow:${POSTGRES_PASSWORD:?POSTGRES_PASSWORD must be set}@postgres:5432/miragenflow
+ REDIS_URL: redis://redis:6379
+ MIRAGENFLOW_PERSISTENCE_ADAPTER: postgres
+ MIRAGENFLOW_QUEUE_ADAPTER: redis
+ MIRAGENFLOW_STAGING_TTL_SECONDS: 604800
+ MIRAGENFLOW_STAGING_DIR: /var/lib/miragenflow/staging
+ expose:
+ - "3100"
+ volumes:
+ - miragenflow-server:/var/lib/miragenflow
+ depends_on:
+ postgres:
+ condition: service_healthy
+ redis:
+ condition: service_healthy
+ healthcheck:
+ test: ["CMD", "wget", "-qO-", "http://127.0.0.1:3100/api/ready"]
+ interval: 10s
+ timeout: 3s
+ retries: 5
app:
- image: ghcr.io/basketikun/infinite-canvas:latest
+ build:
+ context: .
+ dockerfile: Dockerfile
container_name: miragenflow
ports:
- "3000:3000"
restart: unless-stopped
- # 统计分析(可选,默认关闭)。每家一个独立变量,填了谁就启用谁,可同时启用多家:
- # environment:
- # ANALYTICS_GA4_ID: G-XXXXXXXXXX # Google Analytics 4 衡量 ID
- # ANALYTICS_BAIDU_ID: xxxxxxxxxxxxxxxxxxxxxxxxxxxx # 百度统计站点 ID
+ depends_on:
+ server:
+ condition: service_healthy
+
+volumes:
+ miragenflow-postgres:
+ miragenflow-redis:
+ miragenflow-server:
diff --git a/docs/content/docs/business/license.mdx b/docs/content/docs/business/license.mdx
index 246d87a..a0a9dea 100644
--- a/docs/content/docs/business/license.mdx
+++ b/docs/content/docs/business/license.mdx
@@ -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.
diff --git a/docs/content/docs/business/license.zh-CN.mdx b/docs/content/docs/business/license.zh-CN.mdx
index 7a5ea99..17a5744 100644
--- a/docs/content/docs/business/license.zh-CN.mdx
+++ b/docs/content/docs/business/license.zh-CN.mdx
@@ -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)。任何个人、团队和企业都可以免费、自由地使用本项目,没有使用场景和商业用途限制。
## 项目初衷
diff --git a/docs/content/docs/development/local-development.mdx b/docs/content/docs/development/local-development.mdx
index 582a52a..8e7be2c 100644
--- a/docs/content/docs/development/local-development.mdx
+++ b/docs/content/docs/development/local-development.mdx
@@ -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
diff --git a/docs/content/docs/development/local-development.zh-CN.mdx b/docs/content/docs/development/local-development.zh-CN.mdx
index 2806497..706ed49 100644
--- a/docs/content/docs/development/local-development.zh-CN.mdx
+++ b/docs/content/docs/development/local-development.zh-CN.mdx
@@ -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. 启动文档站
diff --git a/docs/content/docs/overview/docker.mdx b/docs/content/docs/overview/docker.mdx
index 2a22b87..28d4dc6 100644
--- a/docs/content/docs/overview/docker.mdx
+++ b/docs/content/docs/overview/docker.mdx
@@ -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.
diff --git a/docs/content/docs/overview/docker.zh-CN.mdx b/docs/content/docs/overview/docker.zh-CN.mdx
index 13eda0d..b9fec3a 100644
--- a/docs/content/docs/overview/docker.zh-CN.mdx
+++ b/docs/content/docs/overview/docker.zh-CN.mdx
@@ -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。
-
-
-请只在你自己的站点上配置统计 ID,不要把 ID 提交进仓库;fork 或自托管本项目的用户默认不会产生任何统计。
-
+前端不会注入第三方统计脚本,也不会把页面访问数据发送到外部统计服务。认证、余额、任务、队列和 WebSocket 仍依赖同一 Compose 中的前台、服务端、PostgreSQL 与 Redis。
diff --git a/docs/content/docs/overview/features.mdx b/docs/content/docs/overview/features.mdx
index 64d811e..83e447c 100644
--- a/docs/content/docs/overview/features.mdx
+++ b/docs/content/docs/overview/features.mdx
@@ -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.
diff --git a/docs/content/docs/overview/features.zh-CN.mdx b/docs/content/docs/overview/features.zh-CN.mdx
index fd8b5ab..6fbeb29 100644
--- a/docs/content/docs/overview/features.zh-CN.mdx
+++ b/docs/content/docs/overview/features.zh-CN.mdx
@@ -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 静态资源路径仍需要更广泛的生产部署验证。
diff --git a/docs/content/docs/overview/quick-start.mdx b/docs/content/docs/overview/quick-start.mdx
index 8e0dc3a..000fdf0 100644
--- a/docs/content/docs/overview/quick-start.mdx
+++ b/docs/content/docs/overview/quick-start.mdx
@@ -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.
diff --git a/docs/content/docs/overview/quick-start.zh-CN.mdx b/docs/content/docs/overview/quick-start.zh-CN.mdx
index 52c751c..9f2e22d 100644
--- a/docs/content/docs/overview/quick-start.zh-CN.mdx
+++ b/docs/content/docs/overview/quick-start.zh-CN.mdx
@@ -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 凭证只在服务端密文保存。
diff --git a/docs/content/docs/overview/render.mdx b/docs/content/docs/overview/render.mdx
index 54c047b..5a95d71 100644
--- a/docs/content/docs/overview/render.mdx
+++ b/docs/content/docs/overview/render.mdx
@@ -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.
diff --git a/docs/content/docs/overview/render.zh-CN.mdx b/docs/content/docs/overview/render.zh-CN.mdx
index d022eba..4528e16 100644
--- a/docs/content/docs/overview/render.zh-CN.mdx
+++ b/docs/content/docs/overview/render.zh-CN.mdx
@@ -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 和服务端环境变量。
## 部署步骤
diff --git a/docs/content/docs/progress/pending-test.mdx b/docs/content/docs/progress/pending-test.mdx
index 5574202..00b595b 100644
--- a/docs/content/docs/progress/pending-test.mdx
+++ b/docs/content/docs/progress/pending-test.mdx
@@ -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 ``, `.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 ``, 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 ``, 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 和图表;可选字段缺失时由单项统计或图表显示数字兜底/中文空态,不再因为一个字段缺失隐藏整块页面。仍需用空对象、部分字段和真实全零数据分别验证空态与零值的区分。
+- 静态源码扫描已通过:用户端和管理端没有业务层原生 ``、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 `` 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.
diff --git a/docs/content/docs/progress/pending-test.zh-CN.mdx b/docs/content/docs/progress/pending-test.zh-CN.mdx
index c793269..fa9c0ae 100644
--- a/docs/content/docs/progress/pending-test.zh-CN.mdx
+++ b/docs/content/docs/progress/pending-test.zh-CN.mdx
@@ -5,18 +5,280 @@ description: 当前版本已实现但仍需人工验证的变更项
# 待测试
+- 最新回归证据:`npm run build:all` 已通过服务端类型检查、用户端生产构建和管理端生产构建;`npm run test:all` 已通过服务端 102/102 测试及用户端/管理端类型检查。精确禁用项扫描未发现用户端 Ant Design、ProComponents、Radix 控件、Lucide、原生 ``、`.ant-*`、CDN 或 WebFont;剩余命中仅为项目自有 TDesign `Select` 适配器名称和原生文件输入 API 类型。构建中的大包、动态导入和 Browserslist 提示仍为既有提示。
+- 独立浏览器标签在 390×844 下复核了 `/`、`/auth/reset`、`/style-preview` 和 `/admin/login`:页面均显示中文内容,无横向溢出、原生 `` 或控制台错误。管理员登录态业务流程、受保护用户路由、画布内部交互以及外部 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 和管理台登录壳:均渲染中文内容,无原生 ``、横向溢出或控制台错误。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 和图表;可选字段缺失时由单项统计或图表显示数字兜底/中文空态,不再因为一个字段缺失隐藏整块页面。仍需用空对象、部分字段和真实全零数据分别验证空态与零值的区分。
+- 静态源码扫描已通过:用户端和管理端没有业务层原生 ``、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 布局。
+- 用户端模型选择、任务筛选、图片参数和画布图片设置下拉统一使用带主题样式的自定义弹层,不再渲染浏览器原生 ``;外层弹层与 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 更新。
diff --git a/docs/content/docs/progress/todo.mdx b/docs/content/docs/progress/todo.mdx
index 596380d..bcf1dd2 100644
--- a/docs/content/docs/progress/todo.mdx
+++ b/docs/content/docs/progress/todo.mdx
@@ -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 `` 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.
diff --git a/docs/content/docs/progress/todo.zh-CN.mdx b/docs/content/docs/progress/todo.zh-CN.mdx
index 1d04166..5ac6aec 100644
--- a/docs/content/docs/progress/todo.zh-CN.mdx
+++ b/docs/content/docs/progress/todo.zh-CN.mdx
@@ -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:迁移普通控件、主题化下拉、反馈、弹层和表单,清理原生 ``,验证键盘、遮罩、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 和内部扩展能力展开。
diff --git a/docs/content/docs/support/security.mdx b/docs/content/docs/support/security.mdx
index 1138bbf..c349003 100644
--- a/docs/content/docs/support/security.mdx
+++ b/docs/content/docs/support/security.mdx
@@ -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.
diff --git a/docs/content/docs/support/security.zh-CN.mdx b/docs/content/docs/support/security.zh-CN.mdx
index dd56ab5..62dbe19 100644
--- a/docs/content/docs/support/security.zh-CN.mdx
+++ b/docs/content/docs/support/security.zh-CN.mdx
@@ -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、截图中的敏感信息或真实用户数据。
diff --git a/docs/design-reference/MiragenFlow-front/.gitignore b/docs/design-reference/MiragenFlow-front/.gitignore
new file mode 100644
index 0000000..a547bf3
--- /dev/null
+++ b/docs/design-reference/MiragenFlow-front/.gitignore
@@ -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?
diff --git a/docs/design-reference/MiragenFlow-front/.oxlintrc.json b/docs/design-reference/MiragenFlow-front/.oxlintrc.json
new file mode 100644
index 0000000..6fa991d
--- /dev/null
+++ b/docs/design-reference/MiragenFlow-front/.oxlintrc.json
@@ -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 }]
+ }
+}
diff --git a/docs/design-reference/MiragenFlow-front/README.md b/docs/design-reference/MiragenFlow-front/README.md
new file mode 100644
index 0000000..d6af7e3
--- /dev/null
+++ b/docs/design-reference/MiragenFlow-front/README.md
@@ -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.
diff --git a/docs/design-reference/MiragenFlow-front/docs/COMPONENTS.md b/docs/design-reference/MiragenFlow-front/docs/COMPONENTS.md
new file mode 100644
index 0000000..6fc2d97
--- /dev/null
+++ b/docs/design-reference/MiragenFlow-front/docs/COMPONENTS.md
@@ -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.
diff --git a/docs/design-reference/MiragenFlow-front/index.html b/docs/design-reference/MiragenFlow-front/index.html
new file mode 100644
index 0000000..d71d94b
--- /dev/null
+++ b/docs/design-reference/MiragenFlow-front/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+ miragenflow-front
+
+
+
+
+
+
diff --git a/docs/design-reference/MiragenFlow-front/package-lock.json b/docs/design-reference/MiragenFlow-front/package-lock.json
new file mode 100644
index 0000000..e7131a8
--- /dev/null
+++ b/docs/design-reference/MiragenFlow-front/package-lock.json
@@ -0,0 +1,2529 @@
+{
+ "name": "miragenflow-front",
+ "version": "0.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "miragenflow-front",
+ "version": "0.0.0",
+ "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"
+ }
+ },
+ "node_modules/@adobe/css-tools": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmmirror.com/@adobe/css-tools/-/css-tools-4.5.0.tgz",
+ "integrity": "sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@asamuzakjp/css-color": {
+ "version": "6.0.7",
+ "resolved": "https://registry.npmmirror.com/@asamuzakjp/css-color/-/css-color-6.0.7.tgz",
+ "integrity": "sha512-vC/bk1Lz7Tn/EfU9/apOTBk80/8dyGyWMowPoV1tJ52muDGsDqt2HPT2klrFUiY60MQmQv9q8yIht15JnBgDGw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@csstools/css-calc": "^3.3.0",
+ "@csstools/css-color-parser": "^4.1.10",
+ "@csstools/css-parser-algorithms": "^4.0.0",
+ "@csstools/css-tokenizer": "^4.0.0",
+ "lru-cache": "^11.5.2"
+ },
+ "engines": {
+ "node": "^22.13.0 || >=24.0.0"
+ }
+ },
+ "node_modules/@asamuzakjp/dom-selector": {
+ "version": "8.3.2",
+ "resolved": "https://registry.npmmirror.com/@asamuzakjp/dom-selector/-/dom-selector-8.3.2.tgz",
+ "integrity": "sha512-93Z1N+BQNXysodoicpOIyNh2drHfz/CTf9nnT0FEx72GJcIiwgydD7tGAr78j41LsYn3hlRn+LdGPuBLn1Bl8Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "bidi-js": "^1.0.3",
+ "css-tree": "^3.2.1",
+ "is-potential-custom-element-name": "^1.0.1",
+ "lru-cache": "^11.5.2"
+ },
+ "engines": {
+ "node": "^22.13.0 || >=24.0.0"
+ }
+ },
+ "node_modules/@babel/code-frame": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmmirror.com/@babel/code-frame/-/code-frame-7.29.7.tgz",
+ "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@babel/helper-validator-identifier": "^7.29.7",
+ "js-tokens": "^4.0.0",
+ "picocolors": "^1.1.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz",
+ "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/runtime": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.29.7.tgz",
+ "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@bramus/specificity": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmmirror.com/@bramus/specificity/-/specificity-2.4.2.tgz",
+ "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "css-tree": "^3.0.0"
+ },
+ "bin": {
+ "specificity": "bin/cli.js"
+ }
+ },
+ "node_modules/@csstools/color-helpers": {
+ "version": "6.1.1",
+ "resolved": "https://registry.npmmirror.com/@csstools/color-helpers/-/color-helpers-6.1.1.tgz",
+ "integrity": "sha512-gLNsunvwf3mCi5u5o46/Z/JcJMnhbHSaZ69rkgPzNM3J4s8hWwpPUQB6/tt0EDFyCiWzxANlx+2LJwpYj4zS1w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT-0",
+ "engines": {
+ "node": ">=20.19.0"
+ }
+ },
+ "node_modules/@csstools/css-calc": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmmirror.com/@csstools/css-calc/-/css-calc-3.3.0.tgz",
+ "integrity": "sha512-c5ihYsPkdG6JCkU2zTMm4+k6r7RXuGxtWYhu5DHMIiF1FHzrfmHL5so11AoFpUv/tu61xfcmT4AmKoFfMPoqdQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=20.19.0"
+ },
+ "peerDependencies": {
+ "@csstools/css-parser-algorithms": "^4.0.0",
+ "@csstools/css-tokenizer": "^4.0.0"
+ }
+ },
+ "node_modules/@csstools/css-color-parser": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmmirror.com/@csstools/css-color-parser/-/css-color-parser-4.2.0.tgz",
+ "integrity": "sha512-5+5LEmFuY1AjXdYhmgjTJogtQnP1evJ1zrBZGUNZ0thkpwnnmKxcHdAMn/OtFjAb25zA+jKDVYVRl+5G7rjv1A==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "@csstools/color-helpers": "^6.1.1",
+ "@csstools/css-calc": "^3.3.0"
+ },
+ "engines": {
+ "node": ">=20.19.0"
+ },
+ "peerDependencies": {
+ "@csstools/css-parser-algorithms": "^4.0.0",
+ "@csstools/css-tokenizer": "^4.0.0"
+ }
+ },
+ "node_modules/@csstools/css-parser-algorithms": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmmirror.com/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz",
+ "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=20.19.0"
+ },
+ "peerDependencies": {
+ "@csstools/css-tokenizer": "^4.0.0"
+ }
+ },
+ "node_modules/@csstools/css-syntax-patches-for-csstree": {
+ "version": "1.1.8",
+ "resolved": "https://registry.npmmirror.com/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.8.tgz",
+ "integrity": "sha512-CpMLjAvwQg3BL5S0IeqsZNMH7EQrEWi0kLKOC13ZBF0ZwERiLWlibNPJr8G1kdU3Ms/r2KiNrF81pUh2HwAHdg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT-0",
+ "peerDependencies": {
+ "css-tree": "^3.2.1"
+ },
+ "peerDependenciesMeta": {
+ "css-tree": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@csstools/css-tokenizer": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmmirror.com/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz",
+ "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=20.19.0"
+ }
+ },
+ "node_modules/@exodus/bytes": {
+ "version": "1.15.1",
+ "resolved": "https://registry.npmmirror.com/@exodus/bytes/-/bytes-1.15.1.tgz",
+ "integrity": "sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
+ },
+ "peerDependencies": {
+ "@noble/hashes": "^1.8.0 || ^2.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@noble/hashes": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.5",
+ "resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@oxc-project/types": {
+ "version": "0.144.0",
+ "resolved": "https://registry.npmmirror.com/@oxc-project/types/-/types-0.144.0.tgz",
+ "integrity": "sha512-nuhZIOLuI6TFQ32I/WnUx+SCPY7SdSKwgnFHydAuoS1+Z4BRcaP+RRJmGzl9lw+0OFF7UmaESf7KQRXaNLHypg==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/Boshen"
+ }
+ },
+ "node_modules/@oxlint/binding-android-arm64": {
+ "version": "1.79.0",
+ "resolved": "https://registry.npmmirror.com/@oxlint/binding-android-arm64/-/binding-android-arm64-1.79.0.tgz",
+ "integrity": "sha512-KqqnOtAVgNsPPF0YSodkFZA1O80jcKoCZCTu3bgsszxA+MrMP9TLzfXitKjEj1FmrPprKDMdRDMmY3weESO9sg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxlint/binding-darwin-arm64": {
+ "version": "1.79.0",
+ "resolved": "https://registry.npmmirror.com/@oxlint/binding-darwin-arm64/-/binding-darwin-arm64-1.79.0.tgz",
+ "integrity": "sha512-BVC2nsMzqQzRDPc5RhixkZ+m1p7iH4bxRRvqkbwDXX0PlQKm1BPy8J8cRjnAFafOq2QzI+BfO3vE8w2GZ3CBag==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxlint/binding-freebsd-x64": {
+ "version": "1.79.0",
+ "resolved": "https://registry.npmmirror.com/@oxlint/binding-freebsd-x64/-/binding-freebsd-x64-1.79.0.tgz",
+ "integrity": "sha512-qDMm0dXZnoHyRqSL4N4xUq82T4sqK5cbKSjvd/dF/YbMUXc2R1wEPf+vmA5S0qUmi0nwXfNbjXBtZaIqzQLIMg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxlint/binding-linux-arm64-gnu": {
+ "version": "1.79.0",
+ "resolved": "https://registry.npmmirror.com/@oxlint/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.79.0.tgz",
+ "integrity": "sha512-lu158FR4nGqGeRS3BQvtG85wRgU/Fy4MD5Cxp1hzJXizGiLo6u2742wJSCDKh8cFcZntvX7fcxlq4mMmfryH1g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxlint/binding-linux-x64-gnu": {
+ "version": "1.79.0",
+ "resolved": "https://registry.npmmirror.com/@oxlint/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.79.0.tgz",
+ "integrity": "sha512-iFZL02deziHslb3jEX9KdqlAkYoo4fGyotchKDzdfK1f5mxlIBeiQeHhvK3iFpuEJSB4ma/qeFn9oxPiwnhUPQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxlint/binding-linux-x64-musl": {
+ "version": "1.79.0",
+ "resolved": "https://registry.npmmirror.com/@oxlint/binding-linux-x64-musl/-/binding-linux-x64-musl-1.79.0.tgz",
+ "integrity": "sha512-3DtZR2raqObnh7wXZoFYFd0Fw7skBvcb3f7A+/lkEiDuh8hrE6vv9b/62Qxao1a9/OeHLw/FcXlXzgsW9wTRFg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxlint/binding-openharmony-arm64": {
+ "version": "1.79.0",
+ "resolved": "https://registry.npmmirror.com/@oxlint/binding-openharmony-arm64/-/binding-openharmony-arm64-1.79.0.tgz",
+ "integrity": "sha512-Oatt4GuA1WJkqzk2ozx4HrWROOi7opV3AKDw/U8qDIqeTqzsjn5K2x3REJMNjU3/KU/Bkq96Zi3CknaiDTaC/Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxlint/binding-win32-arm64-msvc": {
+ "version": "1.79.0",
+ "resolved": "https://registry.npmmirror.com/@oxlint/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.79.0.tgz",
+ "integrity": "sha512-NAgZr9Qp8nIA9rpo0JEvwiabTF/2UVqBNnupBG9X4kxXcQoScJUTi+qHhvabb9s/thgj5wQ4XcIaJvb+ZMgoKw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxlint/binding-win32-x64-msvc": {
+ "version": "1.79.0",
+ "resolved": "https://registry.npmmirror.com/@oxlint/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.79.0.tgz",
+ "integrity": "sha512-mEelcCMMBS57sIXh2veGMNy+pQwuGtcMxHxGIZWQ5Ba9pJ5jCCUFOZB9E2JhBaxGsURe+WGe0zJp4RVre52gpQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-android-arm64": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmmirror.com/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.4.tgz",
+ "integrity": "sha512-jHC2cnyKz5xU2fhECtFl8OZ83cYNt13GZQD+0uMJ/X3o+ijmd56okHhTUwxVSHPx1IRVIJEZ1/1pPzeLCU6XKA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-darwin-arm64": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmmirror.com/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.4.tgz",
+ "integrity": "sha512-Dc5mPD8F5F/FS8i01syd7FTF6yB2fVthH/TRkjwJkzUK6EpoxHtqvZQP5Zwq80/5z19TWYHIg1KOHboCgVx/aQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-darwin-x64": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmmirror.com/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.4.tgz",
+ "integrity": "sha512-fpDm4oBo6SqLvWUYCmFhdde3U9KH2fRNNMeAnAPAIwxRL345xutL0EtEUcuoxsoazdJGv/MuDBQHlCDrtbvqOg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-freebsd-x64": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmmirror.com/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.4.tgz",
+ "integrity": "sha512-rSJoreDE/HoIzoaib6MTp5jQtCTdMHKIvItAKT/ImS6Y6Ww76oUaeMyp4Vc/fAgd/ehji068IxetHXAnqUwN9A==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm-gnueabihf": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmmirror.com/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.4.tgz",
+ "integrity": "sha512-/jm8OGHgn7oGaJu3i/qZI9spUGcJ+y/lk43ttQ/iO1tOd9NissG6o97bighBCiL+BKRngmcDuR6ikfwYdJmVuQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm64-gnu": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmmirror.com/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.4.tgz",
+ "integrity": "sha512-tIP06BeD9EqvECBrPZ+sqdPlYrT+aYaAiu1wYziVx5elRK/ftm33JxVDy2bXGbr6J0CrtirCkR87/X5a2euEng==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm64-musl": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmmirror.com/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.4.tgz",
+ "integrity": "sha512-Ql1Q0EQqVThvn9VAVlwNzsUvbSFtCMGjLpRRi4pk5i7NZZ4n5ISiLMjHYtus4VQ2PvkSw24zyaCVsiS+sXPj1w==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-ppc64-gnu": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmmirror.com/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.4.tgz",
+ "integrity": "sha512-GjbjXD4XXfN19D0LZNbmiCBUoDiRACsYHr0yaIbbn8aFsXjHZifcYqu/W5Er5X2X990WjHXFrxarn5chzItorQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-s390x-gnu": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmmirror.com/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.4.tgz",
+ "integrity": "sha512-p5WR0NOwaRmJ/B1b6IjEFLLivwEsf3PrdBIhRbhTCQisbo2SvHHpG4ELB/+FgQNnB88LTOF86upmJmbvZdQ2lw==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-x64-gnu": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmmirror.com/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.4.tgz",
+ "integrity": "sha512-4/GyVjmhR+Tc6HLJvwc1sOhPqAZtySiSMesOZyX6JQ5XBxoTDEMKQzvo07NIK6nTon/SivlZqvhzvuVBNQhObQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-x64-musl": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmmirror.com/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.4.tgz",
+ "integrity": "sha512-l9eeLsCNvPpmSXUej0etw/J1eqV0Jj1D5G/xG6YTijmE6dkv6E2QezgWbTfQk63v952DPqrjOCoiqxq7Bw0YUQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-openharmony-arm64": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmmirror.com/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.4.tgz",
+ "integrity": "sha512-e0F355MSTMm3+UOqtV3L24gFUp2N5m1f8L/7d56deik6va+AXdrt9F8LbzGpeWGWRbZEDq4m8NVnJDeBtf9DZg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-win32-arm64-msvc": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmmirror.com/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.4.tgz",
+ "integrity": "sha512-AWLi0uBRYh6QlE7OKhiz+phZC0qwtij2QZmhmOdsLdFn64m7oMpooE9ICE3lhm9xMb4SpDo2WbHcxX1iFLFtqw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-win32-x64-msvc": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmmirror.com/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.4.tgz",
+ "integrity": "sha512-UwSDJOg3dqCAejWdxclJjCsh3Qq4vLYMDxmyHqo1btz3stK2VqgwNd3mm5tuIwzSlGIQ/1H9Hr+Zn09mrezNqQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/pluginutils": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmmirror.com/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz",
+ "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@standard-schema/spec": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmmirror.com/@standard-schema/spec/-/spec-1.1.0.tgz",
+ "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@tanstack/query-core": {
+ "version": "5.101.4",
+ "resolved": "https://registry.npmmirror.com/@tanstack/query-core/-/query-core-5.101.4.tgz",
+ "integrity": "sha512-gNwcvOJcRbLWPOLG/2OBm+zM+Yv+MKsXKEOWC57USuZDEsI71hEErQsiEGx5wX9rzWWkfwM0fVSPoiIFSsxfiw==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ }
+ },
+ "node_modules/@tanstack/react-query": {
+ "version": "5.101.4",
+ "resolved": "https://registry.npmmirror.com/@tanstack/react-query/-/react-query-5.101.4.tgz",
+ "integrity": "sha512-yRg2pfOCxIs4ZJW3XYYHU/WgtD04FHSnfHlpRT7h7pR77hwkdRG4wxbKe4aq6P0RvXUTBSQpQeadS1SUYUe+KA==",
+ "license": "MIT",
+ "dependencies": {
+ "@tanstack/query-core": "5.101.4"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ },
+ "peerDependencies": {
+ "react": "^18 || ^19"
+ }
+ },
+ "node_modules/@testing-library/dom": {
+ "version": "10.4.1",
+ "resolved": "https://registry.npmmirror.com/@testing-library/dom/-/dom-10.4.1.tgz",
+ "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@babel/code-frame": "^7.10.4",
+ "@babel/runtime": "^7.12.5",
+ "@types/aria-query": "^5.0.1",
+ "aria-query": "5.3.0",
+ "dom-accessibility-api": "^0.5.9",
+ "lz-string": "^1.5.0",
+ "picocolors": "1.1.1",
+ "pretty-format": "^27.0.2"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@testing-library/jest-dom": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmmirror.com/@testing-library/jest-dom/-/jest-dom-7.0.1.tgz",
+ "integrity": "sha512-oMDTC3oA+6CXSO2JZnvOI7CA6oVub6kij5ggk9ohwye5slmkwxYDXcPOVxgMw/RQlticjtO0C1RZkR97HgrWMw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@adobe/css-tools": "^4.4.0",
+ "aria-query": "^5.0.0",
+ "css.escape": "^1.5.1",
+ "dom-accessibility-api": "^0.6.3",
+ "picocolors": "^1.1.1",
+ "redent": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=22",
+ "npm": ">=6",
+ "yarn": ">=1"
+ },
+ "peerDependencies": {
+ "@testing-library/dom": ">=10 <11",
+ "vitest": ">= 0.32"
+ },
+ "peerDependenciesMeta": {
+ "vitest": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmmirror.com/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz",
+ "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@testing-library/react": {
+ "version": "16.3.2",
+ "resolved": "https://registry.npmmirror.com/@testing-library/react/-/react-16.3.2.tgz",
+ "integrity": "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.12.5"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@testing-library/dom": "^10.0.0",
+ "@types/react": "^18.0.0 || ^19.0.0",
+ "@types/react-dom": "^18.0.0 || ^19.0.0",
+ "react": "^18.0.0 || ^19.0.0",
+ "react-dom": "^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@types/aria-query": {
+ "version": "5.0.4",
+ "resolved": "https://registry.npmmirror.com/@types/aria-query/-/aria-query-5.0.4.tgz",
+ "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/@types/chai": {
+ "version": "5.2.3",
+ "resolved": "https://registry.npmmirror.com/@types/chai/-/chai-5.2.3.tgz",
+ "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/deep-eql": "*",
+ "assertion-error": "^2.0.1"
+ }
+ },
+ "node_modules/@types/deep-eql": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmmirror.com/@types/deep-eql/-/deep-eql-4.0.2.tgz",
+ "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.9",
+ "resolved": "https://registry.npmmirror.com/@types/estree/-/estree-1.0.9.tgz",
+ "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/node": {
+ "version": "24.13.3",
+ "resolved": "https://registry.npmmirror.com/@types/node/-/node-24.13.3.tgz",
+ "integrity": "sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~7.18.0"
+ }
+ },
+ "node_modules/@types/react": {
+ "version": "19.2.18",
+ "resolved": "https://registry.npmmirror.com/@types/react/-/react-19.2.18.tgz",
+ "integrity": "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "csstype": "^3.2.2"
+ }
+ },
+ "node_modules/@types/react-dom": {
+ "version": "19.2.4",
+ "resolved": "https://registry.npmmirror.com/@types/react-dom/-/react-dom-19.2.4.tgz",
+ "integrity": "sha512-Bsc+QHgp+P/F02XDzNCY9jnZNCUuLki36KT7VKrTXXLdHf+vHMNZnW1rVu5DNW/rCK+fya3DATySbLM4yhtKUw==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "^19.2.0"
+ }
+ },
+ "node_modules/@vitejs/plugin-react": {
+ "version": "6.0.5",
+ "resolved": "https://registry.npmmirror.com/@vitejs/plugin-react/-/plugin-react-6.0.5.tgz",
+ "integrity": "sha512-BOVzne/NL162sMdResB25mUv+vWMF5NoAjNf09TeGlE7ZpszZWSD3winycicLJw72yeVsoCn/2kOhEuCvEShMA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@rolldown/pluginutils": "^1.0.1"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "peerDependencies": {
+ "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0",
+ "babel-plugin-react-compiler": "^1.0.0",
+ "vite": "^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@rolldown/plugin-babel": {
+ "optional": true
+ },
+ "babel-plugin-react-compiler": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@vitest/expect": {
+ "version": "4.1.11",
+ "resolved": "https://registry.npmmirror.com/@vitest/expect/-/expect-4.1.11.tgz",
+ "integrity": "sha512-VX2x5vNJXET47KAFzwERI+KRMtTTCSWTfSMKsW7JsUsXV4psq++e3DvZpuTDOpHcxytiDs6p2nhVb2tVDiiUYw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@standard-schema/spec": "^1.1.0",
+ "@types/chai": "^5.2.2",
+ "@vitest/spy": "4.1.11",
+ "@vitest/utils": "4.1.11",
+ "chai": "^6.2.2",
+ "tinyrainbow": "^3.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/mocker": {
+ "version": "4.1.11",
+ "resolved": "https://registry.npmmirror.com/@vitest/mocker/-/mocker-4.1.11.tgz",
+ "integrity": "sha512-2XJVD55d1o5AZous5CCGKS74g/riOj9odEt2bQpCVZeblHyHdnMeFl4jl0XjU21stf4mbjUkew2eXQZt65g5CQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/spy": "4.1.11",
+ "estree-walker": "^3.0.3",
+ "magic-string": "^0.30.21"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "msw": "^2.4.9",
+ "vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "msw": {
+ "optional": true
+ },
+ "vite": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@vitest/pretty-format": {
+ "version": "4.1.11",
+ "resolved": "https://registry.npmmirror.com/@vitest/pretty-format/-/pretty-format-4.1.11.tgz",
+ "integrity": "sha512-yiZzPbGTS9Sr/JpFl8zHrcIkAofNbFV6k21vIgQN/cY/oxZeXhJv5sc/MBJ5jFKWmWs+oJHw0UXLZjmf931+Vw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tinyrainbow": "^3.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/runner": {
+ "version": "4.1.11",
+ "resolved": "https://registry.npmmirror.com/@vitest/runner/-/runner-4.1.11.tgz",
+ "integrity": "sha512-LztvUgdwMNJMIkj3hQnnxiC2Xy1zNxq928W/xhjCLaNCzqTZOudjwbQf6v9IntZGPw132i2Lq2rgTRZHD3JHNw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/utils": "4.1.11",
+ "pathe": "^2.0.3"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/snapshot": {
+ "version": "4.1.11",
+ "resolved": "https://registry.npmmirror.com/@vitest/snapshot/-/snapshot-4.1.11.tgz",
+ "integrity": "sha512-pN7ikn1ON7h8ee4gIAp4AzyK+zBtJPzVbqOgu5LCEh4VaJVbPQcgYQYJIMGQPXVeJJq1fnfazis7a5pFNPahog==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/pretty-format": "4.1.11",
+ "@vitest/utils": "4.1.11",
+ "magic-string": "^0.30.21",
+ "pathe": "^2.0.3"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/spy": {
+ "version": "4.1.11",
+ "resolved": "https://registry.npmmirror.com/@vitest/spy/-/spy-4.1.11.tgz",
+ "integrity": "sha512-apNa/prQy2qCeywhnixOHPRCgGNhvg7T4Dapfl1GahLp/R+uhBm5cPyFoNVyqsNd2h1nJxL6BqqdIjiABL60YA==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/utils": {
+ "version": "4.1.11",
+ "resolved": "https://registry.npmmirror.com/@vitest/utils/-/utils-4.1.11.tgz",
+ "integrity": "sha512-zTCVGpyFsGWBhllOyKlTw/vnr6D9qxsfSDyfbyZmTyjHw5N/VuvzHpHoQjm2ZJzn4RJgx5w4r7V0er69CmLgPQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/pretty-format": "4.1.11",
+ "convert-source-map": "^2.0.0",
+ "tinyrainbow": "^3.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ansi-styles": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-5.2.0.tgz",
+ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/aria-query": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmmirror.com/aria-query/-/aria-query-5.3.0.tgz",
+ "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "dequal": "^2.0.3"
+ }
+ },
+ "node_modules/assertion-error": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmmirror.com/assertion-error/-/assertion-error-2.0.1.tgz",
+ "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/bidi-js": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmmirror.com/bidi-js/-/bidi-js-1.0.3.tgz",
+ "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "require-from-string": "^2.0.2"
+ }
+ },
+ "node_modules/chai": {
+ "version": "6.2.2",
+ "resolved": "https://registry.npmmirror.com/chai/-/chai-6.2.2.tgz",
+ "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/clsx": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmmirror.com/clsx/-/clsx-2.1.1.tgz",
+ "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/convert-source-map": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmmirror.com/convert-source-map/-/convert-source-map-2.0.0.tgz",
+ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/cookie": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmmirror.com/cookie/-/cookie-1.1.1.tgz",
+ "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/css-tree": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmmirror.com/css-tree/-/css-tree-3.2.1.tgz",
+ "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "mdn-data": "2.27.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0"
+ }
+ },
+ "node_modules/css.escape": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmmirror.com/css.escape/-/css.escape-1.5.1.tgz",
+ "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/csstype": {
+ "version": "3.2.3",
+ "resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.2.3.tgz",
+ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
+ "devOptional": true,
+ "license": "MIT"
+ },
+ "node_modules/data-urls": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmmirror.com/data-urls/-/data-urls-7.0.0.tgz",
+ "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "whatwg-mimetype": "^5.0.0",
+ "whatwg-url": "^16.0.0"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
+ }
+ },
+ "node_modules/data-urls/node_modules/whatwg-url": {
+ "version": "16.0.1",
+ "resolved": "https://registry.npmmirror.com/whatwg-url/-/whatwg-url-16.0.1.tgz",
+ "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@exodus/bytes": "^1.11.0",
+ "tr46": "^6.0.0",
+ "webidl-conversions": "^8.0.1"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
+ }
+ },
+ "node_modules/decimal.js": {
+ "version": "10.6.0",
+ "resolved": "https://registry.npmmirror.com/decimal.js/-/decimal.js-10.6.0.tgz",
+ "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/dequal": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmmirror.com/dequal/-/dequal-2.0.3.tgz",
+ "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/detect-libc": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmmirror.com/detect-libc/-/detect-libc-2.1.2.tgz",
+ "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/dom-accessibility-api": {
+ "version": "0.5.16",
+ "resolved": "https://registry.npmmirror.com/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
+ "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/entities": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmmirror.com/entities/-/entities-8.0.0.tgz",
+ "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=20.19.0"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
+ "node_modules/es-module-lexer": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmmirror.com/es-module-lexer/-/es-module-lexer-2.3.2.tgz",
+ "integrity": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/estree-walker": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-3.0.3.tgz",
+ "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "^1.0.0"
+ }
+ },
+ "node_modules/expect-type": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmmirror.com/expect-type/-/expect-type-1.4.0.tgz",
+ "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
+ "node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmmirror.com/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/html-encoding-sniffer": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmmirror.com/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz",
+ "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@exodus/bytes": "^1.6.0"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
+ }
+ },
+ "node_modules/indent-string": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmmirror.com/indent-string/-/indent-string-4.0.0.tgz",
+ "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-potential-custom-element-name": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmmirror.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
+ "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmmirror.com/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/jsdom": {
+ "version": "30.0.1",
+ "resolved": "https://registry.npmmirror.com/jsdom/-/jsdom-30.0.1.tgz",
+ "integrity": "sha512-52v7mUVUfNQVYYqE1lcdaymWL0njO7lTLUog6ZvW2U5KsbiLk/GnZlVJ+qx0xfNJZ6Gn+KSpPNE52vurbxZwrA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@asamuzakjp/css-color": "^6.0.5",
+ "@asamuzakjp/dom-selector": "^8.3.0",
+ "@bramus/specificity": "^2.4.2",
+ "@csstools/css-syntax-patches-for-csstree": "^1.1.7",
+ "@exodus/bytes": "^1.15.1",
+ "css-tree": "^3.2.1",
+ "data-urls": "^7.0.0",
+ "decimal.js": "^10.6.0",
+ "html-encoding-sniffer": "^6.0.0",
+ "is-potential-custom-element-name": "^1.0.1",
+ "lru-cache": "^11.5.2",
+ "parse5": "^8.0.1",
+ "saxes": "^6.0.0",
+ "symbol-tree": "^3.2.4",
+ "tough-cookie": "^6.0.2",
+ "undici": "^8.9.0",
+ "w3c-xmlserializer": "^5.0.0",
+ "webidl-conversions": "^8.0.1",
+ "whatwg-mimetype": "^5.0.0",
+ "whatwg-url": "^17.1.0",
+ "xml-name-validator": "^5.0.0"
+ },
+ "engines": {
+ "node": "^22.22.2 || ^24.15.0 || >=26.0.0"
+ },
+ "peerDependencies": {
+ "canvas": "^3.2.3"
+ },
+ "peerDependenciesMeta": {
+ "canvas": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/lightningcss": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmmirror.com/lightningcss/-/lightningcss-1.33.0.tgz",
+ "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==",
+ "dev": true,
+ "license": "MPL-2.0",
+ "dependencies": {
+ "detect-libc": "^2.0.3"
+ },
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ },
+ "optionalDependencies": {
+ "lightningcss-android-arm64": "1.33.0",
+ "lightningcss-darwin-arm64": "1.33.0",
+ "lightningcss-darwin-x64": "1.33.0",
+ "lightningcss-freebsd-x64": "1.33.0",
+ "lightningcss-linux-arm-gnueabihf": "1.33.0",
+ "lightningcss-linux-arm64-gnu": "1.33.0",
+ "lightningcss-linux-arm64-musl": "1.33.0",
+ "lightningcss-linux-x64-gnu": "1.33.0",
+ "lightningcss-linux-x64-musl": "1.33.0",
+ "lightningcss-win32-arm64-msvc": "1.33.0",
+ "lightningcss-win32-x64-msvc": "1.33.0"
+ }
+ },
+ "node_modules/lightningcss-android-arm64": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmmirror.com/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz",
+ "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-darwin-arm64": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmmirror.com/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz",
+ "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-darwin-x64": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmmirror.com/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz",
+ "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-freebsd-x64": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmmirror.com/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz",
+ "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm-gnueabihf": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmmirror.com/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz",
+ "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm64-gnu": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmmirror.com/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz",
+ "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm64-musl": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmmirror.com/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz",
+ "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-x64-gnu": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmmirror.com/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz",
+ "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-x64-musl": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmmirror.com/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz",
+ "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-win32-arm64-msvc": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmmirror.com/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz",
+ "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-win32-x64-msvc": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmmirror.com/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz",
+ "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lru-cache": {
+ "version": "11.5.2",
+ "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-11.5.2.tgz",
+ "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "engines": {
+ "node": "20 || >=22"
+ }
+ },
+ "node_modules/lucide-react": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmmirror.com/lucide-react/-/lucide-react-1.32.0.tgz",
+ "integrity": "sha512-txX56hMFnRxPi1f9/nH69YN8uvAO6a7Y1KSWKjCDAtdD9+soEgmWuCt6iRm1pkxUZo2+YntSdsE1L6bIuKoY8Q==",
+ "license": "ISC",
+ "peerDependencies": {
+ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/lz-string": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmmirror.com/lz-string/-/lz-string-1.5.0.tgz",
+ "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "bin": {
+ "lz-string": "bin/bin.js"
+ }
+ },
+ "node_modules/magic-string": {
+ "version": "0.30.21",
+ "resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.21.tgz",
+ "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.5"
+ }
+ },
+ "node_modules/mdn-data": {
+ "version": "2.27.1",
+ "resolved": "https://registry.npmmirror.com/mdn-data/-/mdn-data-2.27.1.tgz",
+ "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==",
+ "dev": true,
+ "license": "CC0-1.0"
+ },
+ "node_modules/min-indent": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmmirror.com/min-indent/-/min-indent-1.0.1.tgz",
+ "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.18",
+ "resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.18.tgz",
+ "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/obug": {
+ "version": "2.1.4",
+ "resolved": "https://registry.npmmirror.com/obug/-/obug-2.1.4.tgz",
+ "integrity": "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==",
+ "dev": true,
+ "funding": [
+ "https://github.com/sponsors/sxzz",
+ "https://opencollective.com/debug"
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.20.0"
+ }
+ },
+ "node_modules/oxlint": {
+ "version": "1.79.0",
+ "resolved": "https://registry.npmmirror.com/oxlint/-/oxlint-1.79.0.tgz",
+ "integrity": "sha512-hVJ9hq9m2unPS+Of4eJJgCPdIeCC+3DHEUX3tkmrPJr3OK2hz7PhXwgC+ZP71ZcYu8cCDEtQrqLxWNvxBppBVg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "oxlint": "bin/oxlint"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/Boshen"
+ },
+ "optionalDependencies": {
+ "@oxlint/binding-android-arm-eabi": "1.79.0",
+ "@oxlint/binding-android-arm64": "1.79.0",
+ "@oxlint/binding-darwin-arm64": "1.79.0",
+ "@oxlint/binding-darwin-x64": "1.79.0",
+ "@oxlint/binding-freebsd-x64": "1.79.0",
+ "@oxlint/binding-linux-arm-gnueabihf": "1.79.0",
+ "@oxlint/binding-linux-arm-musleabihf": "1.79.0",
+ "@oxlint/binding-linux-arm64-gnu": "1.79.0",
+ "@oxlint/binding-linux-arm64-musl": "1.79.0",
+ "@oxlint/binding-linux-ppc64-gnu": "1.79.0",
+ "@oxlint/binding-linux-riscv64-gnu": "1.79.0",
+ "@oxlint/binding-linux-riscv64-musl": "1.79.0",
+ "@oxlint/binding-linux-s390x-gnu": "1.79.0",
+ "@oxlint/binding-linux-x64-gnu": "1.79.0",
+ "@oxlint/binding-linux-x64-musl": "1.79.0",
+ "@oxlint/binding-openharmony-arm64": "1.79.0",
+ "@oxlint/binding-win32-arm64-msvc": "1.79.0",
+ "@oxlint/binding-win32-ia32-msvc": "1.79.0",
+ "@oxlint/binding-win32-x64-msvc": "1.79.0"
+ },
+ "peerDependencies": {
+ "oxlint-tsgolint": ">=7.0.2001",
+ "vite-plus": "*"
+ },
+ "peerDependenciesMeta": {
+ "oxlint-tsgolint": {
+ "optional": true
+ },
+ "vite-plus": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/oxlint/node_modules/@oxlint/binding-android-arm-eabi": {
+ "dev": true,
+ "optional": true
+ },
+ "node_modules/oxlint/node_modules/@oxlint/binding-darwin-x64": {
+ "dev": true,
+ "optional": true
+ },
+ "node_modules/oxlint/node_modules/@oxlint/binding-linux-arm-gnueabihf": {
+ "dev": true,
+ "optional": true
+ },
+ "node_modules/oxlint/node_modules/@oxlint/binding-linux-arm-musleabihf": {
+ "dev": true,
+ "optional": true
+ },
+ "node_modules/oxlint/node_modules/@oxlint/binding-linux-arm64-musl": {
+ "dev": true,
+ "optional": true
+ },
+ "node_modules/oxlint/node_modules/@oxlint/binding-linux-ppc64-gnu": {
+ "dev": true,
+ "optional": true
+ },
+ "node_modules/oxlint/node_modules/@oxlint/binding-linux-riscv64-gnu": {
+ "dev": true,
+ "optional": true
+ },
+ "node_modules/oxlint/node_modules/@oxlint/binding-linux-riscv64-musl": {
+ "dev": true,
+ "optional": true
+ },
+ "node_modules/oxlint/node_modules/@oxlint/binding-linux-s390x-gnu": {
+ "dev": true,
+ "optional": true
+ },
+ "node_modules/oxlint/node_modules/@oxlint/binding-win32-ia32-msvc": {
+ "dev": true,
+ "optional": true
+ },
+ "node_modules/parse5": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmmirror.com/parse5/-/parse5-8.0.1.tgz",
+ "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "entities": "^8.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/inikulin/parse5?sponsor=1"
+ }
+ },
+ "node_modules/pathe": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmmirror.com/pathe/-/pathe-2.0.3.tgz",
+ "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/picomatch": {
+ "version": "4.0.5",
+ "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.5.tgz",
+ "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.5.26",
+ "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.26.tgz",
+ "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.17",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/pretty-format": {
+ "version": "27.5.1",
+ "resolved": "https://registry.npmmirror.com/pretty-format/-/pretty-format-27.5.1.tgz",
+ "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "ansi-regex": "^5.0.1",
+ "ansi-styles": "^5.0.0",
+ "react-is": "^17.0.1"
+ },
+ "engines": {
+ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
+ }
+ },
+ "node_modules/punycode": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmmirror.com/punycode/-/punycode-2.3.1.tgz",
+ "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/react": {
+ "version": "19.2.8",
+ "resolved": "https://registry.npmmirror.com/react/-/react-19.2.8.tgz",
+ "integrity": "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-dom": {
+ "version": "19.2.8",
+ "resolved": "https://registry.npmmirror.com/react-dom/-/react-dom-19.2.8.tgz",
+ "integrity": "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==",
+ "license": "MIT",
+ "dependencies": {
+ "scheduler": "^0.27.0"
+ },
+ "peerDependencies": {
+ "react": "^19.2.8"
+ }
+ },
+ "node_modules/react-is": {
+ "version": "17.0.2",
+ "resolved": "https://registry.npmmirror.com/react-is/-/react-is-17.0.2.tgz",
+ "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/react-router": {
+ "version": "7.18.2",
+ "resolved": "https://registry.npmmirror.com/react-router/-/react-router-7.18.2.tgz",
+ "integrity": "sha512-aUVMjFm3GAPTTZL7oYr5E7ETiqfQCHRLH+B+5afnICvf0r7kkK4eR6SMuwbSTJw/7t+12khT/Kahij49fqOCIg==",
+ "license": "MIT",
+ "dependencies": {
+ "cookie": "^1.0.1",
+ "set-cookie-parser": "^2.6.0"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=18",
+ "react-dom": ">=18"
+ },
+ "peerDependenciesMeta": {
+ "react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react-router-dom": {
+ "version": "7.18.2",
+ "resolved": "https://registry.npmmirror.com/react-router-dom/-/react-router-dom-7.18.2.tgz",
+ "integrity": "sha512-AIKJ/jgGlFb3EbfCXk5Gzshiwt+l3mqbCrNjmEWMMjqQxNJ3svBa6bgzFyCC2Sw3RA0VWF1kg3uQf2OFhxb8hw==",
+ "license": "MIT",
+ "dependencies": {
+ "react-router": "7.18.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=18",
+ "react-dom": ">=18"
+ }
+ },
+ "node_modules/redent": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmmirror.com/redent/-/redent-3.0.0.tgz",
+ "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "indent-string": "^4.0.0",
+ "strip-indent": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/require-from-string": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmmirror.com/require-from-string/-/require-from-string-2.0.2.tgz",
+ "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/rolldown": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmmirror.com/rolldown/-/rolldown-1.2.4.tgz",
+ "integrity": "sha512-rSr7irW0K7QRWzjdJXqZowkcRdDtjRduh43rBltnVKd0VFq839l1lJoDvGJb6gl7+4rTTCrPWu+YfujUL8Ug7w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@oxc-project/types": "=0.144.0",
+ "@rolldown/pluginutils": "^1.0.0"
+ },
+ "bin": {
+ "rolldown": "bin/cli.mjs"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "optionalDependencies": {
+ "@rolldown/binding-android-arm64": "1.2.4",
+ "@rolldown/binding-darwin-arm64": "1.2.4",
+ "@rolldown/binding-darwin-x64": "1.2.4",
+ "@rolldown/binding-freebsd-x64": "1.2.4",
+ "@rolldown/binding-linux-arm-gnueabihf": "1.2.4",
+ "@rolldown/binding-linux-arm64-gnu": "1.2.4",
+ "@rolldown/binding-linux-arm64-musl": "1.2.4",
+ "@rolldown/binding-linux-ppc64-gnu": "1.2.4",
+ "@rolldown/binding-linux-s390x-gnu": "1.2.4",
+ "@rolldown/binding-linux-x64-gnu": "1.2.4",
+ "@rolldown/binding-linux-x64-musl": "1.2.4",
+ "@rolldown/binding-openharmony-arm64": "1.2.4",
+ "@rolldown/binding-win32-arm64-msvc": "1.2.4",
+ "@rolldown/binding-win32-x64-msvc": "1.2.4"
+ }
+ },
+ "node_modules/saxes": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmmirror.com/saxes/-/saxes-6.0.0.tgz",
+ "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "xmlchars": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=v12.22.7"
+ }
+ },
+ "node_modules/scheduler": {
+ "version": "0.27.0",
+ "resolved": "https://registry.npmmirror.com/scheduler/-/scheduler-0.27.0.tgz",
+ "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
+ "license": "MIT"
+ },
+ "node_modules/set-cookie-parser": {
+ "version": "2.7.2",
+ "resolved": "https://registry.npmmirror.com/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz",
+ "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==",
+ "license": "MIT"
+ },
+ "node_modules/siginfo": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmmirror.com/siginfo/-/siginfo-2.0.0.tgz",
+ "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/stackback": {
+ "version": "0.0.2",
+ "resolved": "https://registry.npmmirror.com/stackback/-/stackback-0.0.2.tgz",
+ "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/std-env": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmmirror.com/std-env/-/std-env-4.2.0.tgz",
+ "integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/strip-indent": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmmirror.com/strip-indent/-/strip-indent-3.0.0.tgz",
+ "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "min-indent": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/symbol-tree": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmmirror.com/symbol-tree/-/symbol-tree-3.2.4.tgz",
+ "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/tinybench": {
+ "version": "2.9.0",
+ "resolved": "https://registry.npmmirror.com/tinybench/-/tinybench-2.9.0.tgz",
+ "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/tinyexec": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmmirror.com/tinyexec/-/tinyexec-1.3.0.tgz",
+ "integrity": "sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tinyglobby": {
+ "version": "0.2.17",
+ "resolved": "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.17.tgz",
+ "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.4"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/tinyrainbow": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmmirror.com/tinyrainbow/-/tinyrainbow-3.1.1.tgz",
+ "integrity": "sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/tldts": {
+ "version": "7.4.10",
+ "resolved": "https://registry.npmmirror.com/tldts/-/tldts-7.4.10.tgz",
+ "integrity": "sha512-GgouD1B+sWwvkaEq8vXC15DjQitxbvs12oIXELpconwm+Tg3zfcEv4jgzq3vtKverDXsg3VI8aRgNL2Nra0Iog==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tldts-core": "^7.4.10"
+ },
+ "bin": {
+ "tldts": "bin/cli.js"
+ }
+ },
+ "node_modules/tldts-core": {
+ "version": "7.4.10",
+ "resolved": "https://registry.npmmirror.com/tldts-core/-/tldts-core-7.4.10.tgz",
+ "integrity": "sha512-KnQjp53ZekKgm/r3l+u8kJGGzYgrWdP8+Mql7a4vijh2WE0IrZWspQj/TpTxDho/YxO+AnOZnIjQcCD+q6iJsw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/tough-cookie": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmmirror.com/tough-cookie/-/tough-cookie-6.0.2.tgz",
+ "integrity": "sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "tldts": "^7.0.5"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/tr46": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmmirror.com/tr46/-/tr46-6.0.0.tgz",
+ "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "punycode": "^2.3.1"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmmirror.com/typescript/-/typescript-6.0.3.tgz",
+ "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/undici": {
+ "version": "8.10.0",
+ "resolved": "https://registry.npmmirror.com/undici/-/undici-8.10.0.tgz",
+ "integrity": "sha512-HvltHd7avK13QIw/oLe4qoOLyoVSoafqJ2jYOrtMRBkbYT31eiBQ8O0ehRKZiEZCMEyLFQNIADpgCWC5fALvYQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=22.19.0"
+ }
+ },
+ "node_modules/undici-types": {
+ "version": "7.18.2",
+ "resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-7.18.2.tgz",
+ "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/vite": {
+ "version": "8.2.1",
+ "resolved": "https://registry.npmmirror.com/vite/-/vite-8.2.1.tgz",
+ "integrity": "sha512-EU/eS7BH3XROHh2YnBefjM6DBKA6ZeMZEYQbj7NLWg5wHYlhB8B/Mayd5XsgWq+NFYccDOTemRpdETWR6Ka/lw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "lightningcss": "^1.33.0",
+ "picomatch": "^4.0.5",
+ "postcss": "^8.5.25",
+ "rolldown": "~1.2.1",
+ "tinyglobby": "^0.2.17"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^20.19.0 || >=22.12.0",
+ "@vitejs/devtools": "^0.4.0",
+ "esbuild": "^0.27.0 || ^0.28.0",
+ "jiti": ">=1.21.0",
+ "less": "^4.0.0",
+ "sass": "^1.70.0",
+ "sass-embedded": "^1.70.0",
+ "stylus": ">=0.54.8",
+ "sugarss": "^5.0.0",
+ "terser": "^5.16.0",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "@vitejs/devtools": {
+ "optional": true
+ },
+ "esbuild": {
+ "optional": true
+ },
+ "jiti": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vitest": {
+ "version": "4.1.11",
+ "resolved": "https://registry.npmmirror.com/vitest/-/vitest-4.1.11.tgz",
+ "integrity": "sha512-fhACrNXUidIbGSBr5FlbuBkO7VWC1ZyLl0DO4CU2DrQoAPxX84Ysxs+HeGQpii5lZWV1Q4gBZTTu49mF+A6Edw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/expect": "4.1.11",
+ "@vitest/mocker": "4.1.11",
+ "@vitest/pretty-format": "4.1.11",
+ "@vitest/runner": "4.1.11",
+ "@vitest/snapshot": "4.1.11",
+ "@vitest/spy": "4.1.11",
+ "@vitest/utils": "4.1.11",
+ "es-module-lexer": "^2.0.0",
+ "expect-type": "^1.3.0",
+ "magic-string": "^0.30.21",
+ "obug": "^2.1.1",
+ "pathe": "^2.0.3",
+ "picomatch": "^4.0.3",
+ "std-env": "^4.0.0-rc.1",
+ "tinybench": "^2.9.0",
+ "tinyexec": "^1.0.2",
+ "tinyglobby": "^0.2.15",
+ "tinyrainbow": "^3.1.0",
+ "vite": "^6.0.0 || ^7.0.0 || ^8.0.0",
+ "why-is-node-running": "^2.3.0"
+ },
+ "bin": {
+ "vitest": "vitest.mjs"
+ },
+ "engines": {
+ "node": "^20.0.0 || ^22.0.0 || >=24.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "@edge-runtime/vm": "*",
+ "@opentelemetry/api": "^1.9.0",
+ "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0",
+ "@vitest/browser-playwright": "4.1.11",
+ "@vitest/browser-preview": "4.1.11",
+ "@vitest/browser-webdriverio": "4.1.11",
+ "@vitest/coverage-istanbul": "4.1.11",
+ "@vitest/coverage-v8": "4.1.11",
+ "@vitest/ui": "4.1.11",
+ "happy-dom": "*",
+ "jsdom": "*",
+ "vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@edge-runtime/vm": {
+ "optional": true
+ },
+ "@opentelemetry/api": {
+ "optional": true
+ },
+ "@types/node": {
+ "optional": true
+ },
+ "@vitest/browser-playwright": {
+ "optional": true
+ },
+ "@vitest/browser-preview": {
+ "optional": true
+ },
+ "@vitest/browser-webdriverio": {
+ "optional": true
+ },
+ "@vitest/coverage-istanbul": {
+ "optional": true
+ },
+ "@vitest/coverage-v8": {
+ "optional": true
+ },
+ "@vitest/ui": {
+ "optional": true
+ },
+ "happy-dom": {
+ "optional": true
+ },
+ "jsdom": {
+ "optional": true
+ },
+ "vite": {
+ "optional": false
+ }
+ }
+ },
+ "node_modules/w3c-xmlserializer": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmmirror.com/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",
+ "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "xml-name-validator": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/webidl-conversions": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmmirror.com/webidl-conversions/-/webidl-conversions-8.0.1.tgz",
+ "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/whatwg-mimetype": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmmirror.com/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz",
+ "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/whatwg-url": {
+ "version": "17.1.0",
+ "resolved": "https://registry.npmmirror.com/whatwg-url/-/whatwg-url-17.1.0.tgz",
+ "integrity": "sha512-3GeworPmc2ZfEEHP7lEbUfBX/L75wdEsi0rLNhXcXxnoN5jyq0SL5gCy06SGW2cyTIZdTvWIDQNQoza++vKeaw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@exodus/bytes": "^1.15.1",
+ "tr46": "^6.0.0",
+ "webidl-conversions": "^8.0.1"
+ },
+ "engines": {
+ "node": "^22.14.0 || >=24.0.0"
+ }
+ },
+ "node_modules/why-is-node-running": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmmirror.com/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
+ "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "siginfo": "^2.0.0",
+ "stackback": "0.0.2"
+ },
+ "bin": {
+ "why-is-node-running": "cli.js"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/xml-name-validator": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmmirror.com/xml-name-validator/-/xml-name-validator-5.0.0.tgz",
+ "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/xmlchars": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmmirror.com/xmlchars/-/xmlchars-2.2.0.tgz",
+ "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/zustand": {
+ "version": "5.0.15",
+ "resolved": "https://registry.npmmirror.com/zustand/-/zustand-5.0.15.tgz",
+ "integrity": "sha512-MpSEjRiBkA9crSYeOUH32rJC7SVqAbm0Fqcqge/bUi2PPoLcBWKOsG+C8mevmpr8TwXHBVkChbbJiyvkE+i/3A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.20.0"
+ },
+ "peerDependencies": {
+ "@types/react": ">=18.0.0",
+ "immer": ">=9.0.6",
+ "react": ">=18.0.0",
+ "use-sync-external-store": ">=1.2.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "immer": {
+ "optional": true
+ },
+ "react": {
+ "optional": true
+ },
+ "use-sync-external-store": {
+ "optional": true
+ }
+ }
+ }
+ }
+}
diff --git a/docs/design-reference/MiragenFlow-front/package.json b/docs/design-reference/MiragenFlow-front/package.json
new file mode 100644
index 0000000..8aba93f
--- /dev/null
+++ b/docs/design-reference/MiragenFlow-front/package.json
@@ -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"
+ }
+}
diff --git a/docs/design-reference/MiragenFlow-front/public/favicon.svg b/docs/design-reference/MiragenFlow-front/public/favicon.svg
new file mode 100644
index 0000000..6893eb1
--- /dev/null
+++ b/docs/design-reference/MiragenFlow-front/public/favicon.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/docs/design-reference/MiragenFlow-front/public/icons.svg b/docs/design-reference/MiragenFlow-front/public/icons.svg
new file mode 100644
index 0000000..e952219
--- /dev/null
+++ b/docs/design-reference/MiragenFlow-front/public/icons.svg
@@ -0,0 +1,24 @@
+
diff --git a/docs/design-reference/MiragenFlow-front/src/App.tsx b/docs/design-reference/MiragenFlow-front/src/App.tsx
new file mode 100644
index 0000000..7e72493
--- /dev/null
+++ b/docs/design-reference/MiragenFlow-front/src/App.tsx
@@ -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, { 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(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 =
+ else if (route === 'components') page =
+ else page = navigate('components')} />
+
+ return {page}
+}
+
+export default App
diff --git a/docs/design-reference/MiragenFlow-front/src/assets/holopix/alimama.woff2 b/docs/design-reference/MiragenFlow-front/src/assets/holopix/alimama.woff2
new file mode 100644
index 0000000..01be5bf
Binary files /dev/null and b/docs/design-reference/MiragenFlow-front/src/assets/holopix/alimama.woff2 differ
diff --git a/docs/design-reference/MiragenFlow-front/src/assets/holopix/avatar.png b/docs/design-reference/MiragenFlow-front/src/assets/holopix/avatar.png
new file mode 100644
index 0000000..58401a9
Binary files /dev/null and b/docs/design-reference/MiragenFlow-front/src/assets/holopix/avatar.png differ
diff --git a/docs/design-reference/MiragenFlow-front/src/assets/holopix/banner-main-2.webp b/docs/design-reference/MiragenFlow-front/src/assets/holopix/banner-main-2.webp
new file mode 100644
index 0000000..b5abc43
Binary files /dev/null and b/docs/design-reference/MiragenFlow-front/src/assets/holopix/banner-main-2.webp differ
diff --git a/docs/design-reference/MiragenFlow-front/src/assets/holopix/banner-main-3.webp b/docs/design-reference/MiragenFlow-front/src/assets/holopix/banner-main-3.webp
new file mode 100644
index 0000000..0218cc4
Binary files /dev/null and b/docs/design-reference/MiragenFlow-front/src/assets/holopix/banner-main-3.webp differ
diff --git a/docs/design-reference/MiragenFlow-front/src/assets/holopix/banner-main.webp b/docs/design-reference/MiragenFlow-front/src/assets/holopix/banner-main.webp
new file mode 100644
index 0000000..20ddd24
Binary files /dev/null and b/docs/design-reference/MiragenFlow-front/src/assets/holopix/banner-main.webp differ
diff --git a/docs/design-reference/MiragenFlow-front/src/assets/holopix/banner-side-1.webp b/docs/design-reference/MiragenFlow-front/src/assets/holopix/banner-side-1.webp
new file mode 100644
index 0000000..401105f
Binary files /dev/null and b/docs/design-reference/MiragenFlow-front/src/assets/holopix/banner-side-1.webp differ
diff --git a/docs/design-reference/MiragenFlow-front/src/assets/holopix/banner-side-2.webp b/docs/design-reference/MiragenFlow-front/src/assets/holopix/banner-side-2.webp
new file mode 100644
index 0000000..2869e42
Binary files /dev/null and b/docs/design-reference/MiragenFlow-front/src/assets/holopix/banner-side-2.webp differ
diff --git a/docs/design-reference/MiragenFlow-front/src/assets/holopix/board.webp b/docs/design-reference/MiragenFlow-front/src/assets/holopix/board.webp
new file mode 100644
index 0000000..504790d
Binary files /dev/null and b/docs/design-reference/MiragenFlow-front/src/assets/holopix/board.webp differ
diff --git a/docs/design-reference/MiragenFlow-front/src/assets/holopix/card01.jpg b/docs/design-reference/MiragenFlow-front/src/assets/holopix/card01.jpg
new file mode 100644
index 0000000..dfd0dcc
Binary files /dev/null and b/docs/design-reference/MiragenFlow-front/src/assets/holopix/card01.jpg differ
diff --git a/docs/design-reference/MiragenFlow-front/src/assets/holopix/card02.jpg b/docs/design-reference/MiragenFlow-front/src/assets/holopix/card02.jpg
new file mode 100644
index 0000000..564008a
Binary files /dev/null and b/docs/design-reference/MiragenFlow-front/src/assets/holopix/card02.jpg differ
diff --git a/docs/design-reference/MiragenFlow-front/src/assets/holopix/card03.jpg b/docs/design-reference/MiragenFlow-front/src/assets/holopix/card03.jpg
new file mode 100644
index 0000000..c94d556
Binary files /dev/null and b/docs/design-reference/MiragenFlow-front/src/assets/holopix/card03.jpg differ
diff --git a/docs/design-reference/MiragenFlow-front/src/assets/holopix/card04.jpg b/docs/design-reference/MiragenFlow-front/src/assets/holopix/card04.jpg
new file mode 100644
index 0000000..886dd02
Binary files /dev/null and b/docs/design-reference/MiragenFlow-front/src/assets/holopix/card04.jpg differ
diff --git a/docs/design-reference/MiragenFlow-front/src/assets/holopix/card05.jpg b/docs/design-reference/MiragenFlow-front/src/assets/holopix/card05.jpg
new file mode 100644
index 0000000..4e773ea
Binary files /dev/null and b/docs/design-reference/MiragenFlow-front/src/assets/holopix/card05.jpg differ
diff --git a/docs/design-reference/MiragenFlow-front/src/assets/holopix/card06.jpg b/docs/design-reference/MiragenFlow-front/src/assets/holopix/card06.jpg
new file mode 100644
index 0000000..972b9a4
Binary files /dev/null and b/docs/design-reference/MiragenFlow-front/src/assets/holopix/card06.jpg differ
diff --git a/docs/design-reference/MiragenFlow-front/src/assets/holopix/card07.webp b/docs/design-reference/MiragenFlow-front/src/assets/holopix/card07.webp
new file mode 100644
index 0000000..894e290
Binary files /dev/null and b/docs/design-reference/MiragenFlow-front/src/assets/holopix/card07.webp differ
diff --git a/docs/design-reference/MiragenFlow-front/src/assets/holopix/card08.jpg b/docs/design-reference/MiragenFlow-front/src/assets/holopix/card08.jpg
new file mode 100644
index 0000000..7423482
Binary files /dev/null and b/docs/design-reference/MiragenFlow-front/src/assets/holopix/card08.jpg differ
diff --git a/docs/design-reference/MiragenFlow-front/src/assets/holopix/card09.jpg b/docs/design-reference/MiragenFlow-front/src/assets/holopix/card09.jpg
new file mode 100644
index 0000000..b2f962b
Binary files /dev/null and b/docs/design-reference/MiragenFlow-front/src/assets/holopix/card09.jpg differ
diff --git a/docs/design-reference/MiragenFlow-front/src/assets/holopix/card10.jpg b/docs/design-reference/MiragenFlow-front/src/assets/holopix/card10.jpg
new file mode 100644
index 0000000..83119e4
Binary files /dev/null and b/docs/design-reference/MiragenFlow-front/src/assets/holopix/card10.jpg differ
diff --git a/docs/design-reference/MiragenFlow-front/src/assets/holopix/card11.webp b/docs/design-reference/MiragenFlow-front/src/assets/holopix/card11.webp
new file mode 100644
index 0000000..d60babc
Binary files /dev/null and b/docs/design-reference/MiragenFlow-front/src/assets/holopix/card11.webp differ
diff --git a/docs/design-reference/MiragenFlow-front/src/assets/holopix/card12.webp b/docs/design-reference/MiragenFlow-front/src/assets/holopix/card12.webp
new file mode 100644
index 0000000..69c48ea
Binary files /dev/null and b/docs/design-reference/MiragenFlow-front/src/assets/holopix/card12.webp differ
diff --git a/docs/design-reference/MiragenFlow-front/src/assets/holopix/card13.webp b/docs/design-reference/MiragenFlow-front/src/assets/holopix/card13.webp
new file mode 100644
index 0000000..c221f81
Binary files /dev/null and b/docs/design-reference/MiragenFlow-front/src/assets/holopix/card13.webp differ
diff --git a/docs/design-reference/MiragenFlow-front/src/assets/holopix/card14.jpg b/docs/design-reference/MiragenFlow-front/src/assets/holopix/card14.jpg
new file mode 100644
index 0000000..3fcf7e5
Binary files /dev/null and b/docs/design-reference/MiragenFlow-front/src/assets/holopix/card14.jpg differ
diff --git a/docs/design-reference/MiragenFlow-front/src/assets/holopix/card15.webp b/docs/design-reference/MiragenFlow-front/src/assets/holopix/card15.webp
new file mode 100644
index 0000000..3c67331
Binary files /dev/null and b/docs/design-reference/MiragenFlow-front/src/assets/holopix/card15.webp differ
diff --git a/docs/design-reference/MiragenFlow-front/src/assets/holopix/card16.webp b/docs/design-reference/MiragenFlow-front/src/assets/holopix/card16.webp
new file mode 100644
index 0000000..3fa900c
Binary files /dev/null and b/docs/design-reference/MiragenFlow-front/src/assets/holopix/card16.webp differ
diff --git a/docs/design-reference/MiragenFlow-front/src/assets/holopix/card17.webp b/docs/design-reference/MiragenFlow-front/src/assets/holopix/card17.webp
new file mode 100644
index 0000000..e875cac
Binary files /dev/null and b/docs/design-reference/MiragenFlow-front/src/assets/holopix/card17.webp differ
diff --git a/docs/design-reference/MiragenFlow-front/src/assets/holopix/card18.webp b/docs/design-reference/MiragenFlow-front/src/assets/holopix/card18.webp
new file mode 100644
index 0000000..d025504
Binary files /dev/null and b/docs/design-reference/MiragenFlow-front/src/assets/holopix/card18.webp differ
diff --git a/docs/design-reference/MiragenFlow-front/src/assets/holopix/card19.webp b/docs/design-reference/MiragenFlow-front/src/assets/holopix/card19.webp
new file mode 100644
index 0000000..02799d3
Binary files /dev/null and b/docs/design-reference/MiragenFlow-front/src/assets/holopix/card19.webp differ
diff --git a/docs/design-reference/MiragenFlow-front/src/assets/holopix/card20.jpg b/docs/design-reference/MiragenFlow-front/src/assets/holopix/card20.jpg
new file mode 100644
index 0000000..01b8897
Binary files /dev/null and b/docs/design-reference/MiragenFlow-front/src/assets/holopix/card20.jpg differ
diff --git a/docs/design-reference/MiragenFlow-front/src/assets/holopix/explore.webp b/docs/design-reference/MiragenFlow-front/src/assets/holopix/explore.webp
new file mode 100644
index 0000000..a29f379
Binary files /dev/null and b/docs/design-reference/MiragenFlow-front/src/assets/holopix/explore.webp differ
diff --git a/docs/design-reference/MiragenFlow-front/src/assets/holopix/login-reward-label.svg b/docs/design-reference/MiragenFlow-front/src/assets/holopix/login-reward-label.svg
new file mode 100644
index 0000000..3de2907
--- /dev/null
+++ b/docs/design-reference/MiragenFlow-front/src/assets/holopix/login-reward-label.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/docs/design-reference/MiragenFlow-front/src/assets/holopix/login-reward-panel.webp b/docs/design-reference/MiragenFlow-front/src/assets/holopix/login-reward-panel.webp
new file mode 100644
index 0000000..a9f8ae4
Binary files /dev/null and b/docs/design-reference/MiragenFlow-front/src/assets/holopix/login-reward-panel.webp differ
diff --git a/docs/design-reference/MiragenFlow-front/src/assets/holopix/login-reward.gif b/docs/design-reference/MiragenFlow-front/src/assets/holopix/login-reward.gif
new file mode 100644
index 0000000..fe2ec75
Binary files /dev/null and b/docs/design-reference/MiragenFlow-front/src/assets/holopix/login-reward.gif differ
diff --git a/docs/design-reference/MiragenFlow-front/src/assets/holopix/visitor-card-bg.png b/docs/design-reference/MiragenFlow-front/src/assets/holopix/visitor-card-bg.png
new file mode 100644
index 0000000..98fe18a
Binary files /dev/null and b/docs/design-reference/MiragenFlow-front/src/assets/holopix/visitor-card-bg.png differ
diff --git a/docs/design-reference/MiragenFlow-front/src/assets/holopix/visitor-panel.png b/docs/design-reference/MiragenFlow-front/src/assets/holopix/visitor-panel.png
new file mode 100644
index 0000000..abb6d99
Binary files /dev/null and b/docs/design-reference/MiragenFlow-front/src/assets/holopix/visitor-panel.png differ
diff --git a/docs/design-reference/MiragenFlow-front/src/components/content/ArtworkCard.module.css b/docs/design-reference/MiragenFlow-front/src/components/content/ArtworkCard.module.css
new file mode 100644
index 0000000..93c653c
--- /dev/null
+++ b/docs/design-reference/MiragenFlow-front/src/components/content/ArtworkCard.module.css
@@ -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; }
diff --git a/docs/design-reference/MiragenFlow-front/src/components/content/ArtworkCard.tsx b/docs/design-reference/MiragenFlow-front/src/components/content/ArtworkCard.tsx
new file mode 100644
index 0000000..dbec858
--- /dev/null
+++ b/docs/design-reference/MiragenFlow-front/src/components/content/ArtworkCard.tsx
@@ -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
+ 
{artwork.video &&
}
+ {artwork.category}
+ {artwork.author}{artwork.likes}{artwork.uses} } onClick={() => onUse?.(artwork)}>一键同款
+
+}
diff --git a/docs/design-reference/MiragenFlow-front/src/components/content/ArtworkGrid.module.css b/docs/design-reference/MiragenFlow-front/src/components/content/ArtworkGrid.module.css
new file mode 100644
index 0000000..cee533a
--- /dev/null
+++ b/docs/design-reference/MiragenFlow-front/src/components/content/ArtworkGrid.module.css
@@ -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; } }
diff --git a/docs/design-reference/MiragenFlow-front/src/components/content/ArtworkGrid.tsx b/docs/design-reference/MiragenFlow-front/src/components/content/ArtworkGrid.tsx
new file mode 100644
index 0000000..1b6ffd9
--- /dev/null
+++ b/docs/design-reference/MiragenFlow-front/src/components/content/ArtworkGrid.tsx
@@ -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 {columns.map((column, index) =>
{column.map(artwork =>
)}
)}
+}
diff --git a/docs/design-reference/MiragenFlow-front/src/components/content/HeroCarousel.module.css b/docs/design-reference/MiragenFlow-front/src/components/content/HeroCarousel.module.css
new file mode 100644
index 0000000..2b88d33
--- /dev/null
+++ b/docs/design-reference/MiragenFlow-front/src/components/content/HeroCarousel.module.css
@@ -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; } }
diff --git a/docs/design-reference/MiragenFlow-front/src/components/content/HeroCarousel.tsx b/docs/design-reference/MiragenFlow-front/src/components/content/HeroCarousel.tsx
new file mode 100644
index 0000000..6540e09
--- /dev/null
+++ b/docs/design-reference/MiragenFlow-front/src/components/content/HeroCarousel.tsx
@@ -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
+ {slides.map((src, itemIndex) =>

)}
} size="sm" onClick={() => change(index - 1)} />} size="sm" onClick={() => change(index + 1)} />
{slides.map((src, itemIndex) =>
+
+
+
+}
+
+function ToolEntry({ image, title, description }: { image: string; title: string; description: string }) {
+ return
+}
diff --git a/docs/design-reference/MiragenFlow-front/src/components/layout/AppShell.module.css b/docs/design-reference/MiragenFlow-front/src/components/layout/AppShell.module.css
new file mode 100644
index 0000000..eb7698b
--- /dev/null
+++ b/docs/design-reference/MiragenFlow-front/src/components/layout/AppShell.module.css
@@ -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; } }
diff --git a/docs/design-reference/MiragenFlow-front/src/components/layout/AppShell.tsx b/docs/design-reference/MiragenFlow-front/src/components/layout/AppShell.tsx
new file mode 100644
index 0000000..711982e
--- /dev/null
+++ b/docs/design-reference/MiragenFlow-front/src/components/layout/AppShell.tsx
@@ -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
+}
diff --git a/docs/design-reference/MiragenFlow-front/src/components/layout/Avatar.module.css b/docs/design-reference/MiragenFlow-front/src/components/layout/Avatar.module.css
new file mode 100644
index 0000000..bd67273
--- /dev/null
+++ b/docs/design-reference/MiragenFlow-front/src/components/layout/Avatar.module.css
@@ -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; }
diff --git a/docs/design-reference/MiragenFlow-front/src/components/layout/Avatar.tsx b/docs/design-reference/MiragenFlow-front/src/components/layout/Avatar.tsx
new file mode 100644
index 0000000..8f11696
--- /dev/null
+++ b/docs/design-reference/MiragenFlow-front/src/components/layout/Avatar.tsx
@@ -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 ?
: {fallback}
+}
diff --git a/docs/design-reference/MiragenFlow-front/src/components/primitives/Button.module.css b/docs/design-reference/MiragenFlow-front/src/components/primitives/Button.module.css
new file mode 100644
index 0000000..b7b5fb2
--- /dev/null
+++ b/docs/design-reference/MiragenFlow-front/src/components/primitives/Button.module.css
@@ -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); } }
diff --git a/docs/design-reference/MiragenFlow-front/src/components/primitives/Button.tsx b/docs/design-reference/MiragenFlow-front/src/components/primitives/Button.tsx
new file mode 100644
index 0000000..9992df1
--- /dev/null
+++ b/docs/design-reference/MiragenFlow-front/src/components/primitives/Button.tsx
@@ -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 {
+ 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 (
+
+ )
+}
diff --git a/docs/design-reference/MiragenFlow-front/src/components/primitives/Feedback.module.css b/docs/design-reference/MiragenFlow-front/src/components/primitives/Feedback.module.css
new file mode 100644
index 0000000..32cd4c5
--- /dev/null
+++ b/docs/design-reference/MiragenFlow-front/src/components/primitives/Feedback.module.css
@@ -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; }
diff --git a/docs/design-reference/MiragenFlow-front/src/components/primitives/Feedback.tsx b/docs/design-reference/MiragenFlow-front/src/components/primitives/Feedback.tsx
new file mode 100644
index 0000000..588c94c
--- /dev/null
+++ b/docs/design-reference/MiragenFlow-front/src/components/primitives/Feedback.tsx
@@ -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 {label}
+}
+
+export function Skeleton({ height = 80 }: { height?: number }) {
+ return
+}
+
+export function EmptyState({ title = '暂无内容', description = '新的内容会显示在这里', action }: { title?: string; description?: string; action?: ReactNode }) {
+ return {title}{description}{action}
+}
+
+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 event.stopPropagation()}>{title}{description && {description}}} onClick={onClose} />{children}
+}
+
+export function InlineAlert({ tone = 'info', children }: { tone?: 'info' | 'danger'; children: ReactNode }) {
+ return {tone === 'danger' ?
:
}{children}
+}
diff --git a/docs/design-reference/MiragenFlow-front/src/components/primitives/FormControls.module.css b/docs/design-reference/MiragenFlow-front/src/components/primitives/FormControls.module.css
new file mode 100644
index 0000000..f1bba57
--- /dev/null
+++ b/docs/design-reference/MiragenFlow-front/src/components/primitives/FormControls.module.css
@@ -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; }
+
diff --git a/docs/design-reference/MiragenFlow-front/src/components/primitives/FormControls.tsx b/docs/design-reference/MiragenFlow-front/src/components/primitives/FormControls.tsx
new file mode 100644
index 0000000..88c100e
--- /dev/null
+++ b/docs/design-reference/MiragenFlow-front/src/components/primitives/FormControls.tsx
@@ -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
+}
+
+interface TextInputProps extends InputHTMLAttributes {
+ invalid?: boolean
+ icon?: ReactNode
+}
+
+export function TextInput({ invalid, icon, className, ...props }: TextInputProps) {
+ return {icon}
+}
+
+export function SearchInput(props: InputHTMLAttributes) {
+ return } {...props} />
+}
+
+export function Select({ className, children, ...props }: SelectHTMLAttributes) {
+ return {children}
+}
+
+interface SwitchProps extends Omit, 'type'> { label?: string }
+export function Switch({ label, className, ...props }: SwitchProps) {
+ return
+}
diff --git a/docs/design-reference/MiragenFlow-front/src/components/primitives/IconButton.module.css b/docs/design-reference/MiragenFlow-front/src/components/primitives/IconButton.module.css
new file mode 100644
index 0000000..13bbdb6
--- /dev/null
+++ b/docs/design-reference/MiragenFlow-front/src/components/primitives/IconButton.module.css
@@ -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; }
diff --git a/docs/design-reference/MiragenFlow-front/src/components/primitives/IconButton.tsx b/docs/design-reference/MiragenFlow-front/src/components/primitives/IconButton.tsx
new file mode 100644
index 0000000..4f374b3
--- /dev/null
+++ b/docs/design-reference/MiragenFlow-front/src/components/primitives/IconButton.tsx
@@ -0,0 +1,25 @@
+import type { ButtonHTMLAttributes, ReactNode } from 'react'
+import clsx from 'clsx'
+import styles from './IconButton.module.css'
+
+interface IconButtonProps extends ButtonHTMLAttributes {
+ label: string
+ icon: ReactNode
+ selected?: boolean
+ size?: 'sm' | 'md'
+}
+
+export function IconButton({ label, icon, selected, size = 'md', className, ...props }: IconButtonProps) {
+ return (
+
+ )
+}
diff --git a/docs/design-reference/MiragenFlow-front/src/components/primitives/Selection.module.css b/docs/design-reference/MiragenFlow-front/src/components/primitives/Selection.module.css
new file mode 100644
index 0000000..cf7e37e
--- /dev/null
+++ b/docs/design-reference/MiragenFlow-front/src/components/primitives/Selection.module.css
@@ -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); }
diff --git a/docs/design-reference/MiragenFlow-front/src/components/primitives/Selection.tsx b/docs/design-reference/MiragenFlow-front/src/components/primitives/Selection.tsx
new file mode 100644
index 0000000..539595e
--- /dev/null
+++ b/docs/design-reference/MiragenFlow-front/src/components/primitives/Selection.tsx
@@ -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 {items.map(item => )}
+}
+
+interface ChipProps { selected?: boolean; children: ReactNode; onClick?: () => void }
+export function FilterChip({ selected, children, onClick }: ChipProps) {
+ return
+}
+
+interface BadgeProps { tone?: 'neutral' | 'info' | 'success' | 'warning' | 'danger'; children: ReactNode }
+export function Badge({ tone = 'neutral', children }: BadgeProps) {
+ return {children}
+}
diff --git a/docs/design-reference/MiragenFlow-front/src/components/primitives/primitives.test.tsx b/docs/design-reference/MiragenFlow-front/src/components/primitives/primitives.test.tsx
new file mode 100644
index 0000000..594d224
--- /dev/null
+++ b/docs/design-reference/MiragenFlow-front/src/components/primitives/primitives.test.tsx
@@ -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()
+ expect(screen.getByRole('button', { name: '生成' })).toBeDisabled()
+ })
+})
+
+describe('Tabs', () => {
+ it('reports the selected tab and emits changes', () => {
+ const onChange = vi.fn()
+ render()
+ expect(screen.getByRole('tab', { name: '作品灵感' })).toHaveAttribute('aria-selected', 'true')
+ fireEvent.click(screen.getByRole('tab', { name: '画布模板' }))
+ expect(onChange).toHaveBeenCalledWith('two')
+ })
+})
diff --git a/docs/design-reference/MiragenFlow-front/src/main.tsx b/docs/design-reference/MiragenFlow-front/src/main.tsx
new file mode 100644
index 0000000..b5d9579
--- /dev/null
+++ b/docs/design-reference/MiragenFlow-front/src/main.tsx
@@ -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(
+
+
+ ,
+)
diff --git a/docs/design-reference/MiragenFlow-front/src/pages/ComponentsPage.module.css b/docs/design-reference/MiragenFlow-front/src/pages/ComponentsPage.module.css
new file mode 100644
index 0000000..19c23d4
--- /dev/null
+++ b/docs/design-reference/MiragenFlow-front/src/pages/ComponentsPage.module.css
@@ -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; } }
diff --git a/docs/design-reference/MiragenFlow-front/src/pages/ComponentsPage.tsx b/docs/design-reference/MiragenFlow-front/src/pages/ComponentsPage.tsx
new file mode 100644
index 0000000..4b390a2
--- /dev/null
+++ b/docs/design-reference/MiragenFlow-front/src/pages/ComponentsPage.tsx
@@ -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
+
DESIGN SYSTEMMiragen Flow 组件中心
用户端组件、交互状态与视觉约束的集中验收页面。
}>新建组件
+
+ {tab === 'primitives' &&
+
}>开始生成}>保存}>下载}>删除
+
} />} selected />} disabled />草稿生成中已完成等待中失败
+
+
{['全部','游戏界面','角色','场景'].map(item => setChip(item)}>{item})} setEnabled(event.target.checked)} />
+
}
+ {tab === 'feedback' &&
+
模型参数已经自动保存。上传失败,请检查文件格式。正在生成预览
+
开始创作} />
+
}
+ {tab === 'business' &&
}
+
+
+}
+
+function Showcase({ title, description, children }: { title: string; description: string; children: React.ReactNode }) { return }
+function BusinessItem({ title, items }: { title: string; items: string }) { return {title}{items}
已规划 }
diff --git a/docs/design-reference/MiragenFlow-front/src/pages/HomePage.module.css b/docs/design-reference/MiragenFlow-front/src/pages/HomePage.module.css
new file mode 100644
index 0000000..323232c
--- /dev/null
+++ b/docs/design-reference/MiragenFlow-front/src/pages/HomePage.module.css
@@ -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; }
+
diff --git a/docs/design-reference/MiragenFlow-front/src/pages/HomePage.tsx b/docs/design-reference/MiragenFlow-front/src/pages/HomePage.tsx
new file mode 100644
index 0000000..fceb3d7
--- /dev/null
+++ b/docs/design-reference/MiragenFlow-front/src/pages/HomePage.tsx
@@ -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(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
+
+
+
{categories.map(item => setCategory(item)}>{item})}} />
} onClick={() => setSort(value => value === '最热' ? '最新' : '最热')}>{sort}
+ {tab === 'inspiration' ?
:
{tabs.find(item => item.id === tab)?.label}该模块的组件结构已纳入后续实现队列。}
+
+
+}
diff --git a/docs/design-reference/MiragenFlow-front/src/pages/PlaceholderPage.module.css b/docs/design-reference/MiragenFlow-front/src/pages/PlaceholderPage.module.css
new file mode 100644
index 0000000..2f1be29
--- /dev/null
+++ b/docs/design-reference/MiragenFlow-front/src/pages/PlaceholderPage.module.css
@@ -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); }
diff --git a/docs/design-reference/MiragenFlow-front/src/pages/PlaceholderPage.tsx b/docs/design-reference/MiragenFlow-front/src/pages/PlaceholderPage.tsx
new file mode 100644
index 0000000..80e34c9
--- /dev/null
+++ b/docs/design-reference/MiragenFlow-front/src/pages/PlaceholderPage.tsx
@@ -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 MODULE SCAFFOLD{title}
{description}
} onClick={onGoComponents}>查看组件规划
+}
diff --git a/docs/design-reference/MiragenFlow-front/src/styles/global.css b/docs/design-reference/MiragenFlow-front/src/styles/global.css
new file mode 100644
index 0000000..fc54841
--- /dev/null
+++ b/docs/design-reference/MiragenFlow-front/src/styles/global.css
@@ -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;
+}
diff --git a/docs/design-reference/MiragenFlow-front/src/styles/tokens.css b/docs/design-reference/MiragenFlow-front/src/styles/tokens.css
new file mode 100644
index 0000000..5246e7a
--- /dev/null
+++ b/docs/design-reference/MiragenFlow-front/src/styles/tokens.css
@@ -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;
+}
diff --git a/docs/design-reference/MiragenFlow-front/tsconfig.app.json b/docs/design-reference/MiragenFlow-front/tsconfig.app.json
new file mode 100644
index 0000000..6830b6f
--- /dev/null
+++ b/docs/design-reference/MiragenFlow-front/tsconfig.app.json
@@ -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"]
+}
diff --git a/docs/design-reference/MiragenFlow-front/tsconfig.json b/docs/design-reference/MiragenFlow-front/tsconfig.json
new file mode 100644
index 0000000..1ffef60
--- /dev/null
+++ b/docs/design-reference/MiragenFlow-front/tsconfig.json
@@ -0,0 +1,7 @@
+{
+ "files": [],
+ "references": [
+ { "path": "./tsconfig.app.json" },
+ { "path": "./tsconfig.node.json" }
+ ]
+}
diff --git a/docs/design-reference/MiragenFlow-front/tsconfig.node.json b/docs/design-reference/MiragenFlow-front/tsconfig.node.json
new file mode 100644
index 0000000..8455dcb
--- /dev/null
+++ b/docs/design-reference/MiragenFlow-front/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"]
+}
diff --git a/docs/design-reference/MiragenFlow-front/vite.config.ts b/docs/design-reference/MiragenFlow-front/vite.config.ts
new file mode 100644
index 0000000..292dcb0
--- /dev/null
+++ b/docs/design-reference/MiragenFlow-front/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',
+ },
+})
diff --git a/docs/design-reference/README.md b/docs/design-reference/README.md
new file mode 100644
index 0000000..e63a4a5
--- /dev/null
+++ b/docs/design-reference/README.md
@@ -0,0 +1,7 @@
+# 设计参考
+
+`MiragenFlow-front/` 是一期视觉参考快照,不是运行项目、工作区或生产静态资源。生产入口只有 `web/`、`admin/` 和 `server/`;该快照中的 hash 路由、占位页和静态演示数据不参与构建。
+
+其中的图片和字体仅用于设计核验,未作为生产业务数据或用户资产发布。
+
+已接入的生产视觉组件位于 `web/src/user-console/`;该目录只迁移壳层、作品灵感、模型广场和基础 primitives,业务数据继续由 `web/src/services` 与 Zustand stores 提供。
diff --git a/docs/docker-compose.yml b/docs/docker-compose.yml
index 7f67c5e..3715d87 100644
--- a/docs/docker-compose.yml
+++ b/docs/docker-compose.yml
@@ -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
diff --git a/docs/index.md b/docs/index.md
index 2c7739c..e115cfa 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -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.
diff --git a/docs/miragenflow-architecture-analysis.md b/docs/miragenflow-architecture-analysis.md
new file mode 100644
index 0000000..1b5498d
--- /dev/null
+++ b/docs/miragenflow-architecture-analysis.md
@@ -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 验证和生成。
diff --git a/docs/miragenflow-architecture.html b/docs/miragenflow-architecture.html
new file mode 100644
index 0000000..db1ef25
--- /dev/null
+++ b/docs/miragenflow-architecture.html
@@ -0,0 +1,14801 @@
+
+
+
+
+
+
+ MiragenFlow 全平台架构
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 引导视图
+
+
+
+
+
探索此系统
+
沿精选路径逐步查看,而不改变源图表。
+
+
+ 节点
+
+
+
+ 下一步
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 就绪
+ 章节 01 / 01
+
+
+ 引导章节
+
+
+
+
+
+
+
+
+ 图表指南
+ 探索此系统
+
+
+
+
正在检查已编译语义
+
+
+
+
+
+
+
+
+
+ E 导出T 主题S 风格0 重置+ 放大- 缩小Esc 关闭
+
+
+
+
+
+ 查找节点
+
+
+
+ ⌕
+
+ /
+
+
+
没有匹配的节点
+
+
+
+
+
+
语义护照
+
+
+
+
+
+
+
+
+
+
+
+
+
编写可达范围
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 路径探测
+ 选择起点节点
+
+
+
+
+
+
+
+
+ 在图表中选择两个语义节点
+
+
+
+
+
+
+
+
先选择来源,再选择目标;方向很重要。
+
+
+
+
+ 语义透镜
+ 比较系统角色
+
+
+
+
最多选择两种语义类型。选择一种可显示其真实流量;选择两种只比较直接编写的关系。
+
+
选择一种类型以检查其节点和相连关系。
+
+
+
+
+
+
+
+
+
+ 语义雷达
+ 正在构建总览
+
+
+
+
+
+
点击节点拖动平移
+
+
语义雷达需要更多地图可见空间。
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ - • 用户端和管理台共享 TypeScript 合约
+ - • 统一 HTTP 分发,写请求带 CSRF 与幂等键
+ - • 管理台额外经过角色 Scope 门禁
+
+
+
+
+
+
+ - • 创建任务先校验、计价并预留余额
+ - • Outbox → 队列 → Worker 租约保证可恢复
+ - • 渠道重试、断路器与消息/WebDAV 作业
+
+
+
+
+
+
+ - • 输出统一落到暂存对象并生成资产
+ - • 结算、释放和审计写入同一领域状态
+ - • 未知结果对账;WebSocket 游标回放
+
+
+
+
+
+
+
+
+
diff --git a/docs/miragenflow-architecture.json b/docs/miragenflow-architecture.json
new file mode 100644
index 0000000..a6d381d
--- /dev/null
+++ b/docs/miragenflow-architecture.json
@@ -0,0 +1,198 @@
+{
+ "schema_version": 1,
+ "diagram_type": "architecture",
+ "meta": {
+ "title": "MiragenFlow 全平台架构",
+ "locale": "zh-CN",
+ "output": "miragenflow-architecture.html",
+ "quality_profile": "showcase",
+ "views": [
+ {
+ "id": "request-and-auth",
+ "label": "请求与身份",
+ "focus": ["user-client", "admin-client", "contracts", "api", "security"],
+ "note": "从两个前端入口沿共享契约进入 API,并经过会话、权限与 CSRF 校验。"
+ },
+ {
+ "id": "task-execution",
+ "label": "任务执行",
+ "focus": ["api", "store", "queue", "worker", "provider", "objects"],
+ "note": "创建任务时预留余额,随后经 Outbox、队列、租约和供应商重试完成生成。"
+ },
+ {
+ "id": "events-and-persistence",
+ "label": "事件与持久化",
+ "focus": ["store", "events", "objects", "side-workers", "user-client", "admin-client"],
+ "note": "领域状态提交后发布可过滤事件,资产、消息和 WebDAV 由后台作业继续处理。"
+ }
+ ]
+ },
+ "components": [
+ {
+ "id": "user-client",
+ "type": "frontend",
+ "label": "用户端",
+ "sublabel": "React · web/",
+ "tag": "工作台 / 画布 / 资产",
+ "pos": [30, 150],
+ "size": [180, 92]
+ },
+ {
+ "id": "admin-client",
+ "type": "frontend",
+ "label": "管理台",
+ "sublabel": "React + Redux · admin/",
+ "tag": "权限化运营界面",
+ "pos": [30, 390],
+ "size": [180, 92]
+ },
+ {
+ "id": "contracts",
+ "type": "backend",
+ "label": "共享契约",
+ "sublabel": "packages/contracts",
+ "tag": "任务 / 会话 / 事件类型",
+ "pos": [240, 270],
+ "size": [180, 92]
+ },
+ {
+ "id": "api",
+ "type": "backend",
+ "label": "HTTP API",
+ "sublabel": "http.ts · /api/v1",
+ "tag": "用户与管理路由统一分发",
+ "pos": [465, 266],
+ "size": [190, 100]
+ },
+ {
+ "id": "security",
+ "type": "security",
+ "label": "身份与权限",
+ "sublabel": "JWT · CSRF · MFA",
+ "tag": "角色 Scope 与会话族",
+ "pos": [465, 90],
+ "size": [190, 92]
+ },
+ {
+ "id": "store",
+ "type": "database",
+ "label": "领域状态与账本",
+ "sublabel": "Store · 持久化",
+ "tag": "余额 / 任务 / 审计 / 配置",
+ "pos": [700, 266],
+ "size": [200, 100]
+ },
+ {
+ "id": "queue",
+ "type": "messagebus",
+ "label": "任务队列与 Outbox",
+ "sublabel": "Memory / Redis · Outbox",
+ "tag": "优先级 · 租约 · 死信",
+ "pos": [700, 460],
+ "size": [200, 92]
+ },
+ {
+ "id": "worker",
+ "type": "backend",
+ "label": "任务 Worker",
+ "sublabel": "租约 · 重试 · 对账",
+ "tag": "Attempt 与断路器",
+ "pos": [930, 456],
+ "size": [180, 100]
+ },
+ {
+ "id": "provider",
+ "type": "external",
+ "label": "外部供应商",
+ "sublabel": "OpenAI 兼容端点",
+ "tag": "安全出站 · 超时分类",
+ "pos": [1140, 456],
+ "size": [180, 100]
+ },
+ {
+ "id": "objects",
+ "type": "cloud",
+ "label": "暂存对象与资产",
+ "sublabel": "staging · objects",
+ "tag": "生成结果落地",
+ "pos": [1140, 270],
+ "size": [180, 92]
+ },
+ {
+ "id": "events",
+ "type": "messagebus",
+ "label": "任务事件流",
+ "sublabel": "WS · 游标回放",
+ "tag": "去重与公开投影",
+ "pos": [700, 90],
+ "size": [200, 92]
+ },
+ {
+ "id": "side-workers",
+ "type": "backend",
+ "label": "后台作业",
+ "sublabel": "消息 · WebDAV · GC",
+ "tag": "恢复与定时维护",
+ "pos": [930, 660],
+ "size": [210, 100]
+ }
+ ],
+ "boundaries": [
+ {
+ "kind": "region",
+ "label": "浏览器端",
+ "wraps": ["user-client", "admin-client"],
+ "pad": 28
+ },
+ {
+ "kind": "region",
+ "label": "MiragenFlow 服务端",
+ "wraps": ["api", "security", "store", "queue", "worker", "events", "side-workers"],
+ "pad": 34
+ },
+ {
+ "kind": "security-group",
+ "label": "受保护写路径",
+ "wraps": ["api", "security", "store", "queue"],
+ "pad": 18
+ },
+ {
+ "kind": "region",
+ "label": "外部边界",
+ "wraps": ["provider", "objects"],
+ "pad": 28
+ }
+ ],
+ "connections": [
+ { "id": "user-contracts", "from": "user-client", "to": "contracts", "label": "页面请求与类型", "variant": "emphasis", "labelAt": [210, 256] },
+ { "id": "admin-contracts", "from": "admin-client", "to": "contracts", "label": "管理请求与类型", "variant": "emphasis", "labelAt": [210, 376] },
+ { "id": "contracts-api", "from": "contracts", "to": "api", "label": "REST /api/v1", "labelAt": [442, 250] },
+ { "id": "api-security", "from": "api", "to": "security", "label": "令牌 / CSRF / Scope", "variant": "security", "fromSide": "top", "toSide": "bottom" },
+ { "id": "api-store", "from": "api", "to": "store", "label": "事务与领域命令", "labelAt": [678, 400] },
+ { "id": "api-queue", "from": "api", "to": "queue", "label": "任务 + Outbox", "variant": "dashed", "fromSide": "bottom", "toSide": "top" },
+ { "id": "queue-worker", "from": "queue", "to": "worker", "label": "租约派发", "variant": "emphasis", "labelAt": [915, 590] },
+ { "id": "worker-provider", "from": "worker", "to": "provider", "label": "HTTPS 生成 / 重试" , "variant": "emphasis", "labelAt": [1125, 430]},
+ { "id": "worker-objects", "from": "worker", "to": "objects", "label": "b64 / URL → 对象", "variant": "dashed", "fromSide": "top", "toSide": "bottom" },
+ { "id": "worker-store", "from": "worker", "to": "store", "label": "结算与账本", "fromSide": "top", "toSide": "bottom" },
+ { "id": "worker-events", "from": "worker", "to": "events", "label": "追加任务事件", "fromSide": "top", "toSide": "bottom" },
+ { "id": "api-events", "from": "api", "to": "events", "label": "WS / 游标回放", "variant": "dashed" },
+ { "id": "side-provider", "from": "side-workers", "to": "provider", "label": "WebDAV / 消息外部调用", "variant": "dashed", "fromSide": "right", "toSide": "bottom" }
+ ],
+ "cards": [
+ {
+ "dot": "cyan",
+ "title": "入口与契约",
+ "items": ["用户端和管理台共享 TypeScript 合约", "统一 HTTP 分发,写请求带 CSRF 与幂等键", "管理台额外经过角色 Scope 门禁"]
+ },
+ {
+ "dot": "emerald",
+ "title": "任务主链",
+ "items": ["创建任务先校验、计价并预留余额", "Outbox → 队列 → Worker 租约保证可恢复", "渠道重试、断路器与消息/WebDAV 作业"]
+ },
+ {
+ "dot": "violet",
+ "title": "结果与一致性",
+ "items": ["输出统一落到暂存对象并生成资产", "结算、释放和审计写入同一领域状态", "未知结果对账;WebSocket 游标回放"]
+ }
+ ]
+}
diff --git a/docs/miragenflow-architecture.visual-check.1440x900.dark.png b/docs/miragenflow-architecture.visual-check.1440x900.dark.png
new file mode 100644
index 0000000..5037aef
Binary files /dev/null and b/docs/miragenflow-architecture.visual-check.1440x900.dark.png differ
diff --git a/docs/miragenflow-architecture.visual-check.1440x900.light.png b/docs/miragenflow-architecture.visual-check.1440x900.light.png
new file mode 100644
index 0000000..9fba6bf
Binary files /dev/null and b/docs/miragenflow-architecture.visual-check.1440x900.light.png differ
diff --git a/docs/miragenflow-architecture.visual-check.2048x1320.dark.png b/docs/miragenflow-architecture.visual-check.2048x1320.dark.png
new file mode 100644
index 0000000..ddf981e
Binary files /dev/null and b/docs/miragenflow-architecture.visual-check.2048x1320.dark.png differ
diff --git a/docs/miragenflow-architecture.visual-check.2048x1320.light.png b/docs/miragenflow-architecture.visual-check.2048x1320.light.png
new file mode 100644
index 0000000..e9eab25
Binary files /dev/null and b/docs/miragenflow-architecture.visual-check.2048x1320.light.png differ
diff --git a/docs/miragenflow-architecture.visual-check.html b/docs/miragenflow-architecture.visual-check.html
new file mode 100644
index 0000000..a82d8f4
--- /dev/null
+++ b/docs/miragenflow-architecture.visual-check.html
@@ -0,0 +1,32 @@
+
+
+
+
+
+Archify visual-check · miragenflow-architecture.html
+
+
+
+
+
+
+
+ LIGHT · 1440×900 · containment pass
+
+
+
+ DARK · 1440×900 · containment pass
+
+
+
+ LIGHT · 2048×1320 · containment pass
+
+
+
+ DARK · 2048×1320 · containment pass
+
+
+
+
diff --git a/docs/miragenflow-architecture.visual-check.json b/docs/miragenflow-architecture.visual-check.json
new file mode 100644
index 0000000..7780dcb
--- /dev/null
+++ b/docs/miragenflow-architecture.visual-check.json
@@ -0,0 +1,482 @@
+{
+ "schemaVersion": 1,
+ "ok": true,
+ "command": "visual-check",
+ "status": "pass",
+ "visualReview": "pending",
+ "artifact": {
+ "path": "/Users/qiu/Desktop/MiragenFlow/docs/miragenflow-architecture.html",
+ "sha256": "796ff76c0ef4e9ed2be9ba742a8abd82491b21a9aff326e90fee2d24a4721cea",
+ "bytes": 720764
+ },
+ "state": {
+ "detail": "read",
+ "motion": "still"
+ },
+ "chrome": {
+ "status": "available",
+ "executable": "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
+ },
+ "containment": {
+ "status": "pass",
+ "viewports": [
+ {
+ "width": 1440,
+ "height": 900,
+ "theme": "light",
+ "innerWidth": 1440,
+ "innerHeight": 900,
+ "scrollWidth": 1440,
+ "scrollHeight": 900,
+ "overflowX": false,
+ "overflowY": false,
+ "ok": true,
+ "readerWidth": 960,
+ "diagramWidth": 930,
+ "viewBoxWidth": 1388,
+ "minimumProjectedNodeTextPx": 6.030259365994236,
+ "minimumProjectedNodeText": "React · web/",
+ "minimumProjectedNodeTextDetail": "context",
+ "minimumRequiredNodeTextPx": 6,
+ "readabilityOk": true,
+ "hasLegend": true,
+ "hasNavigationDock": true,
+ "legendDockIntersectionArea": 0,
+ "viewerChromeReserve": 0,
+ "viewerChromeActive": false,
+ "viewerChromeOk": true,
+ "resolvedTheme": "light"
+ },
+ {
+ "width": 1600,
+ "height": 1000,
+ "theme": "light",
+ "innerWidth": 1600,
+ "innerHeight": 1000,
+ "scrollWidth": 1600,
+ "scrollHeight": 1000,
+ "overflowX": false,
+ "overflowY": false,
+ "ok": true,
+ "readerWidth": 1056,
+ "diagramWidth": 1026,
+ "viewBoxWidth": 1388,
+ "minimumProjectedNodeTextPx": 6.652737752161383,
+ "minimumProjectedNodeText": "React · web/",
+ "minimumProjectedNodeTextDetail": "context",
+ "minimumRequiredNodeTextPx": 6,
+ "readabilityOk": true,
+ "hasLegend": true,
+ "hasNavigationDock": true,
+ "legendDockIntersectionArea": 0,
+ "viewerChromeReserve": 0,
+ "viewerChromeActive": false,
+ "viewerChromeOk": true,
+ "resolvedTheme": "light"
+ },
+ {
+ "width": 1920,
+ "height": 1080,
+ "theme": "light",
+ "innerWidth": 1920,
+ "innerHeight": 1080,
+ "scrollWidth": 1920,
+ "scrollHeight": 1080,
+ "overflowX": false,
+ "overflowY": false,
+ "ok": true,
+ "readerWidth": 1187,
+ "diagramWidth": 1157,
+ "viewBoxWidth": 1388,
+ "minimumProjectedNodeTextPx": 7.502161383285303,
+ "minimumProjectedNodeText": "React · web/",
+ "minimumProjectedNodeTextDetail": "context",
+ "minimumRequiredNodeTextPx": 6,
+ "readabilityOk": true,
+ "hasLegend": true,
+ "hasNavigationDock": true,
+ "legendDockIntersectionArea": 0,
+ "viewerChromeReserve": 0,
+ "viewerChromeActive": false,
+ "viewerChromeOk": true,
+ "resolvedTheme": "light"
+ },
+ {
+ "width": 2048,
+ "height": 1320,
+ "theme": "light",
+ "innerWidth": 2048,
+ "innerHeight": 1320,
+ "scrollWidth": 2048,
+ "scrollHeight": 1320,
+ "overflowX": false,
+ "overflowY": false,
+ "ok": true,
+ "readerWidth": 1515,
+ "diagramWidth": 1465,
+ "viewBoxWidth": 1388,
+ "minimumProjectedNodeTextPx": 9,
+ "minimumProjectedNodeText": "React · web/",
+ "minimumProjectedNodeTextDetail": "context",
+ "minimumRequiredNodeTextPx": 6,
+ "readabilityOk": true,
+ "hasLegend": true,
+ "hasNavigationDock": true,
+ "legendDockIntersectionArea": 0,
+ "viewerChromeReserve": 0,
+ "viewerChromeActive": false,
+ "viewerChromeOk": true,
+ "resolvedTheme": "light"
+ }
+ ]
+ },
+ "readability": {
+ "status": "pass",
+ "minimumProjectedNodeTextPx": 6,
+ "viewports": [
+ {
+ "width": 1440,
+ "height": 900,
+ "theme": "light",
+ "innerWidth": 1440,
+ "innerHeight": 900,
+ "scrollWidth": 1440,
+ "scrollHeight": 900,
+ "overflowX": false,
+ "overflowY": false,
+ "ok": true,
+ "readerWidth": 960,
+ "diagramWidth": 930,
+ "viewBoxWidth": 1388,
+ "minimumProjectedNodeTextPx": 6.030259365994236,
+ "minimumProjectedNodeText": "React · web/",
+ "minimumProjectedNodeTextDetail": "context",
+ "minimumRequiredNodeTextPx": 6,
+ "readabilityOk": true,
+ "hasLegend": true,
+ "hasNavigationDock": true,
+ "legendDockIntersectionArea": 0,
+ "viewerChromeReserve": 0,
+ "viewerChromeActive": false,
+ "viewerChromeOk": true,
+ "resolvedTheme": "light"
+ },
+ {
+ "width": 1600,
+ "height": 1000,
+ "theme": "light",
+ "innerWidth": 1600,
+ "innerHeight": 1000,
+ "scrollWidth": 1600,
+ "scrollHeight": 1000,
+ "overflowX": false,
+ "overflowY": false,
+ "ok": true,
+ "readerWidth": 1056,
+ "diagramWidth": 1026,
+ "viewBoxWidth": 1388,
+ "minimumProjectedNodeTextPx": 6.652737752161383,
+ "minimumProjectedNodeText": "React · web/",
+ "minimumProjectedNodeTextDetail": "context",
+ "minimumRequiredNodeTextPx": 6,
+ "readabilityOk": true,
+ "hasLegend": true,
+ "hasNavigationDock": true,
+ "legendDockIntersectionArea": 0,
+ "viewerChromeReserve": 0,
+ "viewerChromeActive": false,
+ "viewerChromeOk": true,
+ "resolvedTheme": "light"
+ },
+ {
+ "width": 1920,
+ "height": 1080,
+ "theme": "light",
+ "innerWidth": 1920,
+ "innerHeight": 1080,
+ "scrollWidth": 1920,
+ "scrollHeight": 1080,
+ "overflowX": false,
+ "overflowY": false,
+ "ok": true,
+ "readerWidth": 1187,
+ "diagramWidth": 1157,
+ "viewBoxWidth": 1388,
+ "minimumProjectedNodeTextPx": 7.502161383285303,
+ "minimumProjectedNodeText": "React · web/",
+ "minimumProjectedNodeTextDetail": "context",
+ "minimumRequiredNodeTextPx": 6,
+ "readabilityOk": true,
+ "hasLegend": true,
+ "hasNavigationDock": true,
+ "legendDockIntersectionArea": 0,
+ "viewerChromeReserve": 0,
+ "viewerChromeActive": false,
+ "viewerChromeOk": true,
+ "resolvedTheme": "light"
+ },
+ {
+ "width": 2048,
+ "height": 1320,
+ "theme": "light",
+ "innerWidth": 2048,
+ "innerHeight": 1320,
+ "scrollWidth": 2048,
+ "scrollHeight": 1320,
+ "overflowX": false,
+ "overflowY": false,
+ "ok": true,
+ "readerWidth": 1515,
+ "diagramWidth": 1465,
+ "viewBoxWidth": 1388,
+ "minimumProjectedNodeTextPx": 9,
+ "minimumProjectedNodeText": "React · web/",
+ "minimumProjectedNodeTextDetail": "context",
+ "minimumRequiredNodeTextPx": 6,
+ "readabilityOk": true,
+ "hasLegend": true,
+ "hasNavigationDock": true,
+ "legendDockIntersectionArea": 0,
+ "viewerChromeReserve": 0,
+ "viewerChromeActive": false,
+ "viewerChromeOk": true,
+ "resolvedTheme": "light"
+ }
+ ]
+ },
+ "viewerChrome": {
+ "status": "pass",
+ "viewports": [
+ {
+ "width": 1440,
+ "height": 900,
+ "theme": "light",
+ "innerWidth": 1440,
+ "innerHeight": 900,
+ "scrollWidth": 1440,
+ "scrollHeight": 900,
+ "overflowX": false,
+ "overflowY": false,
+ "ok": true,
+ "readerWidth": 960,
+ "diagramWidth": 930,
+ "viewBoxWidth": 1388,
+ "minimumProjectedNodeTextPx": 6.030259365994236,
+ "minimumProjectedNodeText": "React · web/",
+ "minimumProjectedNodeTextDetail": "context",
+ "minimumRequiredNodeTextPx": 6,
+ "readabilityOk": true,
+ "hasLegend": true,
+ "hasNavigationDock": true,
+ "legendDockIntersectionArea": 0,
+ "viewerChromeReserve": 0,
+ "viewerChromeActive": false,
+ "viewerChromeOk": true,
+ "resolvedTheme": "light"
+ },
+ {
+ "width": 1600,
+ "height": 1000,
+ "theme": "light",
+ "innerWidth": 1600,
+ "innerHeight": 1000,
+ "scrollWidth": 1600,
+ "scrollHeight": 1000,
+ "overflowX": false,
+ "overflowY": false,
+ "ok": true,
+ "readerWidth": 1056,
+ "diagramWidth": 1026,
+ "viewBoxWidth": 1388,
+ "minimumProjectedNodeTextPx": 6.652737752161383,
+ "minimumProjectedNodeText": "React · web/",
+ "minimumProjectedNodeTextDetail": "context",
+ "minimumRequiredNodeTextPx": 6,
+ "readabilityOk": true,
+ "hasLegend": true,
+ "hasNavigationDock": true,
+ "legendDockIntersectionArea": 0,
+ "viewerChromeReserve": 0,
+ "viewerChromeActive": false,
+ "viewerChromeOk": true,
+ "resolvedTheme": "light"
+ },
+ {
+ "width": 1920,
+ "height": 1080,
+ "theme": "light",
+ "innerWidth": 1920,
+ "innerHeight": 1080,
+ "scrollWidth": 1920,
+ "scrollHeight": 1080,
+ "overflowX": false,
+ "overflowY": false,
+ "ok": true,
+ "readerWidth": 1187,
+ "diagramWidth": 1157,
+ "viewBoxWidth": 1388,
+ "minimumProjectedNodeTextPx": 7.502161383285303,
+ "minimumProjectedNodeText": "React · web/",
+ "minimumProjectedNodeTextDetail": "context",
+ "minimumRequiredNodeTextPx": 6,
+ "readabilityOk": true,
+ "hasLegend": true,
+ "hasNavigationDock": true,
+ "legendDockIntersectionArea": 0,
+ "viewerChromeReserve": 0,
+ "viewerChromeActive": false,
+ "viewerChromeOk": true,
+ "resolvedTheme": "light"
+ },
+ {
+ "width": 2048,
+ "height": 1320,
+ "theme": "light",
+ "innerWidth": 2048,
+ "innerHeight": 1320,
+ "scrollWidth": 2048,
+ "scrollHeight": 1320,
+ "overflowX": false,
+ "overflowY": false,
+ "ok": true,
+ "readerWidth": 1515,
+ "diagramWidth": 1465,
+ "viewBoxWidth": 1388,
+ "minimumProjectedNodeTextPx": 9,
+ "minimumProjectedNodeText": "React · web/",
+ "minimumProjectedNodeTextDetail": "context",
+ "minimumRequiredNodeTextPx": 6,
+ "readabilityOk": true,
+ "hasLegend": true,
+ "hasNavigationDock": true,
+ "legendDockIntersectionArea": 0,
+ "viewerChromeReserve": 0,
+ "viewerChromeActive": false,
+ "viewerChromeOk": true,
+ "resolvedTheme": "light"
+ }
+ ]
+ },
+ "captures": {
+ "status": "pass",
+ "screenshots": [
+ {
+ "width": 1440,
+ "height": 900,
+ "theme": "light",
+ "innerWidth": 1440,
+ "innerHeight": 900,
+ "scrollWidth": 1440,
+ "scrollHeight": 900,
+ "overflowX": false,
+ "overflowY": false,
+ "ok": true,
+ "readerWidth": 960,
+ "diagramWidth": 930,
+ "viewBoxWidth": 1388,
+ "minimumProjectedNodeTextPx": 6.030259365994236,
+ "minimumProjectedNodeText": "React · web/",
+ "minimumProjectedNodeTextDetail": "context",
+ "minimumRequiredNodeTextPx": 6,
+ "readabilityOk": true,
+ "hasLegend": true,
+ "hasNavigationDock": true,
+ "legendDockIntersectionArea": 0,
+ "viewerChromeReserve": 0,
+ "viewerChromeActive": false,
+ "viewerChromeOk": true,
+ "resolvedTheme": "light",
+ "file": "miragenflow-architecture.visual-check.1440x900.light.png"
+ },
+ {
+ "width": 1440,
+ "height": 900,
+ "theme": "dark",
+ "innerWidth": 1440,
+ "innerHeight": 900,
+ "scrollWidth": 1440,
+ "scrollHeight": 900,
+ "overflowX": false,
+ "overflowY": false,
+ "ok": true,
+ "readerWidth": 960,
+ "diagramWidth": 930,
+ "viewBoxWidth": 1388,
+ "minimumProjectedNodeTextPx": 6.030259365994236,
+ "minimumProjectedNodeText": "React · web/",
+ "minimumProjectedNodeTextDetail": "context",
+ "minimumRequiredNodeTextPx": 6,
+ "readabilityOk": true,
+ "hasLegend": true,
+ "hasNavigationDock": true,
+ "legendDockIntersectionArea": 0,
+ "viewerChromeReserve": 0,
+ "viewerChromeActive": false,
+ "viewerChromeOk": true,
+ "resolvedTheme": "dark",
+ "file": "miragenflow-architecture.visual-check.1440x900.dark.png"
+ },
+ {
+ "width": 2048,
+ "height": 1320,
+ "theme": "light",
+ "innerWidth": 2048,
+ "innerHeight": 1320,
+ "scrollWidth": 2048,
+ "scrollHeight": 1320,
+ "overflowX": false,
+ "overflowY": false,
+ "ok": true,
+ "readerWidth": 1515,
+ "diagramWidth": 1465,
+ "viewBoxWidth": 1388,
+ "minimumProjectedNodeTextPx": 9,
+ "minimumProjectedNodeText": "React · web/",
+ "minimumProjectedNodeTextDetail": "context",
+ "minimumRequiredNodeTextPx": 6,
+ "readabilityOk": true,
+ "hasLegend": true,
+ "hasNavigationDock": true,
+ "legendDockIntersectionArea": 0,
+ "viewerChromeReserve": 0,
+ "viewerChromeActive": false,
+ "viewerChromeOk": true,
+ "resolvedTheme": "light",
+ "file": "miragenflow-architecture.visual-check.2048x1320.light.png"
+ },
+ {
+ "width": 2048,
+ "height": 1320,
+ "theme": "dark",
+ "innerWidth": 2048,
+ "innerHeight": 1320,
+ "scrollWidth": 2048,
+ "scrollHeight": 1320,
+ "overflowX": false,
+ "overflowY": false,
+ "ok": true,
+ "readerWidth": 1515,
+ "diagramWidth": 1465,
+ "viewBoxWidth": 1388,
+ "minimumProjectedNodeTextPx": 9,
+ "minimumProjectedNodeText": "React · web/",
+ "minimumProjectedNodeTextDetail": "context",
+ "minimumRequiredNodeTextPx": 6,
+ "readabilityOk": true,
+ "hasLegend": true,
+ "hasNavigationDock": true,
+ "legendDockIntersectionArea": 0,
+ "viewerChromeReserve": 0,
+ "viewerChromeActive": false,
+ "viewerChromeOk": true,
+ "resolvedTheme": "dark",
+ "file": "miragenflow-architecture.visual-check.2048x1320.dark.png"
+ }
+ ],
+ "contactSheet": "miragenflow-architecture.visual-check.html"
+ },
+ "sidecars": {
+ "receipt": "miragenflow-architecture.visual-check.json",
+ "contactSheet": "miragenflow-architecture.visual-check.html"
+ }
+}
diff --git a/docs/miragenflow-channel-model-logic.html b/docs/miragenflow-channel-model-logic.html
new file mode 100644
index 0000000..4d3577a
--- /dev/null
+++ b/docs/miragenflow-channel-model-logic.html
@@ -0,0 +1,14811 @@
+
+
+
+
+
+
+ 元境幻生渠道、模型与智能调度目标逻辑
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 引导视图
+
+
+
+
+
探索此系统
+
沿精选路径逐步查看,而不改变源图表。
+
+
+ 节点
+
+
+
+ 下一步
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 就绪
+ 章节 01 / 01
+
+
+ 引导章节
+
+
+
+
+
+
+
+
+ 图表指南
+ 探索此系统
+
+
+
+
正在检查已编译语义
+
+
+
+
+
+
+
+
+
+ E 导出T 主题S 风格0 重置+ 放大- 缩小Esc 关闭
+
+
+
+
+
+ 查找节点
+
+
+
+ ⌕
+
+ /
+
+
+
没有匹配的节点
+
+
+
+
+
+
语义护照
+
+
+
+
+
+
+
+
+
+
+
+
+
编写可达范围
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 路径探测
+ 选择起点节点
+
+
+
+
+
+
+
+
+ 在图表中选择两个语义节点
+
+
+
+
+
+
+
+
先选择来源,再选择目标;方向很重要。
+
+
+
+
+ 语义透镜
+ 比较系统角色
+
+
+
+
最多选择两种语义类型。选择一种可显示其真实流量;选择两种只比较直接编写的关系。
+
+
选择一种类型以检查其节点和相连关系。
+
+
+
+
+
+
+
+
+
+ 语义雷达
+ 正在构建总览
+
+
+
+
+
+
点击节点拖动平移
+
+
语义雷达需要更多地图可见空间。
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/miragenflow-channel-model-logic.visual-check.1440x900.dark.png b/docs/miragenflow-channel-model-logic.visual-check.1440x900.dark.png
new file mode 100644
index 0000000..5925b22
Binary files /dev/null and b/docs/miragenflow-channel-model-logic.visual-check.1440x900.dark.png differ
diff --git a/docs/miragenflow-channel-model-logic.visual-check.1440x900.light.png b/docs/miragenflow-channel-model-logic.visual-check.1440x900.light.png
new file mode 100644
index 0000000..0c3f3a8
Binary files /dev/null and b/docs/miragenflow-channel-model-logic.visual-check.1440x900.light.png differ
diff --git a/docs/miragenflow-channel-model-logic.visual-check.2048x1320.dark.png b/docs/miragenflow-channel-model-logic.visual-check.2048x1320.dark.png
new file mode 100644
index 0000000..56e29f0
Binary files /dev/null and b/docs/miragenflow-channel-model-logic.visual-check.2048x1320.dark.png differ
diff --git a/docs/miragenflow-channel-model-logic.visual-check.2048x1320.light.png b/docs/miragenflow-channel-model-logic.visual-check.2048x1320.light.png
new file mode 100644
index 0000000..caebb1f
Binary files /dev/null and b/docs/miragenflow-channel-model-logic.visual-check.2048x1320.light.png differ
diff --git a/docs/miragenflow-channel-model-logic.visual-check.html b/docs/miragenflow-channel-model-logic.visual-check.html
new file mode 100644
index 0000000..4920287
--- /dev/null
+++ b/docs/miragenflow-channel-model-logic.visual-check.html
@@ -0,0 +1,32 @@
+
+
+
+
+
+Archify visual-check · miragenflow-channel-model-logic.html
+
+
+
+
+
+
+
+ LIGHT · 1440×900 · containment pass
+
+
+
+ DARK · 1440×900 · containment pass
+
+
+
+ LIGHT · 2048×1320 · containment pass
+
+
+
+ DARK · 2048×1320 · containment pass
+
+
+
+
diff --git a/docs/miragenflow-channel-model-logic.visual-check.json b/docs/miragenflow-channel-model-logic.visual-check.json
new file mode 100644
index 0000000..3c4c730
--- /dev/null
+++ b/docs/miragenflow-channel-model-logic.visual-check.json
@@ -0,0 +1,482 @@
+{
+ "schemaVersion": 1,
+ "ok": true,
+ "command": "visual-check",
+ "status": "pass",
+ "visualReview": "pending",
+ "artifact": {
+ "path": "/Users/qiu/Desktop/MiragenFlow/docs/miragenflow-channel-model-logic.html",
+ "sha256": "cbd0e47c6a0daf48cbef27ad20c16c125853d95fa19b9f89650488d6396afb50",
+ "bytes": 721863
+ },
+ "state": {
+ "detail": "read",
+ "motion": "still"
+ },
+ "chrome": {
+ "status": "available",
+ "executable": "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
+ },
+ "containment": {
+ "status": "pass",
+ "viewports": [
+ {
+ "width": 1440,
+ "height": 900,
+ "theme": "light",
+ "innerWidth": 1440,
+ "innerHeight": 900,
+ "scrollWidth": 1440,
+ "scrollHeight": 900,
+ "overflowX": false,
+ "overflowY": false,
+ "ok": true,
+ "readerWidth": 965,
+ "diagramWidth": 935,
+ "viewBoxWidth": 930,
+ "minimumProjectedNodeTextPx": 6.1,
+ "minimumProjectedNodeText": "暂停结算与重试",
+ "minimumProjectedNodeTextDetail": "context",
+ "minimumRequiredNodeTextPx": 6,
+ "readabilityOk": true,
+ "hasLegend": true,
+ "hasNavigationDock": true,
+ "legendDockIntersectionArea": 0,
+ "viewerChromeReserve": 0,
+ "viewerChromeActive": false,
+ "viewerChromeOk": true,
+ "resolvedTheme": "light"
+ },
+ {
+ "width": 1600,
+ "height": 1000,
+ "theme": "light",
+ "innerWidth": 1600,
+ "innerHeight": 1000,
+ "scrollWidth": 1600,
+ "scrollHeight": 1000,
+ "overflowX": false,
+ "overflowY": false,
+ "ok": true,
+ "readerWidth": 1127,
+ "diagramWidth": 1097,
+ "viewBoxWidth": 930,
+ "minimumProjectedNodeTextPx": 6.1,
+ "minimumProjectedNodeText": "暂停结算与重试",
+ "minimumProjectedNodeTextDetail": "context",
+ "minimumRequiredNodeTextPx": 6,
+ "readabilityOk": true,
+ "hasLegend": true,
+ "hasNavigationDock": true,
+ "legendDockIntersectionArea": 0,
+ "viewerChromeReserve": 0,
+ "viewerChromeActive": false,
+ "viewerChromeOk": true,
+ "resolvedTheme": "light"
+ },
+ {
+ "width": 1920,
+ "height": 1080,
+ "theme": "light",
+ "innerWidth": 1920,
+ "innerHeight": 1080,
+ "scrollWidth": 1920,
+ "scrollHeight": 1080,
+ "overflowX": false,
+ "overflowY": false,
+ "ok": true,
+ "readerWidth": 1253,
+ "diagramWidth": 1223,
+ "viewBoxWidth": 930,
+ "minimumProjectedNodeTextPx": 6.1,
+ "minimumProjectedNodeText": "暂停结算与重试",
+ "minimumProjectedNodeTextDetail": "context",
+ "minimumRequiredNodeTextPx": 6,
+ "readabilityOk": true,
+ "hasLegend": true,
+ "hasNavigationDock": true,
+ "legendDockIntersectionArea": 0,
+ "viewerChromeReserve": 0,
+ "viewerChromeActive": false,
+ "viewerChromeOk": true,
+ "resolvedTheme": "light"
+ },
+ {
+ "width": 2048,
+ "height": 1320,
+ "theme": "light",
+ "innerWidth": 2048,
+ "innerHeight": 1320,
+ "scrollWidth": 2048,
+ "scrollHeight": 1320,
+ "overflowX": false,
+ "overflowY": false,
+ "ok": true,
+ "readerWidth": 1570,
+ "diagramWidth": 1520,
+ "viewBoxWidth": 930,
+ "minimumProjectedNodeTextPx": 6.1,
+ "minimumProjectedNodeText": "暂停结算与重试",
+ "minimumProjectedNodeTextDetail": "context",
+ "minimumRequiredNodeTextPx": 6,
+ "readabilityOk": true,
+ "hasLegend": true,
+ "hasNavigationDock": true,
+ "legendDockIntersectionArea": 0,
+ "viewerChromeReserve": 0,
+ "viewerChromeActive": false,
+ "viewerChromeOk": true,
+ "resolvedTheme": "light"
+ }
+ ]
+ },
+ "readability": {
+ "status": "pass",
+ "minimumProjectedNodeTextPx": 6,
+ "viewports": [
+ {
+ "width": 1440,
+ "height": 900,
+ "theme": "light",
+ "innerWidth": 1440,
+ "innerHeight": 900,
+ "scrollWidth": 1440,
+ "scrollHeight": 900,
+ "overflowX": false,
+ "overflowY": false,
+ "ok": true,
+ "readerWidth": 965,
+ "diagramWidth": 935,
+ "viewBoxWidth": 930,
+ "minimumProjectedNodeTextPx": 6.1,
+ "minimumProjectedNodeText": "暂停结算与重试",
+ "minimumProjectedNodeTextDetail": "context",
+ "minimumRequiredNodeTextPx": 6,
+ "readabilityOk": true,
+ "hasLegend": true,
+ "hasNavigationDock": true,
+ "legendDockIntersectionArea": 0,
+ "viewerChromeReserve": 0,
+ "viewerChromeActive": false,
+ "viewerChromeOk": true,
+ "resolvedTheme": "light"
+ },
+ {
+ "width": 1600,
+ "height": 1000,
+ "theme": "light",
+ "innerWidth": 1600,
+ "innerHeight": 1000,
+ "scrollWidth": 1600,
+ "scrollHeight": 1000,
+ "overflowX": false,
+ "overflowY": false,
+ "ok": true,
+ "readerWidth": 1127,
+ "diagramWidth": 1097,
+ "viewBoxWidth": 930,
+ "minimumProjectedNodeTextPx": 6.1,
+ "minimumProjectedNodeText": "暂停结算与重试",
+ "minimumProjectedNodeTextDetail": "context",
+ "minimumRequiredNodeTextPx": 6,
+ "readabilityOk": true,
+ "hasLegend": true,
+ "hasNavigationDock": true,
+ "legendDockIntersectionArea": 0,
+ "viewerChromeReserve": 0,
+ "viewerChromeActive": false,
+ "viewerChromeOk": true,
+ "resolvedTheme": "light"
+ },
+ {
+ "width": 1920,
+ "height": 1080,
+ "theme": "light",
+ "innerWidth": 1920,
+ "innerHeight": 1080,
+ "scrollWidth": 1920,
+ "scrollHeight": 1080,
+ "overflowX": false,
+ "overflowY": false,
+ "ok": true,
+ "readerWidth": 1253,
+ "diagramWidth": 1223,
+ "viewBoxWidth": 930,
+ "minimumProjectedNodeTextPx": 6.1,
+ "minimumProjectedNodeText": "暂停结算与重试",
+ "minimumProjectedNodeTextDetail": "context",
+ "minimumRequiredNodeTextPx": 6,
+ "readabilityOk": true,
+ "hasLegend": true,
+ "hasNavigationDock": true,
+ "legendDockIntersectionArea": 0,
+ "viewerChromeReserve": 0,
+ "viewerChromeActive": false,
+ "viewerChromeOk": true,
+ "resolvedTheme": "light"
+ },
+ {
+ "width": 2048,
+ "height": 1320,
+ "theme": "light",
+ "innerWidth": 2048,
+ "innerHeight": 1320,
+ "scrollWidth": 2048,
+ "scrollHeight": 1320,
+ "overflowX": false,
+ "overflowY": false,
+ "ok": true,
+ "readerWidth": 1570,
+ "diagramWidth": 1520,
+ "viewBoxWidth": 930,
+ "minimumProjectedNodeTextPx": 6.1,
+ "minimumProjectedNodeText": "暂停结算与重试",
+ "minimumProjectedNodeTextDetail": "context",
+ "minimumRequiredNodeTextPx": 6,
+ "readabilityOk": true,
+ "hasLegend": true,
+ "hasNavigationDock": true,
+ "legendDockIntersectionArea": 0,
+ "viewerChromeReserve": 0,
+ "viewerChromeActive": false,
+ "viewerChromeOk": true,
+ "resolvedTheme": "light"
+ }
+ ]
+ },
+ "viewerChrome": {
+ "status": "pass",
+ "viewports": [
+ {
+ "width": 1440,
+ "height": 900,
+ "theme": "light",
+ "innerWidth": 1440,
+ "innerHeight": 900,
+ "scrollWidth": 1440,
+ "scrollHeight": 900,
+ "overflowX": false,
+ "overflowY": false,
+ "ok": true,
+ "readerWidth": 965,
+ "diagramWidth": 935,
+ "viewBoxWidth": 930,
+ "minimumProjectedNodeTextPx": 6.1,
+ "minimumProjectedNodeText": "暂停结算与重试",
+ "minimumProjectedNodeTextDetail": "context",
+ "minimumRequiredNodeTextPx": 6,
+ "readabilityOk": true,
+ "hasLegend": true,
+ "hasNavigationDock": true,
+ "legendDockIntersectionArea": 0,
+ "viewerChromeReserve": 0,
+ "viewerChromeActive": false,
+ "viewerChromeOk": true,
+ "resolvedTheme": "light"
+ },
+ {
+ "width": 1600,
+ "height": 1000,
+ "theme": "light",
+ "innerWidth": 1600,
+ "innerHeight": 1000,
+ "scrollWidth": 1600,
+ "scrollHeight": 1000,
+ "overflowX": false,
+ "overflowY": false,
+ "ok": true,
+ "readerWidth": 1127,
+ "diagramWidth": 1097,
+ "viewBoxWidth": 930,
+ "minimumProjectedNodeTextPx": 6.1,
+ "minimumProjectedNodeText": "暂停结算与重试",
+ "minimumProjectedNodeTextDetail": "context",
+ "minimumRequiredNodeTextPx": 6,
+ "readabilityOk": true,
+ "hasLegend": true,
+ "hasNavigationDock": true,
+ "legendDockIntersectionArea": 0,
+ "viewerChromeReserve": 0,
+ "viewerChromeActive": false,
+ "viewerChromeOk": true,
+ "resolvedTheme": "light"
+ },
+ {
+ "width": 1920,
+ "height": 1080,
+ "theme": "light",
+ "innerWidth": 1920,
+ "innerHeight": 1080,
+ "scrollWidth": 1920,
+ "scrollHeight": 1080,
+ "overflowX": false,
+ "overflowY": false,
+ "ok": true,
+ "readerWidth": 1253,
+ "diagramWidth": 1223,
+ "viewBoxWidth": 930,
+ "minimumProjectedNodeTextPx": 6.1,
+ "minimumProjectedNodeText": "暂停结算与重试",
+ "minimumProjectedNodeTextDetail": "context",
+ "minimumRequiredNodeTextPx": 6,
+ "readabilityOk": true,
+ "hasLegend": true,
+ "hasNavigationDock": true,
+ "legendDockIntersectionArea": 0,
+ "viewerChromeReserve": 0,
+ "viewerChromeActive": false,
+ "viewerChromeOk": true,
+ "resolvedTheme": "light"
+ },
+ {
+ "width": 2048,
+ "height": 1320,
+ "theme": "light",
+ "innerWidth": 2048,
+ "innerHeight": 1320,
+ "scrollWidth": 2048,
+ "scrollHeight": 1320,
+ "overflowX": false,
+ "overflowY": false,
+ "ok": true,
+ "readerWidth": 1570,
+ "diagramWidth": 1520,
+ "viewBoxWidth": 930,
+ "minimumProjectedNodeTextPx": 6.1,
+ "minimumProjectedNodeText": "暂停结算与重试",
+ "minimumProjectedNodeTextDetail": "context",
+ "minimumRequiredNodeTextPx": 6,
+ "readabilityOk": true,
+ "hasLegend": true,
+ "hasNavigationDock": true,
+ "legendDockIntersectionArea": 0,
+ "viewerChromeReserve": 0,
+ "viewerChromeActive": false,
+ "viewerChromeOk": true,
+ "resolvedTheme": "light"
+ }
+ ]
+ },
+ "captures": {
+ "status": "pass",
+ "screenshots": [
+ {
+ "width": 1440,
+ "height": 900,
+ "theme": "light",
+ "innerWidth": 1440,
+ "innerHeight": 900,
+ "scrollWidth": 1440,
+ "scrollHeight": 900,
+ "overflowX": false,
+ "overflowY": false,
+ "ok": true,
+ "readerWidth": 965,
+ "diagramWidth": 935,
+ "viewBoxWidth": 930,
+ "minimumProjectedNodeTextPx": 6.1,
+ "minimumProjectedNodeText": "暂停结算与重试",
+ "minimumProjectedNodeTextDetail": "context",
+ "minimumRequiredNodeTextPx": 6,
+ "readabilityOk": true,
+ "hasLegend": true,
+ "hasNavigationDock": true,
+ "legendDockIntersectionArea": 0,
+ "viewerChromeReserve": 0,
+ "viewerChromeActive": false,
+ "viewerChromeOk": true,
+ "resolvedTheme": "light",
+ "file": "miragenflow-channel-model-logic.visual-check.1440x900.light.png"
+ },
+ {
+ "width": 1440,
+ "height": 900,
+ "theme": "dark",
+ "innerWidth": 1440,
+ "innerHeight": 900,
+ "scrollWidth": 1440,
+ "scrollHeight": 900,
+ "overflowX": false,
+ "overflowY": false,
+ "ok": true,
+ "readerWidth": 965,
+ "diagramWidth": 935,
+ "viewBoxWidth": 930,
+ "minimumProjectedNodeTextPx": 6.1,
+ "minimumProjectedNodeText": "暂停结算与重试",
+ "minimumProjectedNodeTextDetail": "context",
+ "minimumRequiredNodeTextPx": 6,
+ "readabilityOk": true,
+ "hasLegend": true,
+ "hasNavigationDock": true,
+ "legendDockIntersectionArea": 0,
+ "viewerChromeReserve": 0,
+ "viewerChromeActive": false,
+ "viewerChromeOk": true,
+ "resolvedTheme": "dark",
+ "file": "miragenflow-channel-model-logic.visual-check.1440x900.dark.png"
+ },
+ {
+ "width": 2048,
+ "height": 1320,
+ "theme": "light",
+ "innerWidth": 2048,
+ "innerHeight": 1320,
+ "scrollWidth": 2048,
+ "scrollHeight": 1320,
+ "overflowX": false,
+ "overflowY": false,
+ "ok": true,
+ "readerWidth": 1570,
+ "diagramWidth": 1520,
+ "viewBoxWidth": 930,
+ "minimumProjectedNodeTextPx": 6.1,
+ "minimumProjectedNodeText": "暂停结算与重试",
+ "minimumProjectedNodeTextDetail": "context",
+ "minimumRequiredNodeTextPx": 6,
+ "readabilityOk": true,
+ "hasLegend": true,
+ "hasNavigationDock": true,
+ "legendDockIntersectionArea": 0,
+ "viewerChromeReserve": 0,
+ "viewerChromeActive": false,
+ "viewerChromeOk": true,
+ "resolvedTheme": "light",
+ "file": "miragenflow-channel-model-logic.visual-check.2048x1320.light.png"
+ },
+ {
+ "width": 2048,
+ "height": 1320,
+ "theme": "dark",
+ "innerWidth": 2048,
+ "innerHeight": 1320,
+ "scrollWidth": 2048,
+ "scrollHeight": 1320,
+ "overflowX": false,
+ "overflowY": false,
+ "ok": true,
+ "readerWidth": 1570,
+ "diagramWidth": 1520,
+ "viewBoxWidth": 930,
+ "minimumProjectedNodeTextPx": 6.1,
+ "minimumProjectedNodeText": "暂停结算与重试",
+ "minimumProjectedNodeTextDetail": "context",
+ "minimumRequiredNodeTextPx": 6,
+ "readabilityOk": true,
+ "hasLegend": true,
+ "hasNavigationDock": true,
+ "legendDockIntersectionArea": 0,
+ "viewerChromeReserve": 0,
+ "viewerChromeActive": false,
+ "viewerChromeOk": true,
+ "resolvedTheme": "dark",
+ "file": "miragenflow-channel-model-logic.visual-check.2048x1320.dark.png"
+ }
+ ],
+ "contactSheet": "miragenflow-channel-model-logic.visual-check.html"
+ },
+ "sidecars": {
+ "receipt": "miragenflow-channel-model-logic.visual-check.json",
+ "contactSheet": "miragenflow-channel-model-logic.visual-check.html"
+ }
+}
diff --git a/docs/miragenflow-channel-model-logic.workflow.json b/docs/miragenflow-channel-model-logic.workflow.json
new file mode 100644
index 0000000..380ed96
--- /dev/null
+++ b/docs/miragenflow-channel-model-logic.workflow.json
@@ -0,0 +1,370 @@
+{
+ "schema_version": 1,
+ "diagram_type": "workflow",
+ "meta": {
+ "title": "元境幻生渠道、模型与智能调度目标逻辑",
+ "locale": "zh-CN",
+ "output": "docs/miragenflow-channel-model-logic.html",
+ "animation": "none",
+ "visual_preset": "blueprint",
+ "quality_profile": "showcase",
+ "viewBox": [930, 590],
+ "legend": {
+ "mode": "all",
+ "entries": {
+ "frontend": { "label": "管理页面" },
+ "backend": { "label": "目录与调度" },
+ "messagebus": { "label": "队列与切换" },
+ "security": { "label": "状态与治理" },
+ "external": { "label": "用户与供应商" }
+ }
+ },
+ "views": [
+ {
+ "id": "page-responsibilities",
+ "label": "页面职责",
+ "focus": ["channel-page", "discovery", "model-page", "group-page", "governance-page"],
+ "note": "四类管理职责分开,模型发现只提供候选,不再承载无关新增表单。"
+ },
+ {
+ "id": "model-routing",
+ "label": "模型与路由",
+ "focus": ["discovery", "model-page", "group-page", "route-policy", "scheduler"],
+ "note": "公开模型目录与渠道路由池分离,分组内保存明确的渠道优先级。"
+ },
+ {
+ "id": "smart-routing",
+ "label": "智能调度",
+ "focus": ["user-task", "scheduler", "queue-node", "worker", "provider"],
+ "note": "用户只选择公开模型,系统按资格、优先级、健康和负载选择实际渠道。"
+ },
+ {
+ "id": "failure-governance",
+ "label": "失败与对账",
+ "focus": ["worker", "fallback", "unknown-result", "reconcile", "governance-page"],
+ "note": "确认失败才切换,供应商结果未知时暂停结算并进入人工对账。"
+ }
+ ]
+ },
+ "lanes": [
+ { "id": "admin", "label": "管理页面" },
+ { "id": "catalog", "label": "目录与路由配置" },
+ { "id": "runtime", "label": "任务运行时" },
+ { "id": "exception", "label": "失败与治理", "variant": "exception" }
+ ],
+ "groups": [
+ { "id": "catalog-boundary", "label": "目录边界", "lane": "catalog", "fromCol": 0, "toCol": 2, "variant": "emphasis" },
+ { "id": "routing-boundary", "label": "路由输入", "lane": "catalog", "fromCol": 3, "toCol": 5, "variant": "dashed" },
+ { "id": "recovery-boundary", "label": "需要人工判断的异常", "lane": "exception", "fromCol": 2, "toCol": 5, "variant": "security" }
+ ],
+ "mainPath": ["user-task", "scheduler", "queue-node", "worker", "provider"],
+ "nodes": [
+ {
+ "id": "channel-page",
+ "lane": "admin",
+ "col": 0,
+ "type": "frontend",
+ "label": "渠道管理",
+ "sublabel": "地址 · 凭证 · 拉取",
+ "tag": "现有职责",
+ "width": 96
+ },
+ {
+ "id": "model-page",
+ "lane": "admin",
+ "col": 1,
+ "type": "frontend",
+ "label": "模型管理",
+ "sublabel": "目录 · 参数 · 上架",
+ "tag": "不手填渠道",
+ "width": 96
+ },
+ {
+ "id": "group-page",
+ "lane": "admin",
+ "col": 3,
+ "type": "frontend",
+ "label": "分组管理",
+ "sublabel": "模型 × 渠道 × 顺序",
+ "tag": "建立路由池",
+ "width": 96
+ },
+ {
+ "id": "governance-page",
+ "lane": "admin",
+ "col": 5,
+ "type": "frontend",
+ "label": "运行治理",
+ "sublabel": "健康 · 任务 · 审计",
+ "tag": "独立页面",
+ "width": 110
+ },
+ {
+ "id": "discovery",
+ "lane": "catalog",
+ "col": 0,
+ "type": "backend",
+ "label": "模型发现",
+ "sublabel": "上游列表 → 候选",
+ "tag": "不自动创建",
+ "width": 96
+ },
+ {
+ "id": "catalog-node",
+ "lane": "catalog",
+ "col": 2,
+ "type": "backend",
+ "label": "公开模型目录",
+ "sublabel": "公开 ID · 能力 · 分辨率",
+ "tag": "人工上架",
+ "width": 110
+ },
+ {
+ "id": "route-policy",
+ "lane": "catalog",
+ "col": 3,
+ "type": "backend",
+ "label": "路由策略",
+ "sublabel": "资格 · 优先级 · 版本",
+ "tag": "调度输入",
+ "width": 96
+ },
+ {
+ "id": "provider",
+ "lane": "catalog",
+ "col": 5,
+ "type": "external",
+ "label": "上游供应商",
+ "sublabel": "公开 ID ≠ 请求 ID",
+ "tag": "外部接口",
+ "width": 110
+ },
+ {
+ "id": "user-task",
+ "lane": "runtime",
+ "col": 0,
+ "type": "external",
+ "label": "用户任务",
+ "sublabel": "选择公开模型",
+ "tag": "不暴露供应商",
+ "width": 96
+ },
+ {
+ "id": "scheduler",
+ "lane": "runtime",
+ "col": 2,
+ "type": "backend",
+ "label": "智能调度",
+ "sublabel": "过滤 · 排序 · 选路",
+ "tag": "保存策略快照",
+ "width": 110
+ },
+ {
+ "id": "queue-node",
+ "lane": "runtime",
+ "col": 3,
+ "type": "messagebus",
+ "label": "任务队列",
+ "sublabel": "幂等入队 · 租约",
+ "tag": "异步",
+ "width": 86
+ },
+ {
+ "id": "worker",
+ "lane": "runtime",
+ "col": 5,
+ "type": "backend",
+ "label": "任务执行",
+ "sublabel": "尝试 · 结果 · 结算",
+ "tag": "记录每次调用",
+ "width": 106
+ },
+ {
+ "id": "fallback",
+ "lane": "exception",
+ "col": 2,
+ "type": "messagebus",
+ "label": "故障切换",
+ "sublabel": "下一候选渠道",
+ "width": 72
+ },
+ {
+ "id": "unknown-result",
+ "lane": "exception",
+ "col": 3,
+ "type": "security",
+ "label": "未知结果",
+ "sublabel": "暂停结算与重试",
+ "width": 60
+ },
+ {
+ "id": "reconcile",
+ "lane": "exception",
+ "col": 5,
+ "type": "frontend",
+ "label": "人工对账",
+ "sublabel": "确认成功或失败",
+ "tag": "恢复闭环",
+ "width": 100
+ }
+ ],
+ "edges": [
+ {
+ "id": "channel-discovery",
+ "from": "channel-page",
+ "to": "discovery",
+ "label": "主动拉取",
+ "variant": "emphasis",
+ "route": "drop",
+ "fromSide": "bottom",
+ "toSide": "top",
+ "labelAt": [150, 166]
+ },
+ {
+ "id": "discovery-model",
+ "from": "discovery",
+ "to": "model-page",
+ "variant": "emphasis"
+ },
+ {
+ "id": "catalog-model",
+ "from": "model-page",
+ "to": "catalog-node",
+ "label": "维护公开参数",
+ "variant": "emphasis",
+ "route": "drop",
+ "fromSide": "bottom",
+ "toSide": "top",
+ "labelAt": [320, 166]
+ },
+ {
+ "id": "model-group",
+ "from": "model-page",
+ "to": "group-page"
+ },
+ {
+ "id": "group-route",
+ "from": "group-page",
+ "to": "route-policy",
+ "variant": "emphasis",
+ "route": "drop",
+ "fromSide": "bottom",
+ "toSide": "top"
+ },
+ {
+ "id": "governance-route",
+ "from": "governance-page",
+ "to": "route-policy",
+ "label": "健康进入策略",
+ "variant": "dashed",
+ "route": "drop",
+ "fromSide": "bottom",
+ "toSide": "top",
+ "bias": 0.5,
+ "labelAt": [530, 166]
+ },
+ {
+ "id": "route-scheduler",
+ "from": "route-policy",
+ "to": "scheduler",
+ "label": "发布策略版本",
+ "variant": "emphasis",
+ "route": "drop",
+ "fromSide": "bottom",
+ "toSide": "top",
+ "labelAt": [365, 290]
+ },
+ {
+ "id": "submit-task",
+ "from": "user-task",
+ "to": "scheduler",
+ "label": "公开模型 + 参数",
+ "variant": "emphasis"
+ },
+ {
+ "id": "schedule-queue",
+ "from": "scheduler",
+ "to": "queue-node",
+ "variant": "emphasis",
+ "route": "straight",
+ "fromSide": "right",
+ "toSide": "left"
+ },
+ {
+ "id": "queue-worker",
+ "from": "queue-node",
+ "to": "worker",
+ "label": "租约执行",
+ "role": "async",
+ "labelAt": [525, 367]
+ },
+ {
+ "id": "call-provider",
+ "from": "worker",
+ "to": "provider",
+ "variant": "emphasis"
+ },
+ {
+ "id": "provider-result",
+ "from": "provider",
+ "to": "worker",
+ "variant": "dashed",
+ "role": "return",
+ "route": "outside-right",
+ "fromSide": "right",
+ "toSide": "right",
+ "channelX": 740
+ },
+ {
+ "id": "retryable-failure",
+ "from": "worker",
+ "to": "fallback",
+ "variant": "security",
+ "role": "error",
+ "route": "drop",
+ "fromSide": "bottom",
+ "toSide": "top"
+ },
+ {
+ "id": "next-candidate",
+ "from": "fallback",
+ "to": "scheduler",
+ "variant": "dashed",
+ "role": "return",
+ "route": "straight",
+ "fromSide": "top",
+ "toSide": "bottom"
+ },
+ {
+ "id": "unknown-provider-result",
+ "from": "provider",
+ "to": "unknown-result",
+ "variant": "security",
+ "role": "error",
+ "via": [[760, 243], [760, 540], [430, 540]],
+ "fromSide": "right",
+ "toSide": "bottom",
+ "labelAt": [760, 540]
+ },
+ {
+ "id": "manual-reconcile",
+ "from": "unknown-result",
+ "to": "reconcile",
+ "label": "人工确认",
+ "variant": "security",
+ "role": "error"
+ }
+ ],
+ "cards": [
+ {
+ "dot": "cyan",
+ "title": "职责分层:渠道 / 模型 / 分组 / 套餐",
+ "items": []
+ },
+ {
+ "dot": "emerald",
+ "title": "调度闭环:过滤 / 优先级 / 健康 / 对账",
+ "items": []
+ }
+ ]
+}
diff --git a/docs/miragenflow-channel-model-page-wireframes.md b/docs/miragenflow-channel-model-page-wireframes.md
new file mode 100644
index 0000000..62e3491
--- /dev/null
+++ b/docs/miragenflow-channel-model-page-wireframes.md
@@ -0,0 +1,355 @@
+# 渠道、模型与调度后台页面草图
+
+这份文档是目标页面规格,不代表页面已经实现。当前只用于确认信息架构和控件职责,画布代码不在范围内。
+
+## 页面总数
+
+最终建议 8 个后台页面。模型发现不是独立页面,而是渠道管理页面中的主动操作。
+
+```text
+后台
+|
+|-- 01 渠道管理 连接、凭证、主动发现、供应商模型映射
+|-- 02 模型管理 公开模型目录、分辨率、能力、上架状态
+|-- 03 分组管理 模型与渠道绑定、渠道优先级、路由策略
+|-- 04 渠道健康 探活、成功率、延迟、容量、熔断
+|-- 05 任务中心 任务状态、每次尝试、取消、重试、结果
+|-- 06 人工对账 处理供应商结果未知的任务
+|-- 07 价格与套餐 价格规则、金币套餐、数量和并发权益
+`-- 08 审计日志 配置变更、调度决策、任务和结算记录
+```
+
+其中 01-06、08 是运行闭环;07 是计费闭环。价格、数量上限、并发上限不再放在“新增模型产品”里。
+
+## 总体关系
+
+```text
+渠道管理
+ | 点击“拉取模型”
+ v
+模型发现候选 --勾选并加入--> 模型管理
+ |
+ | 管理公开参数并主动上架
+ v
+ 分组管理
+ |
+ | 选择渠道并排序
+ v
+ 路由策略
+ |
+用户选择公开模型 ------------+
+ v
+ 智能调度
+ |
+ 过滤 -> 排序 -> 选路
+ |
+ 任务队列 -> 任务执行
+ | |
+ 可确认失败 结果未知
+ | |
+ 故障切换 人工对账
+```
+
+核心数据边界:
+
+- 公开模型 ID 是平台对用户展示和匹配的 ID。
+- 供应商请求模型 ID 只在渠道请求时使用,不能和公开模型 ID 合并。
+- 分组 ID 由系统生成,管理员不手填“渠道组 ID”。
+- 模型发现只产生候选,不自动建模型、不自动上架、不自动进分组。
+
+## 01 渠道管理
+
+职责保持现有页面范围不变:管理连接、凭证、渠道内模型和供应商请求映射。
+
+### 列表
+
+```text
++--------------------------------------------------------------------------------+
+| 渠道管理 [搜索渠道] [状态 v] [+ 新增渠道] |
++--------------------------------------------------------------------------------+
+| 渠道名称 | 类型 | 地址 | 启用 | 模型数 | 健康 | 最近拉取 | 操作 |
+|----------|------|------|------|--------|------|----------|-------------------|
+| 渠道 A | 兼容 | ... | 开 | 12 | 正常 | 10:20 | [详情] [探活] [...] |
++--------------------------------------------------------------------------------+
+| 说明:模型发现必须由管理员点击“拉取模型”,保存渠道时不得隐式请求上游。 |
++--------------------------------------------------------------------------------+
+```
+
+### 新增/编辑渠道抽屉
+
+```text
++----------------------------------+
+| 新增渠道 X |
++----------------------------------+
+| 渠道名称 [________________] | 输入框,管理员自定义名称
+| 接口类型 [OpenAI 兼容 v] | 下拉框
+| 请求地址 [https://... ] | 完整 URL,例如 https://example.com/v1
+| API Key [****************] | 密码框;编辑留空表示沿用原凭证
+| 启用状态 [开关 ] | 是否允许调度使用
+| 请求超时 [______] 秒 | 数字框,正整数
+| |
+| [保存] [取消] |
++----------------------------------+
+```
+
+### 渠道详情中的模型发现
+
+```text
++--------------------------------------------------------------------------------+
+| 渠道 A / 模型 [拉取模型列表] |
++--------------------------------------------------------------------------------+
+| 上次请求:10:20 请求路径:/v1/models 状态:已发起 / 成功 |
+| |
+| [ ] 供应商模型 ID 状态 已加入渠道模型数 操作 |
+| [ ] vendor-model-a 新候选 - [加入渠道] |
+| [x] vendor-model-b 已加入 1 [查看映射] |
++--------------------------------------------------------------------------------+
+| 只有点击“拉取模型列表”才请求上游。勾选结果只加入该渠道,不自动创建公开模型。 |
++--------------------------------------------------------------------------------+
+```
+
+### 渠道模型映射
+
+```text
++----------------------------------------------------------------+
+| 渠道内模型 / 请求映射 |
++----------------------------------------------------------------+
+| 平台公开模型 ID [选择公开模型 v] |
+| 供应商请求模型 ID [________________] |
+| 说明:公开 ID 与请求 ID 相同也要明确保存;不同则只转换请求值。 |
+| [保存] [取消] |
++----------------------------------------------------------------+
+```
+
+控件规则:地址和 API Key 是输入框;类型和状态是下拉/开关;模型发现是独立按钮;候选模型是复选框;映射中的公开模型使用下拉框,供应商请求 ID 使用输入框。
+
+## 02 模型管理
+
+模型页面不再叫“新增模型产品”。它管理平台公开模型目录,不管理渠道连接、不管理价格套餐、不管理渠道组。
+
+### 列表
+
+```text
++--------------------------------------------------------------------------------+
+| 模型管理 [搜索公开 ID/名称] [状态 v] [能力 v] [从候选导入] |
++--------------------------------------------------------------------------------+
+| 公开模型 ID | 展示名称 | 能力 | 分辨率 | 支持渠道 | 状态 | 最近发现 | 操作 |
+|-------------|----------|------|--------|----------|------|----------|-----------|
+| image-pro | 图像专业 | 图像 | 1K/2K | 3 | 上架 | 10:20 | [编辑] [...]|
++--------------------------------------------------------------------------------+
+| 注意:列表展示“支持渠道数”,不在此处填写渠道组 ID、基础价格或并发上限。 |
++--------------------------------------------------------------------------------+
+```
+
+### 导入候选
+
+```text
++----------------------------------------------------------------+
+| 从发现候选导入 X |
++----------------------------------------------------------------+
+| 来源渠道 [全部渠道 v] |
+| [ ] vendor-model-a 供应商 A 最近发现:今天 10:20 |
+| [ ] vendor-model-b 供应商 B 最近发现:今天 10:18 |
+| |
+| [导入为草稿] [取消] |
++----------------------------------------------------------------+
+```
+
+导入后是草稿;管理员必须在编辑抽屉中确认参数,再点击“上架”。
+
+### 模型编辑抽屉
+
+```text
++--------------------------------------------+
+| 编辑公开模型 X |
++--------------------------------------------+
+| 公开模型 ID [image-pro ] | 只读,来自发现候选
+| 展示名称 [图像专业 ] | 输入框
+| 能力 [图像 v] [编辑 v] | 多选框/标签选择
+| |
+| 分辨率配置 [+ 添加] |
+| # 名称 支持比例 操作 |
+| 1 [1024x1024 ] [1:1 v] [删除] |
+| 2 [1536x1024 ] [1:1 v] [删除] |
+| 3 [2048x1152 ] [16:9 v] [删除] |
+| |
+| 上架状态 [草稿 v] | 草稿 / 上架 / 下架
+| |
+| [保存草稿] [上架] [取消] |
++--------------------------------------------+
+```
+
+分辨率的“支持比例”必须是主题化多选控件,选项例如 `1:1`、`16:9`、`9:16`、`4:3`、`3:4`;不要使用逗号分隔字符串输入。每一行使用稳定 key,新增、删除和编辑时不能丢失焦点。
+
+此页面明确不出现:基础价格、数量上限、并发上限、渠道组 ID、创建后自动上架、供应商请求模型 ID。
+
+## 03 分组管理
+
+分组是路由池。它决定“一个公开模型可以由哪些渠道承载,以及渠道按什么顺序尝试”。
+
+### 分组列表
+
+```text
++--------------------------------------------------------------------------------+
+| 分组管理 [搜索分组] [+ 新建分组] |
++--------------------------------------------------------------------------------+
+| 分组名称 | 状态 | 公开模型数 | 路由渠道数 | 策略 | 更新时间 | 操作 |
+|----------|------|------------|------------|------|----------|------------------|
+| 默认图像 | 启用 | 4 | 6 | 优先级| 10:25 | [编辑] [停用] [...]|
++--------------------------------------------------------------------------------+
+```
+
+### 新建/编辑分组
+
+```text
++----------------------------------------------------------------+
+| 编辑分组 X |
++----------------------------------------------------------------+
+| 分组名称 [默认图像 ] |
+| 分组 ID [group_xxx ] 只读,系统生成 |
+| 状态 [启用 v] |
+| 调度策略 [优先级优先 + 健康兜底 v] |
+| |
+| 支持的公开模型 [+ 添加模型] |
+| [image-pro v] |
+| |
+| 渠道候选顺序 |
+| 排序 渠道 请求模型 ID 健康 负载 启用 |
+| [=]1 渠道 A vendor-model-a 正常 32% [开] |
+| [=]2 渠道 B vendor-model-b 正常 18% [开] |
+| [=]3 渠道 C vendor-model-c 熔断 -- [关] |
+| |
+| [保存] [取消] |
++----------------------------------------------------------------+
+```
+
+“支持的公开模型”使用多选下拉;每个模型下面显示可用渠道。渠道使用复选框加入候选池,再用拖拽排序或优先级数字框排序;请求模型 ID、健康、负载是只读信息。一个渠道可以出现在多个分组中,并且在不同分组中有不同顺序。
+
+推荐策略含义:
+
+- 第一级:过滤模型上架、分组启用、能力/分辨率、渠道启用、映射存在、容量、熔断。
+- 第二级:按这里的渠道顺序选首选渠道。
+- 第三级:同级或首选不可用时参考健康状态、并发负载、失败率、延迟。
+
+## 04 渠道健康
+
+```text
++--------------------------------------------------------------------------------+
+| 渠道健康 [渠道 v] [健康状态 v] [刷新] |
++--------------------------------------------------------------------------------+
+| 渠道 | 状态 | 成功率 | P95 延迟 | 当前并发 | 容量 | 熔断 | 最近探活 | 操作 |
+|------|------|--------|----------|----------|------|------|----------|----------|
+| A | 正常 | 99.2% | 840ms | 2/10 | 80% | 关闭 | 10:26 | [探活] |
+| B | 降级 | 91.4% | 2.1s | 8/8 | 100% | 半开 | 10:25 | [详情] |
++--------------------------------------------------------------------------------+
+```
+
+探活按钮必须显示实际请求是否发起、请求路径,以及中文化的失败原因:地址解析、网络连接、HTTP 权限/状态、返回内容。健康配置可以放在详情抽屉:失败阈值、冷却时间、半开探测数量;保存不触发模型拉取。
+
+## 05 任务中心
+
+```text
++--------------------------------------------------------------------------------+
+| 任务中心 [任务 ID/模型] [分组 v] [状态 v] [时间范围] [查询] |
++--------------------------------------------------------------------------------+
+| 任务 ID | 公开模型 | 分组 | 状态 | 当前渠道 | 尝试次数 | 创建时间 | 操作 |
+|---------|----------|------|------|----------|----------|----------|------------|
+| task_001| image-pro| 默认 | 执行中| 渠道 A | 1/3 | 10:27 | [详情] |
+| task_002| image-pro| 默认 | 未知 | 渠道 B | 1/3 | 10:20 | [去对账] |
++--------------------------------------------------------------------------------+
+```
+
+详情页显示:用户提交的公开模型和参数、当时的分组、候选渠道顺序、策略版本快照、每次请求的开始/结束时间、供应商请求模型 ID、HTTP 结果、结算状态。
+
+按钮规则:排队中可取消;确认失败的任务可按失败预算重试;结果未知的任务只能进入人工对账,不能直接盲重试或重复扣费。
+
+## 06 人工对账
+
+```text
++--------------------------------------------------------------------------------+
+| 人工对账 [任务 ID] [供应商 v] [未知原因 v] [查询] |
++--------------------------------------------------------------------------------+
+| 任务 ID | 模型 | 渠道 | 发起时间 | 未知原因 | 金币状态 | 等待时长 | 操作 |
+|---------|------|------|----------|----------|----------|----------|--------------|
+| task_002| ... | B | 10:20 | 超时 | 已冻结 | 8m | [查看] |
++--------------------------------------------------------------------------------+
+```
+
+```text
++--------------------------------------------+
+| 对账 task_002 X |
++--------------------------------------------+
+| 本地状态 结果未知 |
+| 供应商请求 已发起,未收到最终响应 |
+| 请求路径/ID /v1/... / req_xxx |
+| 供应商证据 [查看原始响应/回调记录] |
+| 当前结算 待确认 |
+| 管理员备注 [________________________] |
+| |
+| [确认成功] [确认失败] [取消] |
++--------------------------------------------+
+```
+
+确认成功后只结算一次;确认失败后释放或退回对应费用,并根据策略决定是否允许下一候选渠道继续执行。所有确认动作写入审计日志。
+
+## 07 价格与套餐
+
+这个页面是计费配置,不参与模型发现和渠道选择。
+
+```text
++--------------------------------------------------------------------------------+
+| 价格与套餐 [价格规则] [套餐] [+ 新增] |
++--------------------------------------------------------------------------------+
+| 价格规则:公开模型 | 分辨率 | 计费单位 | 单价 | 生效状态 | 更新时间 | 操作 |
+| image-pro | 1K | 次 | 10 | 生效 | 10:00 | [编辑] |
++--------------------------------------------------------------------------------+
+| 套餐:名称 | 金币数 | 单任务数量上限 | 并发上限 | 状态 | 操作 |
+| 基础套餐 | 1000 | 20 | 2 | 生效 | [编辑] |
++--------------------------------------------------------------------------------+
+```
+
+价格规则表单:公开模型下拉、分辨率下拉、计费单位下拉、单价数字框、生效状态开关。
+
+套餐表单:套餐名称输入框、金币数量数字框、单任务数量上限数字框、并发上限数字框、状态开关。这里不出现渠道地址、API Key、渠道组 ID、供应商请求模型 ID。
+
+## 08 审计日志
+
+```text
++--------------------------------------------------------------------------------+
+| 审计日志 [对象类型 v] [操作者] [时间范围] [查询] |
++--------------------------------------------------------------------------------+
+| 时间 | 操作者 | 对象 | 动作 | 结果 | 摘要 | 详情 |
+|------|--------|------|------|------|------|---------------------------------------|
+| 10:27| admin | 分组 | 修改 | 成功 | 渠道 B 优先级 2 -> 1 | [查看] |
+| 10:20| system | 任务 | 调度 | 成功 | task_001 选择渠道 A | [查看] |
++--------------------------------------------------------------------------------+
+```
+
+详情必须字段化展示:对象、前值、后值、策略版本、候选渠道、最终渠道、任务尝试、结算动作。禁止直接把原始 JSON 当页面主体;原始内容只能作为可展开的辅助信息。
+
+## 明确删除的旧逻辑
+
+```text
+删除:新增模型产品
+删除:模型表单中的渠道组 ID
+删除:模型表单中的基础价格
+删除:模型表单中的数量上限
+删除:模型表单中的并发上限
+删除:保存模型时隐式拉取供应商
+删除:发现模型后自动创建映射、自动上架、自动加入分组
+保留:分辨率名称和支持比例配置
+```
+
+## 一期实现顺序
+
+```text
+01 渠道管理中的主动发现与映射
+ -> 02 模型目录与分辨率配置
+ -> 03 分组路由池与渠道优先级
+ -> 04 健康、容量、熔断数据
+ -> 05 任务中心与路由快照
+ -> 06 未知结果人工对账
+ -> 08 审计日志
+ -> 07 价格与套餐(有计费需求时启用)
+```
+
diff --git a/docs/miragenflow-front-migration.md b/docs/miragenflow-front-migration.md
new file mode 100644
index 0000000..aa26eb7
--- /dev/null
+++ b/docs/miragenflow-front-migration.md
@@ -0,0 +1,48 @@
+# MiragenFlow-front 用户控制台迁移说明
+
+## 结论
+
+`/Users/qiu/Desktop/Task/MiragenFlow-front/` 是一个独立的 Vite 视觉原型,不是完整用户端:路由使用 hash,作品数据是本地静态数组,登录、通知、搜索和生成确认没有接入 API。迁移采用“视觉组件直接复用、业务状态沿用 MiragenFlow、未实现能力保留占位”的方式,避免把演示数据当作真实业务。
+
+## 已迁移并接入
+
+| 原型模块 | 迁移位置 | 接入方式 |
+| ------------------------------------------------- | ---------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ |
+| 深色工作台壳层、侧栏、移动导航 | `web/src/user-console/components/layout/` | `UserLayout` 使用 React Router 路径,登录/注册和画布项目保留沉浸式页面;搜索框和模型广场入口已按产品边界移除 |
+| 设计 token、Alimama 字体、按钮/输入/筛选/反馈组件 | `web/src/user-console/styles/`、`components/primitives/` | token 作用域限定在 `.mf-console`,不覆盖现有 Tailwind/Ant Design 全局主题 |
+| Hero 轮播、侧栏活动卡、画布/创作入口 | `web/src/user-console/components/content/HeroCarousel.tsx` | 入口连接 `/canvas` 和 `/tools/image`,宣传图只承担视觉,不伪造服务状态 |
+| 作品灵感瀑布流、作品卡片、同款弹窗 | `web/src/user-console/components/content/`、`pages/HomePage.tsx` | 数据改为读取真实本地资产库图片;空库展示真实空状态 |
+| 组件中心 | `web/src/user-console/pages/ComponentsPage.tsx` | 作为内部视觉参考 `/components`,不进入用户侧导航、不参与真实业务流程 |
+| 登录、注册、验证、重置密码 | `web/src/pages/auth/` | 改用组件中心的 Field、Tabs、Button、Badge、Alert 视觉语言,认证策略仍读取真实 API |
+| AI 图片/音频创作 | `web/src/pages/tools/` | 改用组件中心的 Field、Select、Button、反馈和结果卡,任务提交/下载/资产保存仍走真实 API |
+| 原型静态资源 | `web/src/assets/reference/` | 复制到项目内并由迁移组件静态引用;不作为用户生成结果或 provider 数据 |
+
+## 保留现有真实能力
+
+- 图片/音频创作、任务队列、WebSocket、取消/重试和结果保存继续使用 `web/src/services/api/` 与现有 Zustand stores。
+- 无限画布、画布项目、资产库、账户、套餐、设置和认证流程继续使用现有实现;新壳层只负责视觉和导航,不替换这些业务状态。
+- `/api`、运行时配置、同源代理、主题持久化、国际化和 AuthGuard 不从原型项目复制。
+
+## 延后或不直接迁移
+
+| 原型内容 | 处理 |
+| --------------------------------------------------------- | --------------------------------------------------------------- |
+| hash 路由 `#/...`、独立 `App.tsx`、Vite/TS 配置 | 不迁移,避免破坏当前 BrowserRouter、`/api` 代理和 `/admin` base |
+| 原型静态 `artworks.ts` | 不作为真实列表使用;首页改读用户资产库 |
+| 模型广场、模型详情画廊、AI 学院、模型定制、社群、通知中心 | 不进入当前用户侧导航;待产品确认和真实能力接入后再配置 |
+| 视频、动画、真实 3D、远程 provider 配置 | 不新增业务入口,符合当前产品边界 |
+| 原型中的游客登录奖励和无动作按钮 | 改为真实登录、账户、任务和退出动作;未支持的操作不显示成功状态 |
+
+## 验收路径
+
+```text
+/ 新迁移首页:Hero + 作品灵感 + 资产空态
+/tools/image 真实图片创作
+/canvas 真实画布项目库
+/assets 真实资产库
+/login 组件中心风格的登录
+/register 组件中心风格的注册与验证
+/components 内部视觉组件验收页(不进入用户导航)
+```
+
+验收重点:壳层在窄窗口不横向溢出且不显示搜索框/模型广场;首页空资产不出现演示卡;AI 创作页可提交真实任务、下载并保存资产;登录/注册不被用户控制台壳层遮挡且关闭策略时不渲染 CAPTCHA/MFA/恢复码;画布项目仍使用现有全屏编辑器。
diff --git a/docs/miragenflow-long-task-goal-prompt.md b/docs/miragenflow-long-task-goal-prompt.md
new file mode 100644
index 0000000..eae6350
--- /dev/null
+++ b/docs/miragenflow-long-task-goal-prompt.md
@@ -0,0 +1,164 @@
+# MiragenFlow 本轮长期目标与目标提示词
+
+> 目标状态:进行中(active)
+>
+> 当前目标线程:`01a033b1-abd1-7ed0-b6a3-c347551b2f78`
+>
+> 本文是本轮需求的综合落库版本。管理台业务细节仍以 `goal.md` 为准,前后台组件迁移细节仍以 `docs/miragenflow-tdesign-unification-goal.md` 为准;本文负责把两条线合并为一个可执行、可验收的长期目标。
+
+> **规则优先级**:本文是本轮长期目标的唯一执行入口;其他旧计划中的示例若与本文冲突,以本文为准。特别是渠道当前只支持 OpenAI 图片端点,新增/编辑只能使用一个统一风格弹窗和三个可自由切换 Tab,不得恢复分步向导或“通用供应商”模板。
+
+## 1. 一句话目标
+
+把 MiragenFlow 做成一个真实可用的 AI 创作平台:用户端 `web/` 和管理端 `admin/` 使用同一锁定版本 `tdesign-react@1.15.1` 的基础组件与本地 Lineicons,但保留各自的视觉和业务边界;后台所有列表、图表、表单、弹窗和操作都能真实工作并持久化,页面只展示结构化 GUI 和中文业务语义,不出现非必要英文、原始 JSON、假按钮、白屏或无法关闭的弹窗。
+
+## 2. 用户可见要求
+
+### 2.1 组件、文字和图标
+
+- 两端的 Button、Input、Select、Switch、Tabs、Modal、Drawer、Dropdown、Form、Table、Pagination、Message、Loading 等基础控件均来自同一锁定版本的 TDesign 或无业务依赖的共享包装层。
+- 用户端保留现有自定义主题、品牌字体、首页、登录、创作、画布、资产、任务和移动端布局;不得把管理台模板直接复制到用户端。
+- 管理端保留现有 TDesign 主题、工作台密度、权限、认证、菜单和 ECharts 视觉;不得因共享组件而合并路由、Redux/Zustand、权限、认证、API 或业务状态。
+- 所有通用 UI 图标使用本地化 Lineicons 实际子集;图标按语义区分,默认 22-24px,点击区域至少 36-40px。禁止 CDN、WebFont、Lucide、全量图标目录和远程图标字体。
+- 可见文案、表头、状态、动作、错误提示和字段名使用中文。只保留必要专有名词或缩写,如 TDesign、Lineicons、ECharts、OpenAI、MFA、API、ID;不得展示英文内部字段名、`action` 原值或类似 “along modern product IDs” 的占位文案。
+- 下拉、选择、级联和搜索选择必须是项目主题化 GUI 组件,禁止原生 ``;需要支持键盘选择、选中态、禁用/空态、外部点击关闭和 ESC 关闭。
+
+### 2.2 管理台业务闭环
+
+- 管理台 22 个可见页面保持 `/admin/...` 路由、分组菜单和权限边界。二级菜单只有一项时直接显示为一级入口,多项时才展开;折叠侧栏时完全隐藏,不保留窄图标栏。
+- 所有新增、编辑、启停、调价、审批、调账和探活按钮必须有真实反馈、请求、错误处理和持久化结果。禁止按钮点击无反应,也禁止用展示用 mock 渠道冒充真实数据。
+- 新增用户只填写用户名、邮箱、手机号和初始密码;套餐、可用模型、欢迎消息、状态和额度在创建后通过独立配置完成。
+- 用户列表的表头为 `ID` 时,当前列表行只显示 `1、2、3...`,不加 `ID1`、`ID2`;模型、套餐、渠道等其他资源保留其真实业务标识。余额、可用金币、预留金币等结构化数据用字段、统计卡和图标展示,禁止输出对象或数组原始 JSON。
+- 用户邮箱继续脱敏;操作人按可读身份展示,普通用户使用用户名,管理员直接使用邮箱,不写 `admin:admin@example.com` 之类的复杂前缀。
+- 金币调整提供独立的增加/扣减图标入口。弹窗只要求正整数数量和原因,显示当前可用余额与预计余额;本地超级管理员可按现有规则自动审批,失败时保留申请并可在审批中心继续处理。
+
+### 2.3 OpenAI 渠道配置
+
+- 当前供应商方式只实现 OpenAI 图片端点,不新增没有真实逻辑的“通用”供应商模板。
+- 新增和编辑使用同一风格的一个弹窗,包含可自由切换的三个 Tab:`基础信息`、`模型列表`、`模型映射`。不使用上一步/下一步,底部始终是 `保存` 和 `取消`。
+- 基础信息至少包含图标/名称、请求地址和 API Key。地址给出 `https://example.com`、`https://example.com/v1` 等完整示例;API Key 去除首尾空白后只校验非空,不限制固定长度。
+- 切换 Tab 不得丢失草稿;编辑必须回填已保存数据;取消、右上角、遮罩和 ESC 都能关闭;异步保存成功自动关闭,失败保留表单。
+- `模型列表` 是独立列表。只有用户点击“拉取模型”按钮时才向 `{基础地址}/v1/models` 发起 GET;显示独立加载、实际路径、请求是否已发起、HTTP/网络/DNS/返回内容失败原因和模型数量。拉取结果只能作为候选项,由用户勾选后批量加入渠道模型列表,不能自动创建映射。
+- `模型映射` 是可选配置。每行明确区分平台显示模型 ID 与供应商请求模型 ID;显示 ID 用于公开产品匹配,请求 ID 只用于上游请求。动态行必须使用稳定 React key,允许连续输入而不丢焦点。
+- 开发环境允许用户明确测试的本机/私网地址,生产环境继续执行 SSRF、DNS 固定、协议和重定向保护;错误提示必须说明真正失败阶段,不能只写“探测未通过”。
+
+### 2.4 图表、日志和响应式
+
+- 系统概览、渠道健康、任务中心、实时指标和审计日志大板全部使用 ECharts;图表要有真实数据、加载、空态、错误态和深浅主题适配。
+- 审计拆成“日志大板”和“详细日志”两个页面。大板展示正常 HTTP 200、4xx/5xx、业务错误等分类的数量、占比和趋势;详细日志按时间倒序显示时间、分类、中文行为、操作人、对象、标识、请求和来源。没有实际可执行动作时删除“操作”列,不保留空列。
+- `action`、状态码、来源和对象要通过中文映射展示;日志不能原样暴露内部字段、密钥、哈希、Cookie、完整提示词或 JSON。
+- 用户端画布、资产和 AI 创作页面不能出现黑色硬编码容器、下半屏塌陷、更新深度错误或白屏;画布根节点使用动态视口高度,图片尊重原始比例,标题、品牌和副标题左对齐。
+- 手机端保持列表语义,使用紧凑表格和横向滚动,不把所有内容改成大卡片;页面和容器左右间距适当缩小。浅色/深色、桌面/390px 都不得溢出、遮挡或失去可操作性。游客不显示个人主页和退出登录入口。
+
+## 3. 服务端和边界要求
+
+- `web/`、`admin/`、`server/`、`packages/contracts/` 保持边界隔离;共享层只能放纯 UI、语义主题令牌、无状态图标和可访问性行为,不能依赖管理台 Redux、路由、权限、请求服务或业务字段。
+- 管理台写操作必须经过真实 API、权限、版本校验、事务和幂等边界;余额、账本、充值、审批、任务结算/释放、支付和审计保持一致。
+- 前台、公开 API、日志和导出不得暴露供应商名称、基础地址(Base URL)、API Key、供应商内部模型 ID、渠道顺序、Cookie、WebDAV 密文或完整敏感内容。
+- 前端业务数据持久化按现有约束使用服务端或 `localforage`;不要用静态 JSON、假成功或刷新页面掩盖状态问题。
+- 所有按路由、筛选或分页发起的异步读取必须绑定当前页面标识和请求代次;切换页面时取消或忽略旧响应,`rows`、加载态和错误态只能由当前请求更新,禁止旧页面数据串入新页面。
+
+## 4. 长任务阶段
+
+1. **P0 基线与方案确认**:读取 `AGENTS.md`、本文件、`goal.md`、`docs/miragenflow-tdesign-unification-goal.md`、架构图 JSON、两端入口/Provider/主题/路由/依赖,记录当前页面和视觉基线;先输出迁移盘点、共享边界、删除/保留清单和风险,再按本目标继续执行,不以方案文档代替代码验收。
+2. **P1 组件统一**:固定 TDesign 版本,完善共享 Provider、主题桥接、Lineicons 和主题化下拉,迁移普通控件、反馈、弹层和表单。
+3. **P2 管理业务**:逐页完成用户、渠道、模型、价格、计费、套餐、支付、审批、任务、存储、设置和审计的真实读写闭环;优先修复空白、原始 JSON、英文和无效按钮。
+4. **P3 用户业务**:回归首页、认证、AI 图片/音频创作、画布、资产、任务、账户和移动端;修复动态视口、黑色容器、标题对齐和游客菜单。
+5. **P4 清理与验收**:仅在 `web/` 引用清零、构建通过和浏览器回归通过后删除用户端 Ant Design、ProComponents、Radix 普通控件、reset 样式及遗留 `.ant-*` 依赖;管理端继续保持既有 TDesign 基线;同步文档并列出未验证项。
+
+## 5. 不得做的事
+
+- 不把管理端页面模板、权限或业务状态复制进用户端。
+- 不一次性删除依赖后再靠错误信息猜迁移结果;不回滚用户已有改动。
+- 不创建展示用 mock 渠道、静态假数据、无动作按钮或未接入供应商的假成功状态。
+- 不用“兼容”分支掩盖字段丢失、弹窗无法关闭、编辑空白或 Tab 切换丢数据。
+- 不在没有人工证据时宣称“全部完成”;文档完成、自动化测试通过和浏览器人工验收必须分别记录。
+
+## 6. 验收门槛
+
+至少执行并记录:
+
+```bash
+npm --prefix server run typecheck
+npm --prefix server test
+npm --prefix web run typecheck
+npm --prefix web run build
+npm --prefix admin run typecheck
+npm --prefix admin run build
+npm --prefix admin run lint
+npm run build:all
+git diff --check
+```
+
+启动 `npm run dev:all` 后,在不关闭用户已有浏览器标签的前提下验证同源 `/`、`/login`、`/tools/image`、`/canvas`、`/assets`、`/tasks`、`/account`、`/admin/` 及 22 个管理台页面;至少覆盖桌面、390px、浅色、深色和刷新场景。逐项检查:
+
+- 无 Ant Design、ProComponents、Radix 普通控件、原生 ``、CDN、WebFont、Lucide、`.ant-*` 和原始 JSON 展示。
+- 所有弹窗/抽屉的保存、取消、右上角、遮罩、ESC、异步成功关闭、失败保留和编辑回填。
+- 用户新增、金币增加/扣减、渠道三 Tab、手动拉取模型、勾选模型、可选映射、探活加载/失败/重试和真实持久化。
+- 日志分类、中文行为、可读操作人、无空操作列、ECharts 图表、空态/错误态和深浅主题。
+- 画布全高、资产和创作页面主题表面、标题左对齐、游客菜单、窄屏列表横向滚动和键盘操作。
+- 快速切换管理台页面时旧列表请求不会覆盖当前列表,加载/错误状态不会被过期请求清除或改写。
+
+## 7. 交付和进度记录
+
+每完成一个可独立验收项,记录文件、符号、命令、API 或截图证据;百分比只按“已通过验收的交付项 / 全部交付项”计算。同步维护:
+
+- `CHANGELOG.md`:只写用户可感知的版本级归纳。
+- `docs/content/docs/progress/todo.mdx`:仍未实现的工作。
+- `docs/content/docs/progress/pending-test.mdx`:已实现但尚未人工验证的工作。
+- `goal.md`、`docs/miragenflow-tdesign-unification-goal.md`:范围或验收发生变化时同步更新。
+
+## 7.1 目标设立信息
+
+- **目标名称**:MiragenFlow 前后台真实可用与统一 TDesign
+- **目标对象**:`/Users/qiu/Desktop/MiragenFlow`
+- **平台目标状态**:`active`
+- **平台目标线程**:`01a033b1-abd1-7ed0-b6a3-c347551b2f78`
+- **设立方式**:将本文第 8 节完整提示词粘贴到长期目标入口;目标执行器必须沿阶段、验收门槛和证据规则持续推进,不能只回复分析结论。
+
+## 8. 可直接复制的目标提示词
+
+```text
+你现在负责持续完成 /Users/qiu/Desktop/MiragenFlow 的“前后台真实可用与统一 TDesign”长期目标。请把任务当作一个必须跑到可验收状态的长任务,不要只做方案或静态展示。
+
+先完整读取:
+- /Users/qiu/Desktop/MiragenFlow/AGENTS.md
+- /Users/qiu/Desktop/MiragenFlow/docs/miragenflow-long-task-goal-prompt.md
+- /Users/qiu/Desktop/MiragenFlow/goal.md
+- /Users/qiu/Desktop/MiragenFlow/docs/miragenflow-tdesign-unification-goal.md
+- /Users/qiu/Desktop/MiragenFlow/develop.md
+- /Users/qiu/Desktop/MiragenFlow/system-diagrams.md
+- /Users/qiu/Desktop/MiragenFlow/admin-plan.md
+- /Users/qiu/Desktop/MiragenFlow/summary.md
+- web、admin、server、packages/contracts 的 package.json、入口、Provider、主题、路由、服务和关键业务页面
+
+总体目标:web 与 admin 使用同一锁定版本 `tdesign-react@1.15.1` 和本地 Lineicons,但 web 保留自定义用户工作台视觉/画布/资产/任务体验,admin 保留现有主题/权限/认证/菜单/ECharts;两端继续独立构建、路由、状态、认证、API 和业务边界。
+
+必须实现并逐项验收:
+1. 所有基础控件、弹层、表单、下拉、提示和表格使用 TDesign 或无业务依赖的共享包装;禁止原生 select、CDN、WebFont、Lucide、全量图标目录和 .ant-* 遗留。
+2. 页面文案、表头、状态、行为、错误和字段使用中文,仅保留必要专有名词/缩写(TDesign、Lineicons、ECharts、OpenAI、MFA、API、ID)。禁止原始 JSON、内部动作字段、敏感凭证和管理专属字段出现在用户可见页面。
+3. 管理台所有按钮都必须有真实交互、加载、成功、空态、错误、403、409、422、503 和重试反馈。所有弹窗/抽屉支持保存、取消、右上角、遮罩、ESC,成功自动关闭,失败保留表单,编辑回填且 Tab 切换不丢数据。
+4. 新增用户弹窗只填用户名、邮箱、手机号、初始密码;用户 ID 列只显示当前列表顺序的 1、2、3,不加 ID 前缀;其他资源 ID 保留真实值;余额和结构化字段用 GUI/图标展示。
+5. 渠道当前只支持 OpenAI 图片端点。新增/编辑使用同一弹窗的“基础信息/模型列表/模型映射”三个可自由切换 Tab,不要上一步/下一步,也不要通用供应商模板。地址示例必须是 https://example.com 或 https://example.com/v1,API Key 只校验非空。切换 Tab 保留统一草稿,编辑回填。
+6. 模型列表只能由按钮主动 GET {基础地址}/v1/models,显示独立加载、实际路径、是否发起、模型数量和中文 DNS/网络/HTTP/内容错误;结果必须由用户勾选加入渠道模型列表,不能自动创建映射。模型映射可选,严格区分平台显示模型 ID 与供应商请求模型 ID,动态行使用稳定 key。
+7. 系统概览、渠道健康、任务中心、实时指标和审计日志大板全部使用 ECharts。审计拆成大板/详细日志,展示 200 与 4xx/5xx/业务错误分类、数量、占比、趋势;详细日志按时间倒序展示中文行为和可读操作人。没有真实动作就删除操作列。
+8. Lineicons 必须本地化且按语义使用不同图标;管理台折叠侧栏完全隐藏,单项二级菜单直接提升为一级入口;手机端使用紧凑列表和横向滚动,不强制变成大卡片。
+9. 用户端画布/资产/AI 创作页面必须无黑色硬编码容器、下半屏塌陷、白屏和更新深度错误;标题左对齐,动态视口全高,浅色/深色/390px 可用;游客不显示个人主页和退出登录。
+10. 服务端保持真实 API、权限、事务、幂等和敏感数据隔离,不用 mock 或静态 JSON 冒充完成;不要回滚已有改动。
+11. 所有管理台列表读取都要有请求代次或 `AbortController` 保护;响应、错误和 `finally` 回写前必须确认仍属于当前路由/筛选/分页,必须用延迟请求的浏览器场景验证不会串页。
+
+按 P0 基线、P1 TDesign/图标/下拉、P2 管理台业务、P3 用户端回归、P4 清理验收推进。每个阶段先读代码和记录基线,再小步修改;不要在 `web/` 引用清零前删除用户端旧依赖。遇到未验证项写入 pending-test.mdx,未实现项写入 todo.mdx,不能用文档替代功能。
+
+执行并记录:
+npm --prefix server run typecheck
+npm --prefix server test
+npm --prefix web run typecheck
+npm --prefix web run build
+npm --prefix admin run typecheck
+npm --prefix admin run build
+npm --prefix admin run lint
+npm run build:all
+git diff --check
+
+然后运行 npm run dev:all,在不关闭用户现有标签的前提下,用桌面/390px、浅色/深色和刷新场景逐页验收用户端与 admin 22 页。最终报告必须列出已完成项、文件/命令/浏览器证据、未完成项、阻塞原因和下一步;没有证据的项目只能写“待验证”,不能宣布目标完成。
+```
diff --git a/docs/miragenflow-task-lifecycle.html b/docs/miragenflow-task-lifecycle.html
new file mode 100644
index 0000000..73cbaab
--- /dev/null
+++ b/docs/miragenflow-task-lifecycle.html
@@ -0,0 +1,14638 @@
+
+
+
+
+
+
+ MiragenFlow 任务状态核心机
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 引导视图
+
+
+
+
+
探索此系统
+
沿精选路径逐步查看,而不改变源图表。
+
+
+ 节点
+
+
+
+ 下一步
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 就绪
+ 章节 01 / 01
+
+
+ 引导章节
+
+
+
+
+
+
+
+
+ 图表指南
+ 探索此系统
+
+
+
+
正在检查已编译语义
+
+
+
+
+
+
+
+
+
+ E 导出T 主题S 风格0 重置+ 放大- 缩小Esc 关闭
+
+
+
+
+
+ 查找节点
+
+
+
+ ⌕
+
+ /
+
+
+
没有匹配的节点
+
+
+
+
+
+
语义护照
+
+
+
+
+
+
+
+
+
+
+
+
+
编写可达范围
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 路径探测
+ 选择起点节点
+
+
+
+
+
+
+
+
+ 在图表中选择两个语义节点
+
+
+
+
+
+
+
+
先选择来源,再选择目标;方向很重要。
+
+
+
+
+ 语义透镜
+ 比较系统角色
+
+
+
+
最多选择两种语义类型。选择一种可显示其真实流量;选择两种只比较直接编写的关系。
+
+
选择一种类型以检查其节点和相连关系。
+
+
+
+
+
+
+
+
+
+ 语义雷达
+ 正在构建总览
+
+
+
+
+
+
点击节点拖动平移
+
+
语义雷达需要更多地图可见空间。
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/miragenflow-task-lifecycle.json b/docs/miragenflow-task-lifecycle.json
new file mode 100644
index 0000000..2be93c7
--- /dev/null
+++ b/docs/miragenflow-task-lifecycle.json
@@ -0,0 +1,44 @@
+{
+ "schema_version": 1,
+ "diagram_type": "lifecycle",
+ "meta": {
+ "title": "MiragenFlow 任务状态核心机",
+ "locale": "zh-CN",
+ "output": "miragenflow-task-lifecycle.html",
+ "viewBox": [1250, 660],
+ "quality_profile": "showcase",
+ "views": [
+ { "id": "main-path", "label": "主线状态", "focus": ["created", "reserved", "queued", "running", "succeeded"], "note": "从校验和预留到执行、落地与结算。" },
+ { "id": "retry-path", "label": "失败重试", "focus": ["running", "failed", "retrying"], "note": "失败按渠道预算回到执行状态。" },
+ { "id": "unknown-path", "label": "未知保护", "focus": ["running", "unknown"], "note": "超时或租约丢失先进入未知状态,等待对账。" }
+ ]
+ },
+ "lanes": [
+ { "id": "main", "label": "任务主线" },
+ { "id": "recovery", "label": "失败与重试" },
+ { "id": "reconcile", "label": "未知保护" },
+ { "id": "terminal", "label": "终态出口" }
+ ],
+ "states": [
+ { "id": "created", "type": "start", "label": "已创建", "lane": "main", "col": 0, "step": "01" },
+ { "id": "reserved", "type": "active", "label": "已预留", "lane": "main", "col": 1, "step": "02" },
+ { "id": "queued", "type": "waiting", "label": "排队中", "lane": "main", "col": 2, "step": "03" },
+ { "id": "running", "type": "active", "label": "执行中", "lane": "main", "col": 3, "step": "04" },
+ { "id": "succeeded", "type": "success", "label": "成功", "lane": "main", "col": 4, "step": "05" },
+ { "id": "unknown", "type": "decision", "label": "未知结果", "lane": "reconcile", "col": 2, "yOffset": -80, "width": 110 },
+ { "id": "failed", "type": "failure", "label": "失败", "lane": "recovery", "col": 0, "yOffset": 30, "width": 110 },
+ { "id": "retrying", "type": "active", "label": "重试中", "lane": "recovery", "col": 1, "yOffset": 30, "width": 110 },
+ { "id": "canceled", "type": "failure", "label": "已取消", "lane": "terminal", "col": 2 }
+ ],
+ "transitions": [
+ { "id": "create-reserve", "from": "created", "to": "reserved", "variant": "emphasis" },
+ { "id": "reserve-queue", "from": "reserved", "to": "queued", "variant": "emphasis" },
+ { "id": "queue-run", "from": "queued", "to": "running", "variant": "emphasis" },
+ { "id": "run-success", "from": "running", "to": "succeeded", "variant": "emphasis" },
+ { "id": "run-failed", "from": "running", "to": "failed", "variant": "security", "fromSide": "bottom", "toSide": "bottom", "via": [[630, 188], [630, 380], [402, 380]] },
+ { "id": "failed-retry", "from": "failed", "to": "retrying", "variant": "emphasis" },
+ { "id": "retry-run", "from": "retrying", "to": "running", "variant": "emphasis", "fromSide": "left", "toSide": "bottom", "via": [[480, 337], [480, 188]] },
+ { "id": "run-unknown", "from": "running", "to": "unknown", "variant": "security", "fromSide": "top", "toSide": "right", "via": [[556, 80], [820, 80], [820, 227]] },
+ { "id": "running-cancel", "from": "running", "to": "canceled", "variant": "security", "fromSide": "top", "toSide": "right", "via": [[556, 64], [880, 64], [880, 479]] }
+ ]
+}
diff --git a/docs/miragenflow-task-lifecycle.visual-check.1440x900.dark.png b/docs/miragenflow-task-lifecycle.visual-check.1440x900.dark.png
new file mode 100644
index 0000000..bb9cca6
Binary files /dev/null and b/docs/miragenflow-task-lifecycle.visual-check.1440x900.dark.png differ
diff --git a/docs/miragenflow-task-lifecycle.visual-check.1440x900.light.png b/docs/miragenflow-task-lifecycle.visual-check.1440x900.light.png
new file mode 100644
index 0000000..f87ed9d
Binary files /dev/null and b/docs/miragenflow-task-lifecycle.visual-check.1440x900.light.png differ
diff --git a/docs/miragenflow-task-lifecycle.visual-check.2048x1320.dark.png b/docs/miragenflow-task-lifecycle.visual-check.2048x1320.dark.png
new file mode 100644
index 0000000..4c5ddd1
Binary files /dev/null and b/docs/miragenflow-task-lifecycle.visual-check.2048x1320.dark.png differ
diff --git a/docs/miragenflow-task-lifecycle.visual-check.2048x1320.light.png b/docs/miragenflow-task-lifecycle.visual-check.2048x1320.light.png
new file mode 100644
index 0000000..2628fe5
Binary files /dev/null and b/docs/miragenflow-task-lifecycle.visual-check.2048x1320.light.png differ
diff --git a/docs/miragenflow-task-lifecycle.visual-check.html b/docs/miragenflow-task-lifecycle.visual-check.html
new file mode 100644
index 0000000..54adbee
--- /dev/null
+++ b/docs/miragenflow-task-lifecycle.visual-check.html
@@ -0,0 +1,32 @@
+
+
+
+
+
+Archify visual-check · miragenflow-task-lifecycle.html
+
+
+
+
+
+
+
+ LIGHT · 1440×900 · containment pass
+
+
+
+ DARK · 1440×900 · containment pass
+
+
+
+ LIGHT · 2048×1320 · containment pass
+
+
+
+ DARK · 2048×1320 · containment pass
+
+
+
+
diff --git a/docs/miragenflow-task-lifecycle.visual-check.json b/docs/miragenflow-task-lifecycle.visual-check.json
new file mode 100644
index 0000000..2cf1407
--- /dev/null
+++ b/docs/miragenflow-task-lifecycle.visual-check.json
@@ -0,0 +1,482 @@
+{
+ "schemaVersion": 1,
+ "ok": true,
+ "command": "visual-check",
+ "status": "pass",
+ "visualReview": "pending",
+ "artifact": {
+ "path": "/Users/qiu/Desktop/MiragenFlow/docs/miragenflow-task-lifecycle.html",
+ "sha256": "9254260fdc63bc05cbf06f79329859ec51ef562d77ce74ecff38fa039a211148",
+ "bytes": 701434
+ },
+ "state": {
+ "detail": "read",
+ "motion": "still"
+ },
+ "chrome": {
+ "status": "available",
+ "executable": "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
+ },
+ "containment": {
+ "status": "pass",
+ "viewports": [
+ {
+ "width": 1440,
+ "height": 900,
+ "theme": "light",
+ "innerWidth": 1440,
+ "innerHeight": 900,
+ "scrollWidth": 1440,
+ "scrollHeight": 900,
+ "overflowX": false,
+ "overflowY": false,
+ "ok": true,
+ "readerWidth": 1341,
+ "diagramWidth": 1311,
+ "viewBoxWidth": 1250,
+ "minimumProjectedNodeTextPx": 10,
+ "minimumProjectedNodeText": "已创建",
+ "minimumProjectedNodeTextDetail": "primary",
+ "minimumRequiredNodeTextPx": 6,
+ "readabilityOk": true,
+ "hasLegend": true,
+ "hasNavigationDock": true,
+ "legendDockIntersectionArea": 0,
+ "viewerChromeReserve": 0,
+ "viewerChromeActive": false,
+ "viewerChromeOk": true,
+ "resolvedTheme": "light"
+ },
+ {
+ "width": 1600,
+ "height": 1000,
+ "theme": "light",
+ "innerWidth": 1600,
+ "innerHeight": 1000,
+ "scrollWidth": 1600,
+ "scrollHeight": 1000,
+ "overflowX": false,
+ "overflowY": false,
+ "ok": true,
+ "readerWidth": 1473,
+ "diagramWidth": 1443,
+ "viewBoxWidth": 1250,
+ "minimumProjectedNodeTextPx": 10,
+ "minimumProjectedNodeText": "已创建",
+ "minimumProjectedNodeTextDetail": "primary",
+ "minimumRequiredNodeTextPx": 6,
+ "readabilityOk": true,
+ "hasLegend": true,
+ "hasNavigationDock": true,
+ "legendDockIntersectionArea": 0,
+ "viewerChromeReserve": 0,
+ "viewerChromeActive": false,
+ "viewerChromeOk": true,
+ "resolvedTheme": "light"
+ },
+ {
+ "width": 1920,
+ "height": 1080,
+ "theme": "light",
+ "innerWidth": 1920,
+ "innerHeight": 1080,
+ "scrollWidth": 1920,
+ "scrollHeight": 1080,
+ "overflowX": false,
+ "overflowY": false,
+ "ok": true,
+ "readerWidth": 1625,
+ "diagramWidth": 1595,
+ "viewBoxWidth": 1250,
+ "minimumProjectedNodeTextPx": 10,
+ "minimumProjectedNodeText": "已创建",
+ "minimumProjectedNodeTextDetail": "primary",
+ "minimumRequiredNodeTextPx": 6,
+ "readabilityOk": true,
+ "hasLegend": true,
+ "hasNavigationDock": true,
+ "legendDockIntersectionArea": 0,
+ "viewerChromeReserve": 0,
+ "viewerChromeActive": false,
+ "viewerChromeOk": true,
+ "resolvedTheme": "light"
+ },
+ {
+ "width": 2048,
+ "height": 1320,
+ "theme": "light",
+ "innerWidth": 2048,
+ "innerHeight": 1320,
+ "scrollWidth": 2048,
+ "scrollHeight": 1320,
+ "overflowX": false,
+ "overflowY": false,
+ "ok": true,
+ "readerWidth": 1920,
+ "diagramWidth": 1870,
+ "viewBoxWidth": 1250,
+ "minimumProjectedNodeTextPx": 10,
+ "minimumProjectedNodeText": "已创建",
+ "minimumProjectedNodeTextDetail": "primary",
+ "minimumRequiredNodeTextPx": 6,
+ "readabilityOk": true,
+ "hasLegend": true,
+ "hasNavigationDock": true,
+ "legendDockIntersectionArea": 0,
+ "viewerChromeReserve": 0,
+ "viewerChromeActive": false,
+ "viewerChromeOk": true,
+ "resolvedTheme": "light"
+ }
+ ]
+ },
+ "readability": {
+ "status": "pass",
+ "minimumProjectedNodeTextPx": 6,
+ "viewports": [
+ {
+ "width": 1440,
+ "height": 900,
+ "theme": "light",
+ "innerWidth": 1440,
+ "innerHeight": 900,
+ "scrollWidth": 1440,
+ "scrollHeight": 900,
+ "overflowX": false,
+ "overflowY": false,
+ "ok": true,
+ "readerWidth": 1341,
+ "diagramWidth": 1311,
+ "viewBoxWidth": 1250,
+ "minimumProjectedNodeTextPx": 10,
+ "minimumProjectedNodeText": "已创建",
+ "minimumProjectedNodeTextDetail": "primary",
+ "minimumRequiredNodeTextPx": 6,
+ "readabilityOk": true,
+ "hasLegend": true,
+ "hasNavigationDock": true,
+ "legendDockIntersectionArea": 0,
+ "viewerChromeReserve": 0,
+ "viewerChromeActive": false,
+ "viewerChromeOk": true,
+ "resolvedTheme": "light"
+ },
+ {
+ "width": 1600,
+ "height": 1000,
+ "theme": "light",
+ "innerWidth": 1600,
+ "innerHeight": 1000,
+ "scrollWidth": 1600,
+ "scrollHeight": 1000,
+ "overflowX": false,
+ "overflowY": false,
+ "ok": true,
+ "readerWidth": 1473,
+ "diagramWidth": 1443,
+ "viewBoxWidth": 1250,
+ "minimumProjectedNodeTextPx": 10,
+ "minimumProjectedNodeText": "已创建",
+ "minimumProjectedNodeTextDetail": "primary",
+ "minimumRequiredNodeTextPx": 6,
+ "readabilityOk": true,
+ "hasLegend": true,
+ "hasNavigationDock": true,
+ "legendDockIntersectionArea": 0,
+ "viewerChromeReserve": 0,
+ "viewerChromeActive": false,
+ "viewerChromeOk": true,
+ "resolvedTheme": "light"
+ },
+ {
+ "width": 1920,
+ "height": 1080,
+ "theme": "light",
+ "innerWidth": 1920,
+ "innerHeight": 1080,
+ "scrollWidth": 1920,
+ "scrollHeight": 1080,
+ "overflowX": false,
+ "overflowY": false,
+ "ok": true,
+ "readerWidth": 1625,
+ "diagramWidth": 1595,
+ "viewBoxWidth": 1250,
+ "minimumProjectedNodeTextPx": 10,
+ "minimumProjectedNodeText": "已创建",
+ "minimumProjectedNodeTextDetail": "primary",
+ "minimumRequiredNodeTextPx": 6,
+ "readabilityOk": true,
+ "hasLegend": true,
+ "hasNavigationDock": true,
+ "legendDockIntersectionArea": 0,
+ "viewerChromeReserve": 0,
+ "viewerChromeActive": false,
+ "viewerChromeOk": true,
+ "resolvedTheme": "light"
+ },
+ {
+ "width": 2048,
+ "height": 1320,
+ "theme": "light",
+ "innerWidth": 2048,
+ "innerHeight": 1320,
+ "scrollWidth": 2048,
+ "scrollHeight": 1320,
+ "overflowX": false,
+ "overflowY": false,
+ "ok": true,
+ "readerWidth": 1920,
+ "diagramWidth": 1870,
+ "viewBoxWidth": 1250,
+ "minimumProjectedNodeTextPx": 10,
+ "minimumProjectedNodeText": "已创建",
+ "minimumProjectedNodeTextDetail": "primary",
+ "minimumRequiredNodeTextPx": 6,
+ "readabilityOk": true,
+ "hasLegend": true,
+ "hasNavigationDock": true,
+ "legendDockIntersectionArea": 0,
+ "viewerChromeReserve": 0,
+ "viewerChromeActive": false,
+ "viewerChromeOk": true,
+ "resolvedTheme": "light"
+ }
+ ]
+ },
+ "viewerChrome": {
+ "status": "pass",
+ "viewports": [
+ {
+ "width": 1440,
+ "height": 900,
+ "theme": "light",
+ "innerWidth": 1440,
+ "innerHeight": 900,
+ "scrollWidth": 1440,
+ "scrollHeight": 900,
+ "overflowX": false,
+ "overflowY": false,
+ "ok": true,
+ "readerWidth": 1341,
+ "diagramWidth": 1311,
+ "viewBoxWidth": 1250,
+ "minimumProjectedNodeTextPx": 10,
+ "minimumProjectedNodeText": "已创建",
+ "minimumProjectedNodeTextDetail": "primary",
+ "minimumRequiredNodeTextPx": 6,
+ "readabilityOk": true,
+ "hasLegend": true,
+ "hasNavigationDock": true,
+ "legendDockIntersectionArea": 0,
+ "viewerChromeReserve": 0,
+ "viewerChromeActive": false,
+ "viewerChromeOk": true,
+ "resolvedTheme": "light"
+ },
+ {
+ "width": 1600,
+ "height": 1000,
+ "theme": "light",
+ "innerWidth": 1600,
+ "innerHeight": 1000,
+ "scrollWidth": 1600,
+ "scrollHeight": 1000,
+ "overflowX": false,
+ "overflowY": false,
+ "ok": true,
+ "readerWidth": 1473,
+ "diagramWidth": 1443,
+ "viewBoxWidth": 1250,
+ "minimumProjectedNodeTextPx": 10,
+ "minimumProjectedNodeText": "已创建",
+ "minimumProjectedNodeTextDetail": "primary",
+ "minimumRequiredNodeTextPx": 6,
+ "readabilityOk": true,
+ "hasLegend": true,
+ "hasNavigationDock": true,
+ "legendDockIntersectionArea": 0,
+ "viewerChromeReserve": 0,
+ "viewerChromeActive": false,
+ "viewerChromeOk": true,
+ "resolvedTheme": "light"
+ },
+ {
+ "width": 1920,
+ "height": 1080,
+ "theme": "light",
+ "innerWidth": 1920,
+ "innerHeight": 1080,
+ "scrollWidth": 1920,
+ "scrollHeight": 1080,
+ "overflowX": false,
+ "overflowY": false,
+ "ok": true,
+ "readerWidth": 1625,
+ "diagramWidth": 1595,
+ "viewBoxWidth": 1250,
+ "minimumProjectedNodeTextPx": 10,
+ "minimumProjectedNodeText": "已创建",
+ "minimumProjectedNodeTextDetail": "primary",
+ "minimumRequiredNodeTextPx": 6,
+ "readabilityOk": true,
+ "hasLegend": true,
+ "hasNavigationDock": true,
+ "legendDockIntersectionArea": 0,
+ "viewerChromeReserve": 0,
+ "viewerChromeActive": false,
+ "viewerChromeOk": true,
+ "resolvedTheme": "light"
+ },
+ {
+ "width": 2048,
+ "height": 1320,
+ "theme": "light",
+ "innerWidth": 2048,
+ "innerHeight": 1320,
+ "scrollWidth": 2048,
+ "scrollHeight": 1320,
+ "overflowX": false,
+ "overflowY": false,
+ "ok": true,
+ "readerWidth": 1920,
+ "diagramWidth": 1870,
+ "viewBoxWidth": 1250,
+ "minimumProjectedNodeTextPx": 10,
+ "minimumProjectedNodeText": "已创建",
+ "minimumProjectedNodeTextDetail": "primary",
+ "minimumRequiredNodeTextPx": 6,
+ "readabilityOk": true,
+ "hasLegend": true,
+ "hasNavigationDock": true,
+ "legendDockIntersectionArea": 0,
+ "viewerChromeReserve": 0,
+ "viewerChromeActive": false,
+ "viewerChromeOk": true,
+ "resolvedTheme": "light"
+ }
+ ]
+ },
+ "captures": {
+ "status": "pass",
+ "screenshots": [
+ {
+ "width": 1440,
+ "height": 900,
+ "theme": "light",
+ "innerWidth": 1440,
+ "innerHeight": 900,
+ "scrollWidth": 1440,
+ "scrollHeight": 900,
+ "overflowX": false,
+ "overflowY": false,
+ "ok": true,
+ "readerWidth": 1341,
+ "diagramWidth": 1311,
+ "viewBoxWidth": 1250,
+ "minimumProjectedNodeTextPx": 10,
+ "minimumProjectedNodeText": "已创建",
+ "minimumProjectedNodeTextDetail": "primary",
+ "minimumRequiredNodeTextPx": 6,
+ "readabilityOk": true,
+ "hasLegend": true,
+ "hasNavigationDock": true,
+ "legendDockIntersectionArea": 0,
+ "viewerChromeReserve": 0,
+ "viewerChromeActive": false,
+ "viewerChromeOk": true,
+ "resolvedTheme": "light",
+ "file": "miragenflow-task-lifecycle.visual-check.1440x900.light.png"
+ },
+ {
+ "width": 1440,
+ "height": 900,
+ "theme": "dark",
+ "innerWidth": 1440,
+ "innerHeight": 900,
+ "scrollWidth": 1440,
+ "scrollHeight": 900,
+ "overflowX": false,
+ "overflowY": false,
+ "ok": true,
+ "readerWidth": 1341,
+ "diagramWidth": 1311,
+ "viewBoxWidth": 1250,
+ "minimumProjectedNodeTextPx": 10,
+ "minimumProjectedNodeText": "已创建",
+ "minimumProjectedNodeTextDetail": "primary",
+ "minimumRequiredNodeTextPx": 6,
+ "readabilityOk": true,
+ "hasLegend": true,
+ "hasNavigationDock": true,
+ "legendDockIntersectionArea": 0,
+ "viewerChromeReserve": 0,
+ "viewerChromeActive": false,
+ "viewerChromeOk": true,
+ "resolvedTheme": "dark",
+ "file": "miragenflow-task-lifecycle.visual-check.1440x900.dark.png"
+ },
+ {
+ "width": 2048,
+ "height": 1320,
+ "theme": "light",
+ "innerWidth": 2048,
+ "innerHeight": 1320,
+ "scrollWidth": 2048,
+ "scrollHeight": 1320,
+ "overflowX": false,
+ "overflowY": false,
+ "ok": true,
+ "readerWidth": 1920,
+ "diagramWidth": 1870,
+ "viewBoxWidth": 1250,
+ "minimumProjectedNodeTextPx": 10,
+ "minimumProjectedNodeText": "已创建",
+ "minimumProjectedNodeTextDetail": "primary",
+ "minimumRequiredNodeTextPx": 6,
+ "readabilityOk": true,
+ "hasLegend": true,
+ "hasNavigationDock": true,
+ "legendDockIntersectionArea": 0,
+ "viewerChromeReserve": 0,
+ "viewerChromeActive": false,
+ "viewerChromeOk": true,
+ "resolvedTheme": "light",
+ "file": "miragenflow-task-lifecycle.visual-check.2048x1320.light.png"
+ },
+ {
+ "width": 2048,
+ "height": 1320,
+ "theme": "dark",
+ "innerWidth": 2048,
+ "innerHeight": 1320,
+ "scrollWidth": 2048,
+ "scrollHeight": 1320,
+ "overflowX": false,
+ "overflowY": false,
+ "ok": true,
+ "readerWidth": 1920,
+ "diagramWidth": 1870,
+ "viewBoxWidth": 1250,
+ "minimumProjectedNodeTextPx": 10,
+ "minimumProjectedNodeText": "已创建",
+ "minimumProjectedNodeTextDetail": "primary",
+ "minimumRequiredNodeTextPx": 6,
+ "readabilityOk": true,
+ "hasLegend": true,
+ "hasNavigationDock": true,
+ "legendDockIntersectionArea": 0,
+ "viewerChromeReserve": 0,
+ "viewerChromeActive": false,
+ "viewerChromeOk": true,
+ "resolvedTheme": "dark",
+ "file": "miragenflow-task-lifecycle.visual-check.2048x1320.dark.png"
+ }
+ ],
+ "contactSheet": "miragenflow-task-lifecycle.visual-check.html"
+ },
+ "sidecars": {
+ "receipt": "miragenflow-task-lifecycle.visual-check.json",
+ "contactSheet": "miragenflow-task-lifecycle.visual-check.html"
+ }
+}
diff --git a/docs/miragenflow-tdesign-unification-goal.md b/docs/miragenflow-tdesign-unification-goal.md
new file mode 100644
index 0000000..341e5ce
--- /dev/null
+++ b/docs/miragenflow-tdesign-unification-goal.md
@@ -0,0 +1,219 @@
+# MiragenFlow 统一 TDesign 长期目标
+
+> 目标状态:进行中(active)
+>
+> 目标线程:`01a033b1-abd1-7ed0-b6a3-c347551b2f78`
+>
+> 本文是本轮“前后台统一组件体系”长任务的执行基线。这里的“统一”只指基础组件、主题语义、图标和交互契约统一;`web/` 与 `admin/` 仍是两个独立应用,不能合并页面、路由、权限、状态或业务 API。
+>
+> 本轮前后台业务、中文化、真实交互、渠道、日志、图表、响应式和验收要求已汇总到 [`docs/miragenflow-long-task-goal-prompt.md`](miragenflow-long-task-goal-prompt.md),长任务优先以该综合目标为入口。
+>
+> 旧版页面蓝本只作为历史背景;当渠道交互与综合目标不一致时,必须采用 OpenAI 单一供应方式、统一弹窗和“基础信息 / 模型列表 / 模型映射”三个可自由切换 Tab。
+
+## 1. 目标定义
+
+将 MiragenFlow 的用户端 `web/` 与管理端 `admin/` 统一到同一锁定版本 `tdesign-react@1.15.1` 的基础组件体系,同时保持两个应用的业务、权限和视觉职责分离。
+
+最终结果必须满足:
+
+- 用户端和管理端的按钮、输入框、下拉、选择器、弹窗、抽屉、提示、表单、表格、分页等基础交互均基于 TDesign。
+- 用户端保留现有自定义视觉,包括主题令牌、品牌字体、布局密度、画布极简工具栏、移动端布局和用户侧文案;不能把管理台模板直接套到用户端。
+- 管理端现有 TDesign 主题、工作台密度、菜单、权限、认证和业务页面保持视觉与行为稳定。
+- 两端都使用本地化 Lineicons 子集,不使用 CDN、WebFont、全量图标目录或未批准的图标库。
+- 所有下拉和弹层使用项目自己的样式适配,不渲染浏览器原生 ``。
+- 页面可见文案、表头、状态、动作和错误提示统一使用中文;仅保留必要的专有名词或缩写(如 TDesign、Lineicons、ECharts、MFA、API、ID),禁止直接展示英文 action、内部字段名或原始 JSON。
+- 服务端、共享接口契约、用户端和管理端仍按边界独立,不能因为统一组件库而互相导入业务代码。
+
+这是一个“统一组件运行时、保留应用视觉和业务边界”的目标,不是复制管理台页面,也不是合并两个应用。
+
+## 2. 当前基线
+
+用户端当前已完成基础 TDesign 适配层和本地 Lineicons 接入,保留 React 19、React Router 7、自定义用户控制台组件和 Zustand;Ant Design、ProComponents、Radix 普通控件及 reset 已从当前源码/依赖中清理,画布节点、侧栏、图片创作、资产入口、参数选项、资源提及输入和主题切换等主要可见控件已统一到共享 TDesign 适配层。剩余工作是隐藏文件上传等明确原生 API 例外、完整浏览器回归和最终证据复核。入口和主题主要位于:
+
+- `web/src/main.tsx`
+- `web/src/components/layout/app-providers.tsx`
+- `web/src/lib/tdesign-theme.ts`
+- `web/src/user-console/components/primitives/`
+
+管理端目前使用 React 18、React Router 6、TDesign、Redux 和 ECharts;入口和管理台业务边界主要位于:
+
+- `admin/src/main.tsx`
+- `admin/src/router/index.ts`
+- `admin/src/layouts/components/AppRouter.tsx`
+- `admin/src/pages/Business/index.tsx`
+- `admin/src/services/platform.ts`
+
+两端 React、路由和主题令牌版本不同,所以不能用一次性依赖替换完成迁移。TDesign 在用户端 React 19 下的兼容性必须先验证;不能未经验证地强行降级或升级 React。
+
+## 3. 目标架构
+
+```text
+用户端 web/ 页面与业务
+ |
+ v
+用户端 TDesign 适配层 ---- 用户端自定义主题、布局、文案
+ |
+ v
+共享 TDesign 基础 UI + 本地 Lineicons
+ ^
+ |
+管理端 admin/ 现有 TDesign 页面与主题
+
+用户端 web/ -----------------------+
+ |--> packages/contracts --> server API
+管理端 admin/ ---------------------+
+```
+
+建议只增加一个无业务依赖的共享 UI 包(例如 `packages/ui`),内部提供纯 props 组件、主题语义令牌和图标导出。共享包不能依赖 `admin` 的 Redux、路由、权限、管理员请求或业务字段。
+
+## 4. 明确边界
+
+### 可以共享
+
+- TDesign 基础控件包装:Button、Input、Select、Switch、Tooltip、Tag、Tabs、Modal、Drawer、Dropdown、Form、Table、Pagination、Empty、Loading、Message。
+- 键盘、焦点、ESC、遮罩关闭、弹层层级和受控字段行为契约。
+- 语义主题令牌:主色、成功/警告/错误、表面、边框、正文、圆角、间距、断点;由各应用分别生成 TDesign 变量。
+- 本地 Lineicons 图标子集、尺寸和点击区域规范。
+- 无状态的统计卡、日期范围控件和错误页展示壳;路由、权限和动作由调用方注入。
+
+### 用户端专属
+
+- 用户端页面、画布、资产、任务、账户、认证和 WebDAV 流程。
+- `.mf-console` 自定义主题、品牌字体、首页作品流、画布工具栏和移动端布局。
+- 用户端 Zustand、React Query、localforage、Motion、Tailwind、CodeMirror 和画布状态。
+- 用户端不得暴露供应商名称、Base URL、API Key、内部模型 ID、渠道顺序或管理配置。
+
+### 管理端专属
+
+- 管理台菜单、路由元数据、scope/role 权限、PermissionGate、管理员 MFA/CAPTCHA 和登录流程。
+- `adminRequest` 的管理员 Bearer、CSRF、幂等键、401 刷新和错误策略。
+- 用户、渠道、渠道组、模型产品、计费、审批、任务、存储、系统设置和审计业务。
+- 管理台现有 TDesign 主题、ECharts 主题映射和工作台布局。
+
+## 5. 迁移阶段
+
+### P0:锁定版本与建立适配边界
+
+1. 固定两端使用的 `tdesign-react` 版本并验证 React 19 兼容性。
+2. 建立共享 UI 包或用户端适配层,先实现 Provider、主题桥接和 Lineicons 导出。
+3. 记录管理端关键页面截图/交互基线;迁移期间禁止新增 Ant Design import。
+4. 保持用户端和管理端入口、路由、状态、认证和 API 完全独立。
+
+### P1:迁移普通控件
+
+按 Button、Input、Switch、Tag、Tooltip、Tabs、Card、Empty、Loading、Select 的顺序迁移,优先覆盖账户、任务、设置和认证页面。当前 `packages/ui/src/` 已提供按组件子路径导出的 TDesign 控件入口,用户端普通控件通过 `@miragenflow/ui/*` 使用,保持按需加载。
+
+### P2:迁移反馈、弹层和表单
+
+迁移 Message、Notification、Modal、Drawer、Dropdown、Popconfirm、DatePicker、Form、Pagination,并统一验证取消、右上角关闭、遮罩关闭、ESC、焦点回收和异步成功自动关闭。
+
+### P3:迁移复杂业务与画布
+
+迁移资产页和画布中的标签多选、搜索下拉、上传、Image 预览、Slider、InputNumber、Popover、复杂受控表单;同步改写所有 `.ant-*` CSS 和 `closest()` 事件边界。
+
+### P4:清理旧依赖
+
+确认用户端所有调用方和测试通过后,从 `web/` 删除 Ant Design、ProComponents、Ant reset 和 Radix 普通控件;管理端本来就以 TDesign 为基线,不为统一目标引入或删除管理端业务依赖。保留仍有业务价值的无框架复合组件和动画组件。
+
+## 6. 删除与保留清单
+
+### 迁移完成后删除或改写
+
+- `web/package.json` 与锁文件中的 `antd`、`@ant-design/pro-components`。
+- `web/src/main.tsx` 中的 `antd/dist/reset.css`。
+- `web/src/components/layout/app-providers.tsx` 中的 Ant `ConfigProvider`、`ProConfigProvider` 和 `App.useApp()` 依赖。
+- 用户端旧的 `web/src/lib/app-theme.ts` Ant 主题文件;主题变量统一维护在 `web/src/lib/tdesign-theme.ts`。
+- 用户端所有 `import ... from 'antd'` 及依赖 Ant DOM 类名的样式和事件判断。
+- 所有普通控件迁移后,删除 `radix-ui` 和 `web/src/components/ui/select.tsx`。
+
+### 必须保留
+
+- `web/src/user-console/components/primitives/`,但把基础控件内部实现替换为 TDesign 包装;`Feedback` 等业务复合组件仍可保留。
+- `animated-theme-toggler.tsx`、`dia-text-reveal.tsx` 等与组件库无关的动画组件。
+- Zustand、React Query、localforage、Tailwind、Motion、CodeMirror、画布和资产存储。
+- 管理端现有 TDesign、Redux、路由、权限、认证、服务层、Business 页面和 ECharts。
+- 两端本地 Lineicons 资源及许可证;必要时再抽成共享本地包。
+- `packages/contracts`、`server` 和现有 API/持久化逻辑。
+
+## 7. 取舍说明
+
+统一 TDesign 会移除用户端 Ant Design、ProComponents 和 Radix 的运行时负担,并减少重复控件实现。它不会自动让两个独立 Vite 应用只下载一份 JavaScript;如果未来要求物理层面共享 vendor 文件,需要另行设计统一构建或共享静态资源,不把两个应用强行合并作为本目标的一部分。
+
+React 18/19、Router 6/7、Redux/Zustand 仍可在各自应用内保留。统一组件库不要求统一这些业务运行时,强行合并会扩大回归范围。
+
+## 8. 验收标准
+
+### 依赖与边界
+
+- 用户端和管理端基础控件最终均来自 TDesign 或共享 TDesign 包装层。
+- 用户端不再有 Ant Design、ProComponents、Radix 普通控件或原生 `` 引用。
+- 用户端和管理端没有互相导入业务页面、Redux store、路由、权限或 API 服务。
+- 没有 CDN、WebFont、Lucide 或全量图标目录;Lineicons 仅加载本地实际使用子集。
+
+### 视觉与交互
+
+- 管理端关键页面视觉与迁移前基线一致。
+- 用户端仍保持现有自定义主题、品牌字体、画布扁平工具栏和移动端列表布局。
+- 浅色/深色主题、桌面/手机尺寸均无溢出、遮挡、黑色错误容器或白屏。
+- 所有弹窗和抽屉支持保存、取消、右上角关闭、遮罩关闭、ESC 关闭;异步成功自动关闭,失败保留表单。
+- 所有受控字段在切换 Tab、重开编辑、保存失败和刷新后不会丢失或被空值覆盖。
+
+### 业务与安全
+
+- 现有用户端画布、资产、任务、账户和认证流程不回归。
+- 管理端 22 个可见页面、权限、真实 API、审计、计费和渠道流程不回归。
+- 页面不展示原始 JSON、内部供应商凭证、Base URL、API Key、内部模型 ID 或管理专属字段。
+- 加载、空态、网络错误、403、409、422、503 和重试路径均有明确中文反馈。
+- 按路由、筛选和分页加载的列表必须具备请求代次或取消保护;过期响应不得回写当前页面的行数据、加载态或错误态。
+
+### 验证命令
+
+```bash
+npm --prefix server run typecheck
+npm --prefix server test
+npm --prefix web run typecheck
+npm --prefix web run build
+npm --prefix admin run typecheck
+npm --prefix admin run build
+npm --prefix admin run lint
+npm run build:all
+git diff --check
+```
+
+使用 `npm run dev:all` 进行同源验证,至少检查 `/`、`/canvas`、`/assets`、`/tools/image`、`/admin/` 和管理台关键页面;使用桌面、390px 手机、浅色和深色主题验证控件、弹层、键盘和滚动。未验证内容只能记录到 `pending-test.mdx`,不能宣布完成。
+
+## 9. 长任务执行规则
+
+1. 先读取 `AGENTS.md`、本目标文档、综合目标提示词、架构图 JSON,以及两端入口、Provider、主题和依赖文件;先形成迁移盘点和删除/保留清单,再开始代码修改。
+2. 每次只推进一个迁移阶段;先改适配层,再逐页迁移,禁止一次性删除 Ant 依赖。
+3. 修改前记录涉及页面、控件、状态契约和视觉基线;修改后检查用户端和管理端边界。
+4. 不回滚已有用户改动,不创建展示用 mock,不把未接入功能伪装成成功。
+5. 每完成一个阶段,运行对应类型检查/构建并记录结果;浏览器未验证的项目写入 `pending-test.mdx`。
+6. 只有在用户端 Ant/Radix 引用清零、所有复杂控件回归通过、管理端视觉基线稳定后,才允许删除旧依赖。
+
+## 9.1 本轮需求转化
+
+### 必须达成
+
+- `web/` 与 `admin/` 使用同一锁定版本的 TDesign 作为基础控件运行时;用户端通过适配层接入,不直接复制管理台页面。
+- 用户端保留现有自定义主题、品牌字体、首页/登录/创作/画布/资产视觉、画布专属交互和移动端紧凑列表;管理端保持现有工作台密度、菜单、权限、认证、表格和 ECharts 视觉。
+- 两端共享的只能是无业务依赖的 TDesign 包装、语义主题令牌、可访问性/键盘行为契约和本地 Lineicons 子集;不得共享管理端 Redux、路由、权限、请求服务或业务字段。
+- 所有 Select、Dropdown、Cascader、TreeSelect、弹层和提示均使用 TDesign 或项目自定义样式适配;禁止浏览器原生 ``、CDN、WebFont、Lucide、全量图标目录和原始 JSON 展示。
+- 页面文案、表头、状态、动作和错误提示使用中文;只保留 TDesign、Lineicons、ECharts、MFA、API、ID 等必要专有名词或缩写。
+
+### 明确不做
+
+- 不把用户端改造成管理台模板,不统一两端的布局、品牌色、信息密度或业务导航。
+- 不合并 React、React Router、Redux/Zustand、认证、API、持久化或画布运行时;不为“减少依赖”删除仍被业务使用的动画、画布和存储能力。
+- 不通过 mock、静态 JSON、假按钮或隐藏错误来宣称迁移完成;未接入或未验证的能力必须明确记录。
+
+### 长任务完成门槛
+
+- 每个阶段先迁移适配层,再逐页迁移,保留可回滚边界;不得在 `web/` 引用尚未清零前删除用户端 Ant Design、ProComponents、Radix 或 reset 样式。
+- 每阶段必须留下类型检查/构建结果;最终还要用同源启动在桌面、390px、浅色和深色下逐页检查首页、登录、创作、画布、资产、任务、账户和管理台关键页面。
+- 必须验证加载、空态、错误、403、409、422、503、重试、键盘 Tab/Enter/Escape、焦点回收、弹层关闭、表单回填、Tab 切换和窄屏滚动;未人工验证的项目只能写入 `pending-test.mdx`。
+- 必须用延迟旧请求后快速切换页面的场景验证管理台列表不会发生跨路由数据串线,过期请求不能清除当前页面的加载或错误状态。
+
+## 10. 可直接设立的目标提示词
+
+本文件不再复制第二份提示词,避免长期执行时出现版本漂移。请直接复制 [`docs/miragenflow-long-task-goal-prompt.md`](miragenflow-long-task-goal-prompt.md) 第 8 节的完整文本;该文本包含本文件的组件迁移规则、前后台边界、OpenAI 三 Tab 渠道规则、阶段顺序和最终验收命令。
diff --git a/docs/miragenflow-ui-unification.architecture.html b/docs/miragenflow-ui-unification.architecture.html
new file mode 100644
index 0000000..5027102
--- /dev/null
+++ b/docs/miragenflow-ui-unification.architecture.html
@@ -0,0 +1,14730 @@
+
+
+
+
+
+
+ MiragenFlow 前后台组件统一迁移方案
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 引导视图
+
+
+
+
+
探索此系统
+
沿精选路径逐步查看,而不改变源图表。
+
+
+ 节点
+
+
+
+ 下一步
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 就绪
+ 章节 01 / 01
+
+
+ 引导章节
+
+
+
+
+
+
+
+
+ 图表指南
+ 探索此系统
+
+
+
+
正在检查已编译语义
+
+
+
+
+
+
+
+
+
+ E 导出T 主题S 风格0 重置+ 放大- 缩小Esc 关闭
+
+
+
+
+
+ 查找节点
+
+
+
+ ⌕
+
+ /
+
+
+
没有匹配的节点
+
+
+
+
+
+
语义护照
+
+
+
+
+
+
+
+
+
+
+
+
+
编写可达范围
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 路径探测
+ 选择起点节点
+
+
+
+
+
+
+
+
+ 在图表中选择两个语义节点
+
+
+
+
+
+
+
+
先选择来源,再选择目标;方向很重要。
+
+
+
+
+ 语义透镜
+ 比较系统角色
+
+
+
+
最多选择两种语义类型。选择一种可显示其真实流量;选择两种只比较直接编写的关系。
+
+
选择一种类型以检查其节点和相连关系。
+
+
+
+
+
+
+
+
+
+ 语义雷达
+ 正在构建总览
+
+
+
+
+
+
点击节点拖动平移
+
+
语义雷达需要更多地图可见空间。
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ - • 可行:UI 组件替换不改变 server、contracts、路由和业务数据
+ - • 管理台 TDesign 作为视觉基准,用户端通过适配层保留现有 tokens 与布局
+ - • 统一后可减少重复弹层、表单状态和依赖加载
+
+
+
+
+
+
+ - • 保留用户端页面、中文文案、主题变量、业务组件和 Lineicons
+ - • 完成等价能力后删除用户端 Ant Design 核心依赖与重复的 Radix 普通控件封装
+ - • 标签多选、搜索下拉、日期、上传和画布控件先由适配层承接
+
+
+
+
+
+
+ - • 先建立 TDesign 主题桥接和用户端适配 API,禁止新页面继续引入 Ant Design
+ - • 每类控件都要验证键盘、ESC、遮罩、主题、移动端和表单受控状态
+ - • 按普通输入/选择器、反馈弹层、复杂业务组件分批切换并逐批删除旧依赖
+
+
+
+
+
+
+
+
+
diff --git a/docs/miragenflow-ui-unification.architecture.json b/docs/miragenflow-ui-unification.architecture.json
new file mode 100644
index 0000000..8e7ee73
--- /dev/null
+++ b/docs/miragenflow-ui-unification.architecture.json
@@ -0,0 +1,47 @@
+{
+ "schema_version": 1,
+ "diagram_type": "architecture",
+ "meta": {
+ "title": "MiragenFlow 前后台组件统一迁移方案",
+ "locale": "zh-CN",
+ "output": "miragenflow-ui-unification.architecture.html",
+ "quality_profile": "showcase",
+ "views": [
+ { "id": "current-state", "label": "当前结构", "focus": ["user-pages", "user-adapter", "shared-tdesign", "admin-pages", "admin-tdesign"], "note": "用户端基础控件已通过 TDesign 适配层接入,Ant Design 与 Radix 普通控件仅保留在历史迁移节点;管理台继续使用 TDesign。" },
+ { "id": "target-state", "label": "目标结构", "focus": ["user-pages", "user-adapter", "shared-tdesign", "admin-pages", "admin-tdesign", "lineicons"], "note": "统一 TDesign 基础组件与本地 Lineicons,页面布局、主题变量和业务边界分别保留。" },
+ { "id": "migration-order", "label": "迁移顺序", "focus": ["user-adapter", "shared-tdesign", "admin-tdesign", "shared-contract", "service-api"], "note": "先建立适配层与主题桥接,再按普通控件、反馈弹层、复杂控件的顺序逐页迁移。" }
+ ]
+ },
+ "components": [
+ { "id": "user-pages", "type": "frontend", "label": "用户端页面", "sublabel": "web/src/pages + user-console", "tag": "保留页面与业务流程", "pos": [60, 170], "size": [190, 72] },
+ { "id": "user-antd", "type": "external", "label": "历史 Ant Design", "sublabel": "已从当前用户端依赖清理", "tag": "历史迁移节点", "pos": [60, 300], "size": [190, 68] },
+ { "id": "user-radix", "type": "external", "label": "历史 Radix 封装", "sublabel": "普通控件已收敛", "tag": "历史迁移节点", "pos": [60, 425], "size": [190, 68] },
+ { "id": "user-adapter", "type": "frontend", "label": "用户端样式适配层", "sublabel": "保留用户 tokens 与页面 API", "tag": "过渡与隔离边界", "pos": [360, 170], "size": [215, 72] },
+ { "id": "admin-pages", "type": "frontend", "label": "管理台页面", "sublabel": "admin/src/pages + layouts", "tag": "页面业务不变", "pos": [60, 520], "size": [190, 72] },
+ { "id": "admin-tdesign", "type": "frontend", "label": "管理台 TDesign", "sublabel": "tdesign-react + 现有主题", "tag": "保持视觉不变", "pos": [360, 520], "size": [215, 72] },
+ { "id": "shared-tdesign", "type": "frontend", "label": "统一 TDesign 基础层", "sublabel": "组件、弹层、交互与无障碍", "tag": "目标共享层", "pos": [660, 305], "size": [225, 78] },
+ { "id": "lineicons", "type": "frontend", "label": "本地 Lineicons", "sublabel": "web 与 admin 精选子集", "tag": "两端共用图标规范", "pos": [960, 165], "size": [220, 72] },
+ { "id": "shared-contract", "type": "backend", "label": "共享接口契约", "sublabel": "packages/contracts", "tag": "与 UI 解耦", "pos": [960, 365], "size": [220, 72] },
+ { "id": "service-api", "type": "backend", "label": "平台服务端", "sublabel": "server/src/app/http.ts", "tag": "业务逻辑不迁移", "pos": [960, 520], "size": [220, 72] }
+ ],
+ "boundaries": [
+ { "kind": "region", "label": "用户端现状与历史节点", "wraps": ["user-pages", "user-adapter", "user-antd", "user-radix"] },
+ { "kind": "region", "label": "现状:管理台依赖", "wraps": ["admin-pages", "admin-tdesign"] },
+ { "kind": "security-group", "label": "目标:统一 TDesign 与 Lineicons", "wraps": ["user-adapter", "shared-tdesign", "lineicons"] },
+ { "kind": "region", "label": "稳定业务边界", "wraps": ["shared-contract", "service-api"] }
+ ],
+ "connections": [
+ { "id": "user-pages-to-adapter", "from": "user-pages", "to": "user-adapter", "label": "页面 props 保留", "variant": "emphasis", "fromSide": "right", "toSide": "left", "labelDy": -20 },
+ { "id": "adapter-to-shared", "from": "user-adapter", "to": "shared-tdesign", "label": "组件 API 适配", "variant": "emphasis", "fromSide": "right", "toSide": "left", "labelDy": -18 },
+ { "id": "admin-pages-to-tdesign", "from": "admin-pages", "to": "admin-tdesign", "label": "当前已接入", "variant": "emphasis", "fromSide": "right", "toSide": "left", "labelDy": -20 },
+ { "id": "admin-tdesign-to-shared", "from": "admin-tdesign", "to": "shared-tdesign", "label": "抽取稳定基础能力", "variant": "emphasis", "fromSide": "top", "toSide": "bottom", "labelDy": 24 },
+ { "id": "shared-to-lineicons", "from": "shared-tdesign", "to": "lineicons", "label": "图标插槽统一", "variant": "emphasis", "fromSide": "right", "toSide": "left", "labelDy": -18 },
+ { "id": "shared-to-contract", "from": "shared-tdesign", "to": "shared-contract", "fromSide": "right", "toSide": "left" },
+ { "id": "contract-to-api", "from": "shared-contract", "to": "service-api", "variant": "emphasis", "fromSide": "bottom", "toSide": "top" }
+ ],
+ "cards": [
+ { "dot": "emerald", "title": "可行性结论", "items": ["可行:UI 组件替换不改变 server、contracts、路由和业务数据", "管理台 TDesign 作为视觉基准,用户端通过适配层保留现有 tokens 与布局", "统一后可减少重复弹层、表单状态和依赖加载"] },
+ { "dot": "violet", "title": "保留与删除", "items": ["保留用户端页面、中文文案、主题变量、业务组件和 Lineicons", "完成等价能力后删除用户端 Ant Design 核心依赖与重复的 Radix 普通控件封装", "标签多选、搜索下拉、日期、上传和画布控件先由适配层承接"] },
+ { "dot": "amber", "title": "迁移门槛", "items": ["先建立 TDesign 主题桥接和用户端适配 API,禁止新页面继续引入 Ant Design", "每类控件都要验证键盘、ESC、遮罩、主题、移动端和表单受控状态", "按普通输入/选择器、反馈弹层、复杂业务组件分批切换并逐批删除旧依赖"] }
+ ]
+}
diff --git a/docs/miragenflow-ui-unification.architecture.visual-check.1440x900.dark.png b/docs/miragenflow-ui-unification.architecture.visual-check.1440x900.dark.png
new file mode 100644
index 0000000..450d0aa
Binary files /dev/null and b/docs/miragenflow-ui-unification.architecture.visual-check.1440x900.dark.png differ
diff --git a/docs/miragenflow-ui-unification.architecture.visual-check.1440x900.light.png b/docs/miragenflow-ui-unification.architecture.visual-check.1440x900.light.png
new file mode 100644
index 0000000..388c83a
Binary files /dev/null and b/docs/miragenflow-ui-unification.architecture.visual-check.1440x900.light.png differ
diff --git a/docs/miragenflow-ui-unification.architecture.visual-check.2048x1320.dark.png b/docs/miragenflow-ui-unification.architecture.visual-check.2048x1320.dark.png
new file mode 100644
index 0000000..34fe497
Binary files /dev/null and b/docs/miragenflow-ui-unification.architecture.visual-check.2048x1320.dark.png differ
diff --git a/docs/miragenflow-ui-unification.architecture.visual-check.2048x1320.light.png b/docs/miragenflow-ui-unification.architecture.visual-check.2048x1320.light.png
new file mode 100644
index 0000000..db4211d
Binary files /dev/null and b/docs/miragenflow-ui-unification.architecture.visual-check.2048x1320.light.png differ
diff --git a/docs/miragenflow-ui-unification.architecture.visual-check.html b/docs/miragenflow-ui-unification.architecture.visual-check.html
new file mode 100644
index 0000000..34e02ed
--- /dev/null
+++ b/docs/miragenflow-ui-unification.architecture.visual-check.html
@@ -0,0 +1,32 @@
+
+
+
+
+
+Archify visual-check · miragenflow-ui-unification.architecture.html
+
+
+
+
+
+
+
+ LIGHT · 1440×900 · containment pass
+
+
+
+ DARK · 1440×900 · containment pass
+
+
+
+ LIGHT · 2048×1320 · containment pass
+
+
+
+ DARK · 2048×1320 · containment pass
+
+
+
+
diff --git a/docs/miragenflow-ui-unification.architecture.visual-check.json b/docs/miragenflow-ui-unification.architecture.visual-check.json
new file mode 100644
index 0000000..142b669
--- /dev/null
+++ b/docs/miragenflow-ui-unification.architecture.visual-check.json
@@ -0,0 +1,482 @@
+{
+ "schemaVersion": 1,
+ "ok": true,
+ "command": "visual-check",
+ "status": "pass",
+ "visualReview": "pending",
+ "artifact": {
+ "path": "/Users/qiu/Desktop/MiragenFlow/docs/miragenflow-ui-unification.architecture.html",
+ "sha256": "938072c739f1168fa950d99ea45e889d673308e931d4f63eda40d60b993442e3",
+ "bytes": 713110
+ },
+ "state": {
+ "detail": "read",
+ "motion": "still"
+ },
+ "chrome": {
+ "status": "available",
+ "executable": "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
+ },
+ "containment": {
+ "status": "pass",
+ "viewports": [
+ {
+ "width": 1440,
+ "height": 900,
+ "theme": "light",
+ "innerWidth": 1440,
+ "innerHeight": 900,
+ "scrollWidth": 1440,
+ "scrollHeight": 900,
+ "overflowX": false,
+ "overflowY": false,
+ "ok": true,
+ "readerWidth": 960,
+ "diagramWidth": 930,
+ "viewBoxWidth": 1250,
+ "minimumProjectedNodeTextPx": 6.696,
+ "minimumProjectedNodeText": "web/src/pages + user-console",
+ "minimumProjectedNodeTextDetail": "context",
+ "minimumRequiredNodeTextPx": 6,
+ "readabilityOk": true,
+ "hasLegend": true,
+ "hasNavigationDock": true,
+ "legendDockIntersectionArea": 0,
+ "viewerChromeReserve": 0,
+ "viewerChromeActive": false,
+ "viewerChromeOk": true,
+ "resolvedTheme": "light"
+ },
+ {
+ "width": 1600,
+ "height": 1000,
+ "theme": "light",
+ "innerWidth": 1600,
+ "innerHeight": 1000,
+ "scrollWidth": 1600,
+ "scrollHeight": 1000,
+ "overflowX": false,
+ "overflowY": false,
+ "ok": true,
+ "readerWidth": 1089,
+ "diagramWidth": 1059,
+ "viewBoxWidth": 1250,
+ "minimumProjectedNodeTextPx": 7.6248,
+ "minimumProjectedNodeText": "web/src/pages + user-console",
+ "minimumProjectedNodeTextDetail": "context",
+ "minimumRequiredNodeTextPx": 6,
+ "readabilityOk": true,
+ "hasLegend": true,
+ "hasNavigationDock": true,
+ "legendDockIntersectionArea": 0,
+ "viewerChromeReserve": 0,
+ "viewerChromeActive": false,
+ "viewerChromeOk": true,
+ "resolvedTheme": "light"
+ },
+ {
+ "width": 1920,
+ "height": 1080,
+ "theme": "light",
+ "innerWidth": 1920,
+ "innerHeight": 1080,
+ "scrollWidth": 1920,
+ "scrollHeight": 1080,
+ "overflowX": false,
+ "overflowY": false,
+ "ok": true,
+ "readerWidth": 1267,
+ "diagramWidth": 1237,
+ "viewBoxWidth": 1250,
+ "minimumProjectedNodeTextPx": 8.9064,
+ "minimumProjectedNodeText": "web/src/pages + user-console",
+ "minimumProjectedNodeTextDetail": "context",
+ "minimumRequiredNodeTextPx": 6,
+ "readabilityOk": true,
+ "hasLegend": true,
+ "hasNavigationDock": true,
+ "legendDockIntersectionArea": 0,
+ "viewerChromeReserve": 0,
+ "viewerChromeActive": false,
+ "viewerChromeOk": true,
+ "resolvedTheme": "light"
+ },
+ {
+ "width": 2048,
+ "height": 1320,
+ "theme": "light",
+ "innerWidth": 2048,
+ "innerHeight": 1320,
+ "scrollWidth": 2048,
+ "scrollHeight": 1320,
+ "overflowX": false,
+ "overflowY": false,
+ "ok": true,
+ "readerWidth": 1695,
+ "diagramWidth": 1645,
+ "viewBoxWidth": 1250,
+ "minimumProjectedNodeTextPx": 9,
+ "minimumProjectedNodeText": "web/src/pages + user-console",
+ "minimumProjectedNodeTextDetail": "context",
+ "minimumRequiredNodeTextPx": 6,
+ "readabilityOk": true,
+ "hasLegend": true,
+ "hasNavigationDock": true,
+ "legendDockIntersectionArea": 0,
+ "viewerChromeReserve": 0,
+ "viewerChromeActive": false,
+ "viewerChromeOk": true,
+ "resolvedTheme": "light"
+ }
+ ]
+ },
+ "readability": {
+ "status": "pass",
+ "minimumProjectedNodeTextPx": 6,
+ "viewports": [
+ {
+ "width": 1440,
+ "height": 900,
+ "theme": "light",
+ "innerWidth": 1440,
+ "innerHeight": 900,
+ "scrollWidth": 1440,
+ "scrollHeight": 900,
+ "overflowX": false,
+ "overflowY": false,
+ "ok": true,
+ "readerWidth": 960,
+ "diagramWidth": 930,
+ "viewBoxWidth": 1250,
+ "minimumProjectedNodeTextPx": 6.696,
+ "minimumProjectedNodeText": "web/src/pages + user-console",
+ "minimumProjectedNodeTextDetail": "context",
+ "minimumRequiredNodeTextPx": 6,
+ "readabilityOk": true,
+ "hasLegend": true,
+ "hasNavigationDock": true,
+ "legendDockIntersectionArea": 0,
+ "viewerChromeReserve": 0,
+ "viewerChromeActive": false,
+ "viewerChromeOk": true,
+ "resolvedTheme": "light"
+ },
+ {
+ "width": 1600,
+ "height": 1000,
+ "theme": "light",
+ "innerWidth": 1600,
+ "innerHeight": 1000,
+ "scrollWidth": 1600,
+ "scrollHeight": 1000,
+ "overflowX": false,
+ "overflowY": false,
+ "ok": true,
+ "readerWidth": 1089,
+ "diagramWidth": 1059,
+ "viewBoxWidth": 1250,
+ "minimumProjectedNodeTextPx": 7.6248,
+ "minimumProjectedNodeText": "web/src/pages + user-console",
+ "minimumProjectedNodeTextDetail": "context",
+ "minimumRequiredNodeTextPx": 6,
+ "readabilityOk": true,
+ "hasLegend": true,
+ "hasNavigationDock": true,
+ "legendDockIntersectionArea": 0,
+ "viewerChromeReserve": 0,
+ "viewerChromeActive": false,
+ "viewerChromeOk": true,
+ "resolvedTheme": "light"
+ },
+ {
+ "width": 1920,
+ "height": 1080,
+ "theme": "light",
+ "innerWidth": 1920,
+ "innerHeight": 1080,
+ "scrollWidth": 1920,
+ "scrollHeight": 1080,
+ "overflowX": false,
+ "overflowY": false,
+ "ok": true,
+ "readerWidth": 1267,
+ "diagramWidth": 1237,
+ "viewBoxWidth": 1250,
+ "minimumProjectedNodeTextPx": 8.9064,
+ "minimumProjectedNodeText": "web/src/pages + user-console",
+ "minimumProjectedNodeTextDetail": "context",
+ "minimumRequiredNodeTextPx": 6,
+ "readabilityOk": true,
+ "hasLegend": true,
+ "hasNavigationDock": true,
+ "legendDockIntersectionArea": 0,
+ "viewerChromeReserve": 0,
+ "viewerChromeActive": false,
+ "viewerChromeOk": true,
+ "resolvedTheme": "light"
+ },
+ {
+ "width": 2048,
+ "height": 1320,
+ "theme": "light",
+ "innerWidth": 2048,
+ "innerHeight": 1320,
+ "scrollWidth": 2048,
+ "scrollHeight": 1320,
+ "overflowX": false,
+ "overflowY": false,
+ "ok": true,
+ "readerWidth": 1695,
+ "diagramWidth": 1645,
+ "viewBoxWidth": 1250,
+ "minimumProjectedNodeTextPx": 9,
+ "minimumProjectedNodeText": "web/src/pages + user-console",
+ "minimumProjectedNodeTextDetail": "context",
+ "minimumRequiredNodeTextPx": 6,
+ "readabilityOk": true,
+ "hasLegend": true,
+ "hasNavigationDock": true,
+ "legendDockIntersectionArea": 0,
+ "viewerChromeReserve": 0,
+ "viewerChromeActive": false,
+ "viewerChromeOk": true,
+ "resolvedTheme": "light"
+ }
+ ]
+ },
+ "viewerChrome": {
+ "status": "pass",
+ "viewports": [
+ {
+ "width": 1440,
+ "height": 900,
+ "theme": "light",
+ "innerWidth": 1440,
+ "innerHeight": 900,
+ "scrollWidth": 1440,
+ "scrollHeight": 900,
+ "overflowX": false,
+ "overflowY": false,
+ "ok": true,
+ "readerWidth": 960,
+ "diagramWidth": 930,
+ "viewBoxWidth": 1250,
+ "minimumProjectedNodeTextPx": 6.696,
+ "minimumProjectedNodeText": "web/src/pages + user-console",
+ "minimumProjectedNodeTextDetail": "context",
+ "minimumRequiredNodeTextPx": 6,
+ "readabilityOk": true,
+ "hasLegend": true,
+ "hasNavigationDock": true,
+ "legendDockIntersectionArea": 0,
+ "viewerChromeReserve": 0,
+ "viewerChromeActive": false,
+ "viewerChromeOk": true,
+ "resolvedTheme": "light"
+ },
+ {
+ "width": 1600,
+ "height": 1000,
+ "theme": "light",
+ "innerWidth": 1600,
+ "innerHeight": 1000,
+ "scrollWidth": 1600,
+ "scrollHeight": 1000,
+ "overflowX": false,
+ "overflowY": false,
+ "ok": true,
+ "readerWidth": 1089,
+ "diagramWidth": 1059,
+ "viewBoxWidth": 1250,
+ "minimumProjectedNodeTextPx": 7.6248,
+ "minimumProjectedNodeText": "web/src/pages + user-console",
+ "minimumProjectedNodeTextDetail": "context",
+ "minimumRequiredNodeTextPx": 6,
+ "readabilityOk": true,
+ "hasLegend": true,
+ "hasNavigationDock": true,
+ "legendDockIntersectionArea": 0,
+ "viewerChromeReserve": 0,
+ "viewerChromeActive": false,
+ "viewerChromeOk": true,
+ "resolvedTheme": "light"
+ },
+ {
+ "width": 1920,
+ "height": 1080,
+ "theme": "light",
+ "innerWidth": 1920,
+ "innerHeight": 1080,
+ "scrollWidth": 1920,
+ "scrollHeight": 1080,
+ "overflowX": false,
+ "overflowY": false,
+ "ok": true,
+ "readerWidth": 1267,
+ "diagramWidth": 1237,
+ "viewBoxWidth": 1250,
+ "minimumProjectedNodeTextPx": 8.9064,
+ "minimumProjectedNodeText": "web/src/pages + user-console",
+ "minimumProjectedNodeTextDetail": "context",
+ "minimumRequiredNodeTextPx": 6,
+ "readabilityOk": true,
+ "hasLegend": true,
+ "hasNavigationDock": true,
+ "legendDockIntersectionArea": 0,
+ "viewerChromeReserve": 0,
+ "viewerChromeActive": false,
+ "viewerChromeOk": true,
+ "resolvedTheme": "light"
+ },
+ {
+ "width": 2048,
+ "height": 1320,
+ "theme": "light",
+ "innerWidth": 2048,
+ "innerHeight": 1320,
+ "scrollWidth": 2048,
+ "scrollHeight": 1320,
+ "overflowX": false,
+ "overflowY": false,
+ "ok": true,
+ "readerWidth": 1695,
+ "diagramWidth": 1645,
+ "viewBoxWidth": 1250,
+ "minimumProjectedNodeTextPx": 9,
+ "minimumProjectedNodeText": "web/src/pages + user-console",
+ "minimumProjectedNodeTextDetail": "context",
+ "minimumRequiredNodeTextPx": 6,
+ "readabilityOk": true,
+ "hasLegend": true,
+ "hasNavigationDock": true,
+ "legendDockIntersectionArea": 0,
+ "viewerChromeReserve": 0,
+ "viewerChromeActive": false,
+ "viewerChromeOk": true,
+ "resolvedTheme": "light"
+ }
+ ]
+ },
+ "captures": {
+ "status": "pass",
+ "screenshots": [
+ {
+ "width": 1440,
+ "height": 900,
+ "theme": "light",
+ "innerWidth": 1440,
+ "innerHeight": 900,
+ "scrollWidth": 1440,
+ "scrollHeight": 900,
+ "overflowX": false,
+ "overflowY": false,
+ "ok": true,
+ "readerWidth": 960,
+ "diagramWidth": 930,
+ "viewBoxWidth": 1250,
+ "minimumProjectedNodeTextPx": 6.696,
+ "minimumProjectedNodeText": "web/src/pages + user-console",
+ "minimumProjectedNodeTextDetail": "context",
+ "minimumRequiredNodeTextPx": 6,
+ "readabilityOk": true,
+ "hasLegend": true,
+ "hasNavigationDock": true,
+ "legendDockIntersectionArea": 0,
+ "viewerChromeReserve": 0,
+ "viewerChromeActive": false,
+ "viewerChromeOk": true,
+ "resolvedTheme": "light",
+ "file": "miragenflow-ui-unification.architecture.visual-check.1440x900.light.png"
+ },
+ {
+ "width": 1440,
+ "height": 900,
+ "theme": "dark",
+ "innerWidth": 1440,
+ "innerHeight": 900,
+ "scrollWidth": 1440,
+ "scrollHeight": 900,
+ "overflowX": false,
+ "overflowY": false,
+ "ok": true,
+ "readerWidth": 960,
+ "diagramWidth": 930,
+ "viewBoxWidth": 1250,
+ "minimumProjectedNodeTextPx": 6.696,
+ "minimumProjectedNodeText": "web/src/pages + user-console",
+ "minimumProjectedNodeTextDetail": "context",
+ "minimumRequiredNodeTextPx": 6,
+ "readabilityOk": true,
+ "hasLegend": true,
+ "hasNavigationDock": true,
+ "legendDockIntersectionArea": 0,
+ "viewerChromeReserve": 0,
+ "viewerChromeActive": false,
+ "viewerChromeOk": true,
+ "resolvedTheme": "dark",
+ "file": "miragenflow-ui-unification.architecture.visual-check.1440x900.dark.png"
+ },
+ {
+ "width": 2048,
+ "height": 1320,
+ "theme": "light",
+ "innerWidth": 2048,
+ "innerHeight": 1320,
+ "scrollWidth": 2048,
+ "scrollHeight": 1320,
+ "overflowX": false,
+ "overflowY": false,
+ "ok": true,
+ "readerWidth": 1695,
+ "diagramWidth": 1645,
+ "viewBoxWidth": 1250,
+ "minimumProjectedNodeTextPx": 9,
+ "minimumProjectedNodeText": "web/src/pages + user-console",
+ "minimumProjectedNodeTextDetail": "context",
+ "minimumRequiredNodeTextPx": 6,
+ "readabilityOk": true,
+ "hasLegend": true,
+ "hasNavigationDock": true,
+ "legendDockIntersectionArea": 0,
+ "viewerChromeReserve": 0,
+ "viewerChromeActive": false,
+ "viewerChromeOk": true,
+ "resolvedTheme": "light",
+ "file": "miragenflow-ui-unification.architecture.visual-check.2048x1320.light.png"
+ },
+ {
+ "width": 2048,
+ "height": 1320,
+ "theme": "dark",
+ "innerWidth": 2048,
+ "innerHeight": 1320,
+ "scrollWidth": 2048,
+ "scrollHeight": 1320,
+ "overflowX": false,
+ "overflowY": false,
+ "ok": true,
+ "readerWidth": 1695,
+ "diagramWidth": 1645,
+ "viewBoxWidth": 1250,
+ "minimumProjectedNodeTextPx": 9,
+ "minimumProjectedNodeText": "web/src/pages + user-console",
+ "minimumProjectedNodeTextDetail": "context",
+ "minimumRequiredNodeTextPx": 6,
+ "readabilityOk": true,
+ "hasLegend": true,
+ "hasNavigationDock": true,
+ "legendDockIntersectionArea": 0,
+ "viewerChromeReserve": 0,
+ "viewerChromeActive": false,
+ "viewerChromeOk": true,
+ "resolvedTheme": "dark",
+ "file": "miragenflow-ui-unification.architecture.visual-check.2048x1320.dark.png"
+ }
+ ],
+ "contactSheet": "miragenflow-ui-unification.architecture.visual-check.html"
+ },
+ "sidecars": {
+ "receipt": "miragenflow-ui-unification.architecture.visual-check.json",
+ "contactSheet": "miragenflow-ui-unification.architecture.visual-check.html"
+ }
+}
diff --git a/docs/src/app/[lang]/docs/progress/changelog/page.tsx b/docs/src/app/[lang]/docs/progress/changelog/page.tsx
index 3035bc4..1499811 100644
--- a/docs/src/app/[lang]/docs/progress/changelog/page.tsx
+++ b/docs/src/app/[lang]/docs/progress/changelog/page.tsx
@@ -11,7 +11,7 @@ const messages = {
en: {
title: 'Changelog',
description: 'Project release history',
- content: '# Changelog\n\nThe detailed changelog is currently maintained in Chinese. See the [source changelog](https://github.com/basketikun/infinite-canvas/blob/main/CHANGELOG.md) for all releases.',
+ content: '# Changelog\n\nThe detailed changelog is currently maintained in Chinese. See the [source changelog](https://git.awaioi.com/awaioi/MiragenFlow/src/branch/main/CHANGELOG.md) for all releases.',
},
'zh-CN': {
title: '更新日志',
diff --git a/docs/src/lib/shared.ts b/docs/src/lib/shared.ts
index 486352e..2f43608 100644
--- a/docs/src/lib/shared.ts
+++ b/docs/src/lib/shared.ts
@@ -5,10 +5,9 @@ export const appNames = {
export const docsRoute = '/docs';
export const docsContentRoute = '/llms.mdx/docs';
-// fill this with your actual GitHub info, for example:
export const gitConfig = {
- user: 'basketikun',
- repo: 'infinite-canvas',
+ user: 'awaioi',
+ repo: 'MiragenFlow',
branch: 'main',
docsContentDir: 'docs/content/docs',
};
diff --git a/goal.md b/goal.md
new file mode 100644
index 0000000..b179b77
--- /dev/null
+++ b/goal.md
@@ -0,0 +1,411 @@
+# MiragenFlow 管理台重构 Goal
+
+> 本轮综合长任务入口:[`docs/miragenflow-long-task-goal-prompt.md`](docs/miragenflow-long-task-goal-prompt.md)。本文继续维护管理台 22 页、权限和业务闭环细节;前后台组件统一规则见该入口及 `docs/miragenflow-tdesign-unification-goal.md`。如本文与综合入口的渠道交互描述冲突,以综合入口的“OpenAI 单一供应方式 + 三个可自由切换 Tab”规则为准。
+
+## 0. 目标状态
+
+- **目标对象**:`/Users/qiu/Desktop/MiragenFlow`
+- **目标名称**:管理台商业业务工作台重构
+- **当前阶段**:目标文档与逐页 SSII 基线已完成;代码已完成 P0 壳层/权限、认证开关、独立系统设置、用户详情分 Tab、任务详情/审批驳回、渠道/分组/模型产品编辑抽屉、渠道健康批量探活、任务并发投递、跨实例任务 dispatch 回载、系统概览趋势/告警、业务维度筛选、PostgreSQL 任务租约心跳/过期恢复、消息 outbox 发送期续租与业务幂等键、WebDAV 完成事务与保留期行锁扫描、管理员 MFA 开关门禁。仍需浏览器逐页验收,以及完整 PostgreSQL 领域表 CAS、远端 WebDAV/消息竞态、管理台列表跨路由异步回写保护和真实 provider 的部署验证。未完成项不得按“已完成”发布。
+- **依据**:`AGENTS.md`、`develop.md`、`system-diagrams.md`、`admin-plan.md`、`summary.md`
+- **跨端组件目标**:用户端与管理端统一 TDesign 的长期执行基线见 `docs/miragenflow-tdesign-unification-goal.md`;本文继续只负责管理台业务目标,不把两端页面、路由、权限或状态合并。
+- **禁止结果**:白屏、无恢复路径的 403、假按钮、首屏堆满配置字段、mock 冒充业务、未接入供应商伪装成已接入。
+
+## 1. 最终目标
+
+把当前平铺的 TDesign Starter 管理台重构为商业化 V1 管理工作台:
+
+1. 管理员从 `/admin/login` 默认只使用邮箱和密码登录(本地默认 `admin@admin.com` / `admin`);MFA/CAPTCHA 仅在后台增强开关显式开启时追加,关闭时界面不得出现验证码、动态码或恢复码输入框。
+2. 用户端认证提供密码登录(邮箱、手机号、用户名)和手机号验证码登录;注册提供邮箱、手机号两条路径,注册邮箱验证与注册手机号验证由 `/admin/settings` 独立开关控制,开启后先完成验证码验证再设置并确认密码。
+3. 登录后加载管理员角色、scope、MFA 状态和可见菜单;权限不足隐藏菜单,不能白屏。
+4. 左侧使用一级分组 + 二级页面,URL 统一为 `/admin/...`。
+5. 新增、编辑、启停、调价、审批、调账全部通过 Modal/Drawer 完成。
+6. 用户、渠道、分组、模型产品、价格、套餐、余额、支付、任务、存储、审计形成真实业务闭环。
+7. 供应商消息暂不接入;消息供应商和模板页面隐藏,只保留认证消息 outbox 状态和详情。
+8. 状态、能力、错误码、按钮全部中文;长文本和 capability 不能撑高表格。
+9. 所有写操作提供成功、加载、空态、403、409、422、503、网络断开和重试反馈。
+
+## 2. 必读文件
+
+实施前依次读取以下绝对路径:
+
+- `/Users/qiu/Desktop/MiragenFlow/AGENTS.md`
+- `/Users/qiu/Desktop/MiragenFlow/develop.md`
+- `/Users/qiu/Desktop/MiragenFlow/system-diagrams.md`
+- `/Users/qiu/Desktop/MiragenFlow/admin-plan.md`
+- `/Users/qiu/Desktop/MiragenFlow/summary.md`
+- `/Users/qiu/Desktop/MiragenFlow/admin/src/router/index.ts`
+- `/Users/qiu/Desktop/MiragenFlow/admin/src/layouts/components/Menu.tsx`
+- `/Users/qiu/Desktop/MiragenFlow/admin/src/layouts/components/AppRouter.tsx`
+- `/Users/qiu/Desktop/MiragenFlow/admin/src/components/ErrorPage/index.tsx`
+- `/Users/qiu/Desktop/MiragenFlow/admin/src/pages/Business/index.tsx`
+- `/Users/qiu/Desktop/MiragenFlow/admin/src/services/platform.ts`
+- `/Users/qiu/Desktop/MiragenFlow/server/src/app/http.ts`
+- `/Users/qiu/Desktop/MiragenFlow/server/src/shared/auth.ts`
+- `/Users/qiu/Desktop/MiragenFlow/packages/contracts/src/index.ts`
+- 根目录、`admin/`、`web/`、`server/` 的 `package.json`
+- `/Users/qiu/Desktop/MiragenFlow/scripts/dev-all.mjs`
+- `/Users/qiu/Desktop/MiragenFlow/scripts/build-all.mjs`
+
+页面/API变更必须同步 `admin-plan.md`、`system-diagrams.md`、`summary.md`、`CHANGELOG.md`、`docs/content/docs/progress/todo.mdx` 和 `docs/content/docs/progress/pending-test.mdx`。
+
+## 3. 产品边界
+
+### 必须进入 V1
+
+管理员登录/RBAC/审计、菜单分组、系统概览、用户管理、认证消息、管理员安全、渠道、渠道健康、渠道组、模型产品、价格、金币单位、换算、套餐、余额账本、充值订单、支付事件、支付适配器、审批、任务中心、WS 指标、存储策略、系统设置、审计日志。
+
+### 不进入可见菜单
+
+消息供应商、消息模板、视频、动画、真实 3D、团队、云端项目 CRUD、内容治理、Local Agent、Codex、MCP、Skill、远程 Prompt、插件市场。支付只保留人工充值、mock 和 adapter 接口,等待第三方支付文档。
+
+前台、公开 API、日志和导出不得暴露 provider 名称、Base URL、API Key、内部 provider model ID、渠道顺序、Cookie、WebDAV 密文或完整敏感提示词。
+
+## 4. 菜单信息架构
+
+```text
+元境幻生管理台
+├─ 总览
+│ └─ 系统概览 /admin/dashboard/base
+├─ 用户与认证
+│ ├─ 用户管理 /admin/users
+│ ├─ 认证消息 /admin/auth-messages
+│ └─ 管理员安全 /admin/security/mfa
+├─ 渠道与模型
+│ ├─ 渠道管理 /admin/channels
+│ ├─ 渠道健康 /admin/channel-health
+│ ├─ 渠道分组 /admin/channel-groups
+│ ├─ 模型产品 /admin/model-products
+│ └─ 价格规则 /admin/pricing
+├─ 计费与套餐
+│ ├─ 金币单位与换算 /admin/billing/units
+│ ├─ 余额与账本 /admin/billing
+│ ├─ 套餐管理 /admin/plans
+│ ├─ 充值订单 /admin/recharges
+│ ├─ 支付事件 /admin/payments
+│ ├─ 支付适配器 /admin/payment-providers
+│ └─ 审批中心 /admin/approvals
+├─ 任务与运行
+│ ├─ 任务中心 /admin/tasks
+│ └─ 实时指标 /admin/ws-metrics
+├─ 存储与系统
+│ ├─ 存储策略 /admin/storage
+│ └─ 系统设置 /admin/settings
+└─ 审计
+ ├─ 日志大板 /admin/audit
+ └─ 详细日志 /admin/audit-logs
+```
+
+菜单规则:一级节点可展开/收起;收起时侧栏完全隐藏,不保留窄图标栏;刷新 URL 自动展开;无 scope 的二级项不渲染;`admin:read` 只读,写操作需 `admin:write` 或 `admin:finance`;登录、403、404 不入业务菜单。
+
+## 5. 统一布局与交互
+
+```text
++--------------------------------------------------------------------------------+
+| 元境幻生 | 一级菜单/二级菜单 | 面包屑 | 刷新 | 帮助 | 管理员菜单 |
++--------------------------------------------------------------------------------+
+| 页面标题+描述 [搜索] [筛选] [批量操作] [新增/配置] |
++--------------------------------------------------------------------------------+
+| KPI/统计卡/当前版本/告警 |
++--------------------------------------------------------------------------------+
+| 固定列宽表格:选择 | 关键字段 | 中文状态 | 更新时间 | 操作 |
++--------------------------------------------------------------------------------+
+| 右侧 Drawer/Modal:字段分组 | 校验 | 风险提示 | 取消 | 保存/提交 |
++--------------------------------------------------------------------------------+
+```
+
+全页面必须有加载骨架、空态、网络错误重试、403权限卡、提交中禁用、成功 Toast、422 字段错误、409 版本冲突、503 保留表单重试;支持 Tab/Enter/Escape 和焦点回收。窄屏页面整体不得产生无意义的横向溢出,但业务表格允许在局部容器内横向滚动,以保留列表语义和列可读性。长文本用摘要/Tooltip/详情抽屉,不能撑高表格。
+
+## 6. 页面目标、布局和验收
+
+### 6.1 登录/错误
+
+- `/admin/login`:居中邮箱密码卡片;开关开启时追加 CAPTCHA/MFA;成功进入 `/admin/dashboard/base`;失败保留输入并显示错误。
+- `/admin/403`、`/admin/404`:错误码、角色、所需 scope、`返回系统概览`、`重新登录`、`切换管理员`;按钮必须可用,禁止静态白板。
+
+### 6.2 系统概览 `/admin/dashboard/base`
+
+顶部时间范围/刷新;KPI 展示用户、活跃用户、任务、金币消耗、充值金额、队列深度、WS 在线;图表展示任务成功/失败率、P50/P95、模型可用率、渠道错误率、邮件/SMS 成功率和支付回调异常;卡片可带筛选跳转,单卡片可重试。
+
+### 6.3 用户管理 `/admin/users`
+
+搜索、状态/等级/验证/创建时间筛选;表格展示脱敏身份、验证状态、MFA、套餐、并发、每日额度、余额、最近登录;`新增用户` 打开 Drawer,填写用户名、邮箱、手机号和初始密码;详情包含账户/会话/任务/账本/权益/审计 Tab;支持冻结、解冻、重置密码、重发认证和调账申请。新增成功只显示一次初始密码,冻结后计费任务必须拒绝。
+
+### 6.4 认证消息 `/admin/auth-messages`
+
+状态/通道/用途/时间筛选;表格展示目标脱敏、用途、通道、状态、重试次数、错误摘要、时间;详情 Drawer 展示状态时间线和完整多行内容;重试需确认;内容使用 `white-space: pre-wrap`;供应商配置不在此页面实现。
+
+### 6.5 管理员安全 `/admin/security/mfa`
+
+状态卡、全局策略、启用/验证/关闭操作和恢复码 Drawer;关闭需要当前 TOTP/原因;开关关闭时登录只需邮箱密码,开启时追加 MFA;恢复码只显示一次,全部操作写审计。
+
+### 6.6 渠道管理 `/admin/channels`
+
+列表首屏仅展示搜索/筛选/`新增渠道`按钮和固定列;列使用中文业务字段(名称、供应方式、环境、区域、能力、状态、健康、优先级、并发/限流、成本、维护窗口、版本),不直接展示密钥、Base URL 或供应商内部模型 ID。新增和编辑共用同一风格的 Drawer,内部只有三个可自由切换的 Tab:`基础信息`、`模型列表`、`模型映射`;不使用上一步/下一步,也不提供没有真实逻辑的“通用”供应商模板。支持编辑、启停、凭证轮换、手动探活和 If-Match;密钥不回显。
+
+基础信息填写名称、完整请求地址(示例 `https://example.com` 或 `https://example.com/v1`)和 API Key;API Key 只校验去除首尾空白后非空。模型列表仅在管理员主动点击按钮后请求 `{基础地址}/v1/models`,结果由管理员勾选加入渠道模型列表;模型映射是可选的,分别保存平台显示模型 ID 与供应商请求模型 ID,不能自动把拉取结果变成映射。切换 Tab、编辑回填、保存失败和关闭重开都必须保留或恢复正确草稿。
+
+### 6.7 渠道健康 `/admin/channel-health`
+
+状态/分组/时间筛选、健康统计卡和固定表格;列为渠道、中文状态、延迟、成功率、连续失败、熔断剩余、探活时间、维护窗口;映射 `healthy=健康`、`degraded=降级`、`open=熔断`、`disabled=已停用`;支持单项/批量探活和错误时间线。
+
+### 6.8 渠道分组 `/admin/channel-groups`
+
+分组列表和 `新增分组`;详情 Drawer 包含用途/档位、启停、可用渠道池、拖拽顺序、重试预算、超时、并发、熔断、退避、half-open、公共失败文案、替代模型、发布和回滚。保存前校验渠道存在、启用、顺序唯一;模拟首渠道失败必须展示 fallback 顺序。
+
+### 6.9 模型产品 `/admin/model-products`
+
+列表和 `新增产品`;Drawer 配置公开 ID、名称、档位、默认质量、能力、分辨率、最大数量、参考图数量、mask、渠道组、价格、替代模型、生效时间、状态和版本。能力用中文标签,超出显示 `+N` 和详情 Drawer;启用产品必须绑定启用渠道组;公开 API 只显示公开 ID。
+
+### 6.10 价格规则 `/admin/pricing`
+
+产品/分辨率/档位筛选;价格版本表;`新增规则`/`调整价格`弹窗包含基础价、数量阶梯、分辨率倍率、金币单位、生效时间、原因;提交携带 If-Match;旧版本返回 409,展示当前版本并允许重载;成功刷新且保留筛选。
+
+### 6.11 金币单位与换算 `/admin/billing/units`
+
+单位卡展示图标、名称、短码、精度、比例、版本;换算规则表支持新增/编辑/停用/生效期;账本使用最小整数金币,比例变更有审计和版本。
+
+### 6.12 套餐 `/admin/plans`
+
+卡片/表格切换;新增和编辑 Drawer 配置公开编码、名称、图标、介绍、排序、时长、包含金币、购买金额、并发、队列优先级、保留期、最大延长、允许模型/渠道组、上架、归档、版本;上下架即时影响公开目录,支持回滚。
+
+### 6.13 余额 `/admin/billing`
+
+余额统计、用户/流水类型筛选、账本表和调账申请;展示 available/reserved、reserve/settle/release/refund,支持按用户/任务/订单追踪;调账必须申请→审批→执行,`available + reserved` 不变量成立。
+
+### 6.14 充值/支付 `/admin/recharges`、`/admin/payments`
+
+充值表展示订单、用户、金额、金币、渠道、状态、创建/回调时间;支付表展示 eventId、订单、签名、金额/币种、状态、重放结果和异常;重复 webhook 必须幂等,状态和账本同事务。
+
+### 6.15 支付适配器 `/admin/payment-providers`
+
+列表展示名称、mock/adapter 模式、状态、密钥状态、版本;Drawer 支持新增 mock、启停、测试 webhook、服务端加密签名密钥;第三方支付等待文档,不伪造 provider。
+
+### 6.16 审批 `/admin/approvals`
+
+待审批/已执行/已驳回/已过期 Tab;详情显示申请人、审批人、原因、金额/对象、审计;人工充值、退款、调账和高风险配置要求申请人/审批人分离,过期自动标记,策略开启时要求审批 MFA。
+
+### 6.17 任务 `/admin/tasks`
+
+状态/类型/模型/用户/时间筛选;列表展示 taskId、用户、类型、公开模型、状态、预扣/实扣、attempt、更新时间;详情 Drawer 展示输入快照、状态时间线、attempt、输出、账本、错误、脱敏 provider request ID;支持取消、重试、unknown 对账。
+
+### 6.18 实时指标 `/admin/ws-metrics`
+
+展示在线连接、订阅、事件吞吐、心跳、断线、轮询降级、事件积压;支持时间窗口、自动刷新暂停、断线恢复和实例/任务筛选;重连 cursor 不重复。
+
+### 6.19 存储/系统 `/admin/storage`、`/admin/settings`
+
+存储策略配置 staging TTL、结果/原图保留、WebDAV manifest 保留、最大延长、归档模式、GC、扫描/quarantine、对象存储 adapter;系统设置必须是独立资源(不能复用 `storage-policies`),配置用户验证/MFA/CAPTCHA、管理员 MFA/CAPTCHA、限流、队列和审计保留;默认关闭所有认证增强,开关即时影响认证并写入版本/审计,状态可追踪,对象存储默认关闭。
+
+### 6.20 审计 `/admin/audit`、`/admin/audit-logs`
+
+`/admin/audit` 展示分类、HTTP 结果和时间趋势,`/admin/audit-logs` 展示具体操作人、动作、对象和请求来源;两者均可打开详情 Drawer,展示脱敏 before/after、IP、requestId、correlationId、hash 链和摘要导出。绝不展示密钥、密码、Cookie、完整提示词、原图或 WebDAV 凭证。
+
+## 6.21 逐页 SSII 索引
+
+详细 ASCII/SSII 线框统一维护在 `/Users/qiu/Desktop/MiragenFlow/admin-plan.md` 第 4-5 节;每页验收按以下结构执行:顶部标题与筛选、统计/告警区、固定列宽列表、详情 Drawer/Modal、加载/空/错误/冲突状态。
+
+| 页面 | 主布局 | 关键交互 |
+|---|---|---|
+| 系统概览 | KPI 卡 + 趋势 + 告警 | 时间范围、单卡跳转、局部重试 |
+| 用户管理 | 筛选栏 + 用户表 + 详情 Drawer | 新增、冻结、重置、重发、强退、调账 |
+| 认证消息 | 状态筛选 + outbox 表 + 消息 Drawer | 多行详情、失败重试 |
+| 管理员安全 | 策略卡 + MFA 操作 Drawer | setup/verify/disable、恢复码一次性展示 |
+| 渠道管理 | 渠道表 + 三 Tab 新增/编辑 Drawer | 地址校验、手动拉取模型、勾选加入、可选映射、启停、轮换密钥 |
+| 渠道健康 | 健康 KPI + 渠道表 | 中文状态、单项/全部探活、日志 |
+| 渠道分组 | 分组表 + 拖拽排序 Drawer | 成员校验、fallback 预览、版本保存 |
+| 模型产品 | 产品表 + 产品 Drawer | 能力折叠、分辨率、渠道绑定、上下架 |
+| 价格规则 | 版本表 + 价格 Modal | 新增/调价、If-Match 冲突重载 |
+| 金币单位/换算 | 单位卡 + 规则表 | 比例、精度、舍入和生效期 |
+| 套餐管理 | 卡片/表格 + 套餐 Drawer | 权益、发布/回滚、归档 |
+| 余额与账本 | 余额 KPI + 流水表 | 调账申请、审批链、reserve/settle/release 追踪 |
+| 充值订单/支付事件 | 订单表 + 事件详情 | 失败标记、退款申请、webhook 幂等 |
+| 支付适配器 | 适配器表 + 配置 Drawer | mock/adapter、启停、密钥状态 |
+| 审批中心 | 状态 Tab + 审批详情 | 申请人/审批人分离、条件 MFA、过期处理 |
+| 任务中心 | 任务表 + 任务 Drawer | attempt、取消、重试、unknown 对账 |
+| 实时指标 | 指标卡 + 时间序列 | WS 心跳、断线恢复、cursor 去重 |
+| 存储策略/系统设置 | 策略表 + 设置 Drawer | TTL、WebDAV、归档、增强开关 |
+| 审计日志 | 筛选表 + 审计 Drawer | 脱敏 before/after、hash 链和导出摘要 |
+
+## 6.22 22 个页面的逐页 SSII 骨架
+
+以下矩阵是开发与浏览器验收的逐页“图片蓝本”。每行对应一张页面线框:`头部` 固定标题/面包屑/刷新,`筛选` 固定在标题下方,`主体` 使用固定列宽表格或指标卡,`侧栏` 使用 Drawer/Modal;加载、空、403、409、422、503 都在同一位置替换主体,不改变页面骨架。
+
+| # | 页面 | 头部与筛选 | 主体与详情 | 强交互验收 |
+|---:|---|---|---|---|
+| 1 | `/admin/dashboard/base` | 时间范围、刷新、告警过滤 | KPI 卡、趋势图、异常列表 | 卡片跳转、单卡重试、断线恢复 |
+| 2 | `/admin/users` | 搜索、状态、等级、验证、时间 | 用户表、账户 Drawer、六个详情 Tab | 新增、冻结、重置、重发、强退、调账 |
+| 3 | `/admin/auth-messages` | 状态、通道、用途、时间 | outbox 表、消息详情 Drawer | 多行内容、失败重试、目标脱敏 |
+| 4 | `/admin/security/mfa` | 管理员 MFA 状态/策略 | 策略卡、setup/verify/disable Drawer | 开关门禁、恢复码一次性展示 |
+| 5 | `/admin/channels` | 供应方式、环境、区域、状态 | 渠道表、三 Tab 新增/编辑 Drawer | 地址校验、手动 `/v1/models`、勾选模型、可选映射、启停、轮换密钥 |
+| 6 | `/admin/channel-health` | 健康、分组、时间 | 健康 KPI、渠道表、错误时间线 | 单项/批量探活、中文状态、重试 |
+| 7 | `/admin/channel-groups` | 用途、档位、状态 | 分组表、成员排序/fallback Drawer | 拖拽排序、成员启用校验、模拟故障 |
+| 8 | `/admin/model-products` | 档位、能力、渠道组、状态 | 产品表、产品配置 Drawer | 能力折叠、渠道绑定、发布/回滚 |
+| 9 | `/admin/pricing` | 产品、分辨率、档位、版本 | 价格版本表、调价 Modal | If-Match 409 展示当前版本并重载 |
+| 10 | `/admin/billing/units` | 单位、状态、版本 | 金币单位卡与换算规则表、编辑 Drawer | 比例/舍入/精度校验、版本审计 |
+| 11 | `/admin/billing` | 用户、流水类型、时间 | 余额 KPI、账本表、调账 Drawer | reserve/settle/release/refund 追踪 |
+| 12 | `/admin/plans` | 上架、归档、版本 | 套餐卡/表、套餐 Drawer | 权益配置、发布、回滚、归档 |
+| 13 | `/admin/recharges` | 用户、状态、时间 | 充值订单表、订单 Drawer | 失败标记、退款申请、幂等重放 |
+| 14 | `/admin/payments` | provider、状态、时间 | 支付事件表、事件 Drawer | 签名/金额/币种校验、异常重试 |
+| 15 | `/admin/payment-providers` | 模式、状态、版本 | 适配器表、配置 Drawer | mock 测试 webhook、密钥只显示状态 |
+| 16 | `/admin/approvals` | 待处理/已执行/已驳回/已过期 Tab | 审批表、申请详情 Drawer | 申请人/审批人分离、条件 MFA、驳回 |
+| 17 | `/admin/tasks` | 状态、类型、模型、用户、时间 | 任务表、任务详情/attempt Drawer | 取消、重试、unknown 对账、脱敏 |
+| 18 | `/admin/ws-metrics` | 时间窗、实例、任务 | WS KPI、吞吐/心跳/积压趋势 | 暂停刷新、断线恢复、cursor 去重 |
+| 19 | `/admin/storage` | adapter、保留期、GC 状态 | 存储策略表、配置 Drawer | TTL、归档、扫描/quarantine、GC |
+| 20 | `/admin/settings` | 安全增强、限流、队列、审计 | 系统设置表、版本化编辑 Drawer | 默认关闭、即时生效、If-Match |
+| 21 | `/admin/audit` | 操作者、动作、对象、结果、时间 | 日志大板、分类占比和趋势 | 200/4xx/5xx 统计、中文分类、局部重试 |
+| 22 | `/admin/audit-logs` | 操作者、动作、对象、结果、时间 | 详细日志表、脱敏详情 Drawer | 中文行为、requestId、hash 链、详情重试 |
+
+## 7. 服务端契约工作项
+
+### 权限和总览
+
+- 统一 `super_admin/operator/finance/support/auditor` 到 scopes;未知角色不能得到空权限。
+- `/api/v1/admin/dashboard`、`me`、菜单权限和前端 scope 一致。
+- 403/404 返回稳定错误码和 requestId,前端必须有可恢复页面。
+
+### 用户
+
+- 增加管理员创建用户 API,校验身份唯一、初始密码、状态、套餐和限制字段;密码只显示一次并只存哈希。
+- 冻结、解冻、重置、重发认证、强退和调账申请审计;详情数据脱敏并按对象权限返回。
+
+### 渠道和模型
+
+- 渠道新增校验 HTTPS、DNS/私网、协议、密钥、超时、并发和限流;密钥服务端密文保存。
+- 渠道组校验成员存在、启用、排序唯一;模型产品校验渠道组存在且可用;价格支持 If-Match 和幂等。
+- 公共 DTO 不得包含 provider model ID、Base URL、Key 或渠道顺序。
+
+### 计费、任务、存储、审计
+
+- 余额、账本、支付订单、支付事件和审批保持一致事务;任务取消/重试/unknown 不重复扣款。
+- WS 指标、attempt、队列租约、WebDAV/GC/归档状态可查询。
+- 审计 before/after 递归脱敏,日志和导出不包含真实密钥或完整敏感内容。
+
+## 8. 实施阶段
+
+### P0 壳层与权限
+
+嵌套路由、一级/二级菜单、权限加载、403/404恢复、角色映射、真实总览指标和错误态。
+
+### P1 核心配置闭环
+
+用户新增;渠道新增/探活/启停;健康中文化;渠道组排序/fallback;模型产品能力/分辨率/绑定;价格调整/版本冲突。
+
+### P2 商业和运维
+
+金币、换算、套餐、余额、充值、支付、支付适配器、审批、任务、WS 指标、存储、设置、审计、认证消息多行详情。
+
+### P3 统一体验
+
+加载/空/错/403/409/503、固定列宽、能力折叠、中文状态、响应式 Drawer、键盘无障碍、敏感扫描、同源回归。
+
+## 9. 验收标准
+
+### 静态和构建
+
+```bash
+npm --prefix server run typecheck
+npm --prefix server test
+npm --prefix web run typecheck
+npm --prefix web run build
+npm --prefix admin run typecheck
+npm --prefix admin run build
+npm --prefix admin run lint
+npm run build:all
+git diff --check
+```
+
+### 路由和菜单
+
+- `/admin/` 自动进入 `/admin/dashboard/base`。
+- 22 个可见二级页面均能进入;刷新后一级展开、二级高亮、面包屑正确。
+- 无权限角色不显示对应菜单;403/404 有返回总览和重新登录按钮。
+- 消息供应商/模板不出现在菜单。
+
+### 业务闭环
+
+1. 创建用户→列表→详情→冻结;冻结后计费任务拒绝。
+2. 新增渠道→探活→分组拖拽→模型绑定→调价。
+3. 首渠道失败→fallback→任务 attempt 时间线。
+4. 旧 If-Match 调价→409→重载→成功。
+5. 重放 webhook→订单/余额/账本不重复增加。
+6. 调账→审批→执行→完整审计。
+7. 取消/失败/unknown→释放或按实际结算,不重复扣款。
+8. WebDAV 保留/归档/删除→队列/GC 状态可见。
+
+### 视觉和交互
+
+- 首屏不铺满配置字段,新增/编辑只能通过按钮打开弹窗/抽屉。
+- 认证消息保留换行,capability 不撑高表格,状态均为中文。
+- 所有操作在加载、失败、403、409、503 有明确反馈。
+- Tab/Enter/Escape 和焦点回收可用;宽屏、窄窗口和缩放下页面整体不产生无意义的横向溢出,业务表格在局部容器内允许横向滚动以保留列可读性。
+- 路由、筛选或分页切换期间,旧列表请求不会回写当前页面的行数据、加载态或错误态;必须有延迟请求回归证据。
+
+### 安全扫描
+
+```bash
+git ls-files | rg '(^|/)(\.env|.*\.(pem|key))$|(^|/)(uploads?|staging|snapshots?)(/|$)'
+rg -n -i 'api[_ -]?key|authorization:|bearer |providerModelId|baseUrl|BEGIN (RSA|EC|OPENSSH) PRIVATE KEY' \
+ web/dist admin/dist server/src web/src admin/src --glob '!*.map'
+```
+
+## 10. 交付物
+
+- 嵌套路由、一级/二级菜单和权限安全壳层。
+- 22 个可见页面的真实布局、弹窗/抽屉、错误态和业务闭环。
+- 用户、渠道、分组、模型、价格、计费、任务、存储、审计 API 对齐。
+- 更新 `admin-plan.md`、`system-diagrams.md`、`summary.md`、`CHANGELOG.md`、`todo.mdx`、`pending-test.mdx`。
+- 完整构建、测试、同源启动和浏览器验收记录;未完成内容继续记录在 TODO/Pending Test。
+
+## 10.1 进度反馈格式
+
+每完成一个可独立验收的小任务,立即用以下格式反馈,不把未验证内容计入完成度:
+
+```text
+已完成:{小任务名称}
+验收证据:{命令、页面、API 或截图路径}
+当前进度:██████▎░░░ {整数}%
+剩余阻塞:{没有则写“无”;有则列出具体文件/接口}
+```
+
+百分比按“已通过验收的交付项 / 全部交付项”计算;文档规划、代码实现、自动化测试、同源运行和浏览器人工验收分别计项,不能用文档完成替代功能完成。
+
+## 11. 目标引导提示词
+
+```text
+你现在负责完成 /Users/qiu/Desktop/MiragenFlow 的管理台重构 Goal。
+
+先完整读取:AGENTS.md、develop.md、system-diagrams.md、admin-plan.md、goal.md、summary.md;
+再读取 admin/src/router/index.ts、Menu.tsx、AppRouter.tsx、ErrorPage、Business/index.tsx、
+admin/src/services/platform.ts、server/src/app/http.ts、server/src/shared/auth.ts、
+packages/contracts/src/index.ts,以及各 package.json、scripts/dev-all.mjs、scripts/build-all.mjs。
+
+必须实现:
+1. 路由统一为 /admin/...,实现 7 个一级菜单和 22 个二级页面;逐页同步 `admin-plan.md` 的 ASCII SSII 与 `system-diagrams.md` 的 Mermaid;权限不足隐藏菜单并显示可恢复 403 页面。
+2. 管理员默认只显示邮箱/账号和密码输入;MFA、验证码、恢复码均由后台增强开关控制,关闭时不得渲染输入框。完成真实系统概览(含 P50/P95、渠道/模型/消息/支付异常指标)、用户新增/详情分 Tab/冻结/重置密码/重发认证、认证消息多行详情和管理员安全。
+3. 完成渠道新增弹窗、编辑/探活/启停、中文健康状态、分组拖拽排序与 fallback。
+4. 完成模型产品能力/分辨率/渠道组绑定、发布/回滚和价格 If-Match 冲突处理。
+5. 完成金币、换算、套餐、余额、充值失败/退款申请、支付事件、支付适配器和审批中心;管理员 MFA 增强开关关闭时审批不追加动态码,开启时才校验。
+6. 完成任务 attempt/unknown/取消/重试、WS 指标、独立 `/api/v1/admin/settings` 系统设置 API/持久化/If-Match、存储策略和审计;系统设置不能复用 `storage-policies`。
+7. 消息供应商和模板本期隐藏,不接入第三方 provider,不伪造可用配置。
+8. 所有新增/编辑/启停/调价/审批/调账均用弹窗或 Drawer;固定列宽,中文状态,长文本和 capability 不撑表格。
+9. 不向前台、公开 API、日志或导出暴露 provider 名称、Base URL、API Key、内部模型 ID、渠道顺序。
+
+按 P0 壳层权限 → P1 渠道/分组/模型/价格 → P2 用户/计费/任务/存储/审计 → P3 视觉回归实施。
+不要做视频、动画、真实 3D、团队、内容治理、Local Agent、远程 Prompt 或插件市场。
+不要回滚已有改动,不要用 mock 冒充业务,不要把配置字段永久铺在首屏。
+
+每个阶段执行并记录:
+npm --prefix server run typecheck
+npm --prefix server test
+npm --prefix web run typecheck && npm --prefix web run build
+npm --prefix admin run typecheck && npm --prefix admin run build && npm --prefix admin run lint
+npm run build:all
+git diff --check
+
+最终启动 npm run dev:all,用同一 origin 验证 /、/admin/、/api/health、/api/ready;
+逐页验证 22 个页面、权限角色、弹窗/抽屉、ASCII SSII 对应布局、加载/空/错误/403/409/503、中文排版、
+渠道→分组→模型→价格、用户新增、任务、余额、审批、WebDAV 和审计;执行敏感扫描,
+同步更新 admin-plan.md、system-diagrams.md、summary.md、CHANGELOG.md、todo.mdx、pending-test.mdx。
+未完成内容必须明确记录,不能宣布全部完成。
+```
diff --git a/nginx.conf b/nginx.conf
index 0a1d42b..6d9c336 100644
--- a/nginx.conf
+++ b/nginx.conf
@@ -8,6 +8,33 @@ server {
return 301 /admin/;
}
+ # The API/WS gateway is kept behind the same public origin. In local
+ # compose it resolves to the `server` service; production can point this
+ # upstream at the colocated gateway process.
+ location /api/ {
+ proxy_pass http://server:3100;
+ proxy_http_version 1.1;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Request-Id $request_id;
+ proxy_set_header Connection "";
+ proxy_request_buffering on;
+ client_max_body_size 10m;
+ }
+
+ location /api/v1/ws/ {
+ proxy_pass http://server:3100;
+ proxy_http_version 1.1;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Request-Id $request_id;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection "upgrade";
+ proxy_read_timeout 1h;
+ proxy_send_timeout 1h;
+ proxy_buffering off;
+ }
+
location /admin/ {
try_files $uri $uri/ /admin/index.html;
}
@@ -16,8 +43,4 @@ server {
try_files $uri $uri/ /index.html;
}
- # 运行期配置由容器启动时生成,禁止缓存,确保改环境变量后即时生效。
- location = /config.js {
- add_header Cache-Control "no-store";
- }
}
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..016b6f5
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,1222 @@
+{
+ "name": "miragenflow-workspace",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "miragenflow-workspace",
+ "workspaces": [
+ "packages/contracts",
+ "packages/ui",
+ "server"
+ ]
+ },
+ "node_modules/@babel/runtime": {
+ "version": "7.26.10",
+ "resolved": "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.26.10.tgz",
+ "integrity": "sha512-2WJMeRQPHKSPemqk/awGrAiuFfzBmOIPXKizAsVhWH9YJqLZ0H+HS4c8loHGgW6utJ3E/ejXQUsiGaQy2NZ9Fw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "regenerator-runtime": "^0.14.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz",
+ "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.28.2.tgz",
+ "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz",
+ "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.28.2.tgz",
+ "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz",
+ "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz",
+ "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz",
+ "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz",
+ "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz",
+ "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz",
+ "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz",
+ "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==",
+ "cpu": [
+ "ia32"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz",
+ "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==",
+ "cpu": [
+ "loong64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz",
+ "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==",
+ "cpu": [
+ "mips64el"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz",
+ "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz",
+ "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==",
+ "cpu": [
+ "riscv64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz",
+ "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==",
+ "cpu": [
+ "s390x"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz",
+ "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-arm64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz",
+ "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz",
+ "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz",
+ "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz",
+ "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openharmony-arm64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmmirror.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz",
+ "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz",
+ "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz",
+ "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz",
+ "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==",
+ "cpu": [
+ "ia32"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz",
+ "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@ioredis/commands": {
+ "version": "1.10.0",
+ "resolved": "https://registry.npmmirror.com/@ioredis/commands/-/commands-1.10.0.tgz",
+ "integrity": "sha512-UmeW7z4LfctwoQ5wkhVzgq8tXkreED2xZGpX+Bg+zA+WJFZCT6c062AfCK/Dfk81xZnnwdhJCUMkitihRaoC2Q==",
+ "license": "MIT"
+ },
+ "node_modules/@miragenflow/contracts": {
+ "resolved": "packages/contracts",
+ "link": true
+ },
+ "node_modules/@miragenflow/server": {
+ "resolved": "server",
+ "link": true
+ },
+ "node_modules/@miragenflow/ui": {
+ "resolved": "packages/ui",
+ "link": true
+ },
+ "node_modules/@popperjs/core": {
+ "version": "2.11.8",
+ "resolved": "https://registry.npmmirror.com/@popperjs/core/-/core-2.11.8.tgz",
+ "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/popperjs"
+ }
+ },
+ "node_modules/@types/node": {
+ "version": "22.20.1",
+ "resolved": "https://registry.npmmirror.com/@types/node/-/node-22.20.1.tgz",
+ "integrity": "sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~6.21.0"
+ }
+ },
+ "node_modules/@types/pg": {
+ "version": "8.23.1",
+ "resolved": "https://registry.npmmirror.com/@types/pg/-/pg-8.23.1.tgz",
+ "integrity": "sha512-fKVHpikPdg4GKks3JuLEhvwSyvwzF23hnabPy6DD8ljVbC7+6J5dQzdv4arV6jqq57djnMgs1HKBxX4P8aBI3A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*",
+ "pg-protocol": "*",
+ "pg-types": "^2.2.0"
+ }
+ },
+ "node_modules/@types/react": {
+ "version": "19.1.12",
+ "resolved": "https://registry.npmmirror.com/@types/react/-/react-19.1.12.tgz",
+ "integrity": "sha512-cMoR+FoAf/Jyq6+Df2/Z41jISvGZZ2eTlnsaJRptmZ76Caldwy1odD4xTr/gNV9VLj0AWgg/nmkevIyUfIIq5w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "csstype": "^3.0.2"
+ }
+ },
+ "node_modules/@types/react-dom": {
+ "version": "19.1.9",
+ "resolved": "https://registry.npmmirror.com/@types/react-dom/-/react-dom-19.1.9.tgz",
+ "integrity": "sha512-qXRuZaOsAdXKFyOhRBg6Lqqc0yay13vN7KrIg4L7N4aaHN68ma9OK3NE1BoDFgFOTfM7zg+3/8+2n8rLUH3OKQ==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "^19.0.0"
+ }
+ },
+ "node_modules/@types/sortablejs": {
+ "version": "1.15.9",
+ "resolved": "https://registry.npmmirror.com/@types/sortablejs/-/sortablejs-1.15.9.tgz",
+ "integrity": "sha512-7HP+rZGE2p886PKV9c9OJzLBI6BBJu1O7lJGYnPyG3fS4/duUCcngkNCjsLwIMV+WMqANe3tt4irrXHSIe68OQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/validator": {
+ "version": "13.15.10",
+ "resolved": "https://registry.npmmirror.com/@types/validator/-/validator-13.15.10.tgz",
+ "integrity": "sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/ws": {
+ "version": "8.18.1",
+ "resolved": "https://registry.npmmirror.com/@types/ws/-/ws-8.18.1.tgz",
+ "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/classnames": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmmirror.com/classnames/-/classnames-2.5.1.tgz",
+ "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/cluster-key-slot": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmmirror.com/cluster-key-slot/-/cluster-key-slot-1.1.1.tgz",
+ "integrity": "sha512-rwHwUfXL40Chm1r08yrhU3qpUvdVlgkKNeyeGPOxnW8/SyVDvgRaed/Uz54AqWNaTCAThlj6QAs3TZcKI0xDEw==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/csstype": {
+ "version": "3.2.3",
+ "resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.2.3.tgz",
+ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/dayjs": {
+ "version": "1.11.10",
+ "resolved": "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.10.tgz",
+ "integrity": "sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/denque": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmmirror.com/denque/-/denque-2.1.0.tgz",
+ "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/dom-helpers": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmmirror.com/dom-helpers/-/dom-helpers-5.2.1.tgz",
+ "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.8.7",
+ "csstype": "^3.0.2"
+ }
+ },
+ "node_modules/esbuild": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.28.2.tgz",
+ "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.28.2",
+ "@esbuild/android-arm": "0.28.2",
+ "@esbuild/android-arm64": "0.28.2",
+ "@esbuild/android-x64": "0.28.2",
+ "@esbuild/darwin-arm64": "0.28.2",
+ "@esbuild/darwin-x64": "0.28.2",
+ "@esbuild/freebsd-arm64": "0.28.2",
+ "@esbuild/freebsd-x64": "0.28.2",
+ "@esbuild/linux-arm": "0.28.2",
+ "@esbuild/linux-arm64": "0.28.2",
+ "@esbuild/linux-ia32": "0.28.2",
+ "@esbuild/linux-loong64": "0.28.2",
+ "@esbuild/linux-mips64el": "0.28.2",
+ "@esbuild/linux-ppc64": "0.28.2",
+ "@esbuild/linux-riscv64": "0.28.2",
+ "@esbuild/linux-s390x": "0.28.2",
+ "@esbuild/linux-x64": "0.28.2",
+ "@esbuild/netbsd-arm64": "0.28.2",
+ "@esbuild/netbsd-x64": "0.28.2",
+ "@esbuild/openbsd-arm64": "0.28.2",
+ "@esbuild/openbsd-x64": "0.28.2",
+ "@esbuild/openharmony-arm64": "0.28.2",
+ "@esbuild/sunos-x64": "0.28.2",
+ "@esbuild/win32-arm64": "0.28.2",
+ "@esbuild/win32-ia32": "0.28.2",
+ "@esbuild/win32-x64": "0.28.2"
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/hoist-non-react-statics": {
+ "version": "3.3.2",
+ "resolved": "https://registry.npmmirror.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
+ "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "react-is": "^16.7.0"
+ }
+ },
+ "node_modules/hoist-non-react-statics/node_modules/react-is": {
+ "version": "16.13.1",
+ "resolved": "https://registry.npmmirror.com/react-is/-/react-is-16.13.1.tgz",
+ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/ioredis": {
+ "version": "5.11.1",
+ "resolved": "https://registry.npmmirror.com/ioredis/-/ioredis-5.11.1.tgz",
+ "integrity": "sha512-ehuGcf94bQXhfagULNXrJdfnWO38v070jxSx/qE87Kjzmu2fU7ro5EFAb+OPituLqgfyuQaym5DlrNydW2sJ9A==",
+ "license": "MIT",
+ "dependencies": {
+ "@ioredis/commands": "1.10.0",
+ "cluster-key-slot": "1.1.1",
+ "debug": "4.4.3",
+ "denque": "2.1.0",
+ "redis-errors": "1.2.0",
+ "redis-parser": "3.0.0",
+ "standard-as-callback": "2.1.0"
+ },
+ "engines": {
+ "node": ">=12.22.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/ioredis"
+ }
+ },
+ "node_modules/ipaddr.js": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmmirror.com/ipaddr.js/-/ipaddr.js-2.5.0.tgz",
+ "integrity": "sha512-aq+t5NAc+cS6rZQQVWC2x98CPqGtKKTMDd4Gaodv0wShnItdKg/51djkGJ1hqH+Oy0ivDftCbSLCQob8zso01w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmmirror.com/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/lodash-es": {
+ "version": "4.18.1",
+ "resolved": "https://registry.npmmirror.com/lodash-es/-/lodash-es-4.18.1.tgz",
+ "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/loose-envify": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmmirror.com/loose-envify/-/loose-envify-1.4.0.tgz",
+ "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "js-tokens": "^3.0.0 || ^4.0.0"
+ },
+ "bin": {
+ "loose-envify": "cli.js"
+ }
+ },
+ "node_modules/mitt": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmmirror.com/mitt/-/mitt-3.0.1.tgz",
+ "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
+ "node_modules/object-assign": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmmirror.com/object-assign/-/object-assign-4.1.1.tgz",
+ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/performance-now": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmmirror.com/performance-now/-/performance-now-2.1.0.tgz",
+ "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/pg": {
+ "version": "8.23.0",
+ "resolved": "https://registry.npmmirror.com/pg/-/pg-8.23.0.tgz",
+ "integrity": "sha512-Ip2EQCngowJLGOfCwkFhPXU7/ljlhn6Rxlmy4XYfL2Y+vyRM59+8uR2xqRWKdYmbXmxCFOAmKxBuSUCdF34qLg==",
+ "license": "MIT",
+ "dependencies": {
+ "pg-connection-string": "^2.14.0",
+ "pg-pool": "^3.14.0",
+ "pg-protocol": "^1.16.0",
+ "pg-types": "2.2.0",
+ "pgpass": "1.0.5"
+ },
+ "engines": {
+ "node": ">= 16.0.0"
+ },
+ "optionalDependencies": {
+ "pg-cloudflare": "^1.4.0"
+ },
+ "peerDependencies": {
+ "pg-native": ">=3.0.1"
+ },
+ "peerDependenciesMeta": {
+ "pg-native": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/pg-cloudflare": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmmirror.com/pg-cloudflare/-/pg-cloudflare-1.4.0.tgz",
+ "integrity": "sha512-Vo7z/6rrQYxpNRylp4Tlob2elzbh+N/MOQbxFVWCxS7oEx6jF53GTJFxK2WWpKuBRkmiin4Mt+xofFDjx09R0A==",
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/pg-connection-string": {
+ "version": "2.14.0",
+ "resolved": "https://registry.npmmirror.com/pg-connection-string/-/pg-connection-string-2.14.0.tgz",
+ "integrity": "sha512-XwWDGcLRGCXAR8F/AM5bG7Q+A3Wm2s6QeEjlOKZLlH3UYcguiqCWKyWXVag5TLTIjR7oOJUY8kcADaZgWPyLeg==",
+ "license": "MIT"
+ },
+ "node_modules/pg-int8": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmmirror.com/pg-int8/-/pg-int8-1.0.1.tgz",
+ "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=4.0.0"
+ }
+ },
+ "node_modules/pg-pool": {
+ "version": "3.14.0",
+ "resolved": "https://registry.npmmirror.com/pg-pool/-/pg-pool-3.14.0.tgz",
+ "integrity": "sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "pg": ">=8.0"
+ }
+ },
+ "node_modules/pg-protocol": {
+ "version": "1.16.0",
+ "resolved": "https://registry.npmmirror.com/pg-protocol/-/pg-protocol-1.16.0.tgz",
+ "integrity": "sha512-sILXutLVjCLjcDuOmvhX5e2Z4cS5qG/6Bu3VkpFwdf/633ElGLpEh9bgmuI5I4sqKqkifQiGyiCcx1HdtrK7tg==",
+ "license": "MIT"
+ },
+ "node_modules/pg-types": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmmirror.com/pg-types/-/pg-types-2.2.0.tgz",
+ "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==",
+ "license": "MIT",
+ "dependencies": {
+ "pg-int8": "1.0.1",
+ "postgres-array": "~2.0.0",
+ "postgres-bytea": "~1.0.0",
+ "postgres-date": "~1.0.4",
+ "postgres-interval": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/pgpass": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmmirror.com/pgpass/-/pgpass-1.0.5.tgz",
+ "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==",
+ "license": "MIT",
+ "dependencies": {
+ "split2": "^4.1.0"
+ }
+ },
+ "node_modules/postgres-array": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmmirror.com/postgres-array/-/postgres-array-2.0.0.tgz",
+ "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/postgres-bytea": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmmirror.com/postgres-bytea/-/postgres-bytea-1.0.1.tgz",
+ "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/postgres-date": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmmirror.com/postgres-date/-/postgres-date-1.0.7.tgz",
+ "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/postgres-interval": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmmirror.com/postgres-interval/-/postgres-interval-1.2.0.tgz",
+ "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==",
+ "license": "MIT",
+ "dependencies": {
+ "xtend": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/prop-types": {
+ "version": "15.8.1",
+ "resolved": "https://registry.npmmirror.com/prop-types/-/prop-types-15.8.1.tgz",
+ "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.4.0",
+ "object-assign": "^4.1.1",
+ "react-is": "^16.13.1"
+ }
+ },
+ "node_modules/prop-types/node_modules/react-is": {
+ "version": "16.13.1",
+ "resolved": "https://registry.npmmirror.com/react-is/-/react-is-16.13.1.tgz",
+ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/raf": {
+ "version": "3.4.1",
+ "resolved": "https://registry.npmmirror.com/raf/-/raf-3.4.1.tgz",
+ "integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "performance-now": "^2.1.0"
+ }
+ },
+ "node_modules/react": {
+ "version": "19.2.5",
+ "resolved": "https://registry.npmmirror.com/react/-/react-19.2.5.tgz",
+ "integrity": "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-dom": {
+ "version": "19.2.5",
+ "resolved": "https://registry.npmmirror.com/react-dom/-/react-dom-19.2.5.tgz",
+ "integrity": "sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "scheduler": "^0.27.0"
+ },
+ "peerDependencies": {
+ "react": "^19.2.5"
+ }
+ },
+ "node_modules/react-fast-compare": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmmirror.com/react-fast-compare/-/react-fast-compare-3.2.2.tgz",
+ "integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/react-is": {
+ "version": "18.3.1",
+ "resolved": "https://registry.npmmirror.com/react-is/-/react-is-18.3.1.tgz",
+ "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/react-transition-group": {
+ "version": "4.4.5",
+ "resolved": "https://registry.npmmirror.com/react-transition-group/-/react-transition-group-4.4.5.tgz",
+ "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@babel/runtime": "^7.5.5",
+ "dom-helpers": "^5.0.1",
+ "loose-envify": "^1.4.0",
+ "prop-types": "^15.6.2"
+ },
+ "peerDependencies": {
+ "react": ">=16.6.0",
+ "react-dom": ">=16.6.0"
+ }
+ },
+ "node_modules/redis-errors": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmmirror.com/redis-errors/-/redis-errors-1.2.0.tgz",
+ "integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/redis-parser": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmmirror.com/redis-parser/-/redis-parser-3.0.0.tgz",
+ "integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==",
+ "license": "MIT",
+ "dependencies": {
+ "redis-errors": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/regenerator-runtime": {
+ "version": "0.14.1",
+ "resolved": "https://registry.npmmirror.com/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
+ "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/scheduler": {
+ "version": "0.27.0",
+ "resolved": "https://registry.npmmirror.com/scheduler/-/scheduler-0.27.0.tgz",
+ "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/sortablejs": {
+ "version": "1.15.7",
+ "resolved": "https://registry.npmmirror.com/sortablejs/-/sortablejs-1.15.7.tgz",
+ "integrity": "sha512-Kk8wLQPlS+yi1ZEf48a4+fzHa4yxjC30M/Sr2AnQu+f/MPwvvX9XjZ6OWejiz8crBsLwSq8GHqaxaET7u6ux0A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/split2": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmmirror.com/split2/-/split2-4.2.0.tgz",
+ "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==",
+ "license": "ISC",
+ "engines": {
+ "node": ">= 10.x"
+ }
+ },
+ "node_modules/standard-as-callback": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmmirror.com/standard-as-callback/-/standard-as-callback-2.1.0.tgz",
+ "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==",
+ "license": "MIT"
+ },
+ "node_modules/tdesign-icons-react": {
+ "version": "0.6.11",
+ "resolved": "https://registry.npmmirror.com/tdesign-icons-react/-/tdesign-icons-react-0.6.11.tgz",
+ "integrity": "sha512-zH9Wr7QvX5y3WRF9hvm25ci04SLaPHB9SPN2FfWMHQf9f5ITmli+3abJkGwcePTeTYE4vmNBnAy6dxRSMCui1g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.16.5",
+ "classnames": "^2.2.6"
+ },
+ "peerDependencies": {
+ "react": ">=16.13.1",
+ "react-dom": ">=16.13.1"
+ }
+ },
+ "node_modules/tdesign-react": {
+ "version": "1.15.1",
+ "resolved": "https://registry.npmmirror.com/tdesign-react/-/tdesign-react-1.15.1.tgz",
+ "integrity": "sha512-5htrIZtYvek54Fpz/8VZp+yC2zWmzY00S7BztIz3khZpyrYBrwQgLdMKkE5I/ueEBiQQFjwNh1oAo4oFZIPSrw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "~7.26.7",
+ "@popperjs/core": "~2.11.2",
+ "@types/sortablejs": "^1.10.7",
+ "@types/validator": "^13.1.3",
+ "classnames": "~2.5.1",
+ "dayjs": "1.11.10",
+ "hoist-non-react-statics": "~3.3.2",
+ "lodash-es": "^4.17.21",
+ "mitt": "^3.0.0",
+ "raf": "~3.4.1",
+ "react-fast-compare": "^3.2.2",
+ "react-is": "^18.2.0",
+ "react-transition-group": "~4.4.1",
+ "sortablejs": "^1.15.0",
+ "tdesign-icons-react": "^0.6.1",
+ "tslib": "~2.3.1",
+ "validator": "~13.15.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.13.1",
+ "react-dom": ">=16.13.1"
+ }
+ },
+ "node_modules/tslib": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.3.1.tgz",
+ "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==",
+ "dev": true,
+ "license": "0BSD"
+ },
+ "node_modules/tsx": {
+ "version": "4.23.12",
+ "resolved": "https://registry.npmmirror.com/tsx/-/tsx-4.23.12.tgz",
+ "integrity": "sha512-FDf4L4sYzKtzWYhU/Xm0AQFdTjdIxNo9ElTf2mxXM6k8YMHXzYUe4yODVaXP4V9uMFbVg8c0qyBccK2OOxb45Q==",
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "~0.28.0"
+ },
+ "bin": {
+ "tsx": "dist/cli.mjs"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "5.9.3",
+ "resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.9.3.tgz",
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/undici": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmmirror.com/undici/-/undici-7.29.0.tgz",
+ "integrity": "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=20.18.1"
+ }
+ },
+ "node_modules/undici-types": {
+ "version": "6.21.0",
+ "resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-6.21.0.tgz",
+ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/validator": {
+ "version": "13.15.35",
+ "resolved": "https://registry.npmmirror.com/validator/-/validator-13.15.35.tgz",
+ "integrity": "sha512-TQ5pAGhd5whStmqWvYF4OjQROlmv9SMFVt37qoCBdqRffuuklWYQlCNnEs2ZaIBD1kZRNnikiZOS1eqgkar0iw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/ws": {
+ "version": "8.21.3",
+ "resolved": "https://registry.npmmirror.com/ws/-/ws-8.21.3.tgz",
+ "integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "peerDependencies": {
+ "bufferutil": "^4.0.1",
+ "utf-8-validate": ">=5.0.2"
+ },
+ "peerDependenciesMeta": {
+ "bufferutil": {
+ "optional": true
+ },
+ "utf-8-validate": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/xtend": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmmirror.com/xtend/-/xtend-4.0.2.tgz",
+ "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4"
+ }
+ },
+ "node_modules/zod": {
+ "version": "3.25.76",
+ "resolved": "https://registry.npmmirror.com/zod/-/zod-3.25.76.tgz",
+ "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/colinhacks"
+ }
+ },
+ "packages/contracts": {
+ "name": "@miragenflow/contracts",
+ "version": "0.1.0"
+ },
+ "packages/ui": {
+ "name": "@miragenflow/ui",
+ "version": "0.1.0",
+ "devDependencies": {
+ "@types/react": "19.1.12",
+ "@types/react-dom": "19.1.9",
+ "react": "19.2.5",
+ "react-dom": "19.2.5",
+ "tdesign-react": "1.15.1"
+ },
+ "peerDependencies": {
+ "react": ">=18.0.0",
+ "react-dom": ">=18.0.0",
+ "tdesign-react": "1.15.1"
+ }
+ },
+ "server": {
+ "name": "@miragenflow/server",
+ "version": "0.1.0",
+ "dependencies": {
+ "ioredis": "^5.11.1",
+ "ipaddr.js": "^2.5.0",
+ "pg": "^8.13.1",
+ "tsx": "^4.19.2",
+ "undici": "^7.29.0",
+ "ws": "^8.18.0",
+ "zod": "^3.25.76"
+ },
+ "devDependencies": {
+ "@types/node": "^22.15.21",
+ "@types/pg": "^8.11.10",
+ "@types/ws": "^8.18.1",
+ "typescript": "^5.8.3"
+ }
+ }
+ }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..4cc57eb
--- /dev/null
+++ b/package.json
@@ -0,0 +1,20 @@
+{
+ "name": "miragenflow-workspace",
+ "private": true,
+ "workspaces": [
+ "packages/contracts",
+ "packages/ui",
+ "server"
+ ],
+ "scripts": {
+ "dev:all": "node scripts/dev-all.mjs",
+ "dev:server": "npm --prefix server run dev",
+ "typecheck:server": "npm --prefix server run typecheck",
+ "test:server": "npm --prefix server test",
+ "typecheck:web": "npm --prefix web run typecheck",
+ "typecheck:admin": "npm --prefix admin run typecheck",
+ "lint:admin": "npm --prefix admin run lint",
+ "build:all": "node scripts/build-all.mjs",
+ "test:all": "npm run test:server && npm run typecheck:web && npm run typecheck:admin"
+ }
+}
diff --git a/packages/contracts/package.json b/packages/contracts/package.json
new file mode 100644
index 0000000..a6bec61
--- /dev/null
+++ b/packages/contracts/package.json
@@ -0,0 +1,9 @@
+{
+ "name": "@miragenflow/contracts",
+ "version": "0.1.0",
+ "private": true,
+ "type": "module",
+ "exports": {
+ ".": "./src/index.ts"
+ }
+}
diff --git a/packages/contracts/src/index.ts b/packages/contracts/src/index.ts
new file mode 100644
index 0000000..5d56cca
--- /dev/null
+++ b/packages/contracts/src/index.ts
@@ -0,0 +1,462 @@
+export const CONTRACT_VERSION = "2026-08-20.v1" as const;
+export const API_PREFIX = "/api/v1" as const;
+export const WS_PATH = "/api/v1/ws/tasks" as const;
+
+export const ErrorCodes = {
+ Unauthorized: "UNAUTHORIZED",
+ Forbidden: "FORBIDDEN",
+ ValidationFailed: "VALIDATION_FAILED",
+ RateLimited: "RATE_LIMITED",
+ NotFound: "NOT_FOUND",
+ Conflict: "CONFLICT",
+ InsufficientBalance: "INSUFFICIENT_BALANCE",
+ CapabilityNotEnabled: "CAPABILITY_NOT_ENABLED",
+ ModelGroupUnavailable: "MODEL_GROUP_UNAVAILABLE",
+ Internal: "INTERNAL_ERROR",
+ VerificationRequired: "VERIFICATION_REQUIRED",
+ VerificationExpired: "VERIFICATION_EXPIRED",
+ SessionReused: "SESSION_REUSED",
+ IdempotencyConflict: "IDEMPOTENCY_CONFLICT",
+ ObjectNotFound: "OBJECT_NOT_FOUND",
+ ObjectOwnershipDenied: "OBJECT_OWNERSHIP_DENIED",
+ FileRejected: "FILE_REJECTED",
+ StorageUnavailable: "STORAGE_UNAVAILABLE",
+ ApprovalRequired: "APPROVAL_REQUIRED",
+ VersionConflict: "VERSION_CONFLICT",
+ AdminMfaRequired: "ADMIN_MFA_REQUIRED",
+} as const;
+
+export type ErrorCode = (typeof ErrorCodes)[keyof typeof ErrorCodes];
+
+export type ApiError = {
+ code: ErrorCode;
+ message: string;
+ requestId: string;
+ details?: Record;
+};
+
+export type ApiEnvelope =
+ | { ok: true; data: T; requestId: string }
+ | { ok: false; error: ApiError };
+
+export type AuthUser = {
+ id: string;
+ username?: string;
+ email?: string;
+ phone?: string;
+ displayName: string;
+ status: "pending_verification" | "active" | "frozen" | "closing";
+ mfaRequired: boolean;
+ mfaEnabled: boolean;
+ mfaSetupRequired: boolean;
+};
+
+export type SessionResponse = {
+ user: AuthUser;
+ accessToken: string;
+ accessTokenExpiresAt: string;
+};
+
+export type MfaEnrollment = {
+ secret: string;
+ provisioningUri: string;
+ recoveryCodes: string[];
+};
+
+export type UserMfaChallengeResponse =
+ | { mfaRequired: true; challengeId: string; challengeType: "verify" }
+ | { mfaRequired: true; challengeId: string; challengeType: "setup"; mfaEnrollment: MfaEnrollment };
+
+export type UserMfaSetupResponse = {
+ enabled: boolean;
+ challengeId: string;
+ mfaEnrollment: MfaEnrollment;
+};
+
+export type UserMfaVerifyResponse =
+ | (SessionResponse & { mfaRequired: false })
+ | { enabled: true; user: AuthUser };
+
+export type CaptchaAction = "register" | "login" | "verify" | "reset" | "admin-login";
+export type CaptchaChallengeResponse = { challengeId: string; question: string; expiresAt: string };
+
+export type ModelCapability = "image" | "text" | "audio" | "utility" | "3d" | "reverse-prompt" | "multi-angle" | "matting" | "split" | "upscale" | "expand";
+
+export type CapabilitySchema = {
+ capability: ModelCapability;
+ inputTypes: Array<"prompt" | "image" | "mask" | "audio" | "text">;
+ maxInputBytes?: number;
+ parameters?: Record;
+ resultTypes: Array<"image" | "text" | "audio" | "archive" | "3d">;
+ async: boolean;
+};
+
+export type PublicResolution = {
+ id: string;
+ label: string;
+ width: number;
+ height: number;
+ priceMultiplier: number;
+ /** Standard image ratios enabled for this billing preset; `auto` is implicit. */
+ ratios?: string[];
+};
+
+export type ReferenceImageInput = {
+ objectId: string;
+ seq: number;
+ name: string;
+};
+
+export type ReferenceImageGroup = {
+ id?: string;
+ images: ReferenceImageInput[];
+};
+
+export type ImageOutputMetadata = {
+ width?: number;
+ height?: number;
+ format?: "png" | "jpeg" | "webp" | string;
+ size?: string;
+ /** Public resolution tier selected for this task, when applicable. */
+ resolution?: "1K" | "2K" | "4K" | string;
+ revisedPrompt?: string;
+ usage?: Record;
+ source?: "base64" | "url";
+};
+
+export type PublicModelProduct = {
+ id: string;
+ publicModelId: string;
+ name: string;
+ tier: "basic" | "advanced" | "flagship";
+ capabilities: ModelCapability[];
+ resolutions: PublicResolution[];
+ basePrice: number;
+ currencyUnit: string;
+ enabled: boolean;
+ version?: number;
+ capabilitiesSchema?: CapabilitySchema[];
+ publicErrorMessages?: Partial>;
+ maxCount?: number;
+ maxConcurrent?: number;
+};
+
+export type InternalModelProduct = PublicModelProduct & {
+ channelGroupId?: string;
+};
+
+export type BalanceSummary = {
+ available: number;
+ reserved: number;
+ unit: { name: string; shortCode: string; icon: string; precision: number; version: number };
+ /** Reserved for future organization/team balances; V1 always omits it. */
+ organizationId?: string;
+};
+
+/** Organization/team scope is deliberately a nullable contract extension in
+ * V1. No organization records or sharing endpoints are enabled yet. */
+export type OrganizationScope = {
+ organizationId?: string;
+ workspaceOwnerId?: string;
+ objectScope?: "user" | "organization";
+};
+
+export type BalanceLedgerEntry = {
+ id: string;
+ userId: string;
+ type: "recharge" | "reserve" | "settle" | "release" | "refund" | "adjustment";
+ amount: number;
+ balanceUnitVersion: number;
+ idempotencyKey: string;
+ createdAt: string;
+ referenceId?: string;
+ bucketId?: string;
+ taskId?: string;
+ outputId?: string;
+ expiresAt?: string;
+ status?: "posted" | "voided";
+};
+
+export type BalanceBucket = OrganizationScope & {
+ id: string;
+ userId: string;
+ source: "recharge" | "plan" | "manual" | "bonus";
+ remaining: number;
+ expiresAt?: string;
+ priority: number;
+};
+
+export type BalanceUnitConfig = {
+ name: string;
+ shortCode: string;
+ icon: string;
+ precision: number;
+ version: number;
+ currencyCode: string;
+ conversionRate: number;
+ rounding: "floor" | "ceil" | "round";
+};
+
+export type MembershipPlan = {
+ id: string;
+ code: string;
+ name: string;
+ icon?: string;
+ description: string;
+ durationDays: number;
+ purchaseAmount: number;
+ includedBalance: number;
+ published: boolean;
+ allowedModelProductIds: string[];
+ allowedChannelGroupIds: string[];
+ maxConcurrent: number;
+ queuePriority: number;
+ retentionDays: number;
+ maxRetentionExtensionDays: number;
+ version: number;
+};
+
+export type TaskType = "image" | "text" | "audio" | "reverse-prompt" | "multi-angle" | "matting" | "split" | "upscale" | "expand" | "3d";
+export type TaskStatus = "queued" | "running" | "succeeded" | "partial" | "failed" | "unknown" | "canceled";
+
+export type CreateTaskRequest = OrganizationScope & {
+ modelProductId: string;
+ resolutionPresetId?: string;
+ prompt?: string;
+ references?: string[];
+ referenceGroup?: ReferenceImageGroup;
+ maskObjectId?: string;
+ count?: number;
+ canvasProjectId?: string;
+ params?: Record;
+};
+
+export type GenerationTask = OrganizationScope & {
+ id: string;
+ ownerId: string;
+ taskType: TaskType;
+ modelProductId: string;
+ status: TaskStatus;
+ estimatedCost: number;
+ reservedCost: number;
+ createdAt: string;
+ updatedAt: string;
+ eventSequence: number;
+ channelGroupId: string;
+ routeSnapshotVersion: number;
+ attempts: GenerationAttempt[];
+ outputs: Array<{ id: string; mimeType: string; url?: string; status: "succeeded" | "failed"; unitPriceSnapshot?: number; chargedAmount?: number; metadata?: ImageOutputMetadata }>;
+ publicErrorCode?: ErrorCode;
+ prompt?: string;
+ resolutionPresetId?: string;
+ count?: number;
+ pricingSnapshot?: { basePrice: number; multiplier: number; unitVersion: number };
+ modelSnapshot?: { publicModelId: string; version: number };
+ routeSnapshot?: { groupId: string; version: number; channelIds: string[]; retryBudget?: number; channelRetryBudgets?: Record };
+ planSnapshot?: { planId?: string; version?: number; queuePriority: number; maxConcurrent: number };
+ leaseExpiresAt?: string;
+ /** Fences a late worker/provider response after a lease is recovered. */
+ leaseToken?: string;
+ providerSubmitted?: boolean;
+ idempotencyKey?: string;
+ requestFingerprint?: string;
+ retentionExpiresAt?: string;
+ retentionState?: "active" | "archiving" | "archive_failed" | "archived" | "deleted";
+ retentionExtensionDays?: number;
+ reserveExpiresAt?: string;
+ inputSnapshot?: { references: string[]; referenceGroup?: ReferenceImageGroup; maskObjectId?: string; params: Record; originalPrompt?: string };
+};
+
+export type GenerationAttempt = {
+ id: string;
+ channelId: string;
+ sequence: number;
+ status: "started" | "succeeded" | "failed" | "unknown";
+ errorCode?: string;
+ providerRequestId?: string;
+ platformIdempotencyKey?: string;
+ startedAt: string;
+ finishedAt?: string;
+ leaseExpiresAt?: string;
+ retryable?: boolean;
+ reconciliationStatus?: "pending" | "confirmed_success" | "confirmed_failure" | "manual_review";
+};
+
+export type PublicGenerationAttempt = {
+ id: string;
+ sequence: number;
+ status: GenerationAttempt["status"];
+ startedAt: string;
+ finishedAt?: string;
+ retryable?: boolean;
+ reconciliationStatus?: GenerationAttempt["reconciliationStatus"];
+};
+
+export type PublicGenerationTask = {
+ id: string;
+ taskType: TaskType;
+ modelProductId: string;
+ status: TaskStatus;
+ estimatedCost: number;
+ reservedCost: number;
+ createdAt: string;
+ updatedAt: string;
+ eventSequence: number;
+ publicErrorCode?: ErrorCode;
+ prompt?: string;
+ resolutionPresetId?: string;
+ count?: number;
+ modelSnapshot?: GenerationTask["modelSnapshot"];
+ retentionExpiresAt?: string;
+ retentionState?: GenerationTask["retentionState"];
+ retentionExtensionDays?: number;
+ inputSnapshot?: GenerationTask["inputSnapshot"];
+ attempts: PublicGenerationAttempt[];
+ outputs: GenerationTask["outputs"];
+};
+
+export type TaskEvent = {
+ eventId: string;
+ sequence: number;
+ taskId: string;
+ attemptId?: string;
+ type: "task.queued" | "task.running" | "task.attempt" | "task.progress" | "task.succeeded" | "task.failed" | "task.canceled" | "task.unknown";
+ occurredAt: string;
+ payload: Record;
+};
+
+export type PaymentWebhookRequest = { orderId: string; eventId: string; status: "paid" | "failed" | "refunded"; amount?: number; currency?: string };
+
+export type UploadObject = OrganizationScope & {
+ id: string;
+ idempotencyKey?: string;
+ ownerId: string;
+ mimeType: string;
+ size: number;
+ checksum: string;
+ status: "pending" | "scanning" | "ready" | "rejected" | "deleted";
+ stagingKey?: string;
+ expiresAt: string;
+};
+
+export type AssetRecord = OrganizationScope & {
+ id: string;
+ ownerId: string;
+ type: "image" | "text" | "audio" | "archive" | "3d";
+ objectId?: string;
+ taskId?: string;
+ sourceOutputId?: string;
+ name: string;
+ tags: string[];
+ deletedAt?: string;
+ retentionExpiresAt?: string;
+ pinned?: boolean;
+ objectUrl?: string;
+ mimeType?: string;
+};
+
+export type WebDavConfigStatus = {
+ configured: boolean;
+ url?: string;
+ username?: string;
+ directory?: string;
+ lastSyncedAt?: string;
+ state: "unconfigured" | "ready" | "syncing" | "error";
+};
+
+export type WsClientMessage =
+ | { type: "subscribe"; taskId: string; cursor?: number }
+ | { type: "unsubscribe"; taskId: string }
+ | { type: "ping" };
+
+export type WsServerMessage =
+ | { type: "ready"; contractVersion: string; heartbeatIntervalMs: number }
+ | { type: "pong"; at: string }
+ | { type: "event"; event: TaskEvent }
+ | { type: "error"; error: ApiError };
+
+export type HealthResponse = {
+ status: "ok";
+ service: "miragenflow-server";
+ contractVersion: string;
+ now: string;
+ dependencies?: { persistence: { adapter: string; status: string; detail?: string }; queue: { adapter: string; status: string; detail?: string } };
+};
+
+export type AdminRole = "super_admin" | "operator" | "finance" | "support" | "auditor";
+
+export type AdminScope = "admin:read" | "admin:write" | "admin:finance" | "admin:security" | "admin:audit";
+
+export type AdminUser = {
+ id: string;
+ email: string;
+ roles: AdminRole[];
+ scopes: AdminScope[];
+ mfaRequired: boolean;
+};
+
+export type AdminSessionResponse = {
+ admin: AdminUser;
+ accessToken?: string;
+ accessTokenExpiresAt?: string;
+ mfaRequired?: boolean;
+ challengeId?: string;
+};
+
+export type AdminPage = {
+ items: T[];
+ total?: number;
+ nextCursor?: string;
+};
+
+export type AdminChannelSummary = {
+ id: string;
+ label: string;
+ enabled: boolean;
+ priority: number;
+ health: "healthy" | "degraded" | "open";
+ version: number;
+ retryBudget: number;
+ consecutiveFailures: number;
+ providerType?: "generic" | "openai-images";
+ providerName?: string;
+ providerModelId?: string;
+ enabledModelIds?: string[];
+ modelMappings?: Array<{ displayModelId: string; requestModelId: string }>;
+ resolutionModelMap?: Record;
+ baseUrl?: string;
+ secretConfigured?: boolean;
+};
+
+export type AdminTaskSummary = {
+ id: string;
+ ownerId: string;
+ taskType: TaskType;
+ modelProductId: string;
+ status: TaskStatus;
+ estimatedCost: number;
+ reservedCost: number;
+ createdAt: string;
+ updatedAt: string;
+ attemptCount: number;
+ outputCount: number;
+ publicErrorCode?: ErrorCode;
+};
+
+export type AdminAuditEntry = {
+ id: string;
+ actorId?: string;
+ action: string;
+ category?: string;
+ actionLabel?: string;
+ actorName?: string;
+ actorType?: "admin" | "user" | "system" | "payment" | "unknown";
+ objectType: string;
+ objectId?: string;
+ requestId: string;
+ createdAt: string;
+ statusCode?: number;
+ level?: "normal" | "error";
+ before?: unknown;
+ after?: unknown;
+};
diff --git a/packages/ui/package.json b/packages/ui/package.json
new file mode 100644
index 0000000..d407936
--- /dev/null
+++ b/packages/ui/package.json
@@ -0,0 +1,53 @@
+{
+ "name": "@miragenflow/ui",
+ "version": "0.1.0",
+ "private": true,
+ "type": "module",
+ "main": "./src/index.tsx",
+ "types": "./src/index.tsx",
+ "exports": {
+ ".": "./src/index.tsx",
+ "./alert": "./src/alert.ts",
+ "./breadcrumb": "./src/breadcrumb.ts",
+ "./button": "./src/button.tsx",
+ "./card": "./src/card.ts",
+ "./checkbox": "./src/checkbox.ts",
+ "./dialog": "./src/dialog.ts",
+ "./drawer": "./src/drawer.ts",
+ "./dropdown": "./src/dropdown.ts",
+ "./form": "./src/form.ts",
+ "./grid": "./src/grid.ts",
+ "./layout": "./src/layout.ts",
+ "./menu": "./src/menu.ts",
+ "./color-picker": "./src/color-picker.ts",
+ "./date-picker": "./src/date-picker.ts",
+ "./input": "./src/input.ts",
+ "./input-number": "./src/input-number.ts",
+ "./loading": "./src/loading.ts",
+ "./message": "./src/message.ts",
+ "./pagination": "./src/pagination.ts",
+ "./popup": "./src/popup.ts",
+ "./select": "./src/select.ts",
+ "./space": "./src/space.ts",
+ "./slider": "./src/slider.ts",
+ "./switch": "./src/switch.ts",
+ "./statistic": "./src/statistic.ts",
+ "./table": "./src/table.ts",
+ "./tag": "./src/tag.ts",
+ "./textarea": "./src/textarea.ts",
+ "./tabs": "./src/tabs.ts",
+ "./tooltip": "./src/tooltip.ts"
+ },
+ "peerDependencies": {
+ "react": ">=18.0.0",
+ "react-dom": ">=18.0.0",
+ "tdesign-react": "1.15.1"
+ },
+ "devDependencies": {
+ "@types/react": "19.1.12",
+ "@types/react-dom": "19.1.9",
+ "react": "19.2.5",
+ "react-dom": "19.2.5",
+ "tdesign-react": "1.15.1"
+ }
+}
diff --git a/packages/ui/src/alert.ts b/packages/ui/src/alert.ts
new file mode 100644
index 0000000..bc19802
--- /dev/null
+++ b/packages/ui/src/alert.ts
@@ -0,0 +1,2 @@
+export { Alert } from "tdesign-react/es/alert";
+export type { AlertProps } from "tdesign-react/es/alert";
diff --git a/packages/ui/src/breadcrumb.ts b/packages/ui/src/breadcrumb.ts
new file mode 100644
index 0000000..47a06c4
--- /dev/null
+++ b/packages/ui/src/breadcrumb.ts
@@ -0,0 +1,2 @@
+export { Breadcrumb } from "tdesign-react/es/breadcrumb";
+export type { BreadcrumbProps, BreadcrumbItemProps } from "tdesign-react/es/breadcrumb";
diff --git a/packages/ui/src/button.css b/packages/ui/src/button.css
new file mode 100644
index 0000000..c75c95d
--- /dev/null
+++ b/packages/ui/src/button.css
@@ -0,0 +1,7 @@
+.t-button.mf-button--rich {
+ gap: 8px;
+}
+
+.t-button.mf-button--rich > .t-button__text {
+ display: contents;
+}
diff --git a/packages/ui/src/button.tsx b/packages/ui/src/button.tsx
new file mode 100644
index 0000000..792163c
--- /dev/null
+++ b/packages/ui/src/button.tsx
@@ -0,0 +1,50 @@
+import { Children, forwardRef, isValidElement, type ReactElement, type ReactNode } from "react";
+import { Button as TDesignButton } from "tdesign-react/es/button";
+import type { ButtonProps as TDesignButtonProps } from "tdesign-react/es/button";
+import "./button.css";
+
+export type MiragenFlowButtonType = "default" | "primary" | "outline" | "dashed" | "link" | "text" | "danger" | "danger-outline";
+
+export interface MiragenFlowButtonProps extends Omit {
+ children?: ReactNode;
+ htmlType?: "button" | "submit" | "reset";
+ icon?: ReactElement;
+ type?: MiragenFlowButtonType;
+}
+
+const buttonTypeMap: Record = {
+ default: { theme: "default", variant: "base" },
+ primary: { theme: "primary", variant: "base" },
+ outline: { theme: "default", variant: "outline" },
+ dashed: { theme: "default", variant: "dashed" },
+ link: { theme: "primary", variant: "text" },
+ text: { theme: "default", variant: "text" },
+ danger: { theme: "danger", variant: "base" },
+ "danger-outline": { theme: "danger", variant: "outline" },
+};
+
+export const MiragenFlowButton = forwardRef(function MiragenFlowButton(
+ { children, className, htmlType = "button", icon, loading, onClick, tag, type = "default", ...rest },
+ ref,
+) {
+ const mapped = buttonTypeMap[type];
+ const hasRichChildren = Children.count(children) > 1 || Children.toArray(children).some(isValidElement);
+ const resolvedClassName = ["mf-button", hasRichChildren && "mf-button--rich", className].filter(Boolean).join(" ");
+
+ return (
+
+ {children}
+
+ );
+});
diff --git a/packages/ui/src/card.ts b/packages/ui/src/card.ts
new file mode 100644
index 0000000..9acd788
--- /dev/null
+++ b/packages/ui/src/card.ts
@@ -0,0 +1,2 @@
+export { Card } from "tdesign-react/es/card";
+export type { CardProps } from "tdesign-react/es/card";
diff --git a/packages/ui/src/checkbox.ts b/packages/ui/src/checkbox.ts
new file mode 100644
index 0000000..322cb66
--- /dev/null
+++ b/packages/ui/src/checkbox.ts
@@ -0,0 +1,2 @@
+export { Checkbox } from "tdesign-react/es/checkbox";
+export type { CheckboxProps } from "tdesign-react/es/checkbox";
diff --git a/packages/ui/src/color-picker.ts b/packages/ui/src/color-picker.ts
new file mode 100644
index 0000000..45f1d36
--- /dev/null
+++ b/packages/ui/src/color-picker.ts
@@ -0,0 +1,2 @@
+export { ColorPicker, ColorPickerPanel } from "tdesign-react/es/color-picker";
+export type { ColorPickerProps } from "tdesign-react/es/color-picker";
diff --git a/packages/ui/src/date-picker.ts b/packages/ui/src/date-picker.ts
new file mode 100644
index 0000000..4bdffe9
--- /dev/null
+++ b/packages/ui/src/date-picker.ts
@@ -0,0 +1,8 @@
+export { DatePicker, DateRangePicker, DatePickerPanel, DateRangePickerPanel } from "tdesign-react/es/date-picker";
+export type {
+ DatePickerProps,
+ DateRangePickerProps,
+ DatePickerPanelProps,
+ DateRangePickerPanelProps,
+ DateRangeValue,
+} from "tdesign-react/es/date-picker";
diff --git a/packages/ui/src/dialog.ts b/packages/ui/src/dialog.ts
new file mode 100644
index 0000000..f3731c8
--- /dev/null
+++ b/packages/ui/src/dialog.ts
@@ -0,0 +1,2 @@
+export { Dialog, DialogPlugin } from "tdesign-react/es/dialog";
+export type { DialogInstance, DialogProps } from "tdesign-react/es/dialog";
diff --git a/packages/ui/src/drawer.ts b/packages/ui/src/drawer.ts
new file mode 100644
index 0000000..fb5fa7d
--- /dev/null
+++ b/packages/ui/src/drawer.ts
@@ -0,0 +1,2 @@
+export { Drawer, DrawerPlugin } from "tdesign-react/es/drawer";
+export type { DrawerInstance, DrawerProps } from "tdesign-react/es/drawer";
diff --git a/packages/ui/src/dropdown.ts b/packages/ui/src/dropdown.ts
new file mode 100644
index 0000000..368a5d8
--- /dev/null
+++ b/packages/ui/src/dropdown.ts
@@ -0,0 +1,2 @@
+export { Dropdown } from "tdesign-react/es/dropdown";
+export type { DropdownProps } from "tdesign-react/es/dropdown";
diff --git a/packages/ui/src/form.ts b/packages/ui/src/form.ts
new file mode 100644
index 0000000..c03a828
--- /dev/null
+++ b/packages/ui/src/form.ts
@@ -0,0 +1,8 @@
+export { Form } from "tdesign-react/es/form";
+export type {
+ FormInstanceFunctions,
+ FormItemProps,
+ FormProps,
+ FormRule,
+ SubmitContext,
+} from "tdesign-react/es/form";
diff --git a/packages/ui/src/grid.ts b/packages/ui/src/grid.ts
new file mode 100644
index 0000000..4c0a50a
--- /dev/null
+++ b/packages/ui/src/grid.ts
@@ -0,0 +1,2 @@
+export { Col, Row } from "tdesign-react/es/grid";
+export type { ColProps, RowProps } from "tdesign-react/es/grid";
diff --git a/packages/ui/src/index.tsx b/packages/ui/src/index.tsx
new file mode 100644
index 0000000..a9a0fcb
--- /dev/null
+++ b/packages/ui/src/index.tsx
@@ -0,0 +1,92 @@
+import type { ReactNode } from "react";
+import { ConfigProvider } from "tdesign-react/es/config-provider";
+import enUS from "tdesign-react/es/locale/en_US";
+import zhCN from "tdesign-react/es/locale/zh_CN";
+
+export { MiragenFlowButton } from "./button";
+export type { MiragenFlowButtonProps, MiragenFlowButtonType } from "./button";
+export { Alert } from "./alert";
+export type { AlertProps } from "./alert";
+export { Card } from "./card";
+export type { CardProps } from "./card";
+export { Breadcrumb } from "./breadcrumb";
+export type { BreadcrumbProps, BreadcrumbItemProps } from "./breadcrumb";
+export { Checkbox } from "./checkbox";
+export type { CheckboxProps } from "./checkbox";
+export { Dialog, DialogPlugin } from "./dialog";
+export type { DialogInstance, DialogProps } from "./dialog";
+export { Drawer, DrawerPlugin } from "./drawer";
+export type { DrawerInstance, DrawerProps } from "./drawer";
+export { Dropdown } from "./dropdown";
+export type { DropdownProps } from "./dropdown";
+export { Form } from "./form";
+export type { FormInstanceFunctions, FormItemProps, FormProps, FormRule, SubmitContext } from "./form";
+export { Col, Row } from "./grid";
+export type { ColProps, RowProps } from "./grid";
+export { Layout } from "./layout";
+export type { LayoutProps } from "./layout";
+export { Menu } from "./menu";
+export type { HeadMenuProps, MenuItemProps, MenuProps, MenuValue, SubMenuProps } from "./menu";
+export { ColorPicker, ColorPickerPanel } from "./color-picker";
+export type { ColorPickerProps } from "./color-picker";
+export { DatePicker, DateRangePicker, DatePickerPanel, DateRangePickerPanel } from "./date-picker";
+export type { DatePickerProps, DateRangePickerProps, DatePickerPanelProps, DateRangePickerPanelProps, DateRangeValue } from "./date-picker";
+export { Input, InputGroup } from "./input";
+export type { InputGroupProps, InputProps, InputRef } from "./input";
+export { InputNumber } from "./input-number";
+export type { InputNumberProps } from "./input-number";
+export { Loading } from "./loading";
+export type { LoadingProps } from "./loading";
+export { MessagePlugin } from "./message";
+export type { MessageOptions } from "./message";
+export { Popup, PopupPlugin } from "./popup";
+export type { PopupProps, PopupRef } from "./popup";
+export { Pagination } from "./pagination";
+export type { PaginationProps } from "./pagination";
+export { Select } from "./select";
+export type { SelectProps } from "./select";
+export { Space } from "./space";
+export type { SpaceProps } from "./space";
+export { Switch } from "./switch";
+export type { SwitchProps } from "./switch";
+export { Statistic } from "./statistic";
+export type { StatisticProps, StatisticRef } from "./statistic";
+export { Table, BaseTable, EnhancedTable, PrimaryTable, SimpleTable } from "./table";
+export type {
+ BaseTableColumns,
+ BaseTableProps,
+ DragSortContext,
+ EnhancedTableProps,
+ PrimaryTableCol,
+ PrimaryTableProps,
+ TableProps,
+ TableRowData,
+} from "./table";
+export { Tag } from "./tag";
+export type { TagProps } from "./tag";
+export { Textarea } from "./textarea";
+export type { TextareaProps } from "./textarea";
+export { Tabs } from "./tabs";
+export type { TabPanelProps, TabValue, TabsProps } from "./tabs";
+export { Slider } from "./slider";
+export type { SliderProps } from "./slider";
+export { Tooltip, TooltipLite } from "./tooltip";
+export type { TooltipLiteProps, TooltipProps } from "./tooltip";
+
+export type MiragenFlowLocale = "zh-CN" | "en-US";
+
+export type TDesignThemeVariables = Record;
+
+export function TDesignProvider({ locale, children }: { locale: MiragenFlowLocale; children: ReactNode }) {
+ return {children};
+}
+
+export function applyTDesignVariables(mode: "light" | "dark", variables: TDesignThemeVariables) {
+ const root = document.documentElement;
+
+ root.setAttribute("theme-mode", mode);
+ root.setAttribute("data-mf-tdesign", "true");
+ Object.entries(variables).forEach(([name, value]) => {
+ root.style.setProperty(name, value);
+ });
+}
diff --git a/packages/ui/src/input-number.ts b/packages/ui/src/input-number.ts
new file mode 100644
index 0000000..a768f03
--- /dev/null
+++ b/packages/ui/src/input-number.ts
@@ -0,0 +1,2 @@
+export { InputNumber } from "tdesign-react/es/input-number";
+export type { InputNumberProps } from "tdesign-react/es/input-number";
diff --git a/packages/ui/src/input.ts b/packages/ui/src/input.ts
new file mode 100644
index 0000000..531b5e2
--- /dev/null
+++ b/packages/ui/src/input.ts
@@ -0,0 +1,2 @@
+export { Input, InputGroup } from "tdesign-react/es/input";
+export type { InputGroupProps, InputProps, InputRef } from "tdesign-react/es/input";
diff --git a/packages/ui/src/layout.ts b/packages/ui/src/layout.ts
new file mode 100644
index 0000000..b6d1ac5
--- /dev/null
+++ b/packages/ui/src/layout.ts
@@ -0,0 +1,2 @@
+export { Layout } from "tdesign-react/es/layout";
+export type { LayoutProps } from "tdesign-react/es/layout";
diff --git a/packages/ui/src/loading.ts b/packages/ui/src/loading.ts
new file mode 100644
index 0000000..ae4f53b
--- /dev/null
+++ b/packages/ui/src/loading.ts
@@ -0,0 +1,2 @@
+export { Loading } from "tdesign-react/es/loading";
+export type { LoadingProps } from "tdesign-react/es/loading";
diff --git a/packages/ui/src/menu.ts b/packages/ui/src/menu.ts
new file mode 100644
index 0000000..7154705
--- /dev/null
+++ b/packages/ui/src/menu.ts
@@ -0,0 +1,2 @@
+export { Menu } from "tdesign-react/es/menu";
+export type { HeadMenuProps, MenuItemProps, MenuProps, MenuValue, SubMenuProps } from "tdesign-react/es/menu";
diff --git a/packages/ui/src/message.ts b/packages/ui/src/message.ts
new file mode 100644
index 0000000..d3b7090
--- /dev/null
+++ b/packages/ui/src/message.ts
@@ -0,0 +1,2 @@
+export { MessagePlugin } from "tdesign-react/es/message";
+export type { MessageOptions } from "tdesign-react/es/message";
diff --git a/packages/ui/src/pagination.ts b/packages/ui/src/pagination.ts
new file mode 100644
index 0000000..3c5c205
--- /dev/null
+++ b/packages/ui/src/pagination.ts
@@ -0,0 +1,2 @@
+export { Pagination } from "tdesign-react/es/pagination";
+export type { PaginationProps } from "tdesign-react/es/pagination";
diff --git a/packages/ui/src/popup.ts b/packages/ui/src/popup.ts
new file mode 100644
index 0000000..73c15d1
--- /dev/null
+++ b/packages/ui/src/popup.ts
@@ -0,0 +1,2 @@
+export { Popup, PopupPlugin } from "tdesign-react/es/popup";
+export type { PopupProps, PopupRef } from "tdesign-react/es/popup";
diff --git a/packages/ui/src/select.ts b/packages/ui/src/select.ts
new file mode 100644
index 0000000..324c0bb
--- /dev/null
+++ b/packages/ui/src/select.ts
@@ -0,0 +1,2 @@
+export { Select } from "tdesign-react/es/select";
+export type { SelectProps } from "tdesign-react/es/select";
diff --git a/packages/ui/src/slider.ts b/packages/ui/src/slider.ts
new file mode 100644
index 0000000..f3dba0b
--- /dev/null
+++ b/packages/ui/src/slider.ts
@@ -0,0 +1,2 @@
+export { Slider } from "tdesign-react/es/slider";
+export type { SliderProps } from "tdesign-react/es/slider";
diff --git a/packages/ui/src/space.ts b/packages/ui/src/space.ts
new file mode 100644
index 0000000..57132e4
--- /dev/null
+++ b/packages/ui/src/space.ts
@@ -0,0 +1,2 @@
+export { Space } from "tdesign-react/es/space";
+export type { SpaceProps } from "tdesign-react/es/space";
diff --git a/packages/ui/src/statistic.ts b/packages/ui/src/statistic.ts
new file mode 100644
index 0000000..a883264
--- /dev/null
+++ b/packages/ui/src/statistic.ts
@@ -0,0 +1,2 @@
+export { Statistic } from "tdesign-react/es/statistic";
+export type { StatisticProps, StatisticRef } from "tdesign-react/es/statistic";
diff --git a/packages/ui/src/switch.ts b/packages/ui/src/switch.ts
new file mode 100644
index 0000000..b9a893f
--- /dev/null
+++ b/packages/ui/src/switch.ts
@@ -0,0 +1,2 @@
+export { Switch } from "tdesign-react/es/switch";
+export type { SwitchProps } from "tdesign-react/es/switch";
diff --git a/packages/ui/src/table.ts b/packages/ui/src/table.ts
new file mode 100644
index 0000000..2cc1794
--- /dev/null
+++ b/packages/ui/src/table.ts
@@ -0,0 +1,11 @@
+export { Table, BaseTable, EnhancedTable, PrimaryTable, SimpleTable } from "tdesign-react/es/table";
+export type {
+ BaseTableColumns,
+ BaseTableProps,
+ DragSortContext,
+ EnhancedTableProps,
+ PrimaryTableCol,
+ PrimaryTableProps,
+ TableProps,
+ TableRowData,
+} from "tdesign-react/es/table";
diff --git a/packages/ui/src/tabs.ts b/packages/ui/src/tabs.ts
new file mode 100644
index 0000000..57bb303
--- /dev/null
+++ b/packages/ui/src/tabs.ts
@@ -0,0 +1,2 @@
+export { Tabs } from "tdesign-react/es/tabs";
+export type { TabPanelProps, TabValue, TabsProps } from "tdesign-react/es/tabs";
diff --git a/packages/ui/src/tag.ts b/packages/ui/src/tag.ts
new file mode 100644
index 0000000..ab486ec
--- /dev/null
+++ b/packages/ui/src/tag.ts
@@ -0,0 +1,2 @@
+export { Tag } from "tdesign-react/es/tag";
+export type { TagProps } from "tdesign-react/es/tag";
diff --git a/packages/ui/src/textarea.ts b/packages/ui/src/textarea.ts
new file mode 100644
index 0000000..3c79a53
--- /dev/null
+++ b/packages/ui/src/textarea.ts
@@ -0,0 +1,2 @@
+export { Textarea } from "tdesign-react/es/textarea";
+export type { TextareaProps } from "tdesign-react/es/textarea";
diff --git a/packages/ui/src/tooltip.ts b/packages/ui/src/tooltip.ts
new file mode 100644
index 0000000..fad0a37
--- /dev/null
+++ b/packages/ui/src/tooltip.ts
@@ -0,0 +1,2 @@
+export { Tooltip, TooltipLite } from "tdesign-react/es/tooltip";
+export type { TooltipLiteProps, TooltipProps } from "tdesign-react/es/tooltip";
diff --git a/plan.md b/plan.md
new file mode 100644
index 0000000..17bb191
--- /dev/null
+++ b/plan.md
@@ -0,0 +1,908 @@
+# MiragenFlow 渠道、模型、分组与智能调度重构计划
+
+状态:待审批。当前只完成计划评审和计划修订,未修改业务代码,未启动浏览器,未创建展示用渠道,未执行浏览器模拟。
+
+本文件是审批稿。负责人批准前,任何代理不得执行下面的代码改动;批准后仍须按阶段完成和验收,不得把“写入计划”当成“功能完成”。
+
+## 0. 评审结论
+
+### 0.1 总结
+
+现有计划的业务方向是清楚的:渠道负责接入和发现模型,模型负责公开能力与分辨率,分组负责模型和渠道的路由关系,健康负责运行状态,任务负责生命周期,价格和套餐负责计费约束,审计负责追溯。
+
+但原计划不能直接执行,原因是它把目标状态和当前状态混在了一起,并遗漏了几个会阻断实施的前置关系:
+
+1. 当前模型仍然承担价格、数量、并发和 `channelGroupId`;直接删除这些字段会破坏任务创建和价格链路。
+2. 计划没有明确一个公开模型能否属于多个分组;没有这个基数规则,任务创建无法确定唯一路由。
+3. 当前分组只保存全局渠道集合和顺序,计划却要求“某个模型下支持哪些渠道”,数据结构尚未表达模型和渠道的对应关系。
+4. 计划要求智能调度、容量和半开状态,但当前 `ProviderChannel` 和 `ChannelGroup` 没有策略、容量或半开状态字段。
+5. 当前健康接口从任务 attempts 临时聚合,既不是独立探活样本,也没有每个渠道固定 12 个样本。
+6. OpenAI 图片 provider 当前明确不支持查询未知任务;自动 unknown 对账不能作为本阶段已实现功能。
+7. 当前人工对账会用 `Math.min` 静默截断超额金额;当前测试也验证的是截断成功,与本计划的 422 规则相反。
+8. 当前审计详情仍可能直接返回 `before`/`after`,列表分页也是前端本地分页;这些无法仅靠页面调整解决。
+
+本次修订将每个目标拆成“现状、代码改法、目标行为、测试、完成条件”,并把相互依赖的改动放在正确顺序中。
+
+### 0.2 已确认的代码事实
+
+| 事实 | 代码依据 | 对计划的影响 |
+| --- | --- | --- |
+| 后台模型、渠道、分组、健康和价格路由都挂在同一个 `BusinessPage` | `admin/src/router/index.ts:54-60` | 先按业务职责拆分页面逻辑,再决定是否拆成独立文件;不重复引入 UI 库 |
+| 模型仍包含价格、数量、并发和分组 ID | `packages/contracts/src/index.ts:127-146`、`server/src/store.ts:120-128` | 必须先建立价格规则、运行限制和模型分组关系,再移除旧字段 |
+| 任务创建直接读取上述旧字段 | `server/src/app/http.ts:873-926` | P1 不能只改表单;必须同步改任务创建、快照和计费解析 |
+| 渠道已有 `displayModelId/requestModelId` 映射类型,但仍保留旧的 `providerModelId` 直连路径 | `server/src/store.ts:10-11`、`server/src/adapters/provider.ts:14-20`、`server/src/app/http.ts:2320-2332` | 删除旧路径,所有 provider 请求必须经过渠道模型实体或显式映射 |
+| 渠道 PATCH 会按 `priority` 回写所有所属分组顺序 | `server/src/app/http.ts:2427-2429` | 渠道属性和分组路由排序必须完全隔离 |
+| 分组排序只检查全局渠道存在,不检查是否属于该分组 | `server/src/app/http.ts:2548-2554` | 排序接口必须检查成员集合完全一致、无遗漏、无越组成员 |
+| 任务已有基础路由快照和 unknown 保留预留 | `server/src/app/http.ts:915-926`、`server/src/jobs/task-worker.ts:435-517` | 保留已有能力,但扩展快照、候选过滤和严格重试语义 |
+| worker 当前只按启用和 `health !== open` 过滤 | `server/src/jobs/task-worker.ts:426-435` | 智能调度必须抽成可测试的候选过滤和排序纯函数 |
+| 熔断当前冷却约 30 秒,直接回到 `degraded`,没有 `half-open` | `server/src/jobs/task-worker.ts:429-433`、`server/src/jobs/task-worker.ts:487` | 重新定义状态机、探测租约和冷却配置;不能只改展示文字 |
+| 健康接口返回单个 `groupId`,并从任务 attempts 计算 | `server/src/app/http.ts:3053-3064` | 新增健康样本来源和 `groupIds` 数组;不得把前 12 个渠道误当成 12 个历史样本 |
+| OpenAI 图片查询返回 `PROVIDER_QUERY_UNAVAILABLE` | `server/src/adapters/provider.ts:250-257` | 当前 unknown 只走人工对账;自动查询列为后续能力门槛 |
+| 对账任务和账本在事务中,但审计调用在事务外 | `server/src/app/http.ts:2875-2934` | 明确审计原子性方案,不能在计划中笼统写“有事务和审计” |
+| 持久化仓储主要保存 JSON snapshot | `server/migrations/0002_v1_domain.sql:61-118`、`server/src/infra/repository.ts:561-583` | 新结构要有 snapshot schema 版本和恢复策略;不凭空假设已有独立实体表 |
+| 测试 fixture 会创建渠道和分组 | `server/src/store.ts:136-143` | 禁止展示 mock 数据,但允许隔离测试 fixture,并验收生产不注入 fixture |
+
+### 0.3 本次明确的架构决策
+
+1. 第一阶段目标是 8 个后台业务模块:渠道、模型、分组、健康、任务、价格、套餐、审计。
+2. 人工对账是任务中心中的独立 Tab/筛选视图,不新造一个与任务生命周期脱节的页面;需要独立深链接时再增加 `/reconciliation`,但不是第一阶段前置条件。
+3. 模型发现是渠道管理中的主动操作,不单独建“模型发现”页面。
+4. 一个公开模型在第一阶段只能归属一个启用的路由分组;一个分组可以管理多个公开模型。这样保留当前任务按模型找到唯一分组的语义,避免一次任务同时命中多个未知策略。
+5. 分组内部使用“模型 × 渠道”关系。一个渠道可以支持同一分组中的部分模型,不要求一个渠道覆盖分组内所有模型;每个模型有独立渠道池、独立顺序和独立重试预算。
+6. 分组默认使用严格优先级。只有管理员明确选择“同优先级智能调度”时,才在最低可用优先级层内使用健康、容量、成功率和 P95 排序;智能指标不能让低优先级渠道越过高优先级渠道。
+7. `publicModelId` 是平台公开 ID,`requestModelId` 是供应商请求 ID。两者在契约、存储、页面、路由和 provider 边界都必须保持分离。
+8. 模型目录只负责公开身份、能力和分辨率。价格倍率归价格规则,数量硬上限归系统运行策略,套餐并发归套餐/租约;这些字段不回到模型弹窗。
+9. 当前 OpenAI 图片 provider 没有可查询的异步任务协议,所以 unknown 结果本阶段不自动查询、不自动退款、不自动切换,只保留预留并进入人工对账。
+10. 健康趋势固定显示 12 根条。绿、黄、红、灰、蓝只表示样本或探活阶段,不能替代当前调度资格。
+
+## 1. 范围和禁止事项
+
+### 1.1 本次范围
+
+- 管理后台渠道、模型、分组、健康、任务、价格、套餐和审计的职责重构。
+- `packages/contracts` 中公开模型、渠道模型、路由快照、价格快照、健康和管理分页契约。
+- `server/src/store.ts`、`server/src/app/http.ts`、`server/src/adapters/provider.ts`、`server/src/jobs/task-worker.ts` 及持久化 snapshot 结构。
+- 管理台请求封装、TDesign 表格/弹窗/Tab/下拉和现有本地 Lineicons 的业务接入。
+- 代码级测试、类型检查、Lint、差异检查和必要的持久化/迁移回归。
+- `CHANGELOG.md`、双语 `todo`、双语 `pending-test`;用户确认浏览器测试通过后再更新正式功能文档。
+
+### 1.2 明确不做
+
+- 不修改 `web/src/pages/canvas`、`web/src/components/canvas`、`web/src/stores/canvas`、`web/src/lib/canvas`,也不修改任何画布底层架构。
+- 不把现有 TDesign 后台重写成 Ant Design、Lucide 或其他控件库;继续使用锁定版本的 TDesign 和本地 `admin/src/components/LineIcons.jsx`。
+- 不使用 CDN、WebFont、原生 `` 或新增第三方业务图标库。
+- 不在开发环境自动创建展示用 mock 渠道,不用静态成功数据伪造供应商结果。测试 fixture 可以存在,但必须限制在测试模式。
+- 不在渠道保存、打开编辑、切 Tab、刷新列表时隐式请求供应商;只有管理员点击“拉取模型”或“探活”才请求上游。
+- 不在模型页面填写基础价格、数量上限、并发上限、渠道组 ID或逗号分隔能力。
+- 不在公开 API、用户端、日志、导出、任务详情或普通管理列表泄露 API Key、Cookie、完整 Base URL、供应商请求模型 ID、供应商请求 ID、完整提示词或原始 JSON。只有具备管理权限的渠道编辑/探活上下文可以读取必要的完整 Base URL 和实际请求路径,API Key 永不回显。
+- 本阶段不启动浏览器,不执行 Playwright、截图和浏览器模拟;浏览器验收由负责人自行完成。
+
+### 1.3 测试 fixture 例外
+
+`server/src/store.ts` 中的测试渠道、测试分组和 `fixtureFailure` 只允许用于隔离自动化测试。代码级验收必须覆盖:
+
+- `NODE_ENV=production` 不创建 fixture 渠道、fixture 分组或模拟支付适配器。
+- 管理页面首次进入不自动创建任何渠道。
+- 测试 fixture 不出现在生产 snapshot、生产导出和真实供应商列表中。
+
+## 2. 目标页面和控件规格
+
+目标是 8 个后台业务模块。现有路由仍可先由一个入口分发,但每个模块必须有独立的字段定义、数据请求、错误处理和验收边界。
+
+### 2.1 渠道管理 `/channels`
+
+**职责**:连接供应商、保存凭证引用、主动拉取供应商模型、维护渠道模型清单和可选映射。
+
+**不负责**:公开模型参数、价格、分组顺序、用户套餐、任务结算。
+
+```text
++--------------------------------------------------------------------------------+
+| 渠道管理 [搜索渠道________] [类型 v] [启用状态 v] [刷新] [+ 新增渠道] |
++--------------------------------------------------------------------------------+
+| 渠道名称 | 类型 | 地址摘要 | 启用 | 渠道模型数 | 当前健康 | 最近拉取 | 操作 |
+| 渠道 A | OpenAI 图片 | example... | 开 | 12 | 正常 | 10:20 | 编辑 探活 |
++--------------------------------------------------------------------------------+
+```
+
+新增/编辑弹窗使用三个 Tab,Tab 切换不丢草稿:
+
+```text
++----------------------------------+
+| 新增/编辑渠道 X |
++----------------------------------+
+| [基础信息] [渠道模型] [模型映射] |
+| 渠道名称 [___________________] |
+| 接口类型 [OpenAI 图片端点 v] |
+| 请求地址 [https://example.com/v1]|
+| API Key [********************] |
+| 请求超时 [________] 毫秒 |
+| 启用状态 [开关] |
+| [取消][保存] |
++----------------------------------+
+```
+
+字段含义和规则:
+
+| 控件 | 填写内容 | 规则 |
+| --- | --- | --- |
+| 渠道名称 | 管理员可读名称 | 必填,去首尾空白,长度有限制 |
+| 接口类型 | 当前选择 `OpenAI 图片端点` | 只展示已有真实 adapter;没有真实调用链的类型不得伪造可用 |
+| 请求地址 | 完整 `https://example.com` 或 `https://example.com/v1` | 禁止账号密码;生产只允许 HTTPS 和公网上游;开发测试的本地地址按现有安全开关处理 |
+| API Key | 供应商密钥 | 去首尾空白后只校验非空;编辑留空表示沿用原凭证;存储必须加密 |
+| 请求超时 | 毫秒正整数 | 存入渠道配置,provider 请求使用该值;设置合理的最小/最大边界 |
+| 启用状态 | 是否参与调度 | 停用立即失去调度资格,但不删除历史任务和健康记录 |
+
+“渠道模型” Tab:
+
+- 只有点击“从供应商拉取模型”才调用真实 `/v1/models`;显示独立 loading、实际请求路径、`requestAttempted`、拉取时间、候选数量和错误阶段。
+- 错误至少区分未发起的 URL/SSRF 校验、DNS、网络连接、TLS、HTTP 401/403/404/429/5xx、JSON 无效和 `data` 为空。
+- 拉取结果只作为候选。管理员勾选候选并点击“加入渠道模型”后才保存,不能自动创建公开模型、映射或上架。
+- 手动补录请求模型 ID必须明确标记为手动,不能为空,不能与现有渠道模型重复。
+- 动态行使用创建时稳定 React key,不能使用正在编辑的模型 ID作为 key。
+
+“模型映射” Tab:
+
+- 左侧是平台公开模型 ID的主题化下拉,右侧是供应商 `requestModelId` 输入框。
+- 目标请求 ID必须存在于该渠道模型清单;平台显示 ID不得重复。
+- 映射保持可选:只有平台公开 ID与供应商请求 ID不同时才保存映射;两者相同时,只有该 ID已经作为明确的渠道模型存在,才允许做同名精确匹配。不得回退到任意首个模型或旧 `providerModelId`。
+- 映射只解决 ID转换,不改变分辨率、能力和价格规则。
+
+弹窗和异步规则:保存成功自动关闭并刷新;失败保留草稿并显示中文错误;保存、取消、右上角关闭、遮罩点击和 ESC 都能关闭;探活和拉取模型不能复用保存按钮的 loading。
+
+### 2.2 模型管理 `/model-products`
+
+页面标题显示为“模型管理”,路由可暂时保留旧路径以减少无意义的地址变更。
+
+**职责**:维护平台公开模型目录和用户可见能力。
+
+**不负责**:供应商连接、供应商请求模型 ID、渠道优先级、价格、套餐并发。
+
+```text
++--------------------------------------------------------------------------------+
+| 模型管理 [搜索公开 ID/名称________] [状态 v] [能力 v] [刷新] [+ 新增模型] |
++--------------------------------------------------------------------------------+
+| 公开模型 ID | 展示名称 | 档位 | 能力 | 分辨率 | 可用渠道 | 状态 | 版本 | 操作 |
+| image-pro | 图像专业 | 旗舰 | 图像 | 1K/2K | 3 | 草稿 | v3 | 编辑 |
++--------------------------------------------------------------------------------+
+```
+
+编辑/新增字段:
+
+```text
++----------------------------------+
+| 新增/编辑公开模型 X |
++----------------------------------+
+| 公开模型 ID [image-pro_______] |
+| 展示名称 [________________] |
+| 档位 [基础 v] |
+| 能力 [x 图像] [ 文本]... |
+| 能力参数 [结构化字段] |
+| 发布状态 [草稿/已上架开关] |
+| 分辨率 [+ 新增分辨率] |
+| 稳定 ID [1K] 名称 [低] |
+| 宽 [1024] 高 [1024] |
+| 支持比例 [1:1] [16:9] ... |
+| [取消][保存] |
++----------------------------------+
+```
+
+字段规则:
+
+- `publicModelId` 创建必填且全局唯一;编辑只读,不允许用改名方式破坏公开 API。
+- 展示名称、档位、能力使用结构化字段;能力是多选/复选框,不再使用逗号分隔字符串。
+- 能力参数按已登记 schema 展示字段,不允许原始 JSON 文本框。
+- 分辨率保留稳定 ID、名称、宽、高、支持比例;比例使用多选,不允许把 `1:116:99...` 这样的拼接字符串当作一个值。
+- 分辨率的价格倍率不放在模型目录,价格由价格规则页面管理。
+- 新模型保存默认为草稿;上架是单独的显式动作。上架前服务端检查合法能力、至少一个合法分辨率(需要分辨率的能力)、至少一个可用路由和版本条件。
+- 列表不显示基础价格、数量上限、并发上限、渠道组 ID、供应商名称或供应商请求模型 ID。
+- “从渠道候选导入”只生成公开模型草稿,必须由管理员补齐公开参数并保存;不能自动建立渠道映射。
+
+### 2.3 分组管理 `/channel-groups`
+
+页面标题显示为“分组管理”。分组是路由策略容器,不是模型产品的附属字段。
+
+**职责**:确定哪些公开模型可通过哪些渠道执行,以及每个模型的渠道顺序、失败预算和调度策略。
+
+```text
++--------------------------------------------------------------------------------+
+| 分组管理 [搜索] [启用状态 v] [刷新] [+ 新增分组] |
++--------------------------------------------------------------------------------+
+| 分组名称 | 公开模型数 | 渠道关系数 | 首选渠道摘要 | 策略 | 重试预算 | 版本 | 操作 |
+| 图像默认 | 2 | 6 | A > B > C | 严格 | 4 | v5 | 配置 |
++--------------------------------------------------------------------------------+
+```
+
+分组编辑采用“模型路由矩阵”,而不是一个全局渠道池:
+
+```text
++--------------------------------------------------------------------------------+
+| 分组:图像默认 X |
++--------------------------------------------------------------------------------+
+| 分组名称 [________________] 启用 [开关] |
+| 公开模型 [x image-pro] [x image-lite] |
++--------------------------------------------------------------------------------+
+| 模型 image-pro 调度 [严格优先级 v] 总重试 [3] |
+| 渠道 | 是否支持 | 优先级层 | 层内顺序 | 失败预算 | 健康 | 容量 | 操作 |
+| A | [x] | 1 | 1 | 1 | 正常 | 2/10 | ↑ ↓ 删除 |
+| B | [x] | 1 | 2 | 1 | 降级 | 8/10 | ↑ ↓ 删除 |
+| C | [ ] | - | - | - | - | - | 加入 |
++--------------------------------------------------------------------------------+
+| 模型 image-lite 调度 [同优先级智能 v] 总重试 [2] |
+| 渠道 ... |
+| [预览故障切换] [取消] [保存] |
++--------------------------------------------------------------------------------+
+```
+
+字段含义和规则:
+
+- 分组 ID由系统生成、只读;分组名称必填;启用状态单独控制。
+- 一个公开模型第一阶段最多有一个启用分组;同一个分组可以包含多个公开模型。保存时拒绝重复归属,并给出中文冲突信息。
+- 每个模型有自己的渠道成员、优先级层、层内顺序、总重试预算和渠道失败预算。一个渠道可以支持同一分组中的一个模型而不支持另一个模型。
+- 渠道候选必须已启用,并且渠道模型清单中存在有效 `requestModelId` 或有效 `displayModelId -> requestModelId` 映射;不满足条件的渠道显示为禁用原因,不能静默加入。
+- 优先级层是正整数,数字越小越先尝试;同一层允许多个渠道。严格模式按“优先级层、层内顺序”执行,智能模式只在同一优先级层内动态排序。
+- 顺序保存拒绝重复、遗漏、未知渠道、不属于该模型路由的渠道,以及同一层内重复的顺序值;使用 `If-Match`,冲突返回 409。
+- 默认策略是严格优先级;“同优先级智能调度”是显式开关,不是后台自动改变管理员顺序。
+- “预览故障切换”是纯函数:输入当前模型路由、健康、容量和模拟失败集合,输出候选过滤理由和排序结果;不得请求上游、创建任务、扣金币或写健康状态。
+- 渠道普通属性编辑不能修改任何分组顺序;排序只能在分组路由接口中修改。
+
+### 2.4 渠道健康 `/channel-health`
+
+**职责**:显示渠道运行历史和当前调度资格,提供单项/批量探活。
+
+```text
++--------------------------------------------------------------------------------+
+| 渠道健康 [分组 v] [状态 v] [全部探活] [刷新] |
++--------------------------------------------------------------------------------+
+| 渠道 | 12 根趋势条 | 当前状态 | 成功率 | P95 | 容量 | 连续失败 | 熔断剩余 | 操作|
+| A | |||||||||||||| | 健康 | 99.2% | 820ms | 2/10 | 0 | - | 探活 |
+| B | |||||||||||||| | 降级 | 84.1% | 2.4s | 8/10 | 2 | 38s | 探活 |
+| C | |||||||||||||| | 未探活 | - | - | - | - | - | 探活 |
++--------------------------------------------------------------------------------+
+```
+
+趋势条规则:
+
+- 每个渠道固定渲染 12 根,数据不足从左侧补灰或按统一时间槽补灰;数组长度永远是 12,不因样本数量改变布局。
+- 绿:探活/任务成功且延迟在正常阈值内。
+- 黄:慢、负载高或结果为降级。
+- 红:网络、超时、HTTP 5xx或供应商失败。
+- 灰:无数据或该时间槽没有样本。
+- 蓝:当前探活请求进行中;探活完成后按结果落为绿/黄/红/灰。
+- 颜色只表达样本趋势;当前状态、启用状态、模型覆盖、容量和熔断资格必须单独展示。
+
+API 必须返回 `groupIds: string[]`、当前状态、成功率、P95、容量、连续失败、`circuitResetAt`、最后错误、最后探活信息和固定长度 `samples`。`samples` 来自独立探活/运行样本记录,不再只从所有任务 attempts 临时聚合。
+
+探活操作必须显示独立 loading、真实请求路径、`requestAttempted` 和中文阶段错误。批量探活不能让某一渠道的失败覆盖其他渠道的结果。
+
+### 2.5 任务中心 `/tasks`
+
+**职责**:查看任务生命周期、路由快照、尝试记录、错误、输出、取消和重试。
+
+```text
++--------------------------------------------------------------------------------+
+| 任务中心 [任务 ID/模型搜索____] [状态 v] [类型 v] [刷新] |
+| [全部任务] [排队中] [运行中] [未知待对账] [已完成] [失败] |
++--------------------------------------------------------------------------------+
+| 任务 ID | 公开模型 | 类型 | 状态 | 预留金币 | 尝试 | 当前渠道 | 更新时间 | 操作 |
++--------------------------------------------------------------------------------+
+```
+
+详情使用字段化组件:
+
+- 公开模型 ID、分组、价格规则、套餐版本和渠道顺序的只读快照。
+- 每次尝试的渠道、状态、开始/结束时间、可重试标记、中文错误分类。
+- 输出数量、类型、MIME、尺寸和结算状态。
+- 不显示 Base URL、API Key、供应商请求模型 ID、供应商请求 ID或原始 JSON。
+- “未知待对账”是任务中心的独立 Tab,使用任务 ID深链接和状态筛选;成功/失败对账必须显示版本冲突、重复提交和余额影响。
+
+### 2.6 价格规则 `/pricing`
+
+**职责**:独立管理公开模型的计费规则,不把价格塞回模型目录。
+
+```text
++--------------------------------------------------------------------------------+
+| 价格规则 [公开模型 v] [分辨率 v] [状态 v] [刷新] [+ 新增规则] |
++--------------------------------------------------------------------------------+
+| 模型 | 分辨率 | 数量阶梯 | 单位价格 | 倍率/计算方式 | 生效版本 | 状态 | 操作 |
+| image-pro | 1K | 1-1/2-4 | 10 | 1.0 | v3 | 生效 | 编辑 |
++--------------------------------------------------------------------------------+
+```
+
+规则字段:公开模型 ID、可选分辨率 ID、数量阶梯、单位价格、价格计算方式、金币单位版本、生效时间/结束时间、启用状态和版本。保存时拒绝重叠生效区间和重叠数量阶梯;调价使用 `If-Match`,冲突返回 409。
+
+任务创建只能调用统一 `resolvePricingRule`,计算出的规则 ID、版本、单位价格、分辨率和数量固化到 `pricingSnapshot`。历史任务不能因后续调价而改变。
+
+### 2.7 套餐与运行额度 `/plans`
+
+**职责**:管理用户权益,不与模型目录字段重复。
+
+```text
++--------------------------------------------------------------------------------+
+| 套餐管理 [状态 v] [刷新] [+ 新增套餐] |
++--------------------------------------------------------------------------------+
+| 套餐 | 金币 | 并发上限 | 队列优先级 | 可用模型 | 可用分组 | 有效期 | 状态 | 操作 |
++--------------------------------------------------------------------------------+
+```
+
+套餐字段:金币额度、有效期、允许的公开模型、允许的路由分组、最大并发、队列优先级、存储保留策略和发布状态。任务创建时只读取套餐权益;不再从模型读取 `maxConcurrent`。数量安全硬上限属于系统运行策略(例如 `SystemSettings.generationMaxCount`),不出现在模型表单。
+
+### 2.8 审计 `/audit`、`/audit-logs`
+
+逻辑上是一个审计模块,可保留当前“日志大板”和“详细日志”两个入口。
+
+- 大板:HTTP 成功、4xx/5xx、业务错误、渠道切换、熔断、对账和配置变更趋势。
+- 详细日志:时间、中文分类、中文行为、操作人、对象、资源公开标识、结果状态和来源。
+- 审计内部可保存不可公开的 `requestId` 用于排查,但页面不展示内部请求 ID;页面只展示审计记录 ID或短的安全追踪标识。
+- HTTP 状态趋势来自独立的请求结果记录或统一响应完成钩子,必须保存真实 `statusCode`;`AuditRecord` 保存配置/业务动作及明确 `outcome`。两者可以通过安全追踪 ID关联,但不得根据 action 名称猜测 200/500。
+- 详情必须经过白名单字段投影和递归脱敏,不能直接返回存储的 `before`/`after`。
+- 禁止显示凭证、Cookie、完整提示词、供应商请求 ID、哈希链内部值和原始 JSON。
+
+## 3. 目标数据模型和字段迁移
+
+### 3.1 公开模型目录
+
+目标结构:
+
+```text
+PublicModelProduct
+ internalId 管理内部稳定 ID
+ publicModelId 平台公开 ID,创建后不可变
+ name
+ tier
+ capabilities[] 结构化多选值
+ capabilityParameters 按 capability schema 保存
+ resolutions[] id/name/width/height/ratios[]
+ enabled 草稿或已上架
+ version
+```
+
+从模型目录移除:`basePrice`、`maxCount`、`maxConcurrent`、`channelGroupId`、分辨率 `priceMultiplier`、逗号能力字符串和供应商字段。
+
+替代关系:
+
+| 旧字段 | 新归属 | 迁移/代码要求 |
+| --- | --- | --- |
+| `basePrice` | `PricingRule.unitPrice` | 先建立价格解析器和价格规则存储,再删除产品读取 |
+| `maxCount` | `SystemSettings.generationMaxCount` | 作为统一安全上限;套餐可进一步限制,不在模型页配置 |
+| `maxConcurrent` | `PlanEntitlement.maxConcurrent`、系统队列和渠道租约 | 任务创建/worker统一读取额度和租约 |
+| `channelGroupId` | `ChannelGroup.routes[publicModelId]` | 每个公开模型第一阶段只允许一个启用分组 |
+| `capabilities: string` | `capabilities: string[]` | 契约和页面统一多选,不再 `split(',')` |
+| `resolutions[].priceMultiplier` | `PricingRule` | 模型仍保留分辨率本身,但不保存价格语义 |
+
+由于项目当前主要使用 JSON snapshot,不能只改 TypeScript 类型而不处理持久化。需要给 snapshot 增加结构版本;开发环境遇到旧结构时按项目未上线规则直接拒绝加载并给出清晰重置/重新初始化提示,不写隐式旧字段兼容分支。
+
+### 3.2 渠道和模型映射
+
+目标结构:
+
+```text
+ProviderChannel
+ id
+ label
+ providerType
+ baseUrl
+ secretRefEncrypted
+ timeoutMs
+ enabled
+ version
+ runtimeHealth
+ circuit
+ capacity
+ models[]
+ requestModelId
+ source: discovered | manual
+ enabled
+ lastSeenAt
+ modelMappings[]
+ displayModelId
+ requestModelId
+```
+
+`providerModelId`、`enabledModelIds`和`resolutionModelMap`不再作为独立旧路径继续参与请求。所有请求模型必须先在 `models[]` 中存在;如果公开模型 ID与请求 ID不同,必须存在显式映射;如果二者相同,可以对渠道模型清单做同名精确匹配。provider 不允许在渠道模型不存在时把公开 ID直接当上游 ID,也不允许回退到任意首个模型。
+
+### 3.3 分组和模型路由
+
+目标结构用模型维度保存路由:
+
+```text
+ChannelGroup
+ id
+ name
+ enabled
+ version
+ routes: [
+ {
+ publicModelId
+ channels: [
+ { channelId, priorityTier, orderWithinTier, retryBudget }
+ ]
+ strategy: strict | smart-within-priority
+ retryBudget
+ }
+ ]
+```
+
+保存不允许同一个 `publicModelId` 在多个启用分组中出现;一个分组内同一个模型只能有一条 route。后续如需一个模型多套路由策略,应另立需求设计用户/套餐到分组的选择规则,不能在本阶段隐式放开多归属。
+
+### 3.4 健康样本和熔断
+
+健康展示与调度运行状态分离:
+
+```text
+ChannelHealthSample
+ channelId
+ source: probe | task
+ status: success | slow | network_error | http_error | provider_error | empty
+ latencyMs?
+ httpStatus?
+ requestPath?
+ requestAttempted
+ errorCode?
+ createdAt
+```
+
+渠道熔断至少需要:`closed/healthy`、`degraded`、`open`、`half-open`和独立 `enabled=false`。`half-open` 只能由一个探测租约持有者执行,成功回到 healthy,失败重新 open;默认冷却 60 秒,实际值可由系统策略配置并写入快照/审计。
+
+### 3.5 任务快照
+
+任务创建完成后保存不可变:
+
+```text
+routeSnapshot
+ groupId
+ groupVersion
+ publicModelId
+ routeVersion
+ orderedCandidates[]
+ channelId
+ priorityTier
+ orderWithinTier
+ requestModelId
+ channelRetryBudget
+ strategy
+ totalRetryBudget
+
+modelSnapshot
+ publicModelId
+ modelVersion
+ capability
+ resolutionId
+
+pricingSnapshot
+ ruleId
+ ruleVersion
+ unitPrice
+ quantity
+ multiplier
+ balanceUnitVersion
+
+planSnapshot
+ planId
+ planVersion
+ maxConcurrent
+ queuePriority
+```
+
+任务入队后不重新读取当前模型、价格、分组、渠道映射或套餐策略。重试若是用户主动创建新任务,必须生成新的任务 ID和新的快照;不能修改原任务快照。
+
+## 4. API 边界和响应规则
+
+### 4.1 资源 API
+
+| 模块 | API 目标 | 关键规则 |
+| --- | --- | --- |
+| 渠道 | `GET/POST/PATCH /api/v1/admin/channels` | 保存连接配置,不触发上游;凭证写入加密引用 |
+| 渠道发现 | `POST /api/v1/admin/channels/:id/discover-models` | 只有显式点击触发;返回路径、是否发起、阶段错误和候选 |
+| 渠道模型 | `POST /api/v1/admin/channels/:id/models`、映射更新 API | 候选加入和映射保存分开;请求 ID必须存在 |
+| 模型 | `GET/POST/PATCH /api/v1/admin/model-products` | 只接收公开目录字段;未知旧字段拒绝或明确忽略,不能重新持久化 |
+| 分组 | `GET/POST/PATCH /api/v1/admin/channel-groups` | 保存模型路由关系、成员资格、策略和版本 |
+| 分组排序 | `PATCH /api/v1/admin/channel-groups/:id/routes/:modelId/order` | 完整集合校验、`If-Match`、审计、冲突 409 |
+| 健康 | `GET /api/v1/admin/channel-health`、`POST /api/v1/admin/channels/:id/probe` | 返回完整 `groupIds` 和 12 样本;探活不改变分组顺序 |
+| 任务 | `GET /api/v1/admin/tasks`、`GET /api/v1/admin/tasks/:id` | 服务端筛选/分页;详情只返回字段化安全投影 |
+| 对账 | `POST /api/v1/admin/tasks/:id/reconcile` | 仅 unknown;金额、数量、文件、版本和幂等严格校验 |
+| 价格 | `GET/POST/PATCH /api/v1/admin/pricing-rules` | 生效区间/数量阶梯不重叠;`If-Match` |
+| 套餐 | 复用现有 `/api/v1/admin/membership-plans` 资源 | 只管理权益和队列额度,不写模型价格字段 |
+| 审计 | `GET /api/v1/admin/audit`、`GET /api/v1/admin/audit-logs` | 分页、状态字段、白名单安全投影 |
+
+### 4.2 写请求通用规则
+
+- 所有管理写接口需要对应 scope;高风险写操作保留现有审批边界。
+- 资源有 `version` 时必须支持 `If-Match`;旧版本返回 409,不静默覆盖。
+- 成功保存、探活、模型发现和对账都写审计;审计内容经过脱敏投影。
+- 重复提交使用已有管理幂等机制;同一幂等键不同 body 返回冲突。
+- 同一个业务事务内完成资源、账本/任务状态和必要审计写入;若底层仓储不能在一个事务内完成,必须设计可靠 outbox,而不是接受“可能没有审计”。
+- 422 表示字段、关系、金额、能力或输出校验失败;409表示版本、状态、幂等或资源关系冲突;503表示暂时不可用或没有可用路由。
+
+### 4.3 列表分页和请求代次
+
+所有管理列表统一使用 `AdminPage`:
+
+```text
+GET /api/v1/admin/tasks?page=1&pageSize=20&status=unknown&query=...
+{ items: [...], total: 42, page: 1, pageSize: 20 }
+```
+
+服务端负责筛选、排序和分页,前端不再对全量数组 `slice` 后假装分页。前端每个列表保存请求代次和 `AbortController`;筛选/分页快速变化时,旧响应不能回写新条件。空态、加载态、失败重试和最后一次成功数据要分别表达。
+
+## 5. 核心运行逻辑
+
+### 5.1 创建任务的候选过滤
+
+任务创建先通过模型和价格校验,再为该模型解析唯一启用分组和 route。候选依次过滤:
+
+1. 分组启用、模型 route 存在。
+2. 渠道启用、provider 类型匹配。
+3. 渠道清单中存在请求模型 ID;映射完整且不指向不存在的请求模型。
+4. 渠道具备任务能力和分辨率覆盖。
+5. 渠道不是 `open`;`half-open`只有探测租约或显式允许的一次验证请求。
+6. 渠道、套餐、系统和当前任务租约有可用容量。
+7. 当前任务已经失败过的渠道不重复尝试,除非是明确的新任务重试。
+8. route 和任务剩余重试预算允许继续尝试。
+
+每个被过滤的候选都保留机器可读原因,例如 `CHANNEL_DISABLED`、`MODEL_NOT_MAPPED`、`CAPACITY_FULL`、`CIRCUIT_OPEN`,管理详情可显示中文映射。没有候选时返回 `MODEL_GROUP_UNAVAILABLE`,不发送任何上游请求。
+
+### 5.2 排序和智能调度
+
+默认严格使用管理员保存的 `priorityTier` 和 `orderWithinTier`。
+
+当 route 明确开启 `smart-within-priority` 时:
+
+- 先取数值最小的可用 `priorityTier`;只在这个优先级层内智能排序。
+- 健康等级优先:healthy 高于 half-open,高于 degraded;open 不参与普通排序。
+- 同等级按可用容量降序、P95 延迟升序、成功率降序、稳定 channel ID升序排序。
+- 数值更大的低优先级层永远不能越过仍有候选的高优先级层。
+- 排序是纯函数,输入必须包含 routeSnapshot 和健康/容量快照;稳定 ID保证相同输入有相同结果。
+- 本阶段不引入无法持久化的随机权重;若未来需要权重或平滑加权轮询,先增加独立数据字段、并发安全游标和测试。
+
+### 5.3 熔断状态机
+
+```text
+disabled --管理员启用--> healthy 或 unknown
+unknown --首次成功--> healthy
+unknown --首次失败--> degraded
+healthy --慢/可确认失败--> degraded
+degraded --连续失败达到 3 次--> open
+open --冷却 60 秒且取得探测租约--> half-open
+half-open --探测成功--> healthy
+half-open --探测失败--> open,并重新计算冷却时间
+任意运行状态 --管理员停用--> disabled
+```
+
+状态转换和探测租约必须在事务/持久化租约边界内完成,防止多 worker 同时把同一渠道当成半开探测。页面显示“停用”来自 `enabled=false`,不能把它伪装成健康颜色。
+
+### 5.4 故障切换
+
+- 只有 provider 明确返回 `status=failed` 且 `retryable=true` 的可确认失败,才允许按 routeSnapshot 尝试下一个渠道。
+- 认证失败、密钥不存在、模型未配置、请求可能已到达上游但响应丢失等不可确认错误不得盲目切换;根据具体原因进入失败或 unknown。
+- transport timeout、AbortError或可能到达上游的响应丢失进入 unknown,停止切换和退款。
+- 每一次尝试都消耗任务总重试预算和对应渠道失败预算,且使用任务创建时的快照值,不读取实时配置覆盖历史任务。
+- 故障切换、过滤原因、最终状态和结算结果都写事件和审计,但不写供应商敏感字段。
+
+### 5.5 Unknown 和人工对账
+
+当前 OpenAI 图片 provider 没有查询接口,因此本阶段规则是:
+
+- unknown 任务保留预留金币,不由普通预留过期回收释放。
+- 不自动切换到下一个渠道,不自动退款,不假设上游失败。
+- 人工成功对账必须校验:任务仍为 unknown、`If-Match`版本正确、实际扣除金额满足 `0 <= chargedAmount <= reservedCost`、上传文件数量等于任务 count、MIME允许、文件大小和图片尺寸符合模型/任务参数、文件内容可解析。
+- 超过预留金额返回 422,不能 `Math.min` 静默截断。
+- 人工成功/失败必须幂等;重复相同请求返回同一结果,冲突 body 返回 409。
+- staging 文件在事务失败、版本冲突或账本失败时清理;已提交文件和任务状态必须有一致的生命周期。
+- 账本、任务状态、输出对象引用和审计要么一起提交,要么通过可靠 outbox补偿;不能接受任务已完成而没有审计的窗口。
+
+自动 unknown 查询不列入本阶段完成条件。只有未来新增 provider 的异步查询能力契约后,才可以启用;启用时必须逐项验证任务类型、count、输出数量、MIME、大小、尺寸、图片内容和 provider 状态,任何一项失败都保持 `unknown/manual_review`。
+
+## 6. 逐阶段实施计划
+
+### P0:冻结基线和关系设计
+
+**目标**:在改代码前把字段、关系、兼容边界和测试入口写成可执行清单。
+
+**需要做的事**:
+
+- 读取并核对 `AGENTS.md`、`plan.md`、`docs/miragenflow-channel-model-page-wireframes.md`、`docs/miragenflow-channel-model-logic.html`。
+- 在代码改动前建立旧字段到新字段的映射表,确认 `PublicModelProduct`、`InternalModelProduct`、`ChannelGroup`、`ProviderChannel`、任务快照和套餐引用的所有读取点。
+- 明确 snapshot schema 版本、测试 fixture边界和本地旧 snapshot 处理方式。
+- 明确本阶段“一模型一个启用分组”的规则,并为多分组未来需求留下错误码而不是隐式行为。
+
+**代码范围**:本阶段只读,不修改业务代码;审批前只允许修改本计划。
+
+**完成条件**:每个旧字段都有替代归属;每个任务创建读取点都有后续阶段负责人;没有“后面再决定”的模型/分组基数。
+
+### P1:契约、存储和价格/分组基础模型
+
+**目标**:先建立不会破坏任务创建的目标数据形状。
+
+**代码改法**:
+
+- 修改 `packages/contracts/src/index.ts`:收窄公开模型字段,增加价格规则、渠道模型、路由、健康样本和分页类型;更新任务快照类型。
+- 修改 `server/src/store.ts`:增加 `pricingRules`、健康样本存储、渠道模型实体、按模型 route 的分组结构和 snapshot schema version;补充默认 fixture。
+- 修改 `server/src/infra/repository.ts` 及必要 migration:让新字段能保存、恢复和校验;不保留旧字段双读。
+- 修改 `server/src/app/http.ts` 的 `createTask` 前置解析:先调用统一的模型、路由和价格解析器,暂时保留旧字段只到 P1 完成的同一提交边界,不允许新写入继续产生旧结构。
+- 增加 `SystemSettings.generationMaxCount` 或同等运行策略字段,替代模型级 `maxCount`;套餐仍负责 `maxConcurrent`。
+
+**目标结果**:模型、价格、分组、套餐和任务快照的责任边界在类型层和保存层一致;服务端不再需要从模型目录读取价格/并发/分组 ID。
+
+**测试**:契约构造测试、snapshot 保存恢复测试、价格规则版本测试、模型重复分组归属测试、旧字段拒绝/不写回测试。
+
+**依赖**:P0完成后才能开始;P2、P3、P4都依赖本阶段的字段形状。
+
+### P2:模型管理和价格规则 API/UI
+
+**目标**:让模型页面只表达公开模型和分辨率,让价格页面真正拥有价格。
+
+**代码改法**:
+
+- 修改 `admin/src/pages/Business/index.tsx` 的模型列表、表单字段和请求 body:删除 `basePrice`、`maxCount`、`maxConcurrent`、`channelGroupId`、逗号能力;改用能力多选、结构化参数和分辨率数组。
+- 将价格列表和编辑弹窗改为读取/写入 `pricingRules`,不再 PATCH 模型产品的 `basePrice`。
+- 修改 `server/src/app/http.ts` 的 `POST/PATCH model-products`:只接受公开模型字段,创建默认为草稿,编辑公开 ID只读;上架前执行能力、分辨率和路由检查。
+- 修改 `server/src/app/http.ts` 的 `pricing-rules`:实现规则实体、数量阶梯/生效区间非重叠校验、版本和 If-Match。
+- 修改 `server/src/app/http.ts` 的 `createTask` 和计费 helper:统一调用 `resolvePricingRule`,生成新的 `pricingSnapshot`。
+- 修改 `admin/src/services/platform.ts`:补齐模型/价格接口的 typed wrapper、中文错误转换和版本头传递。
+
+**目标结果**:打开模型页面看不到无意义的价格、数量、并发和渠道组字段;价格改动只影响新任务,历史任务金额不变。
+
+**测试**:模型请求 body 静态检查;模型上架缺路由失败;能力/分辨率非法值失败;价格规则重叠失败;价格版本冲突 409;任务快照使用规则版本而不是产品旧价格。
+
+**依赖**:P1;P3 的分组页面只能选择已保存模型,P8 的 UI 拆分以本阶段字段为准。
+
+### P3:渠道、渠道模型和映射
+
+**目标**:把供应商接入和平台公开模型彻底分开。
+
+**代码改法**:
+
+- 修改 `server/src/store.ts` 和 contracts:引入渠道模型实体、超时和探活结构;移除旧 `providerModelId` 直连依赖。
+- 修改 `server/src/app/http.ts` 的渠道创建/编辑:保存连接、加密凭证、超时和启用状态;保存过程不得调用上游;普通 PATCH不得写分组优先级。
+- 修改发现接口:把 `/v1/models` 请求封装成显式 action,返回候选、路径、`requestAttempted`、错误阶段和时间。
+- 修改 provider 的 `resolveChannelModel`:优先使用显式 `displayModelId -> requestModelId` 映射;无映射时只允许渠道模型清单中的同名精确匹配,删除旧 `providerModelId`、任意首模型等 fallback。
+- 修改管理台渠道 Tab:候选勾选加入、映射独立保存、稳定 React key、独立 loading、失败保留草稿。
+- 补齐 DNS/TLS/HTTP/JSON/data-empty 的结构化错误字段,前端只做中文显示映射,不从自由文本猜错误。
+
+**目标结果**:保存渠道不会偷偷请求供应商;只有点击发现才访问 `/v1/models`;模型映射缺失在任务创建前就能被识别。
+
+**测试**:保存/编辑不发起上游请求;发现成功和各阶段失败;映射目标不存在失败;未登记公开 ID不能作为请求 ID;已登记同名渠道模型可精确匹配;渠道 PATCH前后所有分组顺序完全相同;超时使用渠道配置。
+
+**依赖**:P1;P4 分组成员资格依赖渠道模型实体。
+
+### P4:分组模型路由和顺序隔离
+
+**目标**:让分组真正表达“哪些模型使用哪些渠道、按什么顺序”。
+
+**代码改法**:
+
+- 修改 `ChannelGroup` 保存按 `publicModelId` 分组的 routes;每个成员保存 `priorityTier`、`orderWithinTier` 和失败预算;删除产品侧 `channelGroupId` 读取和写入。
+- 修改分组创建/编辑 API:校验模型存在、启用分组唯一归属、渠道启用、协议兼容、模型覆盖和映射完整。
+- 修改排序 API:按模型 route校验提交集合与原成员集合完全一致;拒绝重复、遗漏、未知和越组成员;`If-Match`后再更新版本。
+- 删除渠道 PATCH 对分组 `channelPriorities` 的回写逻辑;顺序只能由 route/order 接口修改。
+- 修改任务创建:按公开模型找到唯一 route,保存排序后的 routeSnapshot、请求模型 ID和每渠道预算。
+- 修改管理台分组编辑器:模型选择、每模型渠道矩阵、上移/下移、失败预算、策略选择和纯函数故障切换预览。
+
+**目标结果**:同一渠道可以进入多个分组,也可以在同一分组的不同模型 route中有不同顺序;渠道名称或凭证编辑不会改变任何路由顺序。
+
+**测试**:一模型重复归属失败;同渠道多分组独立顺序;遗漏/越组排序失败;If-Match冲突;渠道 PATCH顺序不变;任务 routeSnapshot与创建时版本一致。
+
+**依赖**:P2、P3;P6 调度过滤以本阶段 routeSnapshot为输入。
+
+### P5:健康样本、容量和熔断
+
+**目标**:建立可用于调度的真实运行状态和固定 12 根趋势数据。
+
+**代码改法**:
+
+- 在 `server/src/store.ts`/repository中增加健康样本记录和容量事实来源;任务租约是并发事实来源,不能只依赖进程内 `activeTaskIds`。
+- 修改 `server/src/app/http.ts` 健康接口:返回完整 `groupIds`、容量、熔断剩余、最后错误、最近样本并统一补足 12 项。
+- 修改探活处理:每次显式探活写入样本,保留真实路径和 `requestAttempted`;不要把任务 attempts 临时聚合结果冒充探活样本。
+- 修改 `task-worker.ts`:实现 healthy/degraded/open/half-open状态、60 秒默认冷却、单探测租约、成功恢复和失败重开。
+- 修改 `admin/src/pages/Business/components/Charts.tsx` 或新的健康私有组件:每一行固定 12 根竖条,颜色和当前状态分开渲染;不能使用 `safeRows.slice(0, 12)`代替样本序列。
+
+**目标结果**:健康页能同时回答“过去 12 次/时间槽发生了什么”和“当前是否允许调度”;多个分组归属完整显示。
+
+**测试**:空样本/1样本/12样本/超过12样本;groupIds完整;容量竞争;半开单租约;30秒旧逻辑不再生效;停用状态不参与调度;探活和任务样本来源区分。
+
+**依赖**:P3、P4;P6 的候选排序需要健康和容量快照。
+
+### P6:智能调度、严格切换和任务执行
+
+**目标**:把“优先级”和“智能调度”变成可证明的纯逻辑。
+
+**代码改法**:
+
+- 在 `server/src/jobs` 或同目录新增纯函数,负责候选过滤、过滤原因和同优先级排序;输入使用任务 routeSnapshot、渠道快照、健康快照和租约容量。
+- 修改 `task-worker.ts`:使用上述纯函数,不再直接遍历所有渠道后只过滤 `enabled/open`;候选为空时不发上游并返回 `MODEL_GROUP_UNAVAILABLE`。
+- 明确 `ProviderResult.retryable` 的切换判断;移除对 `PROVIDER_AUTH`、`PROVIDER_SECRET_UNAVAILABLE`等不可重试错误的无条件切换。
+- 保持 routeSnapshot优先,排除当前任务已失败渠道,按总预算和渠道预算控制 attempts。
+- 把渠道、套餐、系统的有效并发统一到 durable lease;跨进程/跨实例不能只看内存 Set。
+
+**目标结果**:严格模式按优先级层和层内顺序执行;智能模式只在同一优先级层内择优;低优先级不能越级;未知结果不会被错误当作可重试失败。
+
+**测试**:健康/容量/映射/能力/熔断/已失败渠道过滤;同优先级稳定排序;低优先级越级保护;无候选不上游;retryable与non-retryable切换;并发竞争和租约过期;routeSnapshot不受配置后改影响。
+
+**依赖**:P4、P5;P7 对账依赖任务状态和输出校验边界。
+
+### P7:人工对账和输出安全校验
+
+**目标**:保证未知结果不会重复扣费、误退款或接受不完整输出。
+
+**代码改法**:
+
+- 修改 `server/src/app/http.ts` 对账接口:超额金额 422;任务状态、版本、幂等和 count严格校验;人工成功支持与任务 count匹配的输出集合。
+- 抽取 provider 输出校验 helper,覆盖任务类型、数量、MIME、大小、尺寸、图片内容和 staging引用;自动查询尚未有真实 provider能力时不启用。
+- 修改事务边界:任务、账本、对象引用和审计在同一事务或可靠 outbox中完成;staging失败路径清理。
+- 修改现有“clamps the charge”测试为“rejects over-reservation with 422”,并补 0、等于预留、负数、小数、重复请求和并发对账案例。
+- 管理台任务中心增加未知 Tab、对账弹窗、预留金额提示和中文失败原因;失败保留草稿。
+
+**目标结果**:unknown永远由人工确认或明确的未来 provider 查询能力结算;超额输入不会被静默改写;不完整输出不会变成成功任务。
+
+**测试**:金额边界、count>1、错误 MIME/尺寸/内容、事务回滚、staging清理、审计原子性、并发对账和预留过期竞争。
+
+**依赖**:P1、P6;OpenAI 图片的自动 query不作为本阶段完成条件。
+
+### P8:后台模块化、分页、审计和文档收口
+
+**目标**:让已经正确的后端逻辑在后台页面中清晰、可维护、可验收。
+
+**代码改法**:
+
+- 将 `admin/src/pages/Business/index.tsx` 中的巨型 `sections`、字段定义、请求和弹窗按职责拆到同目录私有模块;保留统一布局、TDesign Provider和现有 Lineicons,不做无关视觉重写。
+- 每个模块建立独立的列表列定义、过滤参数、请求代次、loading/empty/error状态和弹窗草稿;避免通过无名 `FormItem`覆盖受控 value。
+- 把管理 API 改为 `AdminPage` 服务端分页;前端保留筛选条件和请求代次,旧响应不得覆盖新页面。
+- HTTP 请求结果增加真实状态字段,审计动作增加明确 outcome;详情经过安全白名单投影;大板只统计真实请求状态和业务结果,不按 action 猜测。
+- 检查所有新增/编辑/确认/提示弹窗的取消、右上角、遮罩和 ESC;异步成功关闭,失败保留草稿。
+- 更新 `CHANGELOG.md` 的 Unreleased;同步 `todo.mdx/todo.zh-CN.mdx` 和 `pending-test.mdx/pending-test.zh-CN.mdx`;用户浏览器确认后再从 pending-test迁移到 `features.mdx/features.zh-CN.mdx`。
+
+**目标结果**:后台 8 个业务模块边界清晰,列表数据不会全量加载,敏感详情不会原样输出;文档准确区分已实现和待人工测试。
+
+**测试**:admin typecheck、Lint、列表响应契约、分页/竞态、详情脱敏、弹窗状态静态检查;不执行浏览器。
+
+**依赖**:P2-P7对应 API稳定后执行;文档状态不能早于实际代码状态。
+
+## 7. 文件级修改清单
+
+以下是批准后允许触碰的主要文件;实际执行仍以每阶段需要为准,不得顺手改无关文件。
+
+### 服务端和契约
+
+- `packages/contracts/src/index.ts`:公开模型、渠道模型、路由快照、价格快照、健康样本、管理分页和审计投影。
+- `server/src/store.ts`:Store字段、默认 fixture、snapshot schema、健康/价格/路由结构。
+- `server/src/app/http.ts`:管理 API、任务创建、价格解析、渠道属性隔离、健康、对账和审计响应。
+- `server/src/adapters/provider.ts`:请求模型解析、超时、探活错误阶段、输出校验边界和 provider能力声明。
+- `server/src/jobs/task-worker.ts`:候选过滤、智能排序、租约、熔断、严格切换和结算。
+- `server/src/infra/repository.ts`:snapshot保存/恢复、事务或 outbox边界。
+- `server/migrations/*.sql`:只有确实需要的 schema/snapshot版本变更;不创建与当前 JSON仓储无关的假实体表。
+- `server/tests/*.test.ts`:每阶段的纯函数、契约、HTTP、事务、provider和 worker 回归。
+
+### 管理后台
+
+- `admin/src/pages/Business/index.tsx`:路由分发和业务模块组装,逐步移除巨型字段/弹窗逻辑。
+- `admin/src/pages/Business/index.module.less`:必要的列表、12根趋势条、窄屏和状态布局样式。
+- `admin/src/pages/Business/components/Charts.tsx`:健康趋势等图表/列表展示;不能复用渠道数量切片表达样本。
+- `admin/src/services/platform.ts`:接口封装、错误分类、If-Match、幂等和分页参数。
+- `admin/src/router/index.ts`:只有确实需要真实独立页面时才调整;现有 URL优先保持稳定。
+- `admin/src/components/LineIcons.jsx`:只复用已有本地图标,不增加外部图标资源。
+
+### 画布和用户端禁改集合
+
+- `web/src/pages/canvas/**`
+- `web/src/components/canvas/**`
+- `web/src/stores/canvas/**`
+- `web/src/lib/canvas/**`
+
+## 8. 代码级验收
+
+### 8.1 每阶段必须验收
+
+- 契约能通过类型检查,旧字段没有新的写入点。
+- 任务创建和 worker 的实际调用链与目标规则一致,不只检查页面文案。
+- 管理写接口具备权限、版本、幂等、事务和审计边界。
+- 敏感字段不出现在公开响应、管理详情、审计详情和错误日志中。
+- 没有修改画布禁改集合,没有新增展示 mock 渠道。
+
+### 8.2 推荐命令
+
+批准并完成代码后,按仓库脚本执行:
+
+```text
+npm --prefix server run typecheck
+npm --prefix server test
+npm --prefix admin run typecheck
+npm --prefix admin run lint
+git diff --check
+```
+
+如修改共享契约、构建入口、分页组件或管理路由,再增加:
+
+```text
+npm --prefix web run typecheck
+npm --prefix admin run build
+npm --prefix web run build
+npm run build:all
+```
+
+本阶段不执行 `npm run dev:all`、浏览器启动、Playwright、截图或模拟操作。
+
+### 8.3 关键验收清单
+
+- [ ] 模型页面和 body没有价格、数量上限、并发上限、渠道组 ID、逗号能力。
+- [ ] 价格规则独立保存,任务快照包含规则 ID和版本。
+- [ ] 一个公开模型最多一个启用分组;分组按模型保存渠道关系。
+- [ ] `publicModelId`和`requestModelId`全链路分离;只有渠道模型清单中的同名精确匹配或显式映射才能得到上游请求 ID。
+- [ ] 渠道保存/编辑/切 Tab/刷新不请求供应商;只有主动发现和探活发起上游请求。
+- [ ] 渠道 PATCH不改变任何分组顺序;排序拒绝遗漏、重复、未知和越组成员,并使用 If-Match。
+- [ ] 健康接口返回完整 `groupIds`、容量、熔断剩余和固定 12 个样本;页面固定显示 12 根彩色竖条。
+- [ ] 熔断有 healthy/degraded/open/half-open/disabled语义和单探测租约。
+- [ ] 严格优先级不会被智能调度越级;智能排序只发生在同一最低可用 `priorityTier`。
+- [ ] 只有明确 retryable失败才切换;可能到达上游的未知结果停止切换和退款。
+- [ ] unknown不被普通过期任务自动退款;OpenAI 图片自动查询不被误报为已实现。
+- [ ] 人工对账超额金额返回 422,不静默截断;count、MIME、大小、尺寸和内容完整校验。
+- [ ] 对账任务、账本、输出引用和审计具备原子事务或可靠 outbox;失败清理 staging。
+- [ ] HTTP 趋势来自真实请求状态,审计动作有明确 outcome;详情使用白名单投影,不返回原始 `before/after`。
+- [ ] 管理列表使用服务端分页和请求代次,旧响应不能覆盖新筛选。
+- [ ] 测试 fixture只存在于测试模式,生产无展示 mock渠道。
+- [ ] 画布禁改集合无 diff。
+
+## 9. 审批后的阶段执行提示词
+
+以下提示词只能在负责人明确批准本计划后使用,并且必须在当前任务中逐阶段执行;不得创建、派生或转交到新的用户任务。每完成一个阶段先报告代码级结果,确认没有跨阶段遗留破坏,再进入下一阶段。
+
+### 9.1 P1 契约和基础模型
+
+```text
+在 /Users/qiu/Desktop/MiragenFlow 当前任务中执行 plan.md 的 P1,不创建新任务,不修改画布。先读 AGENTS.md、plan.md、packages/contracts/src/index.ts、server/src/store.ts、server/src/infra/repository.ts、server/src/app/http.ts 中 createTask 和现有持久化测试。先建立独立 PricingRule、渠道模型、按公开模型保存的 ChannelGroup route、健康样本和新版任务快照契约,再把 store/repository/createTask 调整到新结构。一个公开模型最多属于一个启用分组;项目未上线,不写旧字段双读兼容,但旧 snapshot 必须明确拒绝或按审批方案重新初始化。只改 P1 必需文件,补保存恢复、重复归属、价格版本和旧字段拒绝测试。完成后只做代码级验收并报告,不启动浏览器。
+```
+
+### 9.2 P2 模型和价格
+
+```text
+在当前任务执行 plan.md 的 P2。先读 P1 最终类型和测试,再读 admin/src/pages/Business/index.tsx、admin/src/services/platform.ts、server/src/app/http.ts 的 model-products、pricing-rules 和 createTask。模型管理只保留公开 ID、名称、档位、能力多选、结构化能力参数、分辨率和发布状态,删除价格、数量、并发、渠道组 ID、逗号能力和分辨率价格倍率。价格规则建立独立实体、数量阶梯、生效区间、版本和 If-Match;任务只能通过 resolvePricingRule 生成 pricingSnapshot。保留现有 TDesign 和 Lineicons,不改画布。补上架缺路由、价格重叠、版本冲突和历史任务快照测试,代码级验收后停止报告。
+```
+
+### 9.3 P3 渠道和模型映射
+
+```text
+在当前任务执行 plan.md 的 P3。先读渠道页面、platform service、ProviderChannel、渠道 API、provider adapter 和安全测试。渠道保存只处理连接、加密凭证、timeoutMs、启用、渠道模型和可选映射,不得隐式请求上游。模型发现只能由显式按钮触发 /v1/models,并返回实际路径、requestAttempted 和结构化错误阶段。拉取候选与模型映射必须独立;同名精确匹配只在渠道模型清单已登记该 ID 时允许,ID不同必须显式 displayModelId -> requestModelId,删除 providerModelId 和任意首模型 fallback。渠道普通 PATCH 不得改任何分组顺序。补未发起/DNS/TLS/HTTP/JSON/data空、映射和顺序隔离测试。只做代码级验收,不启动浏览器。
+```
+
+### 9.4 P4 分组模型路由
+
+```text
+在当前任务执行 plan.md 的 P4。将 ChannelGroup 改为按 publicModelId 保存 routes,每个渠道成员保存 priorityTier、orderWithinTier 和 retryBudget。一个公开模型最多属于一个启用分组;一个分组可以管理多个模型;同一渠道可在不同模型或不同分组中有独立优先级。分组保存校验模型、渠道启用、协议、模型覆盖和映射;排序 API 按模型 route 检查成员集合完全一致,拒绝重复、遗漏、未知、越组和层内顺序冲突,使用 If-Match。任务创建固化唯一 routeSnapshot。管理台使用模型路由矩阵和纯函数故障切换预览。补多分组独立顺序、渠道 PATCH 不影响顺序、版本冲突和快照不变测试,不改画布。
+```
+
+### 9.5 P5 健康和熔断
+
+```text
+在当前任务执行 plan.md 的 P5。建立独立 ChannelHealthSample 和 durable 容量/探测租约,健康接口返回完整 groupIds、当前状态、成功率、P95、容量、连续失败、熔断剩余、最后探活和固定 12 个 samples。每个渠道页面固定显示 12 根竖条:绿成功、黄慢/降级、红失败、灰无数据、蓝探活中;趋势与当前调度资格分开。实现 healthy/degraded/open/half-open,默认 60 秒冷却和单 half-open 探测租约;停用独立表达。不要把前 12 个渠道或所有历史 attempts 冒充 12 个探活样本。补样本补齐、groupIds、容量竞争、半开单租约和恢复测试,只做代码级验收。
+```
+
+### 9.6 P6 智能调度
+
+```text
+在当前任务执行 plan.md 的 P6。把候选过滤和排序抽成纯函数,输入 routeSnapshot、渠道/健康/容量快照和当前任务 attempts;输出有序候选与每个被过滤渠道的机器可读原因。严格模式按 priorityTier、orderWithinTier;智能模式只在数值最小的可用 priorityTier 内按健康、可用容量、P95、成功率和稳定 ID排序,低优先级不能越级。worker 只对 status=failed 且 retryable=true 的可确认失败切换;认证、密钥、模型未配置和可能到达上游的 transport unknown 不得盲目切换。统一 durable 租约并发事实。补所有过滤原因、稳定排序、越级保护、无候选不上游、重试预算和跨实例容量测试。
+```
+
+### 9.7 P7 人工对账
+
+```text
+在当前任务执行 plan.md 的 P7。OpenAI 图片查询能力仍标记不可用,unknown 保留预留,不自动查询、切换或退款。重写人工成功/失败对账:只接受 unknown 和匹配 If-Match 的任务;chargedAmount 必须在 0 到 reservedCost,超额返回 422;输出数量必须等于 count,并校验任务类型、MIME、大小、尺寸和图片内容。幂等相同请求复用结果,不同 body 冲突。任务、账本、输出引用和审计在同一事务或可靠 outbox 中完成,失败清理 staging。把现有 clamps 测试改为超额拒绝,并补多输出、事务回滚和并发对账测试。管理台在任务中心 unknown Tab 中提供对账弹窗,不另造无关系页面。
+```
+
+### 9.8 P8 后台收口
+
+```text
+在当前任务执行 plan.md 的 P8。只在 P2-P7 API 稳定后拆分 admin/src/pages/Business/index.tsx:各业务模块使用同目录私有组件/配置,保留现有 TDesign、共享 Provider 和本地 Lineicons,不引入新 UI 库。所有列表改用 AdminPage 服务端分页,前端使用 AbortController 和请求代次防旧响应回写;所有弹窗支持取消、右上角、遮罩和 ESC,失败保留草稿。HTTP 请求结果保存真实 statusCode,审计动作保存 outcome,详情使用白名单字段和递归脱敏。更新 CHANGELOG、双语 todo 和 pending-test;浏览器由负责人验收,未确认内容不写入正式 features。执行完整代码级命令并报告所有剩余风险。
+```
+
+## 10. 交付口径和审批后执行规则
+
+每次阶段交付必须分别报告:
+
+1. 已修改的文件、符号和接口。
+2. 已实现且代码级验收通过的规则。
+3. 已实现但尚未由负责人浏览器确认的交互。
+4. 尚未实现、必须进入 `todo.mdx/todo.zh-CN.mdx` 的事项。
+5. 实际执行的命令及结果,未执行的浏览器验收明确列出。
+
+完成度百分比只能按“阶段目标和验收条件都通过”的交付项计算;计划、ASCII草图、架构图、文档和未人工测试页面不能算完成。
+
+审批前停止在此,不修改代码,不启动服务,不打开浏览器,不另开线程。
diff --git a/scripts/build-all.mjs b/scripts/build-all.mjs
index a66ffc8..0dba6ce 100644
--- a/scripts/build-all.mjs
+++ b/scripts/build-all.mjs
@@ -12,6 +12,7 @@ function run(command, args, cwd) {
if (result.status !== 0) process.exit(result.status ?? 1);
}
+run(npmCommand, ["run", "typecheck"], path.join(projectRoot, "server"));
run(npmCommand, ["run", "build"], path.join(projectRoot, "web"));
run(npmCommand, ["run", "build"], path.join(projectRoot, "admin"));
diff --git a/scripts/dev-all.mjs b/scripts/dev-all.mjs
index d15f572..11b24cd 100644
--- a/scripts/dev-all.mjs
+++ b/scripts/dev-all.mjs
@@ -1,4 +1,5 @@
import { spawn } from "node:child_process";
+import net from "node:net";
import { fileURLToPath } from "node:url";
import path from "node:path";
@@ -6,29 +7,83 @@ const scriptsDir = path.dirname(fileURLToPath(import.meta.url));
const projectRoot = path.resolve(scriptsDir, "..");
const npmCommand = process.platform === "win32" ? "npm.cmd" : "npm";
-const processes = [
- spawn(npmCommand, ["run", "dev"], { cwd: path.join(projectRoot, "web"), stdio: "inherit" }),
- spawn(npmCommand, ["run", "dev:linux", "--", "--force"], { cwd: path.join(projectRoot, "admin"), stdio: "inherit" }),
-];
-
-let stopping = false;
-
-function stopAll(exitCode = 0) {
- if (stopping) return;
- stopping = true;
- for (const child of processes) {
- if (!child.killed) child.kill("SIGTERM");
- }
- setTimeout(() => process.exit(exitCode), 100);
-}
-
-for (const child of processes) {
- child.on("exit", (code, signal) => {
- if (stopping) return;
- const exitCode = typeof code === "number" && code !== 0 ? code : signal ? 1 : 0;
- stopAll(exitCode);
+function canListen(port) {
+ return new Promise((resolve) => {
+ const connection = net.createConnection({ port, host: "127.0.0.1" });
+ connection.once("connect", () => {
+ connection.destroy();
+ resolve(false);
+ });
+ connection.once("error", () => {
+ connection.destroy();
+ const probe = net.createServer();
+ probe.once("error", () => resolve(false));
+ probe.listen(port, "0.0.0.0", () => probe.close(() => resolve(true)));
+ });
});
}
+const reservedPorts = new Set();
+
+async function freePort(start) {
+ for (let port = start; port < start + 100; port += 1) {
+ if (reservedPorts.has(port)) continue;
+ if (await canListen(port)) {
+ reservedPorts.add(port);
+ return port;
+ }
+ }
+ throw new Error(`No free port near ${start}`);
+}
+
+const serverPort = await freePort(Number(process.env.MIRAGENFLOW_SERVER_PORT || 3100));
+const webPort = await freePort(Number(process.env.MIRAGENFLOW_WEB_PORT || 3000));
+const adminPort = await freePort(Number(process.env.MIRAGENFLOW_ADMIN_PORT || 3003));
+// Keep the local preview account/session state across server restarts. Without
+// an explicit persistence setting the server defaults to an in-memory store,
+// which makes a user appear to have the wrong password after Vite/tsx reloads
+// the process. Production deployments can override this with PostgreSQL (or
+// an explicit MIRAGENFLOW_STORE_FILE).
+const previewStoreFile = process.env.MIRAGENFLOW_STORE_FILE?.trim() || path.join("/tmp", "miragenflow-dev", "store.json");
+const commonEnv = {
+ ...process.env,
+ MIRAGENFLOW_SERVER_PORT: String(serverPort),
+ MIRAGENFLOW_ADMIN_PORT: String(adminPort),
+ MIRAGENFLOW_STORE_FILE: previewStoreFile,
+ MIRAGENFLOW_ADMIN_EMAIL: process.env.MIRAGENFLOW_ADMIN_EMAIL?.trim() || "admin@admin.com",
+ MIRAGENFLOW_ADMIN_PASSWORD: process.env.MIRAGENFLOW_ADMIN_PASSWORD || "admin",
+ MIRAGENFLOW_ADMIN_MFA_REQUIRED: process.env.MIRAGENFLOW_ADMIN_MFA_REQUIRED ?? "false",
+ MIRAGENFLOW_ADMIN_CAPTCHA_REQUIRED: process.env.MIRAGENFLOW_ADMIN_CAPTCHA_REQUIRED ?? "false",
+ MIRAGENFLOW_ADMIN_SELF_APPROVAL: process.env.NODE_ENV === "production" ? "false" : process.env.MIRAGENFLOW_ADMIN_SELF_APPROVAL ?? "true",
+ // Keep the local preview clean by default; opt into showcase records explicitly.
+ MIRAGENFLOW_DEMO_DATA: process.env.MIRAGENFLOW_DEMO_DATA ?? "false",
+ MIRAGENFLOW_USER_MFA_REQUIRED: process.env.MIRAGENFLOW_USER_MFA_REQUIRED ?? "false",
+ MIRAGENFLOW_USER_VERIFICATION_REQUIRED: process.env.MIRAGENFLOW_USER_VERIFICATION_REQUIRED ?? "false",
+ MIRAGENFLOW_CAPTCHA_REQUIRED: process.env.MIRAGENFLOW_CAPTCHA_REQUIRED ?? "false",
+ // Local provider emulators commonly run on localhost or a private LAN.
+ // Production is forced closed; non-production can opt out explicitly.
+ MIRAGENFLOW_ALLOW_LOCAL_PROVIDER_URLS: process.env.NODE_ENV === "production" ? "false" : process.env.MIRAGENFLOW_ALLOW_LOCAL_PROVIDER_URLS ?? "true",
+};
+const processes = [
+ spawn(npmCommand, ["run", "dev"], { cwd: path.join(projectRoot, "server"), env: { ...commonEnv, MIRAGENFLOW_PORT: String(serverPort) }, stdio: "inherit" }),
+ spawn(npmCommand, ["run", "dev", "--", "--port", String(webPort)], { cwd: path.join(projectRoot, "web"), env: commonEnv, stdio: "inherit" }),
+ spawn(npmCommand, ["run", "dev:linux", "--", "--force", "--port", String(adminPort)], { cwd: path.join(projectRoot, "admin"), env: commonEnv, stdio: "inherit" }),
+];
+
+console.log(`MiragenFlow dev origin: http://localhost:${webPort}/`);
+console.log(`Admin path: http://localhost:${webPort}/admin/ (admin Vite ${adminPort})`);
+console.log(`API gateway: http://localhost:${webPort}/api/ (server ${serverPort})`);
+
+let stopping = false;
+function stopAll(exitCode = 0) {
+ if (stopping) return;
+ stopping = true;
+ for (const child of processes) if (!child.killed) child.kill("SIGTERM");
+ setTimeout(() => process.exit(exitCode), 100);
+}
+for (const child of processes) child.on("exit", (code, signal) => {
+ if (stopping) return;
+ stopAll(typeof code === "number" && code !== 0 ? code : signal ? 1 : 0);
+});
process.on("SIGINT", () => stopAll(0));
process.on("SIGTERM", () => stopAll(0));
diff --git a/server/.env.example b/server/.env.example
new file mode 100644
index 0000000..d97d6ae
--- /dev/null
+++ b/server/.env.example
@@ -0,0 +1,13 @@
+MIRAGENFLOW_HOST=127.0.0.1
+MIRAGENFLOW_PORT=3100
+MIRAGENFLOW_ACCESS_TOKEN_SECRET=replace-with-a-random-secret
+MIRAGENFLOW_COOKIE_SECURE=false
+MIRAGENFLOW_ADMIN_EMAIL=admin@admin.com
+MIRAGENFLOW_ADMIN_PASSWORD=admin
+MIRAGENFLOW_ADMIN_MFA_REQUIRED=false
+MIRAGENFLOW_ADMIN_CAPTCHA_REQUIRED=false
+MIRAGENFLOW_USER_MFA_REQUIRED=false
+MIRAGENFLOW_USER_VERIFICATION_REQUIRED=false
+MIRAGENFLOW_EMAIL_REGISTRATION_VERIFICATION_REQUIRED=false
+MIRAGENFLOW_PHONE_REGISTRATION_VERIFICATION_REQUIRED=false
+MIRAGENFLOW_CAPTCHA_REQUIRED=false
diff --git a/server/Dockerfile b/server/Dockerfile
new file mode 100644
index 0000000..a64b1bc
--- /dev/null
+++ b/server/Dockerfile
@@ -0,0 +1,15 @@
+FROM node:22-alpine
+WORKDIR /app
+COPY package.json package-lock.json ./
+COPY server/package.json server/package.json
+COPY packages/contracts/package.json packages/contracts/package.json
+RUN npm ci --ignore-scripts --omit=dev
+COPY packages/contracts packages/contracts
+COPY server server
+RUN mkdir -p /var/lib/miragenflow/staging && addgroup -S app && adduser -S -G app app && chown -R app:app /app /var/lib/miragenflow
+ENV NODE_ENV=production
+ENV MIRAGENFLOW_STAGING_DIR=/var/lib/miragenflow/staging
+USER app
+EXPOSE 3100
+HEALTHCHECK --interval=10s --timeout=3s --retries=5 CMD node -e "fetch('http://127.0.0.1:3100/api/ready').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"
+CMD ["node", "--import", "tsx", "server/src/index.ts"]
diff --git a/server/migrations/0001_v1_baseline.sql b/server/migrations/0001_v1_baseline.sql
new file mode 100644
index 0000000..fb0c1a2
--- /dev/null
+++ b/server/migrations/0001_v1_baseline.sql
@@ -0,0 +1,8 @@
+-- V1 baseline. The in-memory adapter is used for local bootstrap; production
+-- migrations must keep these ownership, snapshot, and ledger boundaries.
+CREATE TABLE IF NOT EXISTS schema_migrations (
+ version TEXT PRIMARY KEY,
+ applied_at TIMESTAMPTZ NOT NULL DEFAULT now()
+);
+
+INSERT INTO schema_migrations(version) VALUES ('0001_v1_baseline') ON CONFLICT DO NOTHING;
diff --git a/server/migrations/0002_v1_domain.sql b/server/migrations/0002_v1_domain.sql
new file mode 100644
index 0000000..92ecd46
--- /dev/null
+++ b/server/migrations/0002_v1_domain.sql
@@ -0,0 +1,118 @@
+-- Domain boundary for the first PostgreSQL adapter. Secrets and user files
+-- are referenced by encrypted ids; they are never stored in static assets.
+CREATE TABLE IF NOT EXISTS users (
+ id UUID PRIMARY KEY,
+ status TEXT NOT NULL,
+ display_name TEXT NOT NULL DEFAULT '',
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
+);
+CREATE TABLE IF NOT EXISTS user_identities (
+ id UUID PRIMARY KEY,
+ user_id UUID NOT NULL REFERENCES users(id),
+ kind TEXT NOT NULL,
+ normalized_value TEXT NOT NULL,
+ verified_at TIMESTAMPTZ,
+ UNIQUE(kind, normalized_value)
+);
+CREATE TABLE IF NOT EXISTS password_credentials (
+ user_id UUID PRIMARY KEY REFERENCES users(id),
+ password_hash TEXT NOT NULL,
+ algorithm_version TEXT NOT NULL,
+ changed_at TIMESTAMPTZ NOT NULL DEFAULT now()
+);
+CREATE TABLE IF NOT EXISTS verification_codes (
+ id UUID PRIMARY KEY,
+ target_hash TEXT NOT NULL,
+ purpose TEXT NOT NULL,
+ code_hash TEXT NOT NULL,
+ expires_at TIMESTAMPTZ NOT NULL,
+ attempts INTEGER NOT NULL DEFAULT 0,
+ consumed_at TIMESTAMPTZ,
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now()
+);
+CREATE TABLE IF NOT EXISTS sessions (
+ id UUID PRIMARY KEY,
+ subject_id UUID NOT NULL,
+ scope TEXT NOT NULL,
+ refresh_hash TEXT NOT NULL UNIQUE,
+ rotation_id UUID NOT NULL,
+ expires_at TIMESTAMPTZ NOT NULL,
+ revoked_at TIMESTAMPTZ,
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now()
+);
+CREATE TABLE IF NOT EXISTS balance_accounts (
+ user_id UUID PRIMARY KEY REFERENCES users(id),
+ available NUMERIC(24, 6) NOT NULL DEFAULT 0,
+ reserved NUMERIC(24, 6) NOT NULL DEFAULT 0,
+ version BIGINT NOT NULL DEFAULT 0,
+ CHECK (available >= 0 AND reserved >= 0)
+);
+CREATE TABLE IF NOT EXISTS balance_ledger (
+ id UUID PRIMARY KEY,
+ user_id UUID NOT NULL REFERENCES users(id),
+ type TEXT NOT NULL,
+ amount NUMERIC(24, 6) NOT NULL,
+ unit_version INTEGER NOT NULL,
+ idempotency_key TEXT NOT NULL UNIQUE,
+ reference_id TEXT,
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now()
+);
+CREATE TABLE IF NOT EXISTS model_products (
+ id TEXT PRIMARY KEY,
+ public_model_id TEXT NOT NULL UNIQUE,
+ tier TEXT NOT NULL,
+ enabled BOOLEAN NOT NULL DEFAULT false,
+ version INTEGER NOT NULL DEFAULT 1,
+ snapshot JSONB NOT NULL
+);
+CREATE TABLE IF NOT EXISTS provider_channels (
+ id TEXT PRIMARY KEY,
+ status TEXT NOT NULL,
+ encrypted_secret_ref TEXT NOT NULL,
+ base_url TEXT NOT NULL,
+ health JSONB NOT NULL DEFAULT '{}'::jsonb,
+ version INTEGER NOT NULL DEFAULT 1
+);
+CREATE TABLE IF NOT EXISTS channel_groups (
+ id TEXT PRIMARY KEY,
+ version INTEGER NOT NULL,
+ enabled BOOLEAN NOT NULL DEFAULT false,
+ channel_order JSONB NOT NULL
+);
+CREATE TABLE IF NOT EXISTS generation_tasks (
+ id UUID PRIMARY KEY,
+ user_id UUID NOT NULL REFERENCES users(id),
+ task_type TEXT NOT NULL,
+ status TEXT NOT NULL,
+ idempotency_key TEXT NOT NULL,
+ model_snapshot JSONB NOT NULL,
+ route_snapshot JSONB NOT NULL,
+ pricing_snapshot JSONB NOT NULL,
+ reserved_amount NUMERIC(24, 6) NOT NULL DEFAULT 0,
+ state_version BIGINT NOT NULL DEFAULT 0,
+ lease_expires_at TIMESTAMPTZ,
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
+ UNIQUE(user_id, idempotency_key)
+);
+CREATE TABLE IF NOT EXISTS generation_attempts (
+ id UUID PRIMARY KEY,
+ task_id UUID NOT NULL REFERENCES generation_tasks(id),
+ channel_id TEXT NOT NULL,
+ sequence INTEGER NOT NULL,
+ status TEXT NOT NULL,
+ provider_request_id TEXT,
+ error_code TEXT,
+ started_at TIMESTAMPTZ NOT NULL,
+ finished_at TIMESTAMPTZ
+);
+CREATE TABLE IF NOT EXISTS audit_logs (
+ id UUID PRIMARY KEY,
+ actor_id UUID,
+ action TEXT NOT NULL,
+ object_type TEXT NOT NULL,
+ object_id TEXT,
+ request_id TEXT NOT NULL,
+ payload_digest TEXT NOT NULL,
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now()
+);
diff --git a/server/migrations/0003_v1_indexes.sql b/server/migrations/0003_v1_indexes.sql
new file mode 100644
index 0000000..89589a1
--- /dev/null
+++ b/server/migrations/0003_v1_indexes.sql
@@ -0,0 +1,3 @@
+CREATE INDEX IF NOT EXISTS generation_tasks_user_created_idx ON generation_tasks(user_id, created_at DESC);
+CREATE INDEX IF NOT EXISTS generation_attempts_task_sequence_idx ON generation_attempts(task_id, sequence);
+CREATE INDEX IF NOT EXISTS audit_logs_created_idx ON audit_logs(created_at DESC);
diff --git a/server/migrations/0004_v1_extended.sql b/server/migrations/0004_v1_extended.sql
new file mode 100644
index 0000000..f6f93e5
--- /dev/null
+++ b/server/migrations/0004_v1_extended.sql
@@ -0,0 +1,166 @@
+-- Extended V1 domain tables. Secrets are referenced by encrypted_ref only;
+-- user files live in staging/object adapters and never in static directories.
+CREATE TABLE IF NOT EXISTS balance_units (
+ id TEXT PRIMARY KEY,
+ version INTEGER NOT NULL,
+ name TEXT NOT NULL,
+ short_code TEXT NOT NULL,
+ icon TEXT NOT NULL,
+ precision INTEGER NOT NULL DEFAULT 0,
+ currency_code TEXT NOT NULL,
+ conversion_rate NUMERIC(24, 8) NOT NULL,
+ rounding TEXT NOT NULL,
+ published_at TIMESTAMPTZ
+);
+CREATE TABLE IF NOT EXISTS balance_buckets (
+ id UUID PRIMARY KEY,
+ user_id UUID NOT NULL REFERENCES users(id),
+ source TEXT NOT NULL,
+ remaining NUMERIC(24, 6) NOT NULL CHECK (remaining >= 0),
+ priority INTEGER NOT NULL DEFAULT 0,
+ expires_at TIMESTAMPTZ
+);
+CREATE TABLE IF NOT EXISTS membership_plans (
+ id TEXT PRIMARY KEY,
+ code TEXT NOT NULL UNIQUE,
+ version INTEGER NOT NULL DEFAULT 1,
+ snapshot JSONB NOT NULL,
+ published BOOLEAN NOT NULL DEFAULT false
+);
+CREATE TABLE IF NOT EXISTS user_plan_entitlements (
+ user_id UUID NOT NULL REFERENCES users(id),
+ plan_id TEXT NOT NULL REFERENCES membership_plans(id),
+ version INTEGER NOT NULL,
+ starts_at TIMESTAMPTZ NOT NULL,
+ expires_at TIMESTAMPTZ NOT NULL,
+ PRIMARY KEY (user_id, plan_id, version)
+);
+CREATE TABLE IF NOT EXISTS recharge_orders (
+ id UUID PRIMARY KEY,
+ user_id UUID NOT NULL REFERENCES users(id),
+ provider TEXT NOT NULL,
+ status TEXT NOT NULL,
+ fiat_amount NUMERIC(24, 6) NOT NULL,
+ balance_amount NUMERIC(24, 6) NOT NULL,
+ unit_version INTEGER NOT NULL,
+ idempotency_key TEXT NOT NULL UNIQUE,
+ provider_order_id TEXT,
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
+ paid_at TIMESTAMPTZ
+);
+CREATE TABLE IF NOT EXISTS payment_events (
+ id UUID PRIMARY KEY,
+ provider TEXT NOT NULL,
+ provider_event_id TEXT NOT NULL,
+ order_id UUID REFERENCES recharge_orders(id),
+ payload_digest TEXT NOT NULL,
+ status TEXT NOT NULL,
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
+ UNIQUE(provider, provider_event_id)
+);
+CREATE TABLE IF NOT EXISTS generation_outputs (
+ id UUID PRIMARY KEY,
+ task_id UUID NOT NULL REFERENCES generation_tasks(id),
+ owner_id UUID NOT NULL REFERENCES users(id),
+ object_key TEXT NOT NULL,
+ mime_type TEXT NOT NULL,
+ status TEXT NOT NULL,
+ expires_at TIMESTAMPTZ NOT NULL,
+ revoked_at TIMESTAMPTZ,
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now()
+);
+CREATE TABLE IF NOT EXISTS upload_objects (
+ id UUID PRIMARY KEY,
+ owner_id UUID NOT NULL REFERENCES users(id),
+ object_key TEXT NOT NULL,
+ mime_type TEXT NOT NULL,
+ byte_size BIGINT NOT NULL,
+ checksum TEXT NOT NULL,
+ scan_status TEXT NOT NULL,
+ expires_at TIMESTAMPTZ NOT NULL,
+ deleted_at TIMESTAMPTZ
+);
+CREATE TABLE IF NOT EXISTS assets (
+ id UUID PRIMARY KEY,
+ owner_id UUID NOT NULL REFERENCES users(id),
+ task_id UUID REFERENCES generation_tasks(id),
+ object_id UUID REFERENCES upload_objects(id),
+ asset_type TEXT NOT NULL,
+ snapshot JSONB NOT NULL,
+ deleted_at TIMESTAMPTZ,
+ retention_expires_at TIMESTAMPTZ
+);
+CREATE TABLE IF NOT EXISTS email_sms_outbox (
+ id UUID PRIMARY KEY,
+ target_hash TEXT NOT NULL,
+ channel TEXT NOT NULL,
+ purpose TEXT NOT NULL,
+ payload JSONB NOT NULL,
+ status TEXT NOT NULL DEFAULT 'pending',
+ attempts INTEGER NOT NULL DEFAULT 0,
+ next_attempt_at TIMESTAMPTZ NOT NULL DEFAULT now(),
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now()
+);
+CREATE TABLE IF NOT EXISTS webdav_configs (
+ user_id UUID PRIMARY KEY REFERENCES users(id),
+ encrypted_url_ref TEXT NOT NULL,
+ encrypted_username_ref TEXT,
+ encrypted_password_ref TEXT,
+ directory TEXT NOT NULL,
+ state TEXT NOT NULL,
+ last_synced_at TIMESTAMPTZ,
+ retention_days INTEGER NOT NULL DEFAULT 30
+);
+CREATE TABLE IF NOT EXISTS webdav_manifests (
+ id UUID PRIMARY KEY,
+ user_id UUID NOT NULL REFERENCES users(id),
+ version BIGINT NOT NULL,
+ checksum TEXT NOT NULL,
+ manifest JSONB NOT NULL,
+ state TEXT NOT NULL,
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
+ UNIQUE(user_id, version)
+);
+CREATE TABLE IF NOT EXISTS storage_policies (
+ id TEXT PRIMARY KEY,
+ version INTEGER NOT NULL,
+ snapshot JSONB NOT NULL,
+ published BOOLEAN NOT NULL DEFAULT false
+);
+CREATE TABLE IF NOT EXISTS admin_roles (
+ id TEXT PRIMARY KEY,
+ permissions JSONB NOT NULL,
+ enabled BOOLEAN NOT NULL DEFAULT true
+);
+CREATE TABLE IF NOT EXISTS admin_user_roles (
+ admin_id UUID NOT NULL,
+ role_id TEXT NOT NULL REFERENCES admin_roles(id),
+ PRIMARY KEY (admin_id, role_id)
+);
+CREATE TABLE IF NOT EXISTS admin_approvals (
+ id UUID PRIMARY KEY,
+ requester_id UUID NOT NULL,
+ approver_id UUID,
+ action TEXT NOT NULL,
+ object_type TEXT NOT NULL,
+ object_id TEXT,
+ status TEXT NOT NULL DEFAULT 'pending',
+ before_value JSONB,
+ after_value JSONB,
+ expires_at TIMESTAMPTZ,
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
+ approved_at TIMESTAMPTZ
+);
+CREATE TABLE IF NOT EXISTS task_event_log (
+ task_id UUID NOT NULL REFERENCES generation_tasks(id),
+ sequence BIGINT NOT NULL,
+ event_id UUID NOT NULL UNIQUE,
+ event_type TEXT NOT NULL,
+ payload JSONB NOT NULL,
+ occurred_at TIMESTAMPTZ NOT NULL DEFAULT now(),
+ PRIMARY KEY (task_id, sequence)
+);
+CREATE INDEX IF NOT EXISTS upload_objects_owner_idx ON upload_objects(owner_id, expires_at);
+CREATE INDEX IF NOT EXISTS generation_outputs_owner_idx ON generation_outputs(owner_id, expires_at);
+CREATE INDEX IF NOT EXISTS recharge_orders_user_created_idx ON recharge_orders(user_id, created_at DESC);
+CREATE INDEX IF NOT EXISTS task_event_log_task_sequence_idx ON task_event_log(task_id, sequence);
diff --git a/server/migrations/0005_v1_runtime_extensions.sql b/server/migrations/0005_v1_runtime_extensions.sql
new file mode 100644
index 0000000..d8363a4
--- /dev/null
+++ b/server/migrations/0005_v1_runtime_extensions.sql
@@ -0,0 +1,56 @@
+-- V1 runtime extension tables. The local fixture adapter mirrors these records
+-- in Store maps; production repositories must make these tables authoritative.
+-- payment_events is created by 0004 and reconciled by 0006. Keep this
+-- migration additive so fresh and upgraded databases share one schema.
+ALTER TABLE IF EXISTS payment_events ADD COLUMN IF NOT EXISTS external_event_id TEXT;
+CREATE UNIQUE INDEX IF NOT EXISTS payment_events_provider_external_idx_v1 ON payment_events(provider, external_event_id) WHERE external_event_id IS NOT NULL;
+
+CREATE TABLE IF NOT EXISTS admin_idempotency (
+ actor_id TEXT NOT NULL,
+ route TEXT NOT NULL,
+ idem_key TEXT NOT NULL,
+ request_fingerprint TEXT NOT NULL,
+ state TEXT NOT NULL,
+ status_code INTEGER,
+ response JSONB,
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
+ PRIMARY KEY (actor_id, route, idem_key)
+);
+
+CREATE TABLE IF NOT EXISTS admin_approval_requests (
+ id UUID PRIMARY KEY,
+ kind TEXT NOT NULL,
+ requester_id TEXT NOT NULL,
+ approver_id TEXT,
+ status TEXT NOT NULL,
+ payload JSONB NOT NULL,
+ reason TEXT NOT NULL,
+ version INTEGER NOT NULL DEFAULT 1,
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
+ expires_at TIMESTAMPTZ NOT NULL,
+ approved_at TIMESTAMPTZ,
+ executed_at TIMESTAMPTZ,
+ CHECK (approver_id IS NULL OR approver_id <> requester_id)
+);
+
+CREATE TABLE IF NOT EXISTS plan_purchases (
+ id UUID PRIMARY KEY,
+ user_id UUID NOT NULL,
+ plan_id UUID NOT NULL,
+ plan_version INTEGER NOT NULL,
+ status TEXT NOT NULL,
+ purchase_amount BIGINT NOT NULL,
+ included_balance BIGINT NOT NULL,
+ idempotency_key TEXT NOT NULL,
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
+ paid_at TIMESTAMPTZ,
+ UNIQUE (user_id, idempotency_key)
+);
+
+CREATE TABLE IF NOT EXISTS reserve_allocations (
+ user_id UUID NOT NULL,
+ reference_id UUID NOT NULL,
+ bucket_id UUID NOT NULL,
+ amount BIGINT NOT NULL,
+ PRIMARY KEY (user_id, reference_id, bucket_id)
+);
diff --git a/server/migrations/0006_v1_schema_reconciliation.sql b/server/migrations/0006_v1_schema_reconciliation.sql
new file mode 100644
index 0000000..6952b07
--- /dev/null
+++ b/server/migrations/0006_v1_schema_reconciliation.sql
@@ -0,0 +1,18 @@
+-- Reconcile the early domain migration with the runtime contract. Existing
+-- installations keep their data; the compatibility columns are populated by
+-- the payment adapter and the canonical identifier remains provider/event.
+ALTER TABLE IF EXISTS payment_events ADD COLUMN IF NOT EXISTS external_event_id TEXT;
+ALTER TABLE IF EXISTS payment_events ADD COLUMN IF NOT EXISTS provider_event_id TEXT;
+UPDATE payment_events SET external_event_id = COALESCE(external_event_id, provider_event_id) WHERE external_event_id IS NULL;
+UPDATE payment_events SET provider_event_id = COALESCE(provider_event_id, external_event_id) WHERE provider_event_id IS NULL;
+CREATE UNIQUE INDEX IF NOT EXISTS payment_events_provider_external_idx ON payment_events(provider, external_event_id) WHERE external_event_id IS NOT NULL;
+
+ALTER TABLE IF EXISTS plan_purchases ALTER COLUMN plan_id TYPE TEXT USING plan_id::text;
+ALTER TABLE IF EXISTS plan_purchases ALTER COLUMN purchase_amount TYPE BIGINT USING purchase_amount::bigint;
+ALTER TABLE IF EXISTS plan_purchases ALTER COLUMN included_balance TYPE BIGINT USING included_balance::bigint;
+CREATE UNIQUE INDEX IF NOT EXISTS plan_purchases_user_idempotency_idx ON plan_purchases(user_id, idempotency_key);
+
+ALTER TABLE IF EXISTS sessions ADD COLUMN IF NOT EXISTS absolute_expires_at TIMESTAMPTZ;
+ALTER TABLE IF EXISTS sessions ADD COLUMN IF NOT EXISTS family_id UUID;
+ALTER TABLE IF EXISTS sessions ADD COLUMN IF NOT EXISTS device_id TEXT;
+UPDATE sessions SET absolute_expires_at = COALESCE(absolute_expires_at, expires_at) WHERE absolute_expires_at IS NULL;
diff --git a/server/migrations/0007_runtime_domain_repository.sql b/server/migrations/0007_runtime_domain_repository.sql
new file mode 100644
index 0000000..adb4107
--- /dev/null
+++ b/server/migrations/0007_runtime_domain_repository.sql
@@ -0,0 +1,13 @@
+-- Runtime repository projection. Each root is a domain collection and each
+-- row is independently addressable, so PostgreSQL can be promoted from the
+-- legacy snapshot without exposing a JSON blob as the only source of truth.
+CREATE TABLE IF NOT EXISTS miragenflow_domain_rows (
+ root_key TEXT NOT NULL,
+ entity_id TEXT NOT NULL,
+ shape TEXT NOT NULL CHECK (shape IN ('map', 'array', 'scalar')),
+ payload JSONB,
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
+ PRIMARY KEY (root_key, entity_id)
+);
+CREATE INDEX IF NOT EXISTS miragenflow_domain_rows_updated_idx
+ ON miragenflow_domain_rows(updated_at DESC);
diff --git a/server/migrations/0008_v1_plan_payment_events.sql b/server/migrations/0008_v1_plan_payment_events.sql
new file mode 100644
index 0000000..43a2679
--- /dev/null
+++ b/server/migrations/0008_v1_plan_payment_events.sql
@@ -0,0 +1,143 @@
+-- Plan purchases use a separate payment-event table because the recharge
+-- payment_events.order_id foreign key intentionally points at recharge_orders.
+ALTER TABLE IF EXISTS plan_purchases ADD COLUMN IF NOT EXISTS provider TEXT NOT NULL DEFAULT 'adapter';
+ALTER TABLE IF EXISTS plan_purchases ADD COLUMN IF NOT EXISTS plan_snapshot JSONB;
+ALTER TABLE IF EXISTS plan_purchases ADD COLUMN IF NOT EXISTS credit_bucket_id UUID;
+ALTER TABLE IF EXISTS plan_purchases ADD COLUMN IF NOT EXISTS entitlement_id UUID;
+ALTER TABLE IF EXISTS recharge_orders ADD COLUMN IF NOT EXISTS credit_bucket_id UUID;
+ALTER TABLE IF EXISTS user_plan_entitlements ADD COLUMN IF NOT EXISTS snapshot JSONB;
+CREATE TABLE IF NOT EXISTS plan_payment_events (
+ id UUID PRIMARY KEY,
+ provider TEXT NOT NULL,
+ external_event_id TEXT NOT NULL,
+ purchase_id UUID NOT NULL REFERENCES plan_purchases(id),
+ payload_digest TEXT NOT NULL,
+ status TEXT NOT NULL,
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
+ applied_at TIMESTAMPTZ,
+ UNIQUE(provider, external_event_id)
+);
+
+-- Runtime records that are part of the server's durable state but are not
+-- user-facing business aggregates. They keep authentication challenges,
+-- provider configuration and staging metadata relationally addressable while
+-- preserving encrypted payloads and ownership boundaries.
+ALTER TABLE IF EXISTS verification_codes ADD COLUMN IF NOT EXISTS record_key TEXT;
+ALTER TABLE IF EXISTS verification_codes ADD COLUMN IF NOT EXISTS record_kind TEXT NOT NULL DEFAULT 'verification';
+ALTER TABLE IF EXISTS verification_codes ADD COLUMN IF NOT EXISTS ip TEXT;
+ALTER TABLE IF EXISTS verification_codes ADD COLUMN IF NOT EXISTS device_id TEXT;
+ALTER TABLE IF EXISTS verification_codes ADD COLUMN IF NOT EXISTS max_attempts INTEGER NOT NULL DEFAULT 5;
+CREATE INDEX IF NOT EXISTS verification_codes_record_key_idx ON verification_codes(record_kind, record_key);
+
+ALTER TABLE IF EXISTS users ADD COLUMN IF NOT EXISTS mfa_secret_encrypted TEXT;
+ALTER TABLE IF EXISTS users ADD COLUMN IF NOT EXISTS mfa_recovery_code_hashes JSONB;
+ALTER TABLE IF EXISTS users ADD COLUMN IF NOT EXISTS mfa_last_totp_counter BIGINT;
+ALTER TABLE IF EXISTS users ADD COLUMN IF NOT EXISTS mfa_required BOOLEAN NOT NULL DEFAULT false;
+ALTER TABLE IF EXISTS users ADD COLUMN IF NOT EXISTS mfa_enabled BOOLEAN NOT NULL DEFAULT false;
+ALTER TABLE IF EXISTS users ADD COLUMN IF NOT EXISTS roles JSONB NOT NULL DEFAULT '[]'::jsonb;
+ALTER TABLE IF EXISTS users ADD COLUMN IF NOT EXISTS failed_login_count INTEGER NOT NULL DEFAULT 0;
+ALTER TABLE IF EXISTS users ADD COLUMN IF NOT EXISTS locked_until BIGINT;
+
+ALTER TABLE IF EXISTS sessions ADD COLUMN IF NOT EXISTS replaced_by TEXT;
+ALTER TABLE IF EXISTS sessions ADD COLUMN IF NOT EXISTS roles JSONB;
+ALTER TABLE IF EXISTS sessions ADD COLUMN IF NOT EXISTS scopes JSONB;
+
+CREATE TABLE IF NOT EXISTS captcha_challenges (
+ id TEXT PRIMARY KEY,
+ action TEXT NOT NULL,
+ answer_hash TEXT NOT NULL,
+ expires_at TIMESTAMPTZ NOT NULL,
+ attempts INTEGER NOT NULL DEFAULT 0,
+ max_attempts INTEGER NOT NULL DEFAULT 5,
+ consumed_at TIMESTAMPTZ,
+ ip TEXT NOT NULL,
+ device_id TEXT
+);
+CREATE TABLE IF NOT EXISTS mfa_challenges (
+ id TEXT PRIMARY KEY,
+ subject_id TEXT NOT NULL,
+ scope TEXT NOT NULL,
+ expires_at TIMESTAMPTZ NOT NULL,
+ attempts INTEGER NOT NULL DEFAULT 0
+);
+CREATE TABLE IF NOT EXISTS session_families (
+ family_id TEXT PRIMARY KEY,
+ subject_id TEXT NOT NULL,
+ scope TEXT NOT NULL,
+ revoked_at TIMESTAMPTZ
+);
+CREATE TABLE IF NOT EXISTS admin_accounts (
+ id TEXT PRIMARY KEY,
+ email TEXT NOT NULL UNIQUE,
+ password_hash TEXT NOT NULL,
+ role TEXT NOT NULL,
+ mfa_secret_encrypted TEXT,
+ mfa_required BOOLEAN NOT NULL DEFAULT true,
+ recovery_code_hashes JSONB NOT NULL DEFAULT '[]'::jsonb,
+ mfa_last_totp_counter BIGINT
+);
+CREATE TABLE IF NOT EXISTS runtime_records (
+ record_type TEXT NOT NULL,
+ record_key TEXT NOT NULL,
+ payload JSONB NOT NULL,
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
+ PRIMARY KEY (record_type, record_key)
+);
+CREATE TABLE IF NOT EXISTS message_providers (
+ id TEXT PRIMARY KEY,
+ snapshot JSONB NOT NULL,
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
+);
+CREATE TABLE IF NOT EXISTS message_templates (
+ id TEXT PRIMARY KEY,
+ snapshot JSONB NOT NULL,
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
+);
+CREATE TABLE IF NOT EXISTS payment_providers (
+ id TEXT PRIMARY KEY,
+ snapshot JSONB NOT NULL,
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
+);
+CREATE TABLE IF NOT EXISTS webdav_files (
+ user_id TEXT NOT NULL,
+ path TEXT NOT NULL,
+ mime_type TEXT NOT NULL,
+ data TEXT NOT NULL DEFAULT '',
+ checksum TEXT NOT NULL,
+ etag TEXT NOT NULL,
+ version INTEGER NOT NULL DEFAULT 1,
+ updated_at TIMESTAMPTZ NOT NULL,
+ deleted_at TIMESTAMPTZ,
+ PRIMARY KEY (user_id, path)
+);
+CREATE TABLE IF NOT EXISTS webdav_sync_jobs (
+ id TEXT PRIMARY KEY,
+ user_id TEXT NOT NULL,
+ operation TEXT NOT NULL,
+ path TEXT NOT NULL,
+ data TEXT,
+ mime_type TEXT,
+ if_match TEXT,
+ attempts INTEGER NOT NULL DEFAULT 0,
+ status TEXT NOT NULL,
+ next_attempt_at TIMESTAMPTZ NOT NULL,
+ lease_expires_at TIMESTAMPTZ,
+ last_error TEXT,
+ created_at TIMESTAMPTZ NOT NULL
+);
+CREATE TABLE IF NOT EXISTS stored_objects (
+ id TEXT PRIMARY KEY,
+ owner_id TEXT NOT NULL,
+ mime_type TEXT NOT NULL,
+ data TEXT,
+ staging_key TEXT,
+ expires_at TIMESTAMPTZ NOT NULL,
+ task_id TEXT,
+ revoked BOOLEAN NOT NULL DEFAULT false
+);
+CREATE INDEX IF NOT EXISTS webdav_files_user_idx ON webdav_files(user_id, updated_at DESC);
+CREATE INDEX IF NOT EXISTS webdav_sync_jobs_user_idx ON webdav_sync_jobs(user_id, status, next_attempt_at);
+CREATE INDEX IF NOT EXISTS stored_objects_owner_idx ON stored_objects(owner_id, expires_at);
+
+ALTER TABLE IF EXISTS webdav_configs ADD COLUMN IF NOT EXISTS manifest_retention_expires_at TIMESTAMPTZ;
+ALTER TABLE IF EXISTS webdav_configs ADD COLUMN IF NOT EXISTS manifest_extension_days INTEGER NOT NULL DEFAULT 0;
diff --git a/server/migrations/0009_v1_captcha_target_hash.sql b/server/migrations/0009_v1_captcha_target_hash.sql
new file mode 100644
index 0000000..cbbfa04
--- /dev/null
+++ b/server/migrations/0009_v1_captcha_target_hash.sql
@@ -0,0 +1 @@
+ALTER TABLE captcha_challenges ADD COLUMN IF NOT EXISTS target_hash TEXT NOT NULL DEFAULT '';
diff --git a/server/migrations/0010_v1_consistency_and_webdav.sql b/server/migrations/0010_v1_consistency_and_webdav.sql
new file mode 100644
index 0000000..52f5502
--- /dev/null
+++ b/server/migrations/0010_v1_consistency_and_webdav.sql
@@ -0,0 +1,40 @@
+CREATE TABLE IF NOT EXISTS miragenflow_store_snapshots (
+ snapshot_key TEXT PRIMARY KEY,
+ payload JSONB NOT NULL,
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
+);
+CREATE TABLE IF NOT EXISTS miragenflow_domain_rows (
+ root_key TEXT NOT NULL,
+ entity_id TEXT NOT NULL,
+ shape TEXT NOT NULL,
+ payload JSONB,
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
+ PRIMARY KEY (root_key, entity_id)
+);
+CREATE TABLE IF NOT EXISTS miragenflow_store_revisions (
+ snapshot_key TEXT PRIMARY KEY,
+ revision BIGINT NOT NULL DEFAULT 0,
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
+);
+
+ALTER TABLE webdav_files ADD COLUMN IF NOT EXISTS sync_state TEXT NOT NULL DEFAULT 'synced';
+ALTER TABLE webdav_sync_jobs ADD COLUMN IF NOT EXISTS intent TEXT;
+ALTER TABLE webdav_sync_jobs ADD COLUMN IF NOT EXISTS conflict_copy_path TEXT;
+
+ALTER TABLE generation_tasks ADD COLUMN IF NOT EXISTS event_sequence BIGINT NOT NULL DEFAULT 0;
+ALTER TABLE generation_tasks ADD COLUMN IF NOT EXISTS reserve_expires_at TIMESTAMPTZ;
+ALTER TABLE generation_tasks ADD COLUMN IF NOT EXISTS updated_at TIMESTAMPTZ NOT NULL DEFAULT now();
+ALTER TABLE generation_tasks ADD COLUMN IF NOT EXISTS lease_token TEXT;
+ALTER TABLE generation_attempts ADD COLUMN IF NOT EXISTS platform_idempotency_key TEXT;
+ALTER TABLE generation_attempts ADD COLUMN IF NOT EXISTS reconciliation_status TEXT;
+ALTER TABLE generation_attempts ADD COLUMN IF NOT EXISTS lease_expires_at TIMESTAMPTZ;
+ALTER TABLE generation_outputs ADD COLUMN IF NOT EXISTS unit_price_snapshot BIGINT;
+ALTER TABLE generation_outputs ADD COLUMN IF NOT EXISTS charged_amount BIGINT;
+ALTER TABLE balance_ledger ADD COLUMN IF NOT EXISTS task_id UUID;
+ALTER TABLE balance_ledger ADD COLUMN IF NOT EXISTS output_id UUID;
+ALTER TABLE balance_ledger ADD COLUMN IF NOT EXISTS bucket_id UUID;
+ALTER TABLE balance_ledger ADD COLUMN IF NOT EXISTS expires_at TIMESTAMPTZ;
+ALTER TABLE balance_ledger ADD COLUMN IF NOT EXISTS status TEXT NOT NULL DEFAULT 'posted';
+
+CREATE UNIQUE INDEX IF NOT EXISTS generation_attempts_task_sequence_uq ON generation_attempts(task_id, sequence);
+CREATE UNIQUE INDEX IF NOT EXISTS generation_attempts_platform_idem_uq ON generation_attempts(platform_idempotency_key) WHERE platform_idempotency_key IS NOT NULL;
diff --git a/server/migrations/0011_image_output_metadata.sql b/server/migrations/0011_image_output_metadata.sql
new file mode 100644
index 0000000..3689e04
--- /dev/null
+++ b/server/migrations/0011_image_output_metadata.sql
@@ -0,0 +1,2 @@
+ALTER TABLE generation_outputs
+ ADD COLUMN IF NOT EXISTS metadata JSONB;
diff --git a/server/migrations/0011_v1_webdav_retention_state.sql b/server/migrations/0011_v1_webdav_retention_state.sql
new file mode 100644
index 0000000..9d11ebd
--- /dev/null
+++ b/server/migrations/0011_v1_webdav_retention_state.sql
@@ -0,0 +1,9 @@
+ALTER TABLE webdav_configs
+ ADD COLUMN IF NOT EXISTS retention_state TEXT NOT NULL DEFAULT 'active';
+
+ALTER TABLE webdav_configs
+ DROP CONSTRAINT IF EXISTS webdav_configs_retention_state_check;
+
+ALTER TABLE webdav_configs
+ ADD CONSTRAINT webdav_configs_retention_state_check
+ CHECK (retention_state IN ('active', 'deleting', 'deleted', 'error'));
diff --git a/server/migrations/0012_v1_scope_reservations.sql b/server/migrations/0012_v1_scope_reservations.sql
new file mode 100644
index 0000000..756b506
--- /dev/null
+++ b/server/migrations/0012_v1_scope_reservations.sql
@@ -0,0 +1,26 @@
+-- Nullable organization/team scope columns are schema reservations only. V1
+-- keeps every object user-scoped and exposes no organization API.
+ALTER TABLE balance_accounts ADD COLUMN IF NOT EXISTS organization_id UUID;
+ALTER TABLE balance_accounts ADD COLUMN IF NOT EXISTS object_scope TEXT NOT NULL DEFAULT 'user';
+ALTER TABLE generation_tasks ADD COLUMN IF NOT EXISTS organization_id UUID;
+ALTER TABLE generation_tasks ADD COLUMN IF NOT EXISTS workspace_owner_id UUID;
+ALTER TABLE generation_tasks ADD COLUMN IF NOT EXISTS object_scope TEXT NOT NULL DEFAULT 'user';
+ALTER TABLE upload_objects ADD COLUMN IF NOT EXISTS organization_id UUID;
+ALTER TABLE upload_objects ADD COLUMN IF NOT EXISTS workspace_owner_id UUID;
+ALTER TABLE upload_objects ADD COLUMN IF NOT EXISTS object_scope TEXT NOT NULL DEFAULT 'user';
+ALTER TABLE assets ADD COLUMN IF NOT EXISTS organization_id UUID;
+ALTER TABLE assets ADD COLUMN IF NOT EXISTS workspace_owner_id UUID;
+ALTER TABLE assets ADD COLUMN IF NOT EXISTS object_scope TEXT NOT NULL DEFAULT 'user';
+ALTER TABLE stored_objects ADD COLUMN IF NOT EXISTS organization_id TEXT;
+ALTER TABLE stored_objects ADD COLUMN IF NOT EXISTS workspace_owner_id TEXT;
+ALTER TABLE stored_objects ADD COLUMN IF NOT EXISTS object_scope TEXT NOT NULL DEFAULT 'user';
+
+DO $$
+DECLARE
+ table_name TEXT;
+BEGIN
+ FOR table_name IN SELECT unnest(ARRAY['balance_accounts', 'generation_tasks', 'upload_objects', 'assets', 'stored_objects']) LOOP
+ EXECUTE format('ALTER TABLE %I DROP CONSTRAINT IF EXISTS %I_object_scope_check', table_name, table_name);
+ EXECUTE format('ALTER TABLE %I ADD CONSTRAINT %I_object_scope_check CHECK (object_scope IN (''user'', ''organization''))', table_name, table_name);
+ END LOOP;
+END $$;
diff --git a/server/migrations/0013_v1_upload_idempotency.sql b/server/migrations/0013_v1_upload_idempotency.sql
new file mode 100644
index 0000000..181e71e
--- /dev/null
+++ b/server/migrations/0013_v1_upload_idempotency.sql
@@ -0,0 +1,4 @@
+ALTER TABLE upload_objects ADD COLUMN IF NOT EXISTS idempotency_key TEXT;
+CREATE UNIQUE INDEX IF NOT EXISTS upload_objects_owner_idem_uq
+ ON upload_objects(owner_id, idempotency_key)
+ WHERE idempotency_key IS NOT NULL;
diff --git a/server/migrations/0014_v1_task_dispatch_outbox.sql b/server/migrations/0014_v1_task_dispatch_outbox.sql
new file mode 100644
index 0000000..cb6d2ca
--- /dev/null
+++ b/server/migrations/0014_v1_task_dispatch_outbox.sql
@@ -0,0 +1,18 @@
+ALTER TABLE generation_tasks
+ ADD COLUMN IF NOT EXISTS task_snapshot JSONB NOT NULL DEFAULT '{}'::jsonb;
+
+CREATE TABLE IF NOT EXISTS task_dispatch_outbox (
+ task_id UUID PRIMARY KEY REFERENCES generation_tasks(id) ON DELETE CASCADE,
+ status TEXT NOT NULL DEFAULT 'pending'
+ CHECK (status IN ('pending', 'leased', 'sent', 'failed')),
+ available_at TIMESTAMPTZ NOT NULL DEFAULT now(),
+ attempts INTEGER NOT NULL DEFAULT 0,
+ lease_owner TEXT,
+ lease_expires_at TIMESTAMPTZ,
+ last_error TEXT,
+ dispatched_at TIMESTAMPTZ,
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now()
+);
+
+CREATE INDEX IF NOT EXISTS task_dispatch_outbox_poll_idx
+ ON task_dispatch_outbox(status, available_at);
diff --git a/server/migrations/0015_v1_payment_request_fingerprints.sql b/server/migrations/0015_v1_payment_request_fingerprints.sql
new file mode 100644
index 0000000..6bfbb5b
--- /dev/null
+++ b/server/migrations/0015_v1_payment_request_fingerprints.sql
@@ -0,0 +1,6 @@
+-- Preserve payment/order request fingerprints across PostgreSQL reloads so
+-- idempotency checks cannot fall back to a lossy amount/plan comparison.
+ALTER TABLE IF EXISTS recharge_orders
+ ADD COLUMN IF NOT EXISTS request_fingerprint TEXT;
+ALTER TABLE IF EXISTS plan_purchases
+ ADD COLUMN IF NOT EXISTS request_fingerprint TEXT;
diff --git a/server/migrations/0016_v1_message_outbox_leases.sql b/server/migrations/0016_v1_message_outbox_leases.sql
new file mode 100644
index 0000000..cc8f8e7
--- /dev/null
+++ b/server/migrations/0016_v1_message_outbox_leases.sql
@@ -0,0 +1,9 @@
+-- Preserve message outbox lease and provider result metadata across restarts.
+ALTER TABLE IF EXISTS email_sms_outbox
+ ADD COLUMN IF NOT EXISTS lease_owner TEXT,
+ ADD COLUMN IF NOT EXISTS lease_expires_at TIMESTAMPTZ,
+ ADD COLUMN IF NOT EXISTS provider_message_id TEXT,
+ ADD COLUMN IF NOT EXISTS last_error TEXT,
+ ADD COLUMN IF NOT EXISTS updated_at TIMESTAMPTZ NOT NULL DEFAULT now();
+CREATE INDEX IF NOT EXISTS email_sms_outbox_poll_idx
+ ON email_sms_outbox(status, next_attempt_at);
diff --git a/server/migrations/0017_v1_webdav_job_leases.sql b/server/migrations/0017_v1_webdav_job_leases.sql
new file mode 100644
index 0000000..4aa3e68
--- /dev/null
+++ b/server/migrations/0017_v1_webdav_job_leases.sql
@@ -0,0 +1,5 @@
+ALTER TABLE webdav_sync_jobs
+ ADD COLUMN IF NOT EXISTS lease_owner TEXT;
+
+CREATE INDEX IF NOT EXISTS webdav_sync_jobs_claim_idx
+ ON webdav_sync_jobs(status, next_attempt_at, lease_expires_at, created_at);
diff --git a/server/migrations/0018_v1_webdav_retention_scan.sql b/server/migrations/0018_v1_webdav_retention_scan.sql
new file mode 100644
index 0000000..37859a7
--- /dev/null
+++ b/server/migrations/0018_v1_webdav_retention_scan.sql
@@ -0,0 +1,6 @@
+CREATE INDEX IF NOT EXISTS webdav_retention_scan_idx
+ ON webdav_configs(manifest_retention_expires_at, retention_state)
+ WHERE manifest_retention_expires_at IS NOT NULL;
+
+CREATE INDEX IF NOT EXISTS webdav_retention_job_dedupe_idx
+ ON webdav_sync_jobs(user_id, intent, path, status);
diff --git a/server/migrations/0019_v1_message_idempotency.sql b/server/migrations/0019_v1_message_idempotency.sql
new file mode 100644
index 0000000..359dd09
--- /dev/null
+++ b/server/migrations/0019_v1_message_idempotency.sql
@@ -0,0 +1,6 @@
+ALTER TABLE IF EXISTS email_sms_outbox
+ ADD COLUMN IF NOT EXISTS idempotency_key TEXT;
+
+CREATE UNIQUE INDEX IF NOT EXISTS email_sms_outbox_idempotency_idx
+ ON email_sms_outbox(idempotency_key)
+ WHERE idempotency_key IS NOT NULL;
diff --git a/server/migrations/0020_v1_recharge_order_idempotency_scope.sql b/server/migrations/0020_v1_recharge_order_idempotency_scope.sql
new file mode 100644
index 0000000..5654ad9
--- /dev/null
+++ b/server/migrations/0020_v1_recharge_order_idempotency_scope.sql
@@ -0,0 +1,5 @@
+ALTER TABLE IF EXISTS recharge_orders
+ DROP CONSTRAINT IF EXISTS recharge_orders_idempotency_key_key;
+
+CREATE UNIQUE INDEX IF NOT EXISTS recharge_orders_user_idempotency_idx
+ ON recharge_orders(user_id, idempotency_key);
diff --git a/server/migrations/0021_admin_idempotency_claim_lease.sql b/server/migrations/0021_admin_idempotency_claim_lease.sql
new file mode 100644
index 0000000..20f4547
--- /dev/null
+++ b/server/migrations/0021_admin_idempotency_claim_lease.sql
@@ -0,0 +1,3 @@
+ALTER TABLE admin_idempotency
+ ADD COLUMN IF NOT EXISTS claim_token TEXT,
+ ADD COLUMN IF NOT EXISTS lease_expires_at TIMESTAMPTZ;
diff --git a/server/migrations/0022_v1_user_mfa_enrollment.sql b/server/migrations/0022_v1_user_mfa_enrollment.sql
new file mode 100644
index 0000000..36c42a7
--- /dev/null
+++ b/server/migrations/0022_v1_user_mfa_enrollment.sql
@@ -0,0 +1,4 @@
+ALTER TABLE IF EXISTS mfa_challenges
+ ADD COLUMN IF NOT EXISTS purpose TEXT NOT NULL DEFAULT 'login',
+ ADD COLUMN IF NOT EXISTS pending_secret_encrypted TEXT,
+ ADD COLUMN IF NOT EXISTS pending_recovery_code_hashes JSONB NOT NULL DEFAULT '[]'::jsonb;
diff --git a/server/package-lock.json b/server/package-lock.json
new file mode 100644
index 0000000..d5f16d9
--- /dev/null
+++ b/server/package-lock.json
@@ -0,0 +1,859 @@
+{
+ "name": "@miragenflow/server",
+ "version": "0.1.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "@miragenflow/server",
+ "version": "0.1.0",
+ "dependencies": {
+ "ioredis": "^5.11.1",
+ "ipaddr.js": "^2.5.0",
+ "pg": "^8.13.1",
+ "tsx": "^4.19.2",
+ "undici": "^7.29.0",
+ "ws": "^8.18.0",
+ "zod": "^3.25.76"
+ },
+ "devDependencies": {
+ "@types/node": "^22.15.21",
+ "@types/pg": "^8.11.10",
+ "@types/ws": "^8.18.1",
+ "typescript": "^5.8.3"
+ }
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz",
+ "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.28.2.tgz",
+ "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz",
+ "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.28.2.tgz",
+ "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz",
+ "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz",
+ "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz",
+ "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz",
+ "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz",
+ "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz",
+ "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz",
+ "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==",
+ "cpu": [
+ "ia32"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz",
+ "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==",
+ "cpu": [
+ "loong64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz",
+ "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==",
+ "cpu": [
+ "mips64el"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz",
+ "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz",
+ "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==",
+ "cpu": [
+ "riscv64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz",
+ "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==",
+ "cpu": [
+ "s390x"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz",
+ "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-arm64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz",
+ "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz",
+ "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz",
+ "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz",
+ "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openharmony-arm64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmmirror.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz",
+ "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz",
+ "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz",
+ "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz",
+ "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==",
+ "cpu": [
+ "ia32"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz",
+ "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@ioredis/commands": {
+ "version": "1.10.0",
+ "resolved": "https://registry.npmmirror.com/@ioredis/commands/-/commands-1.10.0.tgz",
+ "integrity": "sha512-UmeW7z4LfctwoQ5wkhVzgq8tXkreED2xZGpX+Bg+zA+WJFZCT6c062AfCK/Dfk81xZnnwdhJCUMkitihRaoC2Q==",
+ "license": "MIT"
+ },
+ "node_modules/@types/node": {
+ "version": "22.20.1",
+ "resolved": "https://registry.npmmirror.com/@types/node/-/node-22.20.1.tgz",
+ "integrity": "sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~6.21.0"
+ }
+ },
+ "node_modules/@types/pg": {
+ "version": "8.23.1",
+ "resolved": "https://registry.npmmirror.com/@types/pg/-/pg-8.23.1.tgz",
+ "integrity": "sha512-fKVHpikPdg4GKks3JuLEhvwSyvwzF23hnabPy6DD8ljVbC7+6J5dQzdv4arV6jqq57djnMgs1HKBxX4P8aBI3A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*",
+ "pg-protocol": "*",
+ "pg-types": "^2.2.0"
+ }
+ },
+ "node_modules/@types/ws": {
+ "version": "8.18.1",
+ "resolved": "https://registry.npmmirror.com/@types/ws/-/ws-8.18.1.tgz",
+ "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/cluster-key-slot": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmmirror.com/cluster-key-slot/-/cluster-key-slot-1.1.1.tgz",
+ "integrity": "sha512-rwHwUfXL40Chm1r08yrhU3qpUvdVlgkKNeyeGPOxnW8/SyVDvgRaed/Uz54AqWNaTCAThlj6QAs3TZcKI0xDEw==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/denque": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmmirror.com/denque/-/denque-2.1.0.tgz",
+ "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/esbuild": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.28.2.tgz",
+ "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.28.2",
+ "@esbuild/android-arm": "0.28.2",
+ "@esbuild/android-arm64": "0.28.2",
+ "@esbuild/android-x64": "0.28.2",
+ "@esbuild/darwin-arm64": "0.28.2",
+ "@esbuild/darwin-x64": "0.28.2",
+ "@esbuild/freebsd-arm64": "0.28.2",
+ "@esbuild/freebsd-x64": "0.28.2",
+ "@esbuild/linux-arm": "0.28.2",
+ "@esbuild/linux-arm64": "0.28.2",
+ "@esbuild/linux-ia32": "0.28.2",
+ "@esbuild/linux-loong64": "0.28.2",
+ "@esbuild/linux-mips64el": "0.28.2",
+ "@esbuild/linux-ppc64": "0.28.2",
+ "@esbuild/linux-riscv64": "0.28.2",
+ "@esbuild/linux-s390x": "0.28.2",
+ "@esbuild/linux-x64": "0.28.2",
+ "@esbuild/netbsd-arm64": "0.28.2",
+ "@esbuild/netbsd-x64": "0.28.2",
+ "@esbuild/openbsd-arm64": "0.28.2",
+ "@esbuild/openbsd-x64": "0.28.2",
+ "@esbuild/openharmony-arm64": "0.28.2",
+ "@esbuild/sunos-x64": "0.28.2",
+ "@esbuild/win32-arm64": "0.28.2",
+ "@esbuild/win32-ia32": "0.28.2",
+ "@esbuild/win32-x64": "0.28.2"
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/ioredis": {
+ "version": "5.11.1",
+ "resolved": "https://registry.npmmirror.com/ioredis/-/ioredis-5.11.1.tgz",
+ "integrity": "sha512-ehuGcf94bQXhfagULNXrJdfnWO38v070jxSx/qE87Kjzmu2fU7ro5EFAb+OPituLqgfyuQaym5DlrNydW2sJ9A==",
+ "license": "MIT",
+ "dependencies": {
+ "@ioredis/commands": "1.10.0",
+ "cluster-key-slot": "1.1.1",
+ "debug": "4.4.3",
+ "denque": "2.1.0",
+ "redis-errors": "1.2.0",
+ "redis-parser": "3.0.0",
+ "standard-as-callback": "2.1.0"
+ },
+ "engines": {
+ "node": ">=12.22.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/ioredis"
+ }
+ },
+ "node_modules/ipaddr.js": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmmirror.com/ipaddr.js/-/ipaddr.js-2.5.0.tgz",
+ "integrity": "sha512-aq+t5NAc+cS6rZQQVWC2x98CPqGtKKTMDd4Gaodv0wShnItdKg/51djkGJ1hqH+Oy0ivDftCbSLCQob8zso01w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
+ "node_modules/pg": {
+ "version": "8.23.0",
+ "resolved": "https://registry.npmmirror.com/pg/-/pg-8.23.0.tgz",
+ "integrity": "sha512-Ip2EQCngowJLGOfCwkFhPXU7/ljlhn6Rxlmy4XYfL2Y+vyRM59+8uR2xqRWKdYmbXmxCFOAmKxBuSUCdF34qLg==",
+ "license": "MIT",
+ "dependencies": {
+ "pg-connection-string": "^2.14.0",
+ "pg-pool": "^3.14.0",
+ "pg-protocol": "^1.16.0",
+ "pg-types": "2.2.0",
+ "pgpass": "1.0.5"
+ },
+ "engines": {
+ "node": ">= 16.0.0"
+ },
+ "optionalDependencies": {
+ "pg-cloudflare": "^1.4.0"
+ },
+ "peerDependencies": {
+ "pg-native": ">=3.0.1"
+ },
+ "peerDependenciesMeta": {
+ "pg-native": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/pg-cloudflare": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmmirror.com/pg-cloudflare/-/pg-cloudflare-1.4.0.tgz",
+ "integrity": "sha512-Vo7z/6rrQYxpNRylp4Tlob2elzbh+N/MOQbxFVWCxS7oEx6jF53GTJFxK2WWpKuBRkmiin4Mt+xofFDjx09R0A==",
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/pg-connection-string": {
+ "version": "2.14.0",
+ "resolved": "https://registry.npmmirror.com/pg-connection-string/-/pg-connection-string-2.14.0.tgz",
+ "integrity": "sha512-XwWDGcLRGCXAR8F/AM5bG7Q+A3Wm2s6QeEjlOKZLlH3UYcguiqCWKyWXVag5TLTIjR7oOJUY8kcADaZgWPyLeg==",
+ "license": "MIT"
+ },
+ "node_modules/pg-int8": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmmirror.com/pg-int8/-/pg-int8-1.0.1.tgz",
+ "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=4.0.0"
+ }
+ },
+ "node_modules/pg-pool": {
+ "version": "3.14.0",
+ "resolved": "https://registry.npmmirror.com/pg-pool/-/pg-pool-3.14.0.tgz",
+ "integrity": "sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "pg": ">=8.0"
+ }
+ },
+ "node_modules/pg-protocol": {
+ "version": "1.16.0",
+ "resolved": "https://registry.npmmirror.com/pg-protocol/-/pg-protocol-1.16.0.tgz",
+ "integrity": "sha512-sILXutLVjCLjcDuOmvhX5e2Z4cS5qG/6Bu3VkpFwdf/633ElGLpEh9bgmuI5I4sqKqkifQiGyiCcx1HdtrK7tg==",
+ "license": "MIT"
+ },
+ "node_modules/pg-types": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmmirror.com/pg-types/-/pg-types-2.2.0.tgz",
+ "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==",
+ "license": "MIT",
+ "dependencies": {
+ "pg-int8": "1.0.1",
+ "postgres-array": "~2.0.0",
+ "postgres-bytea": "~1.0.0",
+ "postgres-date": "~1.0.4",
+ "postgres-interval": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/pgpass": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmmirror.com/pgpass/-/pgpass-1.0.5.tgz",
+ "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==",
+ "license": "MIT",
+ "dependencies": {
+ "split2": "^4.1.0"
+ }
+ },
+ "node_modules/postgres-array": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmmirror.com/postgres-array/-/postgres-array-2.0.0.tgz",
+ "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/postgres-bytea": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmmirror.com/postgres-bytea/-/postgres-bytea-1.0.1.tgz",
+ "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/postgres-date": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmmirror.com/postgres-date/-/postgres-date-1.0.7.tgz",
+ "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/postgres-interval": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmmirror.com/postgres-interval/-/postgres-interval-1.2.0.tgz",
+ "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==",
+ "license": "MIT",
+ "dependencies": {
+ "xtend": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/redis-errors": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmmirror.com/redis-errors/-/redis-errors-1.2.0.tgz",
+ "integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/redis-parser": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmmirror.com/redis-parser/-/redis-parser-3.0.0.tgz",
+ "integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==",
+ "license": "MIT",
+ "dependencies": {
+ "redis-errors": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/split2": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmmirror.com/split2/-/split2-4.2.0.tgz",
+ "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==",
+ "license": "ISC",
+ "engines": {
+ "node": ">= 10.x"
+ }
+ },
+ "node_modules/standard-as-callback": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmmirror.com/standard-as-callback/-/standard-as-callback-2.1.0.tgz",
+ "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==",
+ "license": "MIT"
+ },
+ "node_modules/tsx": {
+ "version": "4.23.12",
+ "resolved": "https://registry.npmmirror.com/tsx/-/tsx-4.23.12.tgz",
+ "integrity": "sha512-FDf4L4sYzKtzWYhU/Xm0AQFdTjdIxNo9ElTf2mxXM6k8YMHXzYUe4yODVaXP4V9uMFbVg8c0qyBccK2OOxb45Q==",
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "~0.28.0"
+ },
+ "bin": {
+ "tsx": "dist/cli.mjs"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "5.9.3",
+ "resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.9.3.tgz",
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/undici": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmmirror.com/undici/-/undici-7.29.0.tgz",
+ "integrity": "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=20.18.1"
+ }
+ },
+ "node_modules/undici-types": {
+ "version": "6.21.0",
+ "resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-6.21.0.tgz",
+ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/ws": {
+ "version": "8.21.3",
+ "resolved": "https://registry.npmmirror.com/ws/-/ws-8.21.3.tgz",
+ "integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "peerDependencies": {
+ "bufferutil": "^4.0.1",
+ "utf-8-validate": ">=5.0.2"
+ },
+ "peerDependenciesMeta": {
+ "bufferutil": {
+ "optional": true
+ },
+ "utf-8-validate": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/xtend": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmmirror.com/xtend/-/xtend-4.0.2.tgz",
+ "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4"
+ }
+ },
+ "node_modules/zod": {
+ "version": "3.25.76",
+ "resolved": "https://registry.npmmirror.com/zod/-/zod-3.25.76.tgz",
+ "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/colinhacks"
+ }
+ }
+ }
+}
diff --git a/server/package.json b/server/package.json
new file mode 100644
index 0000000..8142f66
--- /dev/null
+++ b/server/package.json
@@ -0,0 +1,27 @@
+{
+ "name": "@miragenflow/server",
+ "version": "0.1.0",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "dev": "tsx watch src/index.ts",
+ "start": "tsx src/index.ts",
+ "typecheck": "tsc --noEmit",
+ "test": "NODE_ENV=test node --import tsx --test tests/**/*.test.ts"
+ },
+ "dependencies": {
+ "ioredis": "^5.11.1",
+ "ipaddr.js": "^2.5.0",
+ "pg": "^8.13.1",
+ "tsx": "^4.19.2",
+ "undici": "^7.29.0",
+ "ws": "^8.18.0",
+ "zod": "^3.25.76"
+ },
+ "devDependencies": {
+ "@types/node": "^22.15.21",
+ "@types/pg": "^8.11.10",
+ "@types/ws": "^8.18.1",
+ "typescript": "^5.8.3"
+ }
+}
diff --git a/server/src/adapters/messaging.ts b/server/src/adapters/messaging.ts
new file mode 100644
index 0000000..caac68d
--- /dev/null
+++ b/server/src/adapters/messaging.ts
@@ -0,0 +1,94 @@
+import { randomUUID } from "node:crypto";
+import type { MessageOutboxRecord, Store } from "../store.ts";
+import { decryptSecret, encryptSecret, hashChallenge } from "../shared/auth.ts";
+import { validateProviderUrlResolved } from "./provider.ts";
+import { safeOutboundFetch } from "../infra/outbound-url.ts";
+
+export type MessageChannel = "email" | "sms";
+export type MessagePurpose = "register" | "reset" | "notification";
+const messageWorkerId = `message-worker-${process.pid}-${randomUUID().slice(0, 8)}`;
+const MAX_MESSAGE_ATTEMPTS = 5;
+const MESSAGE_LEASE_MS = 30_000;
+
+export function enqueueMessage(store: Store, input: { channel: MessageChannel; target: string; purpose: MessagePurpose; templateData?: Record