修复 AGC 栏目画布上传素材落「待归类」(#359) #596
@@ -567,6 +567,23 @@ pub(crate) fn upload_local_asset_at(
|
||||
file_name: &str,
|
||||
media_type: &str,
|
||||
bytes: &[u8],
|
||||
) -> Result<UploadLocalAssetResult, String> {
|
||||
upload_local_asset_at_with_category(root, file_name, media_type, bytes, None)
|
||||
}
|
||||
|
||||
/// 带**显式入口栏目**的上传:只给 GUI 工具栏上传用(前端 `targetCategory`)。
|
||||
///
|
||||
/// 上传的 manifest `kind` 只由内容证据推导(图片 / 视频 / 代码 → `unclassified`),
|
||||
/// 与栏目不是同一套词汇:在栏目画布里上传素材时,用户选定的栏目才是归属真相,
|
||||
/// 否则素材会落进「待归类」、在上传它的那一栏里看不见。
|
||||
/// 取值校验与失败关闭沿用 [`register_local_asset_entry_with_category`]:
|
||||
/// 非法值报错、绝不回退到 kind 派生;其它调用方继续走 [`upload_local_asset_at`]。
|
||||
pub(crate) fn upload_local_asset_at_with_category(
|
||||
root: &Path,
|
||||
file_name: &str,
|
||||
media_type: &str,
|
||||
bytes: &[u8],
|
||||
target_category: Option<&str>,
|
||||
) -> Result<UploadLocalAssetResult, String> {
|
||||
if bytes.is_empty() {
|
||||
return Err("上传文件不能为空".to_string());
|
||||
@@ -584,7 +601,7 @@ pub(crate) fn upload_local_asset_at(
|
||||
}
|
||||
crate::write_game_creator_private_file(&absolute_path, bytes, "上传文件")?;
|
||||
|
||||
register_local_asset_entry(
|
||||
register_local_asset_entry_with_category(
|
||||
root,
|
||||
&relative_path,
|
||||
uploaded_asset_kind(file_name, media_type),
|
||||
@@ -602,6 +619,7 @@ pub(crate) fn upload_local_asset_at(
|
||||
generation_kind: None,
|
||||
reference_resource_ids: Vec::new(),
|
||||
},
|
||||
target_category,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2375,6 +2393,70 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// Issue 359:工具栏上传带上入口栏目,素材才不会落进「待归类」。
|
||||
///
|
||||
/// 上传的 manifest `kind` 只由内容证据推导,图片 / 视频 / 代码都派生成 `unclassified`;
|
||||
/// 在栏目画布(如「角色与对象」)里上传时栏目才是归属真相。不传时保持既有行为,
|
||||
/// 非法值失败关闭且不留下半成品登记。
|
||||
#[test]
|
||||
fn upload_registers_into_the_explicit_entry_category() {
|
||||
fn upload_category(root: &Path, asset_id: &str) -> GameCreationAppAssetCategory {
|
||||
read_existing_manifest_for_project(root)
|
||||
.expect("read manifest")
|
||||
.assets
|
||||
.into_iter()
|
||||
.find(|asset| asset.id == asset_id)
|
||||
.expect("uploaded asset is registered")
|
||||
.category
|
||||
}
|
||||
|
||||
let temporary = tempfile::tempdir().expect("tempdir");
|
||||
let root = temporary.path();
|
||||
crate::project::init_local_game_project_at(root, "upload-category-test", "上传入口栏目")
|
||||
.expect("init project");
|
||||
|
||||
let uploaded = upload_local_asset_at_with_category(
|
||||
root,
|
||||
"hero.png",
|
||||
"image/png",
|
||||
b"png-bytes",
|
||||
Some("character"),
|
||||
)
|
||||
.expect("upload with an entry category");
|
||||
assert_eq!(
|
||||
upload_category(root, &uploaded.id),
|
||||
GameCreationAppAssetCategory::Character
|
||||
);
|
||||
|
||||
let plain = upload_local_asset_at(root, "plain.png", "image/png", b"png-bytes")
|
||||
.expect("upload without an entry category");
|
||||
assert_eq!(
|
||||
upload_category(root, &plain.id),
|
||||
GameCreationAppAssetCategory::Unclassified
|
||||
);
|
||||
|
||||
// 非法入口栏目(栏目侧伪值 `version` 不在枚举里)失败关闭:不新增登记。
|
||||
let assets_before = read_existing_manifest_for_project(root)
|
||||
.expect("read manifest")
|
||||
.assets
|
||||
.len();
|
||||
assert!(upload_local_asset_at_with_category(
|
||||
root,
|
||||
"bad.png",
|
||||
"image/png",
|
||||
b"png-bytes",
|
||||
Some("version"),
|
||||
)
|
||||
.is_err());
|
||||
assert_eq!(
|
||||
read_existing_manifest_for_project(root)
|
||||
.expect("read manifest")
|
||||
.assets
|
||||
.len(),
|
||||
assets_before
|
||||
);
|
||||
}
|
||||
|
||||
/// 画板导出推断出的 kind 必须已经是 canonical 值。
|
||||
#[test]
|
||||
fn canvas_export_asset_kind_is_always_canonical() {
|
||||
|
||||
@@ -493,12 +493,19 @@ pub(crate) fn upload_local_asset(
|
||||
file_name: String,
|
||||
media_type: String,
|
||||
bytes: Vec<u8>,
|
||||
target_category: Option<String>,
|
||||
) -> Result<UploadLocalAssetResult, String> {
|
||||
let root = Path::new(project_path.trim());
|
||||
enforce_project_permission_policy(root, "asset.upload")?;
|
||||
let _lock = acquire_project_write_lock(root, "asset.upload")?;
|
||||
advance_agent_runtime_project_revision_locked(root)?;
|
||||
upload_local_asset_at(root, file_name.trim(), media_type.trim(), &bytes)
|
||||
upload_local_asset_at_with_category(
|
||||
root,
|
||||
file_name.trim(),
|
||||
media_type.trim(),
|
||||
&bytes,
|
||||
target_category.as_deref(),
|
||||
)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
|
||||
+7
-1
@@ -99,7 +99,13 @@ export type ResourceCanvasAudioToolAction =
|
||||
audioKind: ResourceCanvasGenerationKind;
|
||||
};
|
||||
|
||||
/** 上传入口:复用既有 `upload_local_asset`。 */
|
||||
/**
|
||||
* 上传入口:复用既有 `upload_local_asset`。
|
||||
*
|
||||
* 调用方按当前栏目带 `targetCategory`(见 `uploadResourceCanvasToolbarFiles`)——上传的
|
||||
* manifest `kind` 只由内容证据推导,图片 / 视频 / 代码都会派生成 `unclassified`,
|
||||
* 不带入口栏目素材就落「待归类」、在上传它的那一栏里看不见。
|
||||
*/
|
||||
export type ResourceCanvasUploadToolAction =
|
||||
ResourceCanvasBottomToolActionBase & {
|
||||
route: 'upload';
|
||||
|
||||
@@ -3348,8 +3348,9 @@ export default function ProjectDevelopmentView({
|
||||
* 两条判据缺一不可:
|
||||
* 1. 这张卡已经进了布局(`resourcePositionById`),否则 reconcile 还没补位、写坐标是空操作;
|
||||
* 2. 这张卡已经有正式归类(投影里的 `category`)——`commitPosition` 的 `section` 必须与该资源
|
||||
* 在 sidecar 里的 section 一致,否则会被静默跳过。生成结果的实际归类与入口栏目本来就可能不同
|
||||
* (普通图片落「待归类」、规范落「文档」),所以这里按**正式归类**写,而不是入口栏目。
|
||||
* 在 sidecar 里的 section 一致,否则会被静默跳过。生成结果的实际归类与入口栏目**不一定相同**
|
||||
* (Agent / Direct 路径不传 `targetCategory`,普通图片仍落「待归类」),所以这里按**正式归类**写,
|
||||
* 而不是入口栏目。
|
||||
*
|
||||
* 写完撤掉占位:结果已经接管了它的位置。占位被用户先删掉时同样清掉这条意图(没有位置可接管)。
|
||||
*/
|
||||
@@ -9062,9 +9063,15 @@ export default function ProjectDevelopmentView({
|
||||
[selectResourceCanvasPage],
|
||||
);
|
||||
|
||||
/** 工具栏「上传」:与资源面板上传同一条「上传 + 配对读清单」链路。 */
|
||||
/**
|
||||
* 工具栏「上传」:与资源面板上传同一条「上传 + 配对读清单」链路。
|
||||
*
|
||||
* 入口栏目随上传一起交给原生(`targetCategory`):上传的 kind 只由内容证据推导
|
||||
* (图片 / 视频 / 代码 → `unclassified`),不带上它,素材会落进「待归类」、
|
||||
* 在上传它的那一栏里看不见。取值就是工具栏自己的栏目,与生成入口同一口径。
|
||||
*/
|
||||
const uploadResourceCanvasToolbarFiles = useCallback(
|
||||
async (files: readonly File[]) => {
|
||||
async (files: readonly File[], targetCategory: ResourceCategory | null) => {
|
||||
const invoke = window.__TAURI__?.core?.invoke;
|
||||
if (!invoke) {
|
||||
setResourceWorkbenchNotice('上传素材需要在客户端内打开');
|
||||
@@ -9088,6 +9095,7 @@ export default function ProjectDevelopmentView({
|
||||
bytes: Array.from(new Uint8Array(await file.arrayBuffer())),
|
||||
})),
|
||||
),
|
||||
targetCategory,
|
||||
invoke,
|
||||
});
|
||||
setResourceWorkbenchNotice(`已上传 ${uploadable.length} 个素材`);
|
||||
@@ -10548,7 +10556,10 @@ export default function ProjectDevelopmentView({
|
||||
}
|
||||
onSelectAction={handleResourceBottomToolAction}
|
||||
onUploadFiles={(files) => {
|
||||
void uploadResourceCanvasToolbarFiles(files);
|
||||
void uploadResourceCanvasToolbarFiles(
|
||||
files,
|
||||
resourceCanvasBottomToolbarCategory,
|
||||
);
|
||||
}}
|
||||
uploading={resourceBottomToolbarUploading}
|
||||
/>
|
||||
|
||||
+13
-1
@@ -1,4 +1,7 @@
|
||||
import type { GameCreationAppManifest } from '../../../../../packages/shared/src/contracts/gameCreationApp';
|
||||
import type {
|
||||
GameCreationAppManifest,
|
||||
ProjectResourceCanvasCategory,
|
||||
} from '../../../../../packages/shared/src/contracts/gameCreationApp';
|
||||
|
||||
export type ProjectManifestSnapshotSource =
|
||||
| 'initial'
|
||||
@@ -285,6 +288,14 @@ export async function uploadProjectAssetFilesAndReadSnapshot(input: {
|
||||
mediaType: string;
|
||||
bytes: number[];
|
||||
}[];
|
||||
/**
|
||||
* 入口栏目:栏目画布里上传时由调用方显式给出当前栏目,原生按它登记归类。
|
||||
*
|
||||
* 上传的 manifest `kind` 只由内容证据推导(图片 / 视频 / 代码 → `unclassified`),
|
||||
* 不传这一项时素材会落进「待归类」、在上传它的那一栏里看不见。
|
||||
* 判定与失败关闭都在原生:非法值报错,前端不做伪分类。
|
||||
*/
|
||||
targetCategory?: ProjectResourceCanvasCategory | null;
|
||||
invoke<T>(command: string, args: Record<string, unknown>): Promise<T>;
|
||||
}): Promise<ProjectManifestSnapshot | null> {
|
||||
for (const file of input.files) {
|
||||
@@ -293,6 +304,7 @@ export async function uploadProjectAssetFilesAndReadSnapshot(input: {
|
||||
fileName: file.fileName,
|
||||
mediaType: file.mediaType,
|
||||
bytes: file.bytes,
|
||||
...(input.targetCategory ? { targetCategory: input.targetCategory } : {}),
|
||||
});
|
||||
}
|
||||
const fresh = await rereadAuthoritativeProjectManifestSnapshot({
|
||||
|
||||
@@ -8839,6 +8839,52 @@ export function registerProjectAgentStatusTests() {
|
||||
);
|
||||
}, 20_000);
|
||||
|
||||
it('uploads toolbar files into the entry column so they stay visible where they were uploaded', async () => {
|
||||
// Issue 359:图片只由内容证据派生成 `unclassified`,在「角色与对象」栏目里上传后
|
||||
// 会落进「待归类」、在上传它的那一栏里看不见。工具栏上传必须把当前栏目交给原生。
|
||||
const manifest = createGameCreationAppManifest(
|
||||
'workbench-bottom-toolbar-upload-category',
|
||||
'底部工具栏上传归类项目',
|
||||
);
|
||||
const { calls } = installResourceBookBottomToolbarInvoke(manifest);
|
||||
|
||||
render(
|
||||
React.createElement(ProjectDevelopmentView, {
|
||||
projectName: manifest.name,
|
||||
projectPath: '/tmp/workbench-bottom-toolbar-upload-category',
|
||||
manifest,
|
||||
attachments: [],
|
||||
recentRunStatus: null,
|
||||
recentRunStopReason: null,
|
||||
chat: React.createElement('div', null, '项目总控'),
|
||||
onHomeOpen: vi.fn(),
|
||||
onProjectsOpen: vi.fn(),
|
||||
}),
|
||||
);
|
||||
|
||||
await openResourceBookCategory('角色与对象');
|
||||
const uploadInput = screen.getByLabelText(
|
||||
'上传素材文件',
|
||||
) as HTMLInputElement;
|
||||
const file = new File(['png'], 'hero.png', { type: 'image/png' });
|
||||
if (typeof file.arrayBuffer !== 'function') {
|
||||
Object.defineProperty(file, 'arrayBuffer', {
|
||||
value: async () => new Uint8Array([112, 110, 103]).buffer,
|
||||
});
|
||||
}
|
||||
fireEvent.change(uploadInput, { target: { files: [file] } });
|
||||
|
||||
await waitFor(() =>
|
||||
expect(uploadCall(calls, 'hero.png')).toMatchObject({
|
||||
projectPath: '/tmp/workbench-bottom-toolbar-upload-category',
|
||||
fileName: 'hero.png',
|
||||
mediaType: 'image/png',
|
||||
targetCategory: 'character',
|
||||
bytes: [112, 110, 103],
|
||||
}),
|
||||
);
|
||||
}, 20_000);
|
||||
|
||||
it('keeps the bottom toolbar clear of the zoom dock and above the book scene', () => {
|
||||
const styles = readFileSync(
|
||||
repoPath('apps/ai-game-creator-shell/src/styles.css'),
|
||||
|
||||
@@ -164,7 +164,7 @@
|
||||
|
||||
- 位置与层级:画布左下角(`left: 14px; bottom: 14px; z-index: 40`)。右下角是既有的缩放 / 撤销 Dock(`right: 14px; bottom: 14px`),左下角是画布上唯一两者都不占的稳定空位。工具栏是管理区 `.game-resource-book-manager` 的**直接子节点**、与画本场景并列,不进带 `scale()` 的场景层;二级菜单与「入口不可用原因」都贴着工具栏上沿弹出,不做内嵌内容。
|
||||
- 外壳用共享 chrome(`packages/image-canvas-react` 的 `CanvasToolbar / CanvasToolbarGroup / CanvasChromeButton`),样式落在 AGC 的 `resourceCanvasChrome.css`;共享包只承接通用表现,不含业务规则。
|
||||
- 接线:图片类入口走本地 IPC `start_local_project_asset_generation`(`kind` ∈ `image / character / spec / icon-spec / ui-prototype / art-spritesheet`;**提交即返回任务记录**,生成由 Rust 后台任务跑完写回项目,进度用 `list_local_project_asset_generations` 读回项目内账本 `.agent/runtime/asset-generation-tasks/tasks.json`);音频入口也走 `start_local_project_asset_generation`(同一份项目内任务账本;`kind` = `sound-effect` / `background-music`,并额外携带该次生成的请求身份 `idempotencyKey`,任务 id 即该次生成的 operation id;生成仍复用既有音频无源生成链路,`generationMode: 'create'`、`editKind` = `sound-effect` / `background-music`,不新增平台路由与请求体口径);「上传」复用 `upload_local_asset`。生成 / 上传成功后一律用「配对读 `(revision, manifest)`」交给 `onManifestChange`,走既有 manifest 刷新与资源投影链路,不重算依赖图、不另写布局。
|
||||
- 接线:图片类入口走本地 IPC `start_local_project_asset_generation`(`kind` ∈ `image / character / spec / icon-spec / ui-prototype / art-spritesheet`;**提交即返回任务记录**,生成由 Rust 后台任务跑完写回项目,进度用 `list_local_project_asset_generations` 读回项目内账本 `.agent/runtime/asset-generation-tasks/tasks.json`);音频入口也走 `start_local_project_asset_generation`(同一份项目内任务账本;`kind` = `sound-effect` / `background-music`,并额外携带该次生成的请求身份 `idempotencyKey`,任务 id 即该次生成的 operation id;生成仍复用既有音频无源生成链路,`generationMode: 'create'`、`editKind` = `sound-effect` / `background-music`,不新增平台路由与请求体口径);「上传」复用 `upload_local_asset`,并带上当前栏目 `targetCategory`(上传的 manifest `kind` 只由内容证据推导,图片 / 视频 / 代码都派生成 `unclassified`;不带入口栏目素材就落进「待归类」、在上传它的那一栏里看不见,Issue 359)。生成 / 上传成功后一律用「配对读 `(revision, manifest)`」交给 `onManifestChange`,走既有 manifest 刷新与资源投影链路,不重算依赖图、不另写布局。
|
||||
- 本地排队与进度可见:AGC 本地 durable 输出槽已按**精确动作指纹**分槽(不同 prompt / 素材名各自独立成槽,具备并行能力),但本批前端仍按「同一时刻只派发一条」排队——真并行派发需要并发收口设计(配对读 + manifest CAS + 聚焦意图互不覆盖),留待下一批;所以第一条未终态时第二条提交停在**前端本地队列**里(不调用提交 IPC,显示本地排队的「排队中。」),前一条终态后自动补发;任务状态与阶段文案(后端 `phaseDetail`)由任务账本提供,前端不拼阶段、不做百分比。进度面是**画布上常驻的可折叠任务侧栏**(位置与开合形态照抄网页端美术画布的任务侧栏的右上角锚点,颜色与外形仍走 AGC 平台 token;2026-09-21 由「左侧贴边 + 工具条入口」改为「右上角锚点 + 常驻开关」):展开是两个分栏「排队/生成中」与「已完成」(各带条数,「已完成」封顶 20 条 + 列表滚动 + 高度有界),关闭入口只保留头部那一枚 ×(底部重复的关闭按钮与其分割线已删除)、折叠即整块让出画布、只留那一枚右上角开关(工具条上不再有重复入口),开合只走画布右上角那一枚「生成任务 · N」开关(两个页签下都在;**展开后开关让位、只留面板**,收起走面板头部 × 或点画布外部);锚点是画布那一格网格里的条目(不是写死 `top` 的绝对定位),工具条换行变高也不会压上去;每项显示状态徽标 / 阶段文案 / 已耗时 / 素材名,可「定位到素材」。侧栏非模态(不铺全屏遮罩、不做焦点陷阱、不参与模态遮挡判据),位置在画布右上角锚点里(照抄美术画布那一处)、**覆盖式**(不 reflow 挤窄画布视口),提交受理后自动展开。锚点按工作面分档:资源栏目画布与 UI 编辑器用画布顶边那一档,运行表现层下移让开右上角的版本入口;锚点是**画布那一格网格里的条目**(不是写死 `top` 的绝对定位),所以工具条换行变高也不会压上去。定位动作**每次点击都终局化**:能定位就定位并选中;素材在别的栏目先切栏目;不在投影里给「素材已不在项目里 / 已登记但尚未同步」的结论;3 秒内有界兜底,不允许提示条永久停在「正在定位生成的素材…」。
|
||||
- 面板形态:独立浮层(`ThemedModal`),**不在当前面板下面追加内容**;面板内不写功能说明或规则解释文案。**点「生成」即同步关闭面板**(不等 IPC、不等排队、不等生成),面板里**不出现**「排队中。」「正在生成。」「提交中…」这类阶段文案——阶段文案的唯一去处是任务侧栏与工具栏提示条。**只有「点击瞬间就失败」**(校验不过、权限拒绝、start IPC 立即报错)才自动重开面板并带回草稿与原因;**受理之后才失败**只在侧栏把该任务收口为失败 + 原因,不重开面板。关闭 ≠ 取消请求(请求挂在任务与账本上,不挂在面板生命周期上)。
|
||||
- 参数口径:比例 / 尺寸选项来自网页端美术画布的纯模型(`src/components/image-editor/ImageCanvasGenerationModel.ts`),并按本地 IPC 白名单收窄(本地通道明确拒绝 `4:3`);默认档 `1:1 · 1K`,生成 UI 设计图沿用网页端 UI 设计面板的默认 `16:9 · 1K`。本地 IPC 没有 `model` 入参,因此面板**不渲染模型选择器**(渲染一个改不了请求的控件就是假控件)。
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
# 决策记录
|
||||
|
||||
## 2026-10-03 AGC 栏目画布上传素材按入口栏目登记(Issue 359)
|
||||
|
||||
- 背景:AGC 客户端在资源栏目子画布(「UI 交互 / 角色与对象 / 场景与环境 / 音频」)左下角工具栏点「上传」后,提示条给出「已上传 1 个素材」,但当前栏目计数不变(仍「0 项」)、素材出现在「待归类」,用户看到的是"上传成功了但它从这一页消失了"。原因是上传登记的 manifest `kind` 只由**内容证据**推导(`assets.rs::uploaded_asset_kind`:图片 / 视频 / 代码 → `unclassified`,音频 → `audio`,文档 / 字体 → `document`),kind 派生分类与栏目词汇(`ui-interaction` / `character` / `scene` / `audio`)不是同一套,而 `upload_local_asset` 原先不接受入口栏目。
|
||||
- 决策:`upload_local_asset` 增加可选 `targetCategory`,Rust 走既有的 `register_local_asset_entry_with_category`(与生成入口 `start_local_project_asset_generation` 的 `targetCategory` **同一口径**:GUI 完成登记以入口栏目为准);前端 `uploadProjectAssetFilesAndReadSnapshot` 透传该字段,栏目画布工具栏上传取工具栏自己的栏目(`resourceCanvasBottomToolbarCategory`)。取值只接受共享分类枚举,非法值由原生失败关闭,前端不做伪分类。
|
||||
- 边界:不传 `targetCategory` 时保持既有 kind 派生行为——资源面板(`ResourceCanvasPanelView`,跨栏目列表而不是栏目工具)、UI 编辑器图片导入(`import_ui_editor_local_files`)、聊天附件上传都不变。`upload_local_asset_at` 签名保持不变(委托到新的 `upload_local_asset_at_with_category`),既有约 25 处调用点与 Rust 单测零改动。
|
||||
- 影响范围:`apps/ai-game-creator-shell/src-tauri/src/{assets.rs,commands/desktop.rs}`、`apps/ai-game-creator-shell/src/view/project-development/{projectResourceLiveUpdateModel.ts,index.tsx}`、`apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts`、PRD §3.10、`docs/technical/【AGC】栏目画布底部工具栏入口矩阵-2026-09-13.md`(§4 载荷表)、`docs/technical/【测试用例】AGC资源工作台V3端到端验收-2026-09-11.md`(S11a)、`pitfalls.md`。
|
||||
- 验证:`cargo test --locked --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml --features=cocos-editor-execute,unity-editor-execute,godot-editor-execute --bin genarrative-ai-game-creator-shell assets::tests`(新增 `upload_registers_into_the_explicit_entry_category`:显式栏目 → `character`、不传 → `unclassified`、非法 `version` 失败关闭且不新增登记);`npx vitest run apps/ai-game-creator-shell/tests/appSurface.test.ts`(196 passed / 9 skipped,含新增「uploads toolbar files into the entry column so they stay visible where they were uploaded」断言工具栏上传载荷带 `targetCategory: 'character'`);`npm run agc:typecheck`、`npm run check:encoding`、`git diff --check`。
|
||||
|
||||
## 2026-10-03 生成绑定不再经 prettier:ts-rs 原始输出即提交形态
|
||||
|
||||
- 背景:`scripts/check-generated-bindings.mjs` 对 AGC 的 `chat/generated` / `services/generated` 在重生成后会就地跑一遍 `npx prettier --write` 再比较。这会直接改写生成文件,还把「Rust 声明真的变了」与「prettier 版本 / 配置造成的格式漂移」混在同一条告警里——本次报出的 `ThreadRequestKind.ts` / `TurnCompletedStatus.ts`「内容变化」无法复现为语义变化(已提交内容与当前 Rust 枚举一致),prettier 归一化把格式差异也报成了「与 Rust 声明不一致」;生成物被仓库格式化工具二次改写后,重跑 `cargo test export_bindings` 也不再幂等。
|
||||
|
||||
@@ -2,6 +2,14 @@
|
||||
|
||||
这里只记录对当前开发仍有用的症状、根因、排查方法和风险边界。同一事实保留一个当前口径;退役对象的专属过程与单轮测试结果由 Git 历史追溯。遇到旧路径或版本时,以现行代码和专题文档为准。
|
||||
|
||||
## 2026-10-03 AGC 栏目画布上传素材落「待归类」:kind 派生分类不等于入口栏目
|
||||
|
||||
- **现象**(Issue 359):在 AGC 资源栏目子画布(如「UI 交互」「角色与对象」)左下角工具栏点「上传」选图片 / 视频 / 代码类文件,提示条给出「已上传 1 个素材」,但当前栏目计数纹丝不动(仍「0 项」),素材出现在「待归类」。用户看到的是"上传成功了,可它就消失在这个页面里"。
|
||||
- **原因**:上传登记的 manifest `kind` 只由**内容证据**推导(`assets.rs::uploaded_asset_kind`:图片 / 视频 / 代码 → `unclassified`,音频 → `audio`,文档 / 字体 → `document`),kind 派生分类与栏目词汇(`ui-interaction` / `character` / `scene` / `audio`)不是同一套;`upload_local_asset` 原先不接受入口栏目,GUI 工具栏上传只能落 kind 派生分类。
|
||||
- **处理(现行口径)**:`upload_local_asset` 增加可选 `targetCategory`,Rust 走既有的 `register_local_asset_entry_with_category`(非法值失败关闭,不回退 kind 派生);栏目画布工具栏上传时取工具栏自己的栏目(`resourceCanvasBottomToolbarCategory`)。生成入口 `start_local_project_asset_generation` 的 `targetCategory` 是同一口径——GUI 完成登记以入口栏目为准。
|
||||
- **判据/取证**:`cargo test --locked ... --bin genarrative-ai-game-creator-shell upload_` 的 `assets::tests::upload_registers_into_the_explicit_entry_category`(显式栏目 → `character`;不传 → `unclassified`;非法 `version` 失败关闭且不新增登记);appSurface「uploads toolbar files into the entry column so they stay visible where they were uploaded」断言工具栏上传载荷带 `targetCategory: 'character'`。
|
||||
- **边界**:资源面板(跨栏目列表)、UI 编辑器图片导入、聊天附件上传都**不带**入口栏目,保持 kind 派生。任何新增的「某个栏目里的上传入口」都必须显式带上当前栏目,否则又会复现本坑。
|
||||
|
||||
## 2026-10-01 用户输错一次密码被当成"客户端出问题了"引导上报
|
||||
|
||||
- **现象**:登录页密码输错(或密码长度不合规)后弹出「发现问题」,报告面板「错误事件(2)」列出 `密码长度需要在 6 到 128 位之间 — auth · 1 次` 与 `手机号或密码错误 — auth · 1 次`,默认全选,与 react-render / 5xx / agent-runtime 终态失败视觉等价。
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
## 1. 一句话
|
||||
|
||||
功能画布 = 资源栏目页;栏目页左下角渲染一条由栏目 `category` 决定的工具栏,图片类与音频入口都走本地 `start_local_project_asset_generation`(提交即返回、后台生成,见 2026-09-20 音频并入后台任务账本),上传复用 `upload_local_asset`;生成 / 上传成功后一律走既有 manifest 刷新与资源定位。
|
||||
功能画布 = 资源栏目页;栏目页左下角渲染一条由栏目 `category` 决定的工具栏,图片类与音频入口都走本地 `start_local_project_asset_generation`(提交即返回、后台生成,见 2026-09-20 音频并入后台任务账本),上传复用 `upload_local_asset`(带当前栏目 `targetCategory`,素材登记进上传它的那一栏);生成 / 上传成功后一律走既有 manifest 刷新与资源定位。
|
||||
|
||||
## 2. 入口矩阵(事实源)
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
| 生成 UI 设计图 | 同上 | `kind: 'ui-design'`,默认 `16:9 · 1K`;前置同上 |
|
||||
| 生成背景音乐 | `start_local_project_asset_generation` | `kind: 'background-music'`、`idempotencyKey`;任务 id 即该次生成的 operation id |
|
||||
| 生成音效 | `start_local_project_asset_generation` | `kind: 'sound-effect'`、其余同上 |
|
||||
| 上传 | `upload_local_asset` | `{ projectPath, fileName, mediaType, bytes }` |
|
||||
| 上传 | `upload_local_asset` | `{ projectPath, fileName, mediaType, bytes, targetCategory }`;`targetCategory` = 当前栏目(Issue 359) |
|
||||
|
||||
`start_local_project_asset_generation` 的完整参数是 `{ projectPath, projectId, taskId, kind, prompt, aspectRatio, imageSize, assetName, outputPath, idempotencyKey }`(Rust `src-tauri/src/asset_generation_tasks.rs`);图片类入口沿用 `{ projectPath, projectId, taskId, kind, prompt, aspectRatio, imageSize, assetName, outputPath }` 逐字不变,音频入口只带 `{ projectPath, projectId, taskId, kind, prompt, assetName, idempotencyKey }`(`idempotencyKey` = 该次生成的幂等键,任务 id 即 operation id;原生命令内部仍复用既有音频无源生成链路,不新增平台路由与请求体口径),**提交即返回**一条任务记录(`taskId / status / phaseDetail / assetId / error` 等);生成在 `tauri::async_runtime::spawn` 出来的后台任务里跑,账本落在项目内 `.agent/runtime/asset-generation-tasks/tasks.json`,进度由 `list_local_project_asset_generations` 读回。截图面板不再「等生成结束」,所以**点「生成」即同步关闭面板**(不等 IPC),面板内不出现阶段文案;只有「点击瞬间就失败」才带草稿重开(关闭 ≠ 取消)。
|
||||
|
||||
|
||||
@@ -329,7 +329,7 @@ Rust 侧在 `server-rs/crates/shared-contracts` 维护唯一权威 `GameCreation
|
||||
- 参考选择范围为同一项目已登记图片,可跨栏目、多选,无规范前置时最多 5 张,有规范前置时最多 4 张用户参考(总计最多 5 张);复用资源引用选择组件,不允许文档、音视频、占位或跨项目素材。本地生成命令补最小引用 ID 参数并转换为当前账号绑定下的远端资源 ID,沿用图片生成 API 已有 `referenceImageSrcs`。需要规范图的普通图片请求合并并去重规范引用,总数不超过现有 API 限制;只接受单规范引用的图集操作不显示用户参考选择器,原生提交拒绝额外参考而非静默丢弃。不得降级成纯提示词。
|
||||
- 占位由宿主按项目与独立草稿 ID 管理,提交后关联任务 ID;失败重试使用同一占位。切项目清理未提交草稿与界面位置,已提交任务继续沿用账本恢复,重开后不承诺恢复未持久化的占位位置。迟到结果先核对项目和任务归属;只有本会话仍存在的占位才应用最新位置。删除占位只隐藏展示,不取消后台任务或丢弃正式结果。
|
||||
- 参考必须是原生可解码的栅格图片,SVG 不进入参考候选;原生在上传任何引用前预校验整组素材的归属、受控路径、文件及解码,失败不静默丢图。需要重新上传当前账号绑定的参考遵循 `asset.upload` 权限。manifest 读侧的引用形状检查不证明远端账号归属,实际生成始终通过当前账号 binding 解析,不凭历史来源 ID 发起请求。
|
||||
- 图片类 GUI 生成通过可选 `targetCategory` 在原生登记时写入入口栏目,使用既有 manifest `category` 字段及合法分类词表;不传时保留按 kind 派生的行为,Agent 不传。该值不改变远端生成内容及计费幂等槽,仅决定本地生成结果分类;重试保持原占位栏目。同路径重新生成时,主产物按本次入口栏目更新分类(包含覆盖此前手动分类),图集附属切片保持既有独立分类规则。
|
||||
- 图片类 GUI 生成通过可选 `targetCategory` 在原生登记时写入入口栏目,使用既有 manifest `category` 字段及合法分类词表;不传时保留按 kind 派生的行为,Agent 不传。该值不改变远端生成内容及计费幂等槽,仅决定本地生成结果分类;重试保持原占位栏目。同路径重新生成时,主产物按本次入口栏目更新分类(包含覆盖此前手动分类),图集附属切片保持既有独立分类规则。GUI 上传(栏目画布底部工具栏的 `upload_local_asset`)与图片类生成**同一口径**:带可选 `targetCategory` 在当前栏目登记,不传(资源面板、UI 编辑器导入、聊天附件)时保持按内容证据派生 kind 的行为。
|
||||
- 生成面板打开后,占位和面板需处于当前画布标题栏与底部工具栏之间;面板复用公共外观,空间不足时面板内部滚动,提交按钮可达。音频/BGM 占位绑定原有 operation/idempotency 身份,进行中不允许换身份重复提交;失败可用原身份重试,成功结果与图片一样接管占位。关闭未提交浮层保留可继续编辑的草稿,删除占位才丢弃该草稿。
|
||||
- 整理范围为当前栏目页全部资源;“所有资源”页为当前项目所有可展示资源,总览不新增整理行为。重排结果成为自动坐标,可撤销恢复原坐标与手动标记;历史仅保留当前会话,切项目清空。多选仅作用于当前画布可见选中资源,不携带筛选隐藏或跨栏目残留选择;取消手势恢复拖动前坐标,切项目清空选择。
|
||||
- 当前素材名以现有正式命名链路为准:生成时 assetName 参与落盘名称,重命名更新文件名;卡片消费正式资源 label,不从临时输入或历史任务名覆盖后续重命名,不新增平行显示名持久化。若原有命名链路丢失 assetName,则修复原链路,而非只在卡片本地伪造。文档卡不显示任何正文摘要,但详情原文与 JSON 识别读取不变。
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user