Merge branch 'master' into agc-resource-canvas-types
This commit is contained in:
@@ -27,6 +27,7 @@
|
||||
- 后续新增 Markdown 文档文件名必须以分类标签开头,格式为 `【标签名】中文标题-日期.md`;历史文档不要求批量重命名,除非本次任务明确涉及。
|
||||
- 工程修改要同步更新对应 `docs/` 文档;产生长期有效的架构约定、接口变化、排障经验、开发流程或协作规则时,同步更新 `docs/project-memory/shared-memory/`。
|
||||
- 默认保持系统简洁:优先复用、修改、扩展现有系统、页面和公共组件,不新建平行系统或平行页面。
|
||||
- UI 开发优先复用现有公共组件;发现跨页面或跨端重复的视觉/交互模式时,先抽取到 `packages/shared` 共享组件库并让现有页面迁移使用,禁止在业务页复制同类 UI。共享组件只承载通用表现与交互,不下沉领域规则、后端副作用或正式业务状态。
|
||||
- 对已明确退役且不存在现役调用方、公开契约、持久化数据、活跃实例或迁移要求的对象,坚持“四不写”:
|
||||
1. 不写历史兼容代码。
|
||||
2. 不写用于维持退役行为的防御性兼容测试。
|
||||
|
||||
@@ -736,7 +736,7 @@ function isAdminImageSequenceFrameUrlUsable(
|
||||
): cached is AdminImageSequenceFrameCacheEntry {
|
||||
return Boolean(
|
||||
cached?.resolvedUrl &&
|
||||
(cached.expiresAtMs === null || cached.expiresAtMs > Date.now()),
|
||||
(cached.expiresAtMs === null || cached.expiresAtMs > Date.now()),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -26,8 +26,7 @@ const wrapperSuite =
|
||||
const configFileName = 'game-creator.config.json';
|
||||
const configSentinelName = '.deterministic-provider-e2e.json';
|
||||
const configSentinelSchema = 'genarrative-deterministic-provider-e2e-config.v1';
|
||||
const platformSessionFixtureEnv =
|
||||
'GENARRATIVE_AGC_PLATFORM_SESSION_FIXTURE';
|
||||
const platformSessionFixtureEnv = 'GENARRATIVE_AGC_PLATFORM_SESSION_FIXTURE';
|
||||
const platformSessionFixtureName = '.deterministic-platform-session.json';
|
||||
const platformSessionFixtureSchema =
|
||||
'genarrative-agc-platform-session-fixture.v1';
|
||||
|
||||
@@ -100,8 +100,7 @@ import { appendBounded, runProcess } from './process.mjs';
|
||||
import { decodeUtf8Fatal, isIsolatedRunnerSuite } from './reporting.mjs';
|
||||
import { killRunnerOnce, readRunnerStatus, runnerBootId } from './runtime.mjs';
|
||||
|
||||
const platformSessionFixtureEnv =
|
||||
'GENARRATIVE_AGC_PLATFORM_SESSION_FIXTURE';
|
||||
const platformSessionFixtureEnv = 'GENARRATIVE_AGC_PLATFORM_SESSION_FIXTURE';
|
||||
const platformSessionFixtureMaxBytes = 16 * 1024;
|
||||
const isolatedPlatformSessionFixtureName =
|
||||
'.deterministic-platform-session.json';
|
||||
@@ -538,7 +537,9 @@ async function readPlatformSessionFixtureForIsolatedSuite(sourceConfigDir) {
|
||||
);
|
||||
let fixture;
|
||||
try {
|
||||
fixture = JSON.parse(decodeUtf8Fatal(bytes, 'platform-session-fixture-invalid-utf8'));
|
||||
fixture = JSON.parse(
|
||||
decodeUtf8Fatal(bytes, 'platform-session-fixture-invalid-utf8'),
|
||||
);
|
||||
} catch (error) {
|
||||
throw codedError(
|
||||
'supervisor-autonomous-playable-platform-session-fixture-invalid',
|
||||
@@ -577,8 +578,12 @@ async function installPlatformSessionFixtureIntoIsolatedAppData(
|
||||
appDataDir,
|
||||
) {
|
||||
if (!isSupervisorAutonomousPlayableLaneDefenseSuite()) return;
|
||||
const source = await readPlatformSessionFixtureForIsolatedSuite(sourceConfigDir);
|
||||
const isolatedPath = path.join(appDataDir, isolatedPlatformSessionFixtureName);
|
||||
const source =
|
||||
await readPlatformSessionFixtureForIsolatedSuite(sourceConfigDir);
|
||||
const isolatedPath = path.join(
|
||||
appDataDir,
|
||||
isolatedPlatformSessionFixtureName,
|
||||
);
|
||||
await fs.copyFile(
|
||||
source.sourcePath,
|
||||
isolatedPath,
|
||||
@@ -609,9 +614,7 @@ async function installPlatformSessionFixtureIntoIsolatedAppData(
|
||||
absolutePathVariants(source.sourcePath, isolatedPath),
|
||||
);
|
||||
const previousLeakCount = state.transcriptScanner?.count ?? 0;
|
||||
state.secrets = [
|
||||
...new Set([...state.secrets, source.fixture.accessToken]),
|
||||
];
|
||||
state.secrets = [...new Set([...state.secrets, source.fixture.accessToken])];
|
||||
rebuildSupervisorSwarmTranscriptScanner();
|
||||
state.transcriptScanner.count = previousLeakCount;
|
||||
}
|
||||
|
||||
@@ -990,7 +990,10 @@ export function createDeterministicLaneDefenseRouter({
|
||||
}
|
||||
|
||||
function recordReadyTaskRun(agentId, runId) {
|
||||
if (!relaxedAutonomous && !deterministicManifestReadyAgentIds.includes(agentId)) {
|
||||
if (
|
||||
!relaxedAutonomous &&
|
||||
!deterministicManifestReadyAgentIds.includes(agentId)
|
||||
) {
|
||||
throw providerError(`provider-ready-agent-unsupported:${agentId}`);
|
||||
}
|
||||
const existingRunId = readyTaskRunIdsByAgent.get(agentId);
|
||||
@@ -1405,14 +1408,10 @@ export function createDeterministicLaneDefenseRouter({
|
||||
]);
|
||||
}
|
||||
if (tools.has(runtimeFunction('task.list'))) {
|
||||
const calls = [
|
||||
nativeAction('task.list', '查看并行任务当前状态', {}),
|
||||
];
|
||||
const calls = [nativeAction('task.list', '查看并行任务当前状态', {})];
|
||||
if (tools.has(runtimeFunction('agent.run_status'))) {
|
||||
stats.runStatusCount += 1;
|
||||
calls.push(
|
||||
runStatusCall('读取并行任务的最新运行状态'),
|
||||
);
|
||||
calls.push(runStatusCall('读取并行任务的最新运行状态'));
|
||||
}
|
||||
return callsResponse('project-supervisor', tools, calls);
|
||||
}
|
||||
@@ -1751,7 +1750,11 @@ export function createDeterministicLaneDefenseRouter({
|
||||
stats.sourceWriteCount += 1;
|
||||
stats.manifestReadyTaskFileWriteCount += 1;
|
||||
return readyCallsResponse(agentId, runId, tools, [
|
||||
fileWriteCall(path, content, `重试写入 ${path} 并交给 Runtime 收束门验证`),
|
||||
fileWriteCall(
|
||||
path,
|
||||
content,
|
||||
`重试写入 ${path} 并交给 Runtime 收束门验证`,
|
||||
),
|
||||
]);
|
||||
}
|
||||
// Runtime validates fixed owner artifacts after the owner responds. Once
|
||||
@@ -1769,12 +1772,7 @@ export function createDeterministicLaneDefenseRouter({
|
||||
if (calls.some((call) => call.name === 'respond_to_user')) {
|
||||
recordReadyTaskCompletion(agentId, runId);
|
||||
}
|
||||
return readyCallsResponse(
|
||||
agentId,
|
||||
runId,
|
||||
tools,
|
||||
calls,
|
||||
);
|
||||
return readyCallsResponse(agentId, runId, tools, calls);
|
||||
}
|
||||
const recovery = runData.get(runId)?.writerRecovery ?? null;
|
||||
const observations = observationContext(context);
|
||||
@@ -2195,13 +2193,18 @@ export function createDeterministicLaneDefenseRouter({
|
||||
if (finalReplyRuns.has(key)) {
|
||||
throw providerError('provider-duplicate-final-reply-request');
|
||||
}
|
||||
if (!relaxedAutonomous &&
|
||||
if (
|
||||
!relaxedAutonomous &&
|
||||
!context.includes('给用户一个正常中文回复') &&
|
||||
!context.includes('给开发者一个正常中文回复')
|
||||
) {
|
||||
throw providerError('provider-unexpected-text-request');
|
||||
}
|
||||
if (!relaxedAutonomous && identity.agentId === 'project-supervisor' && parentStage !== 'done') {
|
||||
if (
|
||||
!relaxedAutonomous &&
|
||||
identity.agentId === 'project-supervisor' &&
|
||||
parentStage !== 'done'
|
||||
) {
|
||||
throw providerError('provider-parent-final-reply-before-acceptance');
|
||||
}
|
||||
finalReplyRuns.add(key);
|
||||
@@ -2586,8 +2589,7 @@ function createDeterministicCanvasFixture(apiKey) {
|
||||
}
|
||||
if (
|
||||
request.method === 'POST' &&
|
||||
canonicalPath ===
|
||||
'/api/external/v1/editor/icon-spritesheets/generations'
|
||||
canonicalPath === '/api/external/v1/editor/icon-spritesheets/generations'
|
||||
) {
|
||||
const idempotencyKey = request.headers['idempotency-key'];
|
||||
if (
|
||||
@@ -2906,7 +2908,9 @@ function createDeterministicCanvasFixture(apiKey) {
|
||||
|
||||
if (
|
||||
request.method === 'POST' &&
|
||||
canonicalPath?.match(/^\/api\/external\/v1\/editor\/projects\/[^/]+\/resources$/)
|
||||
canonicalPath?.match(
|
||||
/^\/api\/external\/v1\/editor\/projects\/[^/]+\/resources$/,
|
||||
)
|
||||
) {
|
||||
const body = await readJsonBody(request);
|
||||
const objectKey =
|
||||
@@ -2959,7 +2963,11 @@ export async function startDeterministicLaneDefenseProvider({
|
||||
relaxed = false,
|
||||
fallbackPorts = DEFAULT_FALLBACK_PORTS,
|
||||
} = {}) {
|
||||
const router = createDeterministicLaneDefenseRouter({ apiKey, model, relaxed });
|
||||
const router = createDeterministicLaneDefenseRouter({
|
||||
apiKey,
|
||||
model,
|
||||
relaxed,
|
||||
});
|
||||
const canvasFixture = createDeterministicCanvasFixture(apiKey);
|
||||
const sockets = new Set();
|
||||
let stopped = false;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
@import 'tailwindcss';
|
||||
@source '../../../packages/shared/src/components';
|
||||
@import '@genarrative/shared/styles.css';
|
||||
@import '../../../packages/shared/src/theme.css';
|
||||
|
||||
:root {
|
||||
|
||||
@@ -22,7 +22,22 @@
|
||||
],
|
||||
"@genarrative/image-canvas-react": [
|
||||
"packages/image-canvas-react/src/index.ts"
|
||||
]
|
||||
],
|
||||
"@genarrative/shared": ["packages/shared/src/index.ts"],
|
||||
"@genarrative/shared/components": [
|
||||
"packages/shared/src/components/index.ts"
|
||||
],
|
||||
"@genarrative/shared/components/account": [
|
||||
"packages/shared/src/components/account.ts"
|
||||
],
|
||||
"@genarrative/shared/components/ui": [
|
||||
"packages/shared/src/components/ui/index.ts"
|
||||
],
|
||||
"@genarrative/shared/components/ui/*": [
|
||||
"packages/shared/src/components/ui/*"
|
||||
],
|
||||
"@genarrative/shared/lib/*": ["packages/shared/src/lib/*"],
|
||||
"@genarrative/shared/lib": ["packages/shared/src/lib/index.ts"]
|
||||
}
|
||||
},
|
||||
"include": ["src", "vite.config.ts"]
|
||||
|
||||
@@ -105,6 +105,46 @@ export default defineConfig({
|
||||
'packages/image-canvas-react/src/index.ts',
|
||||
),
|
||||
},
|
||||
{
|
||||
find: '@genarrative/shared/styles.css',
|
||||
replacement: resolve(
|
||||
repoRoot,
|
||||
'packages/shared/src/components/styles.css',
|
||||
),
|
||||
},
|
||||
{
|
||||
find: '@genarrative/shared/theme.css',
|
||||
replacement: resolve(repoRoot, 'packages/shared/src/theme.css'),
|
||||
},
|
||||
{
|
||||
find: '@genarrative/shared/components/ui',
|
||||
replacement: resolve(
|
||||
repoRoot,
|
||||
'packages/shared/src/components/ui/index.ts',
|
||||
),
|
||||
},
|
||||
{
|
||||
find: '@genarrative/shared/lib',
|
||||
replacement: resolve(repoRoot, 'packages/shared/src/lib'),
|
||||
},
|
||||
{
|
||||
find: '@genarrative/shared/components/account',
|
||||
replacement: resolve(
|
||||
repoRoot,
|
||||
'packages/shared/src/components/account.ts',
|
||||
),
|
||||
},
|
||||
{
|
||||
find: '@genarrative/shared/components',
|
||||
replacement: resolve(
|
||||
repoRoot,
|
||||
'packages/shared/src/components/index.ts',
|
||||
),
|
||||
},
|
||||
{
|
||||
find: '@genarrative/shared',
|
||||
replacement: resolve(repoRoot, 'packages/shared/src/index.ts'),
|
||||
},
|
||||
],
|
||||
dedupe: ['react', 'react-dom'],
|
||||
},
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "new-york",
|
||||
"rsc": false,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "",
|
||||
"css": "src/index.css",
|
||||
"baseColor": "neutral",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"aliases": {
|
||||
"components": "@genarrative/shared/components",
|
||||
"ui": "@genarrative/shared/components/ui",
|
||||
"utils": "@genarrative/shared/lib/utils",
|
||||
"lib": "@genarrative/shared/lib"
|
||||
}
|
||||
}
|
||||
@@ -30,6 +30,7 @@
|
||||
|
||||
## 图片画布与媒体
|
||||
|
||||
- [共享基础组件库与展示页](./technical/【前端架构】共享基础组件库与展示页-2026-08-26.md):网站与客户端复用的无业务 UI chrome、样式边界和 `/components` 展示页。
|
||||
- [客户端素材创作无限画布阶段一合同](./technical/【技术方案】客户端素材创作无限画布阶段一合同-2026-08-05.md)
|
||||
- [图片画布结构化持久化与迁移回滚](./【编辑器】图片画布结构化持久化与迁移回滚方案-2026-07-19.md)
|
||||
- [编辑器生成结果原子提交与幂等重放](./technical/【后端架构】编辑器生成结果原子提交与幂等重放方案-2026-08-06.md)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
# 决策记录
|
||||
|
||||
> 用途:记录已经确认、会影响后续开发的长期技术/产品/协作决策。短期讨论不要写在这里。
|
||||
> 当前口径:历史条目的旧路径、旧版本和已退役对象只用于追溯,不构成现行实现依据;如与当前代码或 `docs/README.md` 冲突,以当前代码和最新专题文档为准。
|
||||
|
||||
@@ -108,6 +109,7 @@
|
||||
- 决策:三个工具调用的 arguments 统一限制为 `1 MiB`,先解析通用 JSON 并迭代检查,再进入递归业务类型。结构识别按每棵树独立限制 `512` 个 LLM 节点 / `32` 层,不跨树求和且不计 Rust 页面根;语义建议限制 `4` 节点 / `4` 层;合并计划限制 `512` 节点 / `32` 层。超限整次拒绝,不截断或交付部分结果,日志不记录 arguments 正文。
|
||||
- 输入边界:`merge_ui` 继续直接接收 `State`,不修改 Tauri/frontend IPC 参数;进入 Rust 后、发起 LLM 前按每棵源树独立限制 `512` 节点 / `32` 层,不跨树求和,并限制 `2 MiB` 序列化投影。UI 设计参考图只设单张 `5 MiB` 上限,不设批次合计或像素数上限;元数据检查、有限读取和 base64 编码进入 blocking worker,不新增命令超时。
|
||||
- 关联文档:`docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md`。
|
||||
|
||||
## 2026-08-19 M1 审查后四项可靠性修复
|
||||
|
||||
- **审批 stale 收口**:`status` 或 `phase` 为 `needs-reconciliation` 的策划根不再满足审批所需的 active 身份;审批命令返回 `PLAN_STALE_APPROVAL`,并且不得创建 approval receipt。
|
||||
@@ -152,6 +154,7 @@
|
||||
- **验证**:`appSurface.test.ts` **381 passed / 0 failed**(378 既有 + 3 新增);`agentTraceSummary` 与 `rememberCommand`(另两个 import `src/App` 的用例文件)13 passed;`agc:typecheck` 通过;6 个改动/新增文件 ESLint `--max-warnings 0` 通过;`check:encoding` 5409 文件通过。不改 Rust——三条全在前端,后端语义已经正确。
|
||||
- **对既有记录的更正**:M1D-1 与 M1D-2 两条记录分别称「Shell TypeScript typecheck 仍被仓库既有依赖缺失阻断」「appSurface UI suite 受仓库现有缺失 Tauri plugin 依赖阻断,未把该基线失败归因于本包」,在原分支主工作树上都不成立:`agc:typecheck` 干净退出,appSurface 378 条全绿;两道门分别位于 CI 的 `check:native-shells`(且 typecheck 排在 cargo test 之前)与 Frontend tests 内,一直是活的。实际情况与记录相反——`bf2185fba` 改名 `taskGroupLabels.design` 后,appSurface 有 8 个用例文件的断言变红,随后由 `c6a08ef98` 修复;把该套件记为「基线阻断、不归因本包」正是让这条自带回归合入的原因。**隔离工作树的依赖缺失不能作为跳过门禁的依据,须回原工作树复跑后再下结论。**
|
||||
- **未修的审查发现(本次不并入,单列后续)**:① hydrate 在校验 GDD/session 的 projectId 与 manifest 一致之前,已执行 `reconcile_plan_gdd_approval_projections_at`、session previous 提升与 index 重建等落盘修复,违反第 18.3 节固定顺序,其中 `session.previous.json` 的提升+删除不可逆(触发需外部篡改 `.agent/`,App 自身流程造不出该分歧);② 项目写锁竞争时 `acquire_project_write_lock` 的错误原文内嵌项目绝对路径,被原样回传前端,违反第 18.3 节「返回值不包含绝对路径」,常态可达;③ design 组展示名只改了 `taskGroupLabels` 一本字典,`agentPresentation.ts` 的 `groupConfigs` 与 `view/project-development/index.tsx` 的 `summarizeAgent` 仍硬编码「策划 Agent」,与新阶段「立项策划」同屏共存,违反第 18.2 节;④ 阶段进度「轮次 X/3」直接透传 0-indexed 的 `clarificationRound` 未 +1(后端自己用的是 `current_round + 1`),最后一轮显示「轮次 2/3」,字面暗示还剩一轮。
|
||||
|
||||
## 2026-08-18 M1E 隔离工作树:Fast GDD submit 有界拒绝与覆盖审计
|
||||
|
||||
- **范围与结论**:在 `codex/genarrative-isolated` 上按 M1E 只接受具备完整触发链的缺陷。确认 Provider 连续输出不合法 `plan.submit_gdd` 时,Runtime 原有「rejected observation → 同 child run 续跑」链没有次数上限,模型可反复请求 tool-plan 并累积历史 observation;这是可达的 prompt 膨胀与资源消耗链。
|
||||
@@ -288,7 +291,6 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在
|
||||
- 不做:改 walker、透明补边、裁切、横纵不同倍率、Lanczos / bilinear、另存逻辑图、前端框缩放、失败路径、新测试。
|
||||
- 关联文档:`docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md`、`docs/【编辑器】画板角色形象生成入口设计-2026-06-15.md`、`docs/【编辑器】画板图标素材生成入口设计-2026-06-15.md`、`docs/【编辑器】图片画布结构化持久化与迁移回滚方案-2026-07-19.md`、`docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md`。
|
||||
|
||||
|
||||
- 背景:真实 `gpt-5.6-sol / max` 验收中,`art-director` 失败后已形成 `ready + needs-repair` delivery,但认领、合同读取、claim observation 和完成 blocker 均硬编码为 Supervisor-only;实际直属父 Run `code-prototype` 无法消费回执,随后又发起 29 次 Provider 请求。
|
||||
- 验证方式:覆盖合法认领与合同精确读取、错误 Agent/Run/delegation 拒绝、delivery 身份篡改阻断、唯一安全默认返工、普通失败零后续 Provider lifecycle,以及新的真实 Provider 空项目轮次。顺带收紧 `validate_executable_inline_javascript_syntax`:正文游离 `<` 不再吞掉后续 `<script>`,未闭合或非标签状 `<` 继续扫描,避免后置脚本被静默跳过语法校验。
|
||||
- 关联文档:`docs/technical/【技术方案】AI游戏创作Agent Runtime V1.1-2026-07-12.md`、`docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md`。
|
||||
@@ -366,7 +368,8 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在
|
||||
**以下三条是经复核后有意保留的取舍,不是待办。后续 PR 不得在未重新裁决的情况下「顺手修掉」。**
|
||||
|
||||
1. **`$isolatedAgentTemplates` 仍会向 plan 根 run 列出全部专业角色名。** `art-director` / `design-foundation` / `art-asset-plan` / `code-prototype` 等名字来自 `$base`(`RUNTIME_PROMPT_RUNTIME_COMPOSITION` 的 `$isolatedAgentTemplates` 段,由 `GAME_CREATOR_AGENT_GROUP_DEFINITIONS` 运行时渲染),用途是广告 `agent.spawn_isolated` 的合法模板 id,**不在本次裁掉的两段之内**。裁掉它要动 `$base` 与隔离模板目录,波及全部 Agent。保留的依据是:`A2` 已对 plan 根 run 硬拒 `spawn_isolated`,该目录对 plan 根 run 是**死文本**,不构成可利用面。**结论:上下文层的收窄边界到此为止;「plan 根 run 的上下文里不出现其它 Agent 名」这一目标 M1 不成立,不要据此写验收句。**
|
||||
3. **上下文层回归是弱断言。** `plan_root_supervisor_prompt_drops_intro_and_visual_contract_sections` 用「不含某几条独有短句」断言,不是对照组那种字节级 `assert_eq`。已实测非永真。已知漏报场景:若将来 `$visualContract` / `supervisorIntro` 被替换成措辞不同但仍暗示专业组扇出的新文本,这条不会报警。**执行层的 `A1`/`A2` 是该场景的唯一保障**——这也是本包把硬门排在裁段之前的原因。
|
||||
2. **上下文层回归是弱断言。** `plan_root_supervisor_prompt_drops_intro_and_visual_contract_sections` 用「不含某几条独有短句」断言,不是对照组那种字节级 `assert_eq`。已实测非永真。已知漏报场景:若将来 `$visualContract` / `supervisorIntro` 被替换成措辞不同但仍暗示专业组扇出的新文本,这条不会报警。**执行层的 `A1`/`A2` 是该场景的唯一保障**——这也是本包把硬门排在裁段之前的原因。
|
||||
|
||||
- 关联文档:`docs/technical/【技术方案】立项策划Agent(Fast GDD)-2026-08-10.md` 第 4.3、22、23.8、24 节。
|
||||
|
||||
## 2026-08-14 M1B-1:planning storage 基础与只挡写隔离完成(待合入)
|
||||
@@ -514,7 +517,7 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在
|
||||
- 决策(D10,问询机制):策划节点不得直接调用 `user.input_request`,改走 **Runtime 直投**——策划节点提交结构化决策字段,Runtime 创建 **owner 为 Project Supervisor** 的 pending,用户在 Supervisor 对话里回答,Supervisor 的 Provider 全程不参与提问。这不是新造机制:`AgentRuntimeUserInputRecord` 已经是唯一事实源并两路投影(`user_input.rs:494-530` 投影成会话消息写进 pending owner 的会话文件,`:595-623` 投影成结构化 observation,`:803-807` 有「observation 重算冲突」校验防漂移),直投只是让两路分别落到 Supervisor 与策划节点。
|
||||
- 依据的产品约束:一、用户侧只有一个对话对象;二、所有呈现给用户的对话内容必须**物理存在于** Supervisor 的会话文件中,不得由前端把多个 Agent 的会话拼成单一视图。第二条不是靠约定满足,而是靠「会话文件按 `agentId` 分目录(`conversation.rs:42`)、消息归属完全由 pending owner 决定」这个物理事实。
|
||||
- 决策卡冻结范围收窄:冻结三个固定选项及顺序(要确定性映射到 decision state 与 answer source)与 question ID 规则(`decisionId` 由它推导);**问题正文措辞不冻结**,作者是策划节点。保真由「Runtime 不经过任何 Provider 搬运」保证,与模板无关。
|
||||
- Goal Contract 处置:原四方案 A/B/C/D 随之作废(共同前提是策划 run 自己就是那个 root)。但入口门 `validate_root_goal_contract_control_plan_at` 无 profile 判断,standard root Supervisor 仍受约束,故替换为三条实测约束:Supervisor 第一轮必须且只能提交 `agent.goal_contract`;验收 `requiredEvidence` 锚定 `game/fast_gdd.md` 配 `tool:file.read`,**不得指向 `.agent/planning/**`**(`reject_agent_runtime_private_control_path` 不区分读写,加进去会把 `file.read` 一并挡死、出口门永久 blocked,planning 的写保护须用只挡写的独立判据);Supervisor 取证必须在 GDD 落盘之后。
|
||||
- Goal Contract 处置:原四方案 A/B/C/D 随之作废(共同前提是策划 run 自己就是那个 root)。但入口门 `validate_root_goal_contract_control_plan_at` 无 profile 判断,standard root Supervisor 仍受约束,故替换为三条实测约束:Supervisor 第一轮必须且只能提交 `agent.goal_contract`;验收 `requiredEvidence` 锚定 `game/fast_gdd.md` 配 `tool:file.read`,**不得指向 `.agent/planning/**`**(`reject_agent_runtime_private_control_path`不区分读写,加进去会把`file.read` 一并挡死、出口门永久 blocked,planning 的写保护须用只挡写的独立判据);Supervisor 取证必须在 GDD 落盘之后。
|
||||
- M0 影响判定:三个代码工作包(`M0A-2` / `M0B-1` / `M0B-2`)**全部不受影响、零回退**——无一行按 D6 编写,全仓库检索 `fast_gdd` / `.agent/planning` / `project-supervisor-plan-chat` / `is_exact_supervisor_plan_run_at` 均零命中。只有 `M0A-1` 交付的文档基线失效,以工作包 `M0A-3` 修订,修订完成前不得声称「M0 全部完成」。**注意 `M0A-2` 虽不受影响,其实现也不可复用**:`autonomous_owner_artifact_validation_available_for_run_at`(`autonomous_completion.rs:416-441`)四重绑死 owner 白名单、profile、source 且要求 `parent_agent_id` 为 Project Supervisor,standard 路径必须另建物理独立实现。
|
||||
- 保留待裁决:策划节点 `agentId` / source 命名(是 `M0A-3` 批二的共同阻塞点,身份常量不定则第 3、8、9、12、13 节无法落笔,第 9.1 节 golden vector 的 SHA-256 必然重算);Supervisor 侧新可信 source 是沿用旧字符串还是取新名;checkpoint handoff 私有持久化(原有项,不受影响);plan run 是否允许 steer,以及 Supervisor 被 steer 时下游策划节点如何收束。
|
||||
|
||||
@@ -527,17 +530,14 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在
|
||||
- M2 裁决二:planning baseline 不是「一次冻结永久有效」。steer 替换协议会终止旧根树并另起 replacement root run,该 run 必须在自己的锁/CAS 边界内重新冻结与被替换根**完全相同**的 `approvedGddRef`,即使期间已有更新的 approved 版本也不换稿;无法证明同一 ref 时失败关闭,不得降级为 `mode=direct-build`。
|
||||
- M2 附带证据:Goal Contract 落在 `.agent/runtime/goal-contracts/{rootAgent}/{rootRun}.json`,按 root run 分文件并绑定 binding fingerprint 与 source SHA-256。这是「自带 root/run 身份」的正面先例,D4 关于 `approvedGddRef` 是否进 manifest 的决策应参照此形态,而非项目级单例。
|
||||
|
||||
|
||||
- 背景:M0B-2 遗留一条挂起项——manifest 没有 root/run 绑定,不能安全决定 cached failed manifest 是否应覆盖活跃 main 状态。本条对该项作出处置,使 M0-4 可以收口。
|
||||
- 事实基线:`GameCreationAppManifest` / `GameCreationAppTaskState`(TS 与 Rust 双侧)不含任何 run/root/agent 身份字段;manifest 是项目级单例文件、跨轮复用;前端唯一读取入口按 `task.id === 'code-prototype'` 做纯字符串过滤。因此"这份 manifest 属于哪一轮"无法从数据本身判定,这是结构性质而非实现疏漏。
|
||||
- 本阶段不加绑定:不给 manifest 或其投影新增 `statusRunId` / `statusSource` 等身份字段,维持 M0B-2「不修改后端 DTO/schema/delivery/route」的范围声明。补字段的方案必须同时覆盖 `update_manifest_task_status_at` 与 `set_task_status` 两条写入路径,否则会制造"校验通过"的假象(详见 pitfalls 2026-08-12 条)。
|
||||
- 残余风险(明示保留,不视为回归):当前 main 到达 Runtime 终态 `completed`、而全局 manifest state 尚未被本轮 `manifestInvalidated` 事件刷新时,跨轮残留的 `failed` 仍会被当作本轮结论显示。该窗口实际宽度未量化;三条候选机制(新鲜度门控、root-scoped 永久缓存、manifest 补身份字段)经审查均不可安全落地,故本阶段只记录不实现。
|
||||
|
||||
|
||||
- 归档边界:`【Supervisor 阶段记录】` 只有在 root、唯一 main、当前动态美术 children 与 manifest `code-prototype` 全部终态且无冲突/reconciliation 时才可写入。等待期间按完整 `agent/session/run` 身份保存 root-scoped Runtime 快照,避免 current-by-agent map 被新 root 覆盖后把新旧证据串线;消息 ID 固定绑定 root run,重载与 hydration 幂等。
|
||||
- 范围:M0B-2 只改前端纯投影、接线、文案与回归,不修改后端 DTO/schema/delivery/route,不迁移历史 manifest,也不提前实现 M1~M3。
|
||||
|
||||
|
||||
- 结构性依据:delivery 的 delegationId 由父动作 ID 派生,且 targetRunId 绑定原 child run;通用 retry 铸造的 `retry:{旧run}:{新run}:{纳秒}` 身份在结构上不可能匹配任何现有 delivery。即使只圈住 retry 的写边界,其产出也没有合法消费者。让 retry 继承 lineage 必须引入可变 delivery、换绑或放宽 exact binding,与不可变事实和失败关闭方向冲突,明确不采纳。
|
||||
- 既有语义确认:同一 main run 内“每个审计缺口最多委派一次”(不含 `Suppressed`、包含终态失败或取消 delivery)是 2026-08-08 单主编排重构的 master 既有防抖语义,本裁决有意保留,不为失败 child 开豁免。同 run 重来与通用 retry 一样被拒绝,恢复只走下一轮 main 重新审计;这与禁止 retry 继承 lineage 是同一设计哲学。
|
||||
- 实现边界:入口守卫落在 `runtime_driver/lifecycle_control.rs` 的 `retry_game_creator_agent_runtime_task_at`,必须使用不要求 child 仍为 running 的结构身份分类;`resolve_game_creator_agent_runtime_retry_configuration_at` 保持不变。纵深防御落在 `runtime_tools/file_ops.rs` 的动态美术分类入口;严格 lineage/Canvas 授权 predicate 本身不放宽。除这两处与对应测试外,不扩展 M0B-1 生产改动面。
|
||||
@@ -565,8 +565,6 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在
|
||||
- 验证方式:M0 验证 tracked 技术方案、索引、注册表、golden 指纹、提交/恢复合同和决策记录自包含一致;M1~M3 分别按关联技术方案的阶段门禁执行,不能以文档合入冒充功能完成。
|
||||
- 关联文档:`docs/technical/【技术方案】立项策划Agent(Fast GDD)-2026-08-10.md`、`docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md`、`docs/prd/【AI游戏创作】项目开发工作台PRD-2026-07-20.md`。
|
||||
|
||||
|
||||
|
||||
## 2026-08-10 资源管理评审阻塞项按第二轮正式合同修复
|
||||
|
||||
- 背景:资源管理第一轮实现后,人工验证继续暴露 WebView 默认缩放、预览队列饥饿、过滤后媒体残留播放、外层滚动串 scope、超深依赖坐标越过 Rust 上限和暂时错误无法重试等问题。部分 PRD / 技术方案仍描述第一轮的中央媒体预览、单全局 Overlay 和统一 section scope,已经与第二轮代码及验收结论冲突。
|
||||
@@ -697,7 +695,6 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在
|
||||
- 验证方式:按权威专题的 28 项矩阵覆盖 Web/Tauri 共用源码、新增/精修、生成响应丢失、重复提交、两窗口并发、事务各崩溃点、草稿恢复、切项目/切状态/改选择/改筛选迟到结果和不刷新即时投影。
|
||||
- 关联文档:`docs/technical/【技术方案】客户端素材创作无限画布阶段一合同-2026-08-05.md`、`docs/prd/【AI游戏创作】项目开发工作台PRD-2026-07-20.md`、`docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md`。
|
||||
|
||||
|
||||
- 验证方式:覆盖路由后只启动 `code-prototype`、既有素材零美术委派、精确缺口才允许单个对应 child、child 的 `game/**`、memory 和 manifest 写入拒绝而 `assets/**` 写入允许、回执恢复同一主 Run,以及主 Agent 的接入、静态 smoke 与双视口试玩;追加真实 scheduler 对旧 canonical task 的同 Run 恢复、旧固定美术 child 及其历史 isolated 后代对 Canvas/memory/manifest/project scope 零 mutation、伪只读 `agent.run_status` 阻断、嵌套美术 child 硬截止对账回执;另跑完整 DAG 非回归。
|
||||
- 关联文档:`docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md`。
|
||||
|
||||
@@ -785,8 +782,6 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在
|
||||
|
||||
---
|
||||
|
||||
|
||||
|
||||
- 背景:ready scheduler 已为当前根 Run 持久化并启动 `code-prototype`,但并发 hydration 持有的旧 manifest 快照随后把该节点从 running 覆盖回 pending。父 Run 只读取 manifest 时会在 child 仍运行的情况下误判固定 Graph 已停滞并先行失败;child 完成门又因 pending 连续拒绝真实交付,最终耗尽 loop。
|
||||
- 失败关闭:GUI/CLI、旧父 Run child、终态 child、错误/伪造绑定、非确定性 runId、WaitingForConfirmation、WaitingForUserInput 和 needs-reconciliation 都不得借用该容忍;创建更新的根 Run 后,旧 child 立即失去父 DAG 活性与 pending 完成资格。
|
||||
- 玩法连续性:普通美术措辞不再触发历史玩法类型回溯;只有正式 failed continuation 继承原完成合同,纯“继续”沿用现有 continuation 识别边界。
|
||||
@@ -796,7 +791,6 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在
|
||||
|
||||
## 2026-08-05 增量接入既有美术时复用已验收 Graph 节点
|
||||
|
||||
|
||||
- 禁止替代方案:不得增加 loop 次数掩盖冲突,不得递增虚构的产物版本号,不得覆盖 art manifest 扩展字段,也不得重放历史图片生成 action。
|
||||
- 验证方式:回归必须证明明确复用时两项美术节点保持 completed、父完成门不再报告 art manifest baseline 未变化、俄罗斯方块场景保持 `tetris-v1`;删除任一切片后豁免立即失效。另以普通新目标和“全新美术”请求证明旧美术不能被认领。
|
||||
- 关联文档:`docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md`、`docs/project-memory/shared-memory/pitfalls.md`。
|
||||
@@ -1015,7 +1009,6 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在
|
||||
|
||||
- 关联文档:`docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md`、`docs/project-memory/shared-memory/development-workflow.md`。
|
||||
|
||||
|
||||
- 背景:External Runner 和 Tauri 客户端各自拥有进程内 `PreviewRegistry`。Runner 完成 `preview.validate` 后,其 server 与 running 状态不会出现在 Tauri registry,导致已有可玩版本时用户预览不自动出现;后续 revision 即使验证成功,既有 iframe 也可能继续显示 WebView 缓存中的旧资源。顶部只写“未启动”还会让用户无法判断是 Runner、Runtime 还是预览未启动。
|
||||
- 验证方式:以当前 run 成功 `preview.validate` revision N 后断言 iframe 自动出现且 server 归 Tauri registry;再完成 revision N+1,断言 server 进程和 loopback origin 不变、iframe 重新加载新内容且所有响应为 `no-store`。Runner registry 单独 running 不得让页面显示预览;相同 / 更低 revision 不得刷新;停止预览后顶部必须显示“预览未启动”;构建产物和安装信息必须为 `0.1.1`。
|
||||
- 关联文档:`docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md`、`docs/project-memory/shared-memory/development-workflow.md`。
|
||||
@@ -1035,7 +1028,6 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在
|
||||
- 验证方式:Rust mock 503 覆盖等待、恢复与耗尽,断言 exact HTTP status、attempt、sidecar 清理和正文零泄漏;前端模型覆盖状态卡优先级、严格字段解析、字段不一致与尾随正文失败关闭;conversation 测试覆盖 Provider URL/query、API Key、绝对路径、fingerprint、chars 和 redaction marker 均不可见。
|
||||
- 关联文档:`docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md`、`docs/project-memory/shared-memory/development-workflow.md`。
|
||||
|
||||
|
||||
- 背景:独立包原先在 Tauri 最终退出时仅调用一次 `runner.shutdown_if_idle`;若 Runner 正忙便返回 busy 且没有稍后关闭闩锁,Runner 和后台命令会永久残留。`command.exec / project.verify` 只有进程组 flag,STDIO MCP、Git 和清理命令也缺少 `CREATE_NO_WINDOW`,因此 Windows release 会连续弹出多个控制台窗口。
|
||||
- 验证方式:定向测试覆盖专用 RPC 的 draining / shutdown 与普通 idle 语义不变;正式安装包在活跃任务期间确认无后台控制台窗口,关闭主窗口后核对 Runner、MCP、command、ConPTY 和孙进程全部退出,再启动确认 durable 状态正确 reconciliation。
|
||||
- 关联文档:`docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md`、`docs/project-memory/shared-memory/development-workflow.md`。
|
||||
@@ -1046,16 +1038,13 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在
|
||||
- 验证方式:Windows 定向测试覆盖 endpoint 原子写入与 TokenUser 复核、new/stale lock owner 修复、活锁不截断、hardlink / reparse 不触碰目标、project-owner 诊断 TokenUser 复核、错误码精确分类,以及子进程退出在 2 秒内返回;正式包在现场 TokenOwner 为 Administrators 的机器上必须依次出现 `startup.runner.start.complete`,创建项目任务时 execution-owner 诊断也必须成功。
|
||||
- 关联文档:`docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md`、`docs/project-memory/shared-memory/development-workflow.md`。
|
||||
|
||||
|
||||
- 关联文档:`docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md`、`docs/project-memory/shared-memory/development-workflow.md`。
|
||||
|
||||
|
||||
- 背景:Windows 安装包首次创建 AppData 时若沿用继承 owner,目录 owner 可能是 Administrators 而非当前登录用户;后续严格 owner 校验会让客户端在 `.setup()` 阶段退出,且无控制台 release 缺少可见诊断。直接修改 foreign-owner 旧目录的 ACL 还可能覆盖其它主体持有的数据或跟随 reparse 路径。
|
||||
- 决策:新建客户端 AppData 时以进程 `TokenUser` SID 显式设置 owner 和当前用户私有 DACL,不使用 `TokenOwner` 代表用户。历史 foreign-owner 真实目录先原子重命名为同级唯一 `.owner-mismatch-backup-*`,再重建并回读验证安全目录;任何 reparse / junction / symlink、备份冲突或迁移失败都失败关闭,不在旧目录上放宽权限。独立 release 的 `startup.log` 和记录 Runner stdout / stderr 摘要的 `agent-runner.log` 均采用 256 KiB 上限、仅一份 previous 和脱敏写入;AppData 日志不可写时 `startup.log` 回退系统 TEMP,Tauri URL、AppData、Runner、`.setup()` 或 `.build()` 初始化失败时在 Windows 显示包含诊断日志位置的错误对话框。
|
||||
- 验证方式:Windows 定向测试覆盖 TokenUser owner、私有 DACL、foreign-owner 同级备份不覆盖和 reparse 拒绝;诊断测试覆盖 256 KiB 单 previous 轮转、凭据与绝对路径脱敏、AppData 不可写时 TEMP 回退和初始化失败对话框。安装包 smoke 后保留旧备份证据并确认新 AppData 可写、Runner 可启动。
|
||||
- 关联文档:`docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md`、`docs/project-memory/shared-memory/development-workflow.md`。
|
||||
|
||||
|
||||
- 关联文档:`docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md`。
|
||||
|
||||
## 2026-07-28 AI 游戏临时页面启动消息采用 URL 消费加页面闩锁
|
||||
@@ -6730,7 +6719,6 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在
|
||||
- 历史边界:成功加入画布时写一条 `perfect-pixel` 历史,中文标签为“完美像素”,并纳入新增结果保护;撤销不得让派生 PNG 消失。像素处理失败或 completion 因占位删除未落画布时不写该历史。
|
||||
- 关联文档:`docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md`、`docs/【图片画布】撤销范围与操作提示方案-2026-07-17.md`、`docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md`、`docs/【编辑器】图片画布结构化持久化与迁移回滚方案-2026-07-19.md`。
|
||||
|
||||
|
||||
- 关联文档:`docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md`、`docs/project-memory/shared-memory/development-workflow.md`。
|
||||
|
||||
## 2026-07-31 autonomous 单一任务图、预览 fail-fast 与逐 Agent 推理默认
|
||||
@@ -6778,7 +6766,6 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在
|
||||
- 验证:顺序断言新增 `tokio::time::timeout_at(` 与超时文案;参照 `937378ab9` 的做法用 `assert_function_occurrence_count` 把 `resolve_editor_pixel_art_source_for_owner` 内的 `.list_editor_projects(` 和 `.get_editor_asset_library(` 各钉为 1 次,并用 `assert_function_not_contains` 禁止该函数重新调用取数包装。后者断言的是调用形式 `resolve_editor_reference_object_key_for_owner(state` 而非裸函数名,否则会命中生产代码里说明「老包装保持不动」的注释——该陷阱在编写时即由测试抓出。
|
||||
- 关联文档:`docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md`。
|
||||
|
||||
|
||||
- 预算:父 Run 接受请求后以 `240` 秒作为首版软预算;父 Run、所有 child Run、等待、回收和确定性验收共享 `300` 秒累计硬上限。硬上限是从 root `bound_at` 计算的绝对 deadline,必须包住 Provider、图片生成、文件写入、静态检查、`preview.validate` 和 final-reply 的在途等待;超时先强制持久化 `failed` 终态,再清理 pending action、Provider batch、confirmation、recovery 和进程会话,不能留下 `needs-reconciliation` 悬空态。软预算后不再扩展 Provider 规划,只能运行受控 fallback、`game.static_smoke` 和 `preview.validate`;硬上限未形成当前 revision 的通过证据时必须失败关闭,单轮确定性收束也必须复核累计时间,不能在上限后补写 completed。
|
||||
- Provider:首版最多一次 Provider 规划 / 写入请求,禁止同一首版自动传输重试、第二次 tool-plan 或无限 repair。Provider 结束后由 Runtime 按当前 revision 依次执行确定性静态 smoke 与浏览器试玩。
|
||||
- 兜底:fallback HTML 必须自包含、无远程运行依赖,从 `ready` 开始并真实绘制 Canvas,持续更新 `playable-web-game-state.v1`,提供键盘 / 触控、start / primary-action / restart 和胜负状态;primary-action 后可保持 `playing`,restart 后可稳定恢复 `ready | playing`,不得开始前固定 `lost` 或用固定失败充当完成。fallback 只有在 `assets/art-spec.png` 已有效登记并真实存在时才能生成,而且必须把该平台图片显著绘制为主要背景、玩家和目标;不得以纯 Canvas 视觉绕过平台图片硬门。
|
||||
@@ -6821,13 +6808,9 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在
|
||||
- 验证:`dropDeadInlineGenerationPlaceholders` 四条单测覆盖「剥离已死 inline 占位」「保留队列型占位(缺字段与显式 false 两种)」「保留已终态的 inline 占位与普通图层」「标记经 hydrate 与序列化往返不丢失」——最后一条钉住白名单式 hydrate 漏字段会让标记在一次「加载→保存」后消失。工作流测试新增 `live-session-dialogs` 探针,正向断言完美像素占位置位、反向断言去除背景占位不置位。`vitest src/components/image-editor` 893 通过 / 72 文件,typecheck、eslint、check:encoding 通过。
|
||||
- 关联文档:`docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md`。
|
||||
|
||||
|
||||
|
||||
- 显式协作合同:autonomous 的旧 `code-prototype + quality-review` 首批合同退出。显式 project collaboration policy 或持久 batch 恢复若进入首批 `agent.delegate` 路径,只允许且要求三个 Director 各一次;策划与程序 Director 是只读规划且 `expectedArtifacts=[]`,美术 Director 是非只读规范图任务且必须交付 `assets/art-spec.png`。任何非 repair 底层委派与 isolated child 都在首批失败关闭;默认 manifest DAG 仍是唯一自动首轮执行链,不额外复制三个 Director 委派。
|
||||
- 输出决策:保留未提交 `streaming / ready` 的当前 revision 门;已提交的专业 Agent final reply 继续使用既有 durable response-stream 身份,后续项目 revision 变化不再隐藏早期阶段回复。
|
||||
|
||||
|
||||
|
||||
- 图集事务退役补充:九路径快照在创建和恢复读取时都使用跨平台不跟随符号链接 / reparse point 的文件句柄核算 64 MiB 总预算,marker、journal 与快照均通过有界双次读取和句柄元数据复核拒绝同长度并发改写;实际读取仍受剩余预算限制,稀疏或并发增长文件不能触发无界分配。恢复开始时锚定可信事务目录句柄,每次读取控制文件前后都复核目录身份,拒绝 rename、junction 或替换目录。恢复必须先把全部 journal 条目和九路径快照完成结构、大小与摘要校验并形成内存计划,随后缓存全部 canonical 路径的恢复前状态;每项落盘前再次校验目标与父目录,后续项失败时按逆序回滚本轮已应用项,但回滚前必须 CAS 证明目标仍等于本轮安装结果,外部修改不得被覆盖并进入 reconciliation。末尾路径竞态或快照损坏不得留下静默的新旧混合合同。`committed` 持久化后先删除并同步 `prepared`,再清理 `.previous / .replacement`、同步 canonical 合同并最后删除事务目录;递归删除中断后最多留下只有 `committed` 的可清理事务,不能重新落入 rollback 分支。
|
||||
- 关联:`apps/ai-game-creator-shell/scripts/start-tauri-dev.mjs`、`start-dev-stack.mjs`、`src-tauri/src/agent/runtime_protocol/autonomous_completion.rs`、`response_stream.rs`、`docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md`。
|
||||
|
||||
@@ -7243,13 +7226,11 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在
|
||||
- 验证:模型测试覆盖允许与拒绝类型,工具栏和两类右键菜单覆盖视频、单个拆分图标、角色动作及音频不展示,打开与提交工作流覆盖视频等不支持类型绕过入口时仍拒绝;后端表驱动测试覆盖全部现役图片素材 / 媒体类型与未知类型,锁定图片编辑端点失败关闭。
|
||||
- 关联:`src/components/image-editor/ImageCanvasGenerationModel.ts`、`ImageCanvasSelectedLayerToolbarView.tsx`、`ImageCanvasContextMenusView.tsx`、`useImageCanvasGenerationWorkflow.ts`、`useImageCanvasGenerationSubmissionWorkflow.ts`。
|
||||
|
||||
|
||||
- Canvas 目的区域证明以 Oxc symbol、调用实参、计数循环和所属 Canvas 身份为权威。大 classic script 的调用图保持整轮 visited;函数内 alias 重绑定必须按作用域和写入位置解析。格子坐标无法进一步化简时只可在 `COLS × ROWS × CELL` 唯一合同下建模为完整棋盘轴区间,已知调用参数或循环边界优先,越界调用继续失败关闭。
|
||||
- 动态坐标只新增一种受限可见性证明:未遮蔽的全局 `Math.min(currentCanvas.width|height - size, Math.max(0, dynamic))`。尺寸成员必须属于创建当前绘图 context 的 Canvas;其它 Canvas、被遮蔽的 `Math`、缺少上下界或普通未知动态坐标均不得作证。
|
||||
- autonomous parent wake 的瞬态重试预算耗尽后必须形成 durable reconciliation。lane 忙时先写 deferred recovery signal;获得同一 execution lane 与项目写锁后,重新读取原始 Runtime state、最新 task、cancel tombstone 和 DAG 进展。只有仍指向同一非终态根 Run 时才能以 CAS 追加 reconciliation task 并原子替换 state;manifest 已损坏时也不能让普通 hydrated writer 先阻断对账证据。
|
||||
- autonomous 测试夹具必须先建立带完整 parent/delegation identity 的 linked Pending child,再由正式启动路径写第一条 Running;禁止先启动无父身份再补 journal,也禁止把 terminal runId 复活成 Running。Completed-only 深验按任务逐项执行:Pending 任务不深验,但同一 manifest 中已 Completed 的美术任务仍必须验证其切片合同。
|
||||
|
||||
|
||||
- code-prototype 只有在本人当前 Run 已有 `status=ok` 的真实 mutation action、对应 mutation revision 已通过 `game.static_smoke`,且完整 `runtime.autonomous_completion` 完成门无阻塞时,才允许快车道返回确定性交付或把结构化计划全部标为 completed。verification gate 的 mutation revision 可能因保守失效策略在失败 patch 前推进,不能单独证明文件已修改。若 static smoke 已过但完成门仍报告素材、正式产物或其它诊断,计划尚有未完成步骤时用单一 in-progress 修复步骤替换首个非终态步骤,并把其余非终态步骤保持 pending;计划已全 completed 且仍有容量时才追加修复步骤。这样既保留 completed 单调历史,也不会因 steer 合并后超过 8 步而永久卡在 `runtime.plan_update`;不得重复返回同一交付计划直至耗尽 loop budget。
|
||||
- mutation ownership 以当前 run 的最后一条同工具调用和 Agent DB receipt 为联合权威;pending action 的 `plannedSteerCursor` 必须在 recent tool-call 与 receipt 中使用同一 fingerprint。结构化计划含 failed 步骤时 code-prototype 立即失败关闭;8 个 completed 步骤仍有 blocker 时不追加第 9 步,改走只允许读取、真实 mutation 与重新验证的外部 repair lane。
|
||||
- 当前根 Run 有 durable active child 时,即使 manifest 快照把全部 seed task 写成 Completed,DAG 仍保持 in-progress;任一 seed task 为 Failed 时继续立即失败关闭。所有项目修改在取得项目写锁后再次核对 ready child 的确定性 runId、父绑定、durable Running 状态和当前活跃根 Run;新根 Run 建立后旧 child 不得推进 revision 或修改文件。
|
||||
@@ -7257,7 +7238,6 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在
|
||||
- 根 Supervisor 的 manifest completion gaps 只对 status 已为 Completed 的 seed task执行正式产物与 Canvas 深验;pending/running/failed 本身已经构成完成阻塞,禁止提前扫描后续波次。自动唤醒 200 次瞬态重试预算耗尽后必须写入 `needs-reconciliation`,不能静默返回并留下假运行状态。
|
||||
- parent wake 的 terminal reconciliation task 是 durable commit marker,state / queue / event / Agent DB audit 是可幂等重建投影;非瞬态 task journal 读取错误直接失败关闭。restart 只在 raw state 具有完整 Agent/task/Session/run/source/profile/binding/task 身份时修复其当前 run;state 缺失、损坏、空对象或关键身份为空时只取 journal 最后 logical run,完整有效的新 Run 阻止历史 marker 覆盖。event/audit 必须完整 payload 唯一匹配,同键冲突或重复失败关闭;旧 task 终态、Runtime 非 waiting 或新 Run 接管时,durable deferred signal 追加 resolved/superseded 后才返回 obsolete。
|
||||
|
||||
|
||||
- 升级恢复兼容 v1 决策,但不沿用旧责任链:读取时严格复核 v1 fingerprint,从根完成合同的有效任务恢复 `intentSummary`,并保留旧 fingerprint 只用于核对已有 route 身份。旧 `code-director` coverage/route 对当前单主完成门表现为 migration pending;当前 `code-prototype` 必须重新 `asset.list`,再原位写入自己的 coverage/route。这样同一根 Run 可以继续,又不会把旧 Director 审计冒充成主 Agent 本人的完成证据。
|
||||
|
||||
## 2026-08-04 静态视觉状态流与可见证据收口
|
||||
@@ -7745,6 +7725,7 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在
|
||||
- 失败结算的 `expectedDraftRevision` CAS、结算意图和公开投影必须在同一草稿锁边界内串行化;显式失败结算和生成流程内部错误都先持久化 `failure-settlement-pending` 或 `reconciliation-settlement-pending`,不能直接跨文件发布终态。
|
||||
- pending 恢复不依赖平台登录或 External API Key。恢复按当前权威草稿幂等补齐 generation 投影和 staging revision,再把私有 ledger 发布为 `failed` 或 `reconciliation-required`;公开投影已经存在时只完成账本,不重复增加草稿 revision。
|
||||
- 回归必须覆盖 pending ledger 写入后、公开草稿写入前,公开草稿写入后、staging revision 写入前,以及 staging revision 写入后、终态 ledger 写入前三种重启切点。
|
||||
|
||||
## 2026-08-23 AGC 本地资源与 External Editor 账号绑定分离
|
||||
|
||||
- 权威边界:本地项目 ID、manifest asset、正式本地文件和内容摘要属于设备上的本地项目;`canvasProjectId / resourceId / assetObjectId / objectKey` 属于具体 External Editor 服务 principal。manifest 中现有远端字段继续保留生成来源,不再承担“当前账号可编辑句柄”,本轮不修改共享 manifest schema。
|
||||
@@ -7783,6 +7764,7 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在
|
||||
- 严格图集崩溃收口:workflow 在严格图集调用前先持久化 `strictSpritesheetPending` 并冻结底层严格事务覆盖的九项旧合同身份;旧路径可精确冻结为缺失。Provider 完成结果先绑定原 retained stage ledger。恢复在同一项目锁内对账严格事务;只有新九项合同、规范图/背景图替换锚点与 retained spritesheet result 三者一致才补写 `completed`,旧九项合同才允许补偿。旧合同判定、写 `compensating`、恢复两项素材与登记、回读和清锚点必须在同一项目锁内,重启已有 `compensating` 也重新判定;第三种混合、漂移或 foreign result 状态进入 reconciliation。不能在主图集与四切片已整体提交后仍按两文件 rollback 制造混合包;若中断前阶段告警尚未进入 durable completed result,恢复结果追加“原阶段告警无法完整重放”的明确 warning,不静默清空。
|
||||
- Direct 对话恢复从新到旧扫描全部合法 User 回合,遇到较新已回答回合继续向前,不得丢失更早未回答回合。成功返回时 Rust 已先持久化 assistant,前端冗余 append 失败也不得重跑 Provider;普通错误终态的显式 append 失败后,恢复 claim 必须保持到 React fallback writer 对同一稳定 assistant messageId 的写入明确成功或失败,不能在 writer 尚在途时按旧 `/history` 快照重跑。fallback 成功后释放 claim;fallback 失败时跳过该 writer 的无界迟到重试并释放 claim,后续显式 `/history` 才可复用原稳定 `clientTurnId`。终态收敛后删除 claim,避免长会话无界增长。
|
||||
- 正式资源提交结算遵守同一顺序:阶段三 commit 成功后先持久化 `asset-commit-settlement-pending`,恢复器幂等补齐 `asset-durable-committed` 公开投影与 staging revision,再发布私有终态;公开投影已经存在时不得重复增加草稿 revision。恢复必须把私有回执与阶段三 commit ledger、transaction journal、manifest 资产和事件 payload 的完整身份绑定,任一错配都保留 pending 并失败关闭。回归同时覆盖三个 durable write cut,以及私有回执、commit ledger、journal 错配。
|
||||
|
||||
## 2026-08-24 AGC Direct 抠图语义工具
|
||||
|
||||
- 决策:将 External v1 `/api/external/v1/editor/images/background-removals` 通过 `agc_remove_background` 加入受控 `agc_tools`。工具只接受当前 manifest 的图片 `sourceLocalAssetId` 与结果名称;客户端负责正式 resourceId、画布/素材目录、稳定 operation/idempotency 身份、权限和错误脱敏,不向 Codex 暴露内部 BgFilter worker、凭据或任意 API。
|
||||
@@ -7836,6 +7818,37 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在
|
||||
- 直连 Runtime 已取得 Developer Key 时,资源编辑的 `remote_credentials` 是该操作的完整身份边界;其中冻结平台快照为空表示 Developer 模式,禁止再从进程全局 GUI 登录态补回账号快照。平台账号模式仍只使用同一组凭据捕获的快照。
|
||||
- 回归覆盖 Direct 系统提示路径合同和 Developer Key / GUI 快照隔离;未触碰用户项目 `.agent` 锁、账本或凭据。
|
||||
|
||||
## 2026-08-26 网站与客户端共享基础组件库
|
||||
|
||||
- 决策:无业务 UI chrome 统一放入 `packages/shared/src/components`,通过 `@genarrative/shared/components` 导出;组件只接受 React props、原生 DOM props、短文案、图标节点和回调,不读取账号、钱包、请求客户端、store、Tauri API 或业务实体。
|
||||
- 样式边界:共享样式位于 `packages/shared/src/components/styles.css`,选择器使用 `.genarrative-ui-*` 前缀并消费 `packages/shared/src/theme.css` 的 `--platform-*` token。网站与客户端各自保留页面壳、路由、业务和玩法视觉,不导入网站总 CSS;账户 DTO 适配器只从 `@genarrative/shared/components/account` 单独导出,不进入通用组件 barrel。
|
||||
- 展示页:网站 `/components`(`/design-system` 兼容别名)展示所有公共组件的变体、状态、Token 和移动端布局,使用本地静态示例,不经过账号 Gate 或调用业务 API。
|
||||
|
||||
## 2026-08-27 共享组件采用 shadcn open-code 渐进迁移
|
||||
|
||||
- 决策:共享 Web UI 采用 shadcn 的源码归属项目模式,新增 canonical source 放入 `packages/shared/src/components/ui`,统一通过 `packages/shared/src/lib/utils.ts` 的 `cn` 与 CVA 组织变体;不引入 MUI / Ant Design 全量组件,也不一次性重写现有业务 common。
|
||||
- 首步:`Button`、`Modal`、`SegmentedTabs`、`Switch`、`Input`、`Textarea`、`Badge`、`Card` 已迁移到 `components/ui` canonical source,保留 `@genarrative/shared/components` 的旧 API 作为兼容适配;Button/Input/Badge 用 CVA,Dialog/Tabs/Switch 按需使用 Radix,Tailwind 4 继续消费现有 `--platform-*` token 与 `.genarrative-ui-*` 样式。原生 Select 暂不迁移。
|
||||
- 边界:网站与 Tauri WebView 可消费该 DOM 源码,移动端 React Native 不导入这套组件与 CSS;Radix primitive 仅在 Dialog、Tabs、Switch 等具体组件迁移时按需加入。
|
||||
|
||||
## 2026-08-28 共享平台 chrome 与基础语义件继续扩展
|
||||
|
||||
- 决策:无业务依赖的 `PlatformAsyncStatePanel`、`PlatformFilterToolbar`、`PlatformIconBadge`、`PlatformInfoBlock`、`PlatformNavigableListItem`、`PlatformRuntimeStatusToast`、`PlatformStatGrid` 实现统一收口到 `packages/shared/src/components`;`src/components/common` 仅保留兼容导出,展示页和部分实际业务页面开始直接消费共享 barrel。
|
||||
- 基础件:`components/ui` 新增语义 `Label`、原生 `Checkbox`、`Skeleton` 和语义 `Table` 组合件;继续沿用项目自有 shadcn open-code 源码,不引入额外运行时依赖。
|
||||
- 样式:筛选工具栏与可导航列表行的 hover/active/focus/disabled 反馈、移动端断点和独立宿主所需 chrome 放入共享样式;共享主题仍只消费 `--platform-*` token,不依赖网站总 CSS。
|
||||
- 验证:相关共享/兼容组件 10 个测试文件共 36 个断言、类型检查、编码检查、定向 ESLint、`git diff --check` 和生产构建均通过。
|
||||
|
||||
## 2026-08-31 共享开关组件与业务页面直引迁移
|
||||
|
||||
- 决策:新增 `packages/shared/src/components/PlatformToggleRow.tsx`,统一承接白底整行 checkbox / status 开关的语义、状态胶囊和禁用态;`src/components/common/PlatformToggleRow.tsx` 仅保留兼容导出,展示页改为直接从共享 barrel 引入。
|
||||
- 迁移:`Match3DResultView`、`PuzzleResultView`、`VisualNovelResultView`、`SquareHoleResultView`、`RpgCreationResultViewImpl`、`RpgCreationResultActionBar`、`RpgCreationAssetDebugPanel`、`CustomWorldCreationHub`、`BabyObjectMatchWorkspace`、`AccountModal`、`CreationAgentWorkspace` 直接消费共享 `Platform*` chrome,玩法专属资源 / 媒体 / 上传 / 弹窗组件继续留在业务层。
|
||||
- 验证:共享 PlatformToggleRow 定向测试、相关前端类型检查、编码检查和 `git diff --check` 通过;未改变业务行为或后端契约。
|
||||
|
||||
## 2026-08-31 第二批共享组件直引
|
||||
|
||||
- 决策:新增 `PlatformBackActionButton` canonical 返回动作组件,统一 compact / regular 尺寸、返回图标和 platform / editorDark surface;`src/components/common/PlatformBackActionButton.tsx` 仅保留兼容出口。
|
||||
- 迁移:`LoginScreen`、`BindPhoneScreen`、`CustomWorldEntityCatalog` 将已有共享 `Platform*` chrome 直接从 `@genarrative/shared/components` 引入;展示页新增返回动作示例并保留整行开关示例。
|
||||
- 边界:媒体、上传、资源换签、业务弹窗等带副作用组件继续留在网站业务层。
|
||||
|
||||
## 2026-08-28 AGC 自主构建放开编排约束
|
||||
|
||||
- `autonomous-game-build` 中,manifest `dependencies` 只作为上下文,不阻塞 ready;代码、设计、美术、音频和发布任务允许并行启动,child 不依赖固定回执顺序或固定 run 身份才能推进。
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
## 开发中
|
||||
|
||||
- 修改范围保持聚焦;优先扩展现有系统、页面、组件、DTO 和脚本,不新建平行入口或业务真相。
|
||||
- UI 开发优先复用现有公共组件;跨页面或跨端重复的视觉/交互模式应沉淀到 `packages/shared`,由现有页面迁移使用,禁止在业务页复制同类 UI。共享组件只承载通用表现与交互,不下沉领域规则、后端副作用或正式业务状态。
|
||||
- 后端遵循 `module-*`、`spacetime-module`、`spacetime-client`、`api-server`、`platform-*`、`shared-contracts` 的现役边界。
|
||||
- 前端只负责表现、交互和临时 UI 状态;正式状态来自后端投影、API 或持久化契约。
|
||||
- 对已明确退役且无现役调用方、公开契约、持久化迁移或活跃实例的对象,不写兼容实现、维持旧行为的测试、墓碑注释或墓碑文档。
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
# 共享基础组件库与展示页
|
||||
|
||||
更新时间:`2026-08-28`
|
||||
|
||||
## 目标
|
||||
|
||||
网站与 Tauri 客户端共享无业务依赖的基础 UI chrome,同时保留各自的页面布局、路由、账号/钱包业务和玩法视觉。共享层只接受 React props、原生 DOM props、短文案、图标节点和回调,不读取请求客户端、store、Tauri API 或业务实体。
|
||||
|
||||
## 包边界
|
||||
|
||||
组件位于 `packages/shared/src/components`,由 `@genarrative/shared` 根入口和 `@genarrative/shared/components` 子路径稳定导出。新增组件按 shadcn 的 open-code 方式归档在 `components/ui`,源码、变体和组合点归项目所有;`packages/shared/src/lib/utils.ts` 提供统一的 `cn` 工具。当前迁移阶段仍复用 `packages/shared/src/components/styles.css` 的 `.genarrative-ui-*` 选择器和 `packages/shared/src/theme.css` 的 `--platform-*` token,避免破坏既有平台视觉契约。
|
||||
|
||||
已有账户 DTO 适配组件单独由 `@genarrative/shared/components/account` 导出;它们不进入本样式库的通用组件 barrel,也不从该入口转出。
|
||||
|
||||
当前基础组件:
|
||||
|
||||
- `Button`、`IconButton`
|
||||
- `TextField`、`SelectField`
|
||||
- `Subpanel`、`Modal`
|
||||
- `Status`、`EmptyState`、`Badge`
|
||||
- `ProgressBar`、`SegmentedTabs`、`Switch`
|
||||
- `Spinner`、`Divider`、`Label`、`Checkbox`、`Skeleton`
|
||||
- `Table`(含 `TableHeader`、`TableBody`、`TableRow`、`TableHead`、`TableCell`、`TableCaption`、`TableFooter`)
|
||||
|
||||
平台 chrome 组件(同样从 `@genarrative/shared/components` 导出):
|
||||
|
||||
- `PlatformActionButton`、`PlatformIconButton`、`PlatformPillBadge`
|
||||
- `PlatformStatusMessage`、`PlatformEmptyState`、`PlatformTextField`
|
||||
- `PlatformProgressBar`、`PlatformSegmentedTabs`、`PlatformFieldLabel`、`PlatformSubpanel`
|
||||
- `PlatformAsyncStatePanel`、`PlatformFilterToolbar`、`PlatformIconBadge`、`PlatformRuntimeStatusToast`
|
||||
- `PlatformInfoBlock`、`PlatformNavigableListItem`、`PlatformStatGrid`
|
||||
- `PlatformToggleRow`(整行 checkbox / 状态开关)
|
||||
- `PlatformBackActionButton`(白底结果页返回动作)
|
||||
|
||||
迁移约定:`Button`、`Modal`、`SegmentedTabs`、`Switch`、`Input`、`Textarea`、`Badge`、`Card` 的 canonical source 位于 `packages/shared/src/components/ui`,旧的 `@genarrative/shared/components` API 作为兼容适配层继续保留。`Button`、`Input`、`Badge` 使用 CVA,Dialog/Tabs/Switch 使用按需 Radix primitive;原生 `SelectField` 暂不迁移,避免破坏现有 `<option>` 与表单事件合同。后续组件按使用面逐个迁移。
|
||||
|
||||
2026-08-28 平台 chrome 的无业务依赖组件继续完成实现收口:新增 `PlatformAsyncStatePanel`、`PlatformFilterToolbar`、`PlatformIconBadge`、`PlatformInfoBlock`、`PlatformNavigableListItem`、`PlatformRuntimeStatusToast`、`PlatformStatGrid`。这些组件的实现统一位于 `packages/shared/src/components`;`src/components/common` 中同名文件仅保留兼容导出,因此现有业务页面无需一次性改写导入路径,实际渲染已复用共享包实现。带 API、store、编辑器或素材解析副作用的组件仍留在业务层,后续按同一边界逐项评估。
|
||||
|
||||
2026-08-31 继续迁移:`PlatformToggleRow` 已收口到共享包,保留 `src/components/common/PlatformToggleRow.tsx` 兼容出口;`Match3DResultView`、`PuzzleResultView`、`VisualNovelResultView`、`SquareHoleResultView`、`RpgCreationResultViewImpl`、`RpgCreationResultActionBar`、`RpgCreationAssetDebugPanel`、`CustomWorldCreationHub`、`BabyObjectMatchWorkspace`、`AccountModal` 和 `CreationAgentWorkspace` 已将共享 chrome 组件改为直接从 `@genarrative/shared/components` 引入。玩法专属的媒体、上传、编辑器、弹窗和资源状态组件仍保留在业务 common 层。
|
||||
|
||||
2026-08-31 第二批:新增 `PlatformBackActionButton` canonical 实现及兼容出口,展示页加入 compact / regular 两种返回动作示例;`LoginScreen`、`BindPhoneScreen`、`CustomWorldEntityCatalog` 继续将共享 chrome 直引到 `@genarrative/shared/components`。媒体、上传、资源换签与业务弹窗仍不迁移。
|
||||
|
||||
同时提供 `Platform*` 别名,便于从现有平台组件命名迁移;别名不携带平台业务语义。
|
||||
|
||||
Web 宿主需要显式启用 Tailwind 4,并引入 `@genarrative/shared/styles.css` 和 `@genarrative/shared/theme.css`;Tauri WebView 与网站共用这套源码,移动端 React Native 不消费 DOM/CSS 组件。筛选工具栏所需的 `platform-category-*` chrome 样式也已放入共享样式文件,独立宿主无需依赖网站 `src/index.css`。组件库不提供页面壳或业务流程。`components.json` 只用于 shadcn CLI 定位源码,不允许覆盖现有业务组件。
|
||||
|
||||
## 展示页
|
||||
|
||||
网站 `/components`(兼容别名 `/design-system`)是共享组件展示页,不经过账号 Gate。页面按“基础组件”“平台通用组件”“Token”“状态”分区,覆盖公共组件的主要变体、交互态、筛选/标签/媒体/上传/异步状态/指标列表等平台 chrome 和移动端布局;展示数据均为本地静态示例,不调用 API。平台组件分区中的筛选按示例素材状态过滤结果,排序按最近使用或名称重排结果,并在筛选按钮、排序按钮和独立筛选面板之间保持同一份本地状态。展示页可用于网站与客户端接入前的视觉回归和人工验收。平台通用组件示例优先从 `@genarrative/shared/components` 直接导入,业务代码仍可通过 `src/components/common` 的兼容出口渐进迁移;展示页只接入不读取请求、store 或业务实体的 chrome,不把账号、发布和编辑器业务流程嵌入展示页。
|
||||
|
||||
## 验收
|
||||
|
||||
- 组件具备原生语义、键盘焦点、禁用态和可访问名称。
|
||||
- 按钮、图标按钮、分段标签和开关具备明确按压态;展示页中的可操作示例在点击后提供可见状态反馈,加载态按钮保持禁用。
|
||||
- `Modal` 支持 ESC、遮罩点击、可选 portal 和移动端底部面板布局。
|
||||
- 组件样式不依赖网站总 CSS;Tauri 只需共享主题、共享组件样式和自己的壳层样式。
|
||||
- 修改后运行 `npm run typecheck`、`npm run check:encoding`、`npm run test -- src/routing/activeAppRoutes.test.ts` 与 `git diff --check`。
|
||||
Generated
+961
-1
File diff suppressed because it is too large
Load Diff
@@ -3,9 +3,27 @@
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./components": "./src/components/index.ts",
|
||||
"./components/ui": "./src/components/ui/index.ts",
|
||||
"./components/ui/button": "./src/components/ui/button.tsx",
|
||||
"./components/ui/*": "./src/components/ui/*.tsx",
|
||||
"./components/account": "./src/components/account.ts",
|
||||
"./lib": "./src/lib/index.ts",
|
||||
"./lib/utils": "./src/lib/utils.ts",
|
||||
"./styles.css": "./src/components/styles.css",
|
||||
"./theme.css": "./src/theme.css"
|
||||
},
|
||||
"dependencies": {
|
||||
"@radix-ui/react-dialog": "^1.1.23",
|
||||
"@radix-ui/react-switch": "^1.3.7",
|
||||
"@radix-ui/react-tabs": "^1.1.21",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-react": "^0.546.0",
|
||||
"qrcode": "^1.5.4",
|
||||
"tailwind-merge": "^3.6.0",
|
||||
"zustand": "^5.0.14"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -16,6 +34,7 @@
|
||||
"vitest": "^0.34.6"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^19.0.0"
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
import type {
|
||||
ButtonHTMLAttributes,
|
||||
LabelHTMLAttributes,
|
||||
ReactNode,
|
||||
} from 'react';
|
||||
|
||||
import {
|
||||
getPlatformActionButtonClassName,
|
||||
type PlatformActionButtonAlign,
|
||||
type PlatformActionButtonShape,
|
||||
type PlatformActionButtonSize,
|
||||
type PlatformActionButtonSurface,
|
||||
type PlatformActionButtonTone,
|
||||
} from './platformActionButtonModel';
|
||||
|
||||
export type {
|
||||
PlatformActionButtonAlign,
|
||||
PlatformActionButtonShape,
|
||||
PlatformActionButtonSize,
|
||||
PlatformActionButtonSurface,
|
||||
PlatformActionButtonTone,
|
||||
} from './platformActionButtonModel';
|
||||
|
||||
type PlatformActionButtonBaseProps = {
|
||||
children: ReactNode;
|
||||
tone?: PlatformActionButtonTone;
|
||||
surface?: PlatformActionButtonSurface;
|
||||
size?: PlatformActionButtonSize;
|
||||
shape?: PlatformActionButtonShape;
|
||||
align?: PlatformActionButtonAlign;
|
||||
fullWidth?: boolean;
|
||||
};
|
||||
|
||||
type PlatformActionButtonButtonProps = Omit<
|
||||
ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
'children'
|
||||
> &
|
||||
PlatformActionButtonBaseProps & {
|
||||
asChild?: false;
|
||||
};
|
||||
|
||||
type PlatformActionButtonLabelProps = Omit<
|
||||
LabelHTMLAttributes<HTMLLabelElement>,
|
||||
'children'
|
||||
> &
|
||||
PlatformActionButtonBaseProps & {
|
||||
asChild: 'label';
|
||||
};
|
||||
|
||||
export type PlatformActionButtonProps =
|
||||
| PlatformActionButtonButtonProps
|
||||
| PlatformActionButtonLabelProps;
|
||||
|
||||
/**
|
||||
* 平台通用动作按钮。
|
||||
* 收口平台与个人中心主动作按钮的样式族、尺寸、圆角和禁用态 class。
|
||||
*/
|
||||
export function PlatformActionButton({
|
||||
tone = 'primary',
|
||||
surface = 'platform',
|
||||
size = 'sm',
|
||||
shape = 'default',
|
||||
align = 'center',
|
||||
fullWidth = false,
|
||||
className,
|
||||
children,
|
||||
asChild,
|
||||
...props
|
||||
}: PlatformActionButtonProps) {
|
||||
const actionClassName = [
|
||||
getPlatformActionButtonClassName({
|
||||
surface,
|
||||
tone,
|
||||
size,
|
||||
shape,
|
||||
align,
|
||||
fullWidth,
|
||||
}),
|
||||
className,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ');
|
||||
|
||||
if (asChild === 'label') {
|
||||
return (
|
||||
<label
|
||||
{...(props as LabelHTMLAttributes<HTMLLabelElement>)}
|
||||
className={actionClassName}
|
||||
>
|
||||
{children}
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
const { type = 'button', ...buttonProps } =
|
||||
props as ButtonHTMLAttributes<HTMLButtonElement>;
|
||||
return (
|
||||
<button {...buttonProps} type={type} className={actionClassName}>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
export type PlatformAsyncStatePanelProps = {
|
||||
errorState?: ReactNode;
|
||||
isLoading?: boolean;
|
||||
loadingState?: ReactNode;
|
||||
isEmpty?: boolean;
|
||||
emptyState?: ReactNode;
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
/**
|
||||
* 平台异步状态面板骨架。
|
||||
* 只负责在错误、读取、空态和内容之间切换,具体文案与外观继续交给调用方传入 slot。
|
||||
*/
|
||||
export function PlatformAsyncStatePanel({
|
||||
errorState,
|
||||
isLoading = false,
|
||||
loadingState = null,
|
||||
isEmpty = false,
|
||||
emptyState = null,
|
||||
children,
|
||||
}: PlatformAsyncStatePanelProps) {
|
||||
if (errorState !== undefined && errorState !== null) {
|
||||
return <>{errorState}</>;
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return <>{loadingState}</>;
|
||||
}
|
||||
|
||||
if (isEmpty) {
|
||||
return <>{emptyState}</>;
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/* @vitest-environment jsdom */
|
||||
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { expect, test } from 'vitest';
|
||||
|
||||
import { PlatformBackActionButton } from './index';
|
||||
|
||||
test('renders compact back action button by default', () => {
|
||||
render(<PlatformBackActionButton />);
|
||||
|
||||
const button = screen.getByRole('button', { name: '返回' });
|
||||
|
||||
expect(button.className).toContain('platform-button--ghost');
|
||||
expect(button.className).toContain('min-h-0');
|
||||
expect(button.className).toContain('text-[11px]');
|
||||
expect(button.className).toContain('gap-1.5');
|
||||
expect(button.querySelector('svg')?.className.baseVal).toContain('h-3.5');
|
||||
expect(button.getAttribute('type')).toBe('button');
|
||||
});
|
||||
|
||||
test('supports regular variant and editor dark surface', () => {
|
||||
render(
|
||||
<PlatformBackActionButton
|
||||
label="返回编辑"
|
||||
variant="regular"
|
||||
surface="editorDark"
|
||||
/>,
|
||||
);
|
||||
|
||||
const button = screen.getByRole('button', { name: '返回编辑' });
|
||||
|
||||
expect(button.className).toContain('platform-action-button--editor-dark');
|
||||
expect(button.className).toContain('text-sm');
|
||||
expect(button.className).toContain('gap-2');
|
||||
expect(button.querySelector('svg')?.className.baseVal).toContain('h-4');
|
||||
});
|
||||
@@ -0,0 +1,53 @@
|
||||
import { ArrowLeft } from 'lucide-react';
|
||||
import type { ButtonHTMLAttributes } from 'react';
|
||||
|
||||
import { PlatformActionButton } from './PlatformActionButton';
|
||||
import type { PlatformActionButtonSurface } from './platformActionButtonModel';
|
||||
|
||||
export type PlatformBackActionButtonVariant = 'compact' | 'regular';
|
||||
|
||||
export type PlatformBackActionButtonProps = Omit<
|
||||
ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
'children'
|
||||
> & {
|
||||
label?: string;
|
||||
variant?: PlatformBackActionButtonVariant;
|
||||
surface?: PlatformActionButtonSurface;
|
||||
};
|
||||
|
||||
const VARIANT_CLASS: Record<PlatformBackActionButtonVariant, string> = {
|
||||
compact: 'gap-1.5 py-1.5 text-[11px]',
|
||||
regular: 'gap-2 py-2 text-sm',
|
||||
};
|
||||
|
||||
const ICON_CLASS: Record<PlatformBackActionButtonVariant, string> = {
|
||||
compact: 'h-3.5 w-3.5',
|
||||
regular: 'h-4 w-4',
|
||||
};
|
||||
|
||||
/**
|
||||
* 平台轻量返回动作按钮。
|
||||
* 统一结果页、工作台等白底场景里的“左箭头 + 返回文案”按钮骨架。
|
||||
*/
|
||||
export function PlatformBackActionButton({
|
||||
label = '返回',
|
||||
variant = 'compact',
|
||||
surface = 'platform',
|
||||
className,
|
||||
...buttonProps
|
||||
}: PlatformBackActionButtonProps) {
|
||||
return (
|
||||
<PlatformActionButton
|
||||
{...buttonProps}
|
||||
surface={surface}
|
||||
tone="ghost"
|
||||
size="xs"
|
||||
className={['min-h-0 self-start', VARIANT_CLASS[variant], className]
|
||||
.filter(Boolean)
|
||||
.join(' ')}
|
||||
>
|
||||
<ArrowLeft className={ICON_CLASS[variant]} />
|
||||
{label}
|
||||
</PlatformActionButton>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export type {
|
||||
BadgeProps as PlatformBadgeProps,
|
||||
BadgeTone as PlatformBadgeTone,
|
||||
} from './ui';
|
||||
export { Badge as PlatformBadge } from './ui';
|
||||
@@ -0,0 +1,128 @@
|
||||
import type { ButtonHTMLAttributes, HTMLAttributes, ReactNode } from 'react';
|
||||
|
||||
export type PlatformEmptyStateSurface =
|
||||
| 'soft'
|
||||
| 'dashed'
|
||||
| 'subpanel'
|
||||
| 'editorDark';
|
||||
export type PlatformEmptyStateSize = 'compact' | 'panel' | 'inline';
|
||||
export type PlatformEmptyStateTone = 'base' | 'soft';
|
||||
|
||||
type PlatformEmptyStateBaseProps = {
|
||||
children: ReactNode;
|
||||
surface?: PlatformEmptyStateSurface;
|
||||
size?: PlatformEmptyStateSize;
|
||||
tone?: PlatformEmptyStateTone;
|
||||
};
|
||||
|
||||
type PlatformEmptyStateDivProps = Omit<
|
||||
HTMLAttributes<HTMLDivElement>,
|
||||
'children'
|
||||
> & {
|
||||
asChild?: false;
|
||||
} & PlatformEmptyStateBaseProps;
|
||||
|
||||
type PlatformEmptyStateButtonProps = Omit<
|
||||
ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
'children'
|
||||
> & {
|
||||
asChild: 'button';
|
||||
} & PlatformEmptyStateBaseProps;
|
||||
|
||||
export type PlatformEmptyStateProps =
|
||||
| PlatformEmptyStateDivProps
|
||||
| PlatformEmptyStateButtonProps;
|
||||
|
||||
type PlatformEmptyStateClassNameOptions = {
|
||||
surface: PlatformEmptyStateSurface;
|
||||
size: PlatformEmptyStateSize;
|
||||
tone: PlatformEmptyStateTone;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
const PLATFORM_EMPTY_STATE_SURFACE_CLASS: Record<
|
||||
PlatformEmptyStateSurface,
|
||||
string
|
||||
> = {
|
||||
soft: 'platform-surface platform-surface--soft rounded-[1.35rem]',
|
||||
dashed:
|
||||
'rounded-[1.35rem] border border-dashed border-[var(--platform-subpanel-border)] bg-white/52',
|
||||
subpanel:
|
||||
'rounded-[1rem] border border-[var(--platform-subpanel-border)] bg-white/74',
|
||||
editorDark: 'rounded-2xl border border-dashed border-white/12 bg-black/20',
|
||||
};
|
||||
|
||||
const PLATFORM_EMPTY_STATE_SIZE_CLASS: Record<PlatformEmptyStateSize, string> =
|
||||
{
|
||||
compact: 'px-4 py-3 text-sm leading-6',
|
||||
panel:
|
||||
'flex min-h-[14rem] items-center justify-center px-6 text-center text-sm',
|
||||
inline: 'px-4 py-5 text-center text-sm font-semibold',
|
||||
};
|
||||
|
||||
const PLATFORM_EMPTY_STATE_TONE_CLASS: Record<PlatformEmptyStateTone, string> =
|
||||
{
|
||||
base: 'text-[var(--platform-text-base)]',
|
||||
soft: 'text-[var(--platform-text-soft)]',
|
||||
};
|
||||
|
||||
/**
|
||||
* 平台通用空态和轻量加载态。
|
||||
* 收口平台列表、作品架和素材选择弹窗中重复的空面板外观。
|
||||
*/
|
||||
function getPlatformEmptyStateClassName({
|
||||
surface,
|
||||
size,
|
||||
tone,
|
||||
className,
|
||||
}: PlatformEmptyStateClassNameOptions) {
|
||||
return [
|
||||
'min-w-0',
|
||||
'platform-empty-state',
|
||||
PLATFORM_EMPTY_STATE_SURFACE_CLASS[surface],
|
||||
PLATFORM_EMPTY_STATE_SIZE_CLASS[size],
|
||||
PLATFORM_EMPTY_STATE_TONE_CLASS[tone],
|
||||
className,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ');
|
||||
}
|
||||
|
||||
export function PlatformEmptyState({
|
||||
children,
|
||||
surface = 'soft',
|
||||
size = 'compact',
|
||||
tone,
|
||||
className,
|
||||
asChild,
|
||||
...emptyStateProps
|
||||
}: PlatformEmptyStateProps) {
|
||||
const resolvedTone =
|
||||
tone ?? (surface === 'subpanel' || size === 'inline' ? 'soft' : 'base');
|
||||
const emptyStateClassName = getPlatformEmptyStateClassName({
|
||||
surface,
|
||||
size,
|
||||
tone: resolvedTone,
|
||||
className,
|
||||
});
|
||||
|
||||
if (asChild === 'button') {
|
||||
const { type = 'button', ...buttonProps } =
|
||||
emptyStateProps as ButtonHTMLAttributes<HTMLButtonElement>;
|
||||
|
||||
return (
|
||||
<button {...buttonProps} type={type} className={emptyStateClassName}>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
{...(emptyStateProps as HTMLAttributes<HTMLDivElement>)}
|
||||
className={emptyStateClassName}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
export type PlatformFieldLabelVariant =
|
||||
| 'field'
|
||||
| 'section'
|
||||
| 'form'
|
||||
| 'inlineForm'
|
||||
| 'pill'
|
||||
| 'accentPill';
|
||||
|
||||
export type PlatformFieldLabelProps = {
|
||||
children: ReactNode;
|
||||
variant?: PlatformFieldLabelVariant;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
const PLATFORM_FIELD_LABEL_CLASS: Record<PlatformFieldLabelVariant, string> = {
|
||||
field: 'text-xs font-bold text-[var(--platform-text-soft)]',
|
||||
section:
|
||||
'text-xs font-bold tracking-[0.18em] text-[var(--platform-text-soft)]',
|
||||
form: 'mb-2 block text-sm font-black text-[var(--platform-text-strong)]',
|
||||
inlineForm:
|
||||
'inline-flex items-center text-sm font-bold text-[var(--platform-text-base)]',
|
||||
pill: 'mb-2 inline-flex rounded-full px-2 py-0.5 text-sm font-black text-[var(--platform-text-strong)]',
|
||||
accentPill:
|
||||
'mb-2 inline-flex rounded-full border border-rose-200/70 bg-rose-50/88 px-2.5 py-1 text-sm font-black text-rose-700 shadow-sm',
|
||||
};
|
||||
|
||||
/** 平台字段标签,供子面板和表单控件复用。 */
|
||||
export function PlatformFieldLabel({
|
||||
children,
|
||||
variant = 'field',
|
||||
className,
|
||||
}: PlatformFieldLabelProps) {
|
||||
return (
|
||||
<span
|
||||
className={[PLATFORM_FIELD_LABEL_CLASS[variant], className]
|
||||
.filter(Boolean)
|
||||
.join(' ')}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
import { ChevronDown, SlidersHorizontal } from 'lucide-react';
|
||||
|
||||
import { PlatformSegmentedTabs } from './PlatformSegmentedTabs';
|
||||
|
||||
export interface PlatformFilterToolbarTabItem {
|
||||
id: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export interface PlatformFilterToolbarProps {
|
||||
filterLabel: string;
|
||||
filterCount: number;
|
||||
tabItems: readonly PlatformFilterToolbarTabItem[];
|
||||
activeTabId: string;
|
||||
sortLabel: string;
|
||||
layout: 'mobile' | 'desktop';
|
||||
onOpenFilter: () => void;
|
||||
onTabChange: (id: string) => void;
|
||||
onToggleSort: () => void;
|
||||
}
|
||||
|
||||
function buildToolbarTabItemClassName(active: boolean) {
|
||||
return [
|
||||
'platform-category-chip shrink-0 !min-h-[2.35rem] !rounded-none !border-0 !bg-transparent !px-0 !shadow-none hover:!bg-transparent',
|
||||
active ? 'platform-category-chip--active' : null,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ');
|
||||
}
|
||||
|
||||
/**
|
||||
* 平台内容列表的筛选、分类和排序工具栏。
|
||||
* 只负责展示入口与转发回调,筛选条件和排序规则由宿主页面持有。
|
||||
*/
|
||||
export function PlatformFilterToolbar({
|
||||
filterLabel,
|
||||
filterCount,
|
||||
tabItems,
|
||||
activeTabId,
|
||||
sortLabel,
|
||||
layout,
|
||||
onOpenFilter,
|
||||
onTabChange,
|
||||
onToggleSort,
|
||||
}: PlatformFilterToolbarProps) {
|
||||
const isMobileLayout = layout === 'mobile';
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={
|
||||
isMobileLayout
|
||||
? 'platform-category-filter-row'
|
||||
: 'mb-4 flex min-w-0 items-center gap-2'
|
||||
}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onOpenFilter}
|
||||
aria-haspopup="dialog"
|
||||
className="platform-category-filter-button"
|
||||
>
|
||||
<SlidersHorizontal className="h-4 w-4" />
|
||||
<span>{filterLabel}</span>
|
||||
<span className="platform-category-filter-button__count">
|
||||
{filterCount}
|
||||
</span>
|
||||
</button>
|
||||
{isMobileLayout ? (
|
||||
<span className="platform-category-filter-divider" />
|
||||
) : null}
|
||||
<PlatformSegmentedTabs
|
||||
items={tabItems}
|
||||
activeId={activeTabId}
|
||||
onChange={onTabChange}
|
||||
layout="scroll"
|
||||
gap="md"
|
||||
frame="bare"
|
||||
surface="transparent"
|
||||
size="sm"
|
||||
tone="neutral"
|
||||
className={
|
||||
isMobileLayout
|
||||
? 'platform-category-chip-scroll min-w-0 flex-1'
|
||||
: 'min-w-0 flex-1 pb-1'
|
||||
}
|
||||
itemClassName={(_, active) => buildToolbarTabItemClassName(active)}
|
||||
/>
|
||||
{!isMobileLayout ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onToggleSort}
|
||||
className="platform-category-sort-button shrink-0"
|
||||
>
|
||||
<span>{sortLabel}</span>
|
||||
<ChevronDown className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
{isMobileLayout ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onToggleSort}
|
||||
className="platform-category-sort-button"
|
||||
>
|
||||
<span>按{sortLabel}排序</span>
|
||||
<ChevronDown className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
export type PlatformIconBadgeSize =
|
||||
| 'xs'
|
||||
| 'sm'
|
||||
| 'base'
|
||||
| 'md'
|
||||
| 'lg'
|
||||
| 'xl'
|
||||
| 'xxl';
|
||||
export type PlatformIconBadgeShape = 'circle' | 'rounded' | 'xl';
|
||||
export type PlatformIconBadgeTone =
|
||||
| 'neutral'
|
||||
| 'soft'
|
||||
| 'softBright'
|
||||
| 'hero'
|
||||
| 'heroMuted'
|
||||
| 'darkAmber'
|
||||
| 'success'
|
||||
| 'danger';
|
||||
|
||||
export type PlatformIconBadgeProps = {
|
||||
icon: ReactNode;
|
||||
label?: string;
|
||||
size?: PlatformIconBadgeSize;
|
||||
shape?: PlatformIconBadgeShape;
|
||||
tone?: PlatformIconBadgeTone;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
const PLATFORM_ICON_BADGE_SIZE_CLASS: Record<PlatformIconBadgeSize, string> = {
|
||||
xs: 'h-7 w-7',
|
||||
sm: 'h-9 w-9',
|
||||
base: 'h-10 w-10',
|
||||
md: 'h-11 w-11',
|
||||
lg: 'h-12 w-12',
|
||||
xl: 'h-14 w-14',
|
||||
xxl: 'h-20 w-20',
|
||||
};
|
||||
|
||||
const PLATFORM_ICON_BADGE_SHAPE_CLASS: Record<PlatformIconBadgeShape, string> =
|
||||
{
|
||||
circle: 'rounded-full',
|
||||
rounded: 'rounded-[0.85rem]',
|
||||
xl: 'rounded-2xl',
|
||||
};
|
||||
|
||||
const PLATFORM_ICON_BADGE_TONE_CLASS: Record<PlatformIconBadgeTone, string> = {
|
||||
neutral:
|
||||
'bg-[var(--platform-neutral-bg)] text-[var(--platform-neutral-text)]',
|
||||
soft: 'bg-white/82 text-[var(--platform-text-strong)] shadow-sm',
|
||||
softBright: 'bg-white/84 text-[var(--platform-text-strong)] shadow-sm',
|
||||
hero: 'bg-white/18 text-white',
|
||||
heroMuted: 'bg-white/18 text-white/72',
|
||||
darkAmber: 'border border-amber-400/30 bg-amber-500/15 text-amber-50',
|
||||
success:
|
||||
'bg-[var(--platform-success-bg)] text-[var(--platform-success-text)]',
|
||||
danger:
|
||||
'bg-[var(--platform-button-danger-fill)] text-[var(--platform-button-danger-text)]',
|
||||
};
|
||||
|
||||
/**
|
||||
* 平台中性图标徽章。
|
||||
* 统一承接弹窗标题、列表项和小卡片里的非交互图标槽。
|
||||
*/
|
||||
export function PlatformIconBadge({
|
||||
icon,
|
||||
label,
|
||||
size = 'sm',
|
||||
shape = 'circle',
|
||||
tone = 'neutral',
|
||||
className,
|
||||
}: PlatformIconBadgeProps) {
|
||||
return (
|
||||
<span
|
||||
aria-label={label}
|
||||
aria-hidden={label ? undefined : true}
|
||||
className={[
|
||||
'platform-icon-badge',
|
||||
'grid shrink-0 place-items-center',
|
||||
PLATFORM_ICON_BADGE_SIZE_CLASS[size],
|
||||
PLATFORM_ICON_BADGE_SHAPE_CLASS[shape],
|
||||
PLATFORM_ICON_BADGE_TONE_CLASS[tone],
|
||||
className,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')}
|
||||
>
|
||||
{icon}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
import type {
|
||||
ButtonHTMLAttributes,
|
||||
HTMLAttributes,
|
||||
KeyboardEvent,
|
||||
LabelHTMLAttributes,
|
||||
ReactNode,
|
||||
Ref,
|
||||
} from 'react';
|
||||
import { forwardRef } from 'react';
|
||||
|
||||
export type PlatformIconButtonVariant =
|
||||
| 'platformIcon'
|
||||
| 'surfaceFloating'
|
||||
| 'darkMini';
|
||||
export type PlatformIconButtonSize = 'sm' | 'md' | 'lg';
|
||||
|
||||
type PlatformIconButtonBaseProps = {
|
||||
label: string;
|
||||
icon: ReactNode;
|
||||
children?: ReactNode;
|
||||
variant?: PlatformIconButtonVariant;
|
||||
};
|
||||
|
||||
type PlatformIconButtonButtonProps = Omit<
|
||||
ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
'aria-label' | 'children'
|
||||
> &
|
||||
PlatformIconButtonBaseProps & {
|
||||
asChild?: false;
|
||||
};
|
||||
|
||||
type PlatformIconButtonLabelProps = Omit<
|
||||
LabelHTMLAttributes<HTMLLabelElement>,
|
||||
'aria-label' | 'children'
|
||||
> &
|
||||
PlatformIconButtonBaseProps & {
|
||||
asChild: 'label';
|
||||
};
|
||||
|
||||
type PlatformIconButtonSpanButtonProps = Omit<
|
||||
HTMLAttributes<HTMLSpanElement>,
|
||||
'aria-label' | 'children' | 'role'
|
||||
> &
|
||||
PlatformIconButtonBaseProps & {
|
||||
asChild: 'spanButton';
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
export type PlatformIconButtonProps =
|
||||
| PlatformIconButtonButtonProps
|
||||
| PlatformIconButtonLabelProps
|
||||
| PlatformIconButtonSpanButtonProps;
|
||||
|
||||
/**
|
||||
* 平台通用图标动作按钮。
|
||||
* 统一承接纯图标动作、图标上传 label 和带短标签的浮动图标动作。
|
||||
*/
|
||||
export const PlatformIconButton = forwardRef<
|
||||
HTMLButtonElement | HTMLLabelElement | HTMLSpanElement,
|
||||
PlatformIconButtonProps
|
||||
>(function PlatformIconButton(
|
||||
{
|
||||
label,
|
||||
icon,
|
||||
children,
|
||||
variant = 'platformIcon',
|
||||
title,
|
||||
className,
|
||||
asChild,
|
||||
...actionProps
|
||||
},
|
||||
ref,
|
||||
) {
|
||||
const variantClassName = {
|
||||
platformIcon: 'platform-icon-button',
|
||||
surfaceFloating:
|
||||
'inline-flex items-center justify-center rounded-full border border-white/80 bg-white/94 text-[var(--platform-text-strong)] shadow-sm backdrop-blur transition hover:text-[var(--platform-accent)] disabled:cursor-not-allowed disabled:opacity-55',
|
||||
darkMini:
|
||||
'inline-flex items-center justify-center rounded-full border border-white/16 bg-black/55 text-white transition-colors hover:bg-black/70 disabled:cursor-not-allowed disabled:opacity-55',
|
||||
}[variant];
|
||||
|
||||
const actionClassName = [variantClassName, className]
|
||||
.filter(Boolean)
|
||||
.join(' ');
|
||||
|
||||
if (asChild === 'label') {
|
||||
return (
|
||||
<label
|
||||
{...(actionProps as LabelHTMLAttributes<HTMLLabelElement>)}
|
||||
ref={ref as Ref<HTMLLabelElement>}
|
||||
title={title}
|
||||
className={actionClassName}
|
||||
>
|
||||
<span className="sr-only">{label}</span>
|
||||
{icon}
|
||||
{children}
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
if (asChild === 'spanButton') {
|
||||
const { disabled, onClick, onKeyDown, tabIndex, ...spanProps } =
|
||||
actionProps as HTMLAttributes<HTMLSpanElement> & {
|
||||
disabled?: boolean;
|
||||
};
|
||||
const handleKeyDown = (event: KeyboardEvent<HTMLSpanElement>) => {
|
||||
onKeyDown?.(event);
|
||||
if (event.defaultPrevented || disabled) {
|
||||
return;
|
||||
}
|
||||
if (event.key === 'Enter' || event.key === ' ') {
|
||||
event.preventDefault();
|
||||
event.currentTarget.click();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<span
|
||||
{...spanProps}
|
||||
ref={ref as Ref<HTMLSpanElement>}
|
||||
aria-disabled={disabled}
|
||||
aria-label={label}
|
||||
className={actionClassName}
|
||||
onClick={disabled ? undefined : onClick}
|
||||
onKeyDown={handleKeyDown}
|
||||
role="button"
|
||||
tabIndex={disabled ? -1 : (tabIndex ?? 0)}
|
||||
title={title}
|
||||
>
|
||||
{icon}
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
const { type = 'button', ...buttonProps } =
|
||||
actionProps as ButtonHTMLAttributes<HTMLButtonElement>;
|
||||
|
||||
return (
|
||||
<button
|
||||
{...buttonProps}
|
||||
ref={ref as Ref<HTMLButtonElement>}
|
||||
type={type}
|
||||
aria-label={label}
|
||||
title={title}
|
||||
className={actionClassName}
|
||||
>
|
||||
{icon}
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user