修复素材画布保存区与项目状态展示
调整素材保存区响应式布局,固定资源用途并保留精修继承语义。 收口普通用户项目路径展示,补充画布与工作台回归测试。 同步更新技术方案、产品需求文档与项目长期记忆。
This commit is contained in:
@@ -1212,6 +1212,18 @@ export function projectNameFromPath(projectPath: string) {
|
||||
);
|
||||
}
|
||||
|
||||
export function projectWorkspaceStatusForDisplay(workspaceStatus: string) {
|
||||
const openedPrefix = '已打开:';
|
||||
if (!workspaceStatus.startsWith(openedPrefix)) {
|
||||
return workspaceStatus;
|
||||
}
|
||||
const projectPath = workspaceStatus.slice(openedPrefix.length).trim();
|
||||
if (!/[\\/]/u.test(projectPath)) {
|
||||
return workspaceStatus;
|
||||
}
|
||||
return `${openedPrefix}${projectNameFromPath(projectPath)}`;
|
||||
}
|
||||
|
||||
export function mergeProjectSupervisorConversation(
|
||||
projectRecords: LocalConversationMessageRecord[],
|
||||
supervisorRecords: LocalConversationMessageRecord[],
|
||||
|
||||
@@ -106,6 +106,13 @@ export type AssetCanvasCommitNotification = {
|
||||
eventId?: string;
|
||||
};
|
||||
|
||||
export const ASSET_CANVAS_KIND_OPTIONS = [
|
||||
{ value: 'game-art', label: '普通游戏美术' },
|
||||
{ value: 'icon-spec', label: '统一视觉规范' },
|
||||
{ value: 'ui-prototype', label: '游戏界面原型' },
|
||||
{ value: 'art-spritesheet', label: '核心美术图集' },
|
||||
] as const;
|
||||
|
||||
type RuntimeCanvasLayer = CanvasLayer & { mediaRef: ImageCanvasMediaRef };
|
||||
|
||||
type PendingGenerationIdentity = {
|
||||
@@ -303,6 +310,8 @@ export function AssetCanvasSurface({
|
||||
scope,
|
||||
sessionId,
|
||||
expectedHostRevision,
|
||||
initialAssetName = '画布素材',
|
||||
initialAssetKind = 'game-art',
|
||||
onCancel,
|
||||
onCommitted,
|
||||
onSaveAttempt,
|
||||
@@ -312,11 +321,15 @@ export function AssetCanvasSurface({
|
||||
scope: ImageCanvasHostScope;
|
||||
sessionId: string;
|
||||
expectedHostRevision: string;
|
||||
initialAssetName?: string;
|
||||
initialAssetKind?: string;
|
||||
onCancel?: () => void;
|
||||
onCommitted?: (input: AssetCanvasCommitNotification) => void;
|
||||
onSaveAttempt?: (input: AssetCanvasSaveAttempt) => void;
|
||||
renderImage?: RenderAssetCanvasImage;
|
||||
}) {
|
||||
const resolvedInitialAssetName = initialAssetName.trim() || '画布素材';
|
||||
const resolvedInitialAssetKind = initialAssetKind.trim() || 'game-art';
|
||||
const stableScope = useMemo<ImageCanvasHostScope>(
|
||||
() => ({
|
||||
projectId: scope.projectId,
|
||||
@@ -339,8 +352,8 @@ export function AssetCanvasSurface({
|
||||
const [backgroundColor, setBackgroundColor] = useState('#f4f4f5');
|
||||
const [selectedLayerIds, setSelectedLayerIds] = useState<string[]>([]);
|
||||
const [notice, setNotice] = useState('');
|
||||
const [assetName, setAssetName] = useState('画布素材');
|
||||
const [assetKind, setAssetKind] = useState('asset');
|
||||
const [assetName, setAssetName] = useState(resolvedInitialAssetName);
|
||||
const [assetKind, setAssetKind] = useState(resolvedInitialAssetKind);
|
||||
const [exportMediaType, setExportMediaType] = useState<
|
||||
'image/png' | 'image/jpeg' | 'image/webp'
|
||||
>('image/png');
|
||||
@@ -385,6 +398,12 @@ export function AssetCanvasSurface({
|
||||
const onWalletBalanceMayHaveChanged = useWalletStore(
|
||||
(state) => state.onWalletBalanceMayHaveChanged,
|
||||
);
|
||||
const assetSettingsDisabled =
|
||||
lifecycle.kind === 'canvas.saving' ||
|
||||
lifecycle.kind === 'canvas.generating';
|
||||
const inheritedUnknownAssetKind =
|
||||
stableScope.intent === 'refine' &&
|
||||
!ASSET_CANVAS_KIND_OPTIONS.some((option) => option.value === assetKind);
|
||||
|
||||
layersRef.current = layers;
|
||||
viewportRef.current = viewport;
|
||||
@@ -1425,33 +1444,56 @@ export function AssetCanvasSurface({
|
||||
className="asset-canvas-surface__savebar"
|
||||
aria-label="素材保存设置"
|
||||
>
|
||||
<input
|
||||
aria-label="素材名称"
|
||||
value={assetName}
|
||||
maxLength={80}
|
||||
onChange={(event) => setAssetName(event.target.value)}
|
||||
disabled={lifecycle.kind === 'canvas.generating'}
|
||||
/>
|
||||
<input
|
||||
aria-label="素材类型"
|
||||
value={assetKind}
|
||||
maxLength={64}
|
||||
onChange={(event) => setAssetKind(event.target.value)}
|
||||
disabled={lifecycle.kind === 'canvas.generating'}
|
||||
/>
|
||||
<select
|
||||
aria-label="导出格式"
|
||||
value={exportMediaType}
|
||||
onChange={(event) =>
|
||||
setExportMediaType(
|
||||
event.target.value as 'image/png' | 'image/jpeg' | 'image/webp',
|
||||
)
|
||||
}
|
||||
>
|
||||
<option value="image/png">PNG</option>
|
||||
<option value="image/jpeg">JPEG</option>
|
||||
<option value="image/webp">WebP</option>
|
||||
</select>
|
||||
<label className="asset-canvas-surface__save-field">
|
||||
<span>素材名称</span>
|
||||
<input
|
||||
aria-label="素材名称"
|
||||
value={assetName}
|
||||
maxLength={80}
|
||||
onChange={(event) => setAssetName(event.target.value)}
|
||||
disabled={assetSettingsDisabled}
|
||||
/>
|
||||
</label>
|
||||
<label className="asset-canvas-surface__save-field">
|
||||
<span>资源用途</span>
|
||||
<select
|
||||
aria-label="资源用途"
|
||||
value={assetKind}
|
||||
onChange={(event) => setAssetKind(event.target.value)}
|
||||
disabled={
|
||||
assetSettingsDisabled || stableScope.intent === 'refine'
|
||||
}
|
||||
>
|
||||
{inheritedUnknownAssetKind ? (
|
||||
<option value={assetKind}>原资源用途</option>
|
||||
) : null}
|
||||
{ASSET_CANVAS_KIND_OPTIONS.map((option) => (
|
||||
<option key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<label className="asset-canvas-surface__save-field">
|
||||
<span>导出格式</span>
|
||||
<select
|
||||
aria-label="导出格式"
|
||||
value={exportMediaType}
|
||||
disabled={assetSettingsDisabled}
|
||||
onChange={(event) =>
|
||||
setExportMediaType(
|
||||
event.target.value as
|
||||
| 'image/png'
|
||||
| 'image/jpeg'
|
||||
| 'image/webp',
|
||||
)
|
||||
}
|
||||
>
|
||||
<option value="image/png">PNG</option>
|
||||
<option value="image/jpeg">JPEG</option>
|
||||
<option value="image/webp">WebP</option>
|
||||
</select>
|
||||
</label>
|
||||
<CanvasChromeButton
|
||||
label="保存到项目"
|
||||
icon={<Save size={15} aria-hidden="true" />}
|
||||
@@ -1459,7 +1501,8 @@ export function AssetCanvasSurface({
|
||||
onClick={() => void saveAsset()}
|
||||
disabled={
|
||||
lifecycle.kind === 'canvas.saving' ||
|
||||
lifecycle.kind === 'canvas.generating'
|
||||
lifecycle.kind === 'canvas.generating' ||
|
||||
assetName.trim().length === 0
|
||||
}
|
||||
>
|
||||
保存到项目
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
container-type: inline-size;
|
||||
overflow: hidden;
|
||||
border-radius: inherit;
|
||||
background: var(--platform-overlay-fill);
|
||||
@@ -24,6 +25,8 @@
|
||||
.asset-canvas-surface__toolbar-shell {
|
||||
z-index: 4;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
grid-template-rows: auto auto;
|
||||
min-width: 0;
|
||||
gap: 8px;
|
||||
padding: 8px 10px 9px;
|
||||
@@ -40,16 +43,33 @@
|
||||
.asset-canvas-surface__savebar {
|
||||
display: grid;
|
||||
grid-template-columns:
|
||||
minmax(108px, 1.35fr) minmax(88px, 0.8fr) minmax(70px, auto)
|
||||
auto;
|
||||
minmax(0, 1.35fr) minmax(0, 1fr) minmax(86px, 0.55fr)
|
||||
max-content;
|
||||
min-width: 0;
|
||||
align-items: center;
|
||||
align-items: end;
|
||||
gap: 7px;
|
||||
}
|
||||
|
||||
.asset-canvas-surface__save-field {
|
||||
display: grid;
|
||||
min-width: 0;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.asset-canvas-surface__save-field > span {
|
||||
overflow: hidden;
|
||||
color: var(--platform-text-muted);
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.asset-canvas-surface__savebar input,
|
||||
.asset-canvas-surface__savebar select,
|
||||
.asset-canvas-surface__generation-form select {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
min-height: 34px;
|
||||
border: 1px solid var(--platform-surface-border);
|
||||
@@ -87,6 +107,13 @@
|
||||
box-shadow: var(--platform-nav-active-shadow);
|
||||
}
|
||||
|
||||
.asset-canvas-surface .asset-canvas-surface__save {
|
||||
min-width: max-content;
|
||||
min-height: 34px;
|
||||
align-self: end;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.asset-canvas-surface
|
||||
.asset-canvas-surface__primary-action:not(:disabled):hover,
|
||||
.asset-canvas-surface .asset-canvas-surface__save:not(:disabled):hover {
|
||||
@@ -367,7 +394,11 @@
|
||||
}
|
||||
|
||||
.asset-canvas-surface__savebar {
|
||||
grid-template-columns: minmax(96px, 1fr) minmax(72px, 0.75fr) 70px;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.asset-canvas-surface__save-field:first-child {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.asset-canvas-surface__save {
|
||||
@@ -395,3 +426,15 @@
|
||||
padding: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
@container (max-width: 620px) {
|
||||
.asset-canvas-surface__savebar {
|
||||
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.asset-canvas-surface__save-field:first-child,
|
||||
.asset-canvas-surface__save {
|
||||
grid-column: 1 / -1;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,10 @@ import type {
|
||||
ChatMessage,
|
||||
PendingUiConfirmation,
|
||||
} from '../../app/types';
|
||||
import { ProjectSupervisorRuntimePanel } from '../agent-runtime';
|
||||
import {
|
||||
ProjectSupervisorRuntimePanel,
|
||||
projectWorkspaceStatusForDisplay,
|
||||
} from '../agent-runtime';
|
||||
import { formatAgentCardRuntimeStatus } from '../project-summary/agentPresentation';
|
||||
import { taskStatusLabels } from '../project-summary/projectSummary';
|
||||
|
||||
@@ -127,7 +130,7 @@ export function ProjectSupervisorView({
|
||||
</button>
|
||||
</form>
|
||||
<small className="project-supervisor-workspace-status">
|
||||
{workspaceStatus}
|
||||
{projectWorkspaceStatusForDisplay(workspaceStatus)}
|
||||
</small>
|
||||
</div>
|
||||
<aside
|
||||
|
||||
+2
-4
@@ -32,6 +32,7 @@ import {
|
||||
formatAgentRuntimeTaskQueue,
|
||||
ProjectSupervisorRuntimePanel,
|
||||
projectSupervisorVisibleConversationText,
|
||||
projectWorkspaceStatusForDisplay,
|
||||
} from '../agent-runtime';
|
||||
import {
|
||||
formatAgentCardLlmStatus,
|
||||
@@ -283,15 +284,12 @@ export function ProjectWorkspaceChatPane({
|
||||
<div className="chat-header-title">
|
||||
<h1>AI 游戏创作</h1>
|
||||
<span>{devMode ? '开发模式' : currentProjectTitle}</span>
|
||||
{!devMode && localProject ? (
|
||||
<small>{localProject.projectPath}</small>
|
||||
) : null}
|
||||
<small>run: {agentRunStatus}</small>
|
||||
<small>preview: {previewStatus}</small>
|
||||
{mainProjectSummary ? (
|
||||
<small aria-label="项目摘要">{mainProjectSummary}</small>
|
||||
) : null}
|
||||
<small>{workspaceStatus}</small>
|
||||
<small>{projectWorkspaceStatusForDisplay(workspaceStatus)}</small>
|
||||
</div>
|
||||
<div className="chat-header-actions">
|
||||
<button type="button" onClick={showChatHelp}>
|
||||
|
||||
+2
-1
@@ -35,6 +35,7 @@ import {
|
||||
projectSupervisorCollaboratingAgentRuntimes,
|
||||
ProjectSupervisorRuntimeControls,
|
||||
projectSupervisorVisibleConversationText,
|
||||
projectWorkspaceStatusForDisplay,
|
||||
} from '../agent-runtime';
|
||||
import { RuntimeConfigDialog } from '../runtime-config/RuntimeConfigDialog';
|
||||
import { GameChatImageViewer } from './GameChatImageViewer';
|
||||
@@ -891,7 +892,7 @@ export function SupervisorChatOnlyView({
|
||||
? '已投递,正在同步 Agent Runner'
|
||||
: runtime
|
||||
? projectSupervisorChatRuntimeStatus(runtime)
|
||||
: workspaceStatus);
|
||||
: projectWorkspaceStatusForDisplay(workspaceStatus));
|
||||
const headerStatus = gameChatMode ? status : previewStatus || status;
|
||||
const runtimeEvents = useMemo(
|
||||
() => collectGameChatRuntimeEvents(runtime, runtimeByAgentId),
|
||||
|
||||
@@ -90,6 +90,8 @@ type AssetCanvasRoute = {
|
||||
flowId: string;
|
||||
sessionId: string;
|
||||
expectedHostRevision: string;
|
||||
initialAssetName: string;
|
||||
initialAssetKind: string;
|
||||
scope: ImageCanvasHostScope;
|
||||
host: TauriImageCanvasHostAdapter;
|
||||
};
|
||||
@@ -239,6 +241,11 @@ function isRasterImageResource(resource: ProjectResource) {
|
||||
);
|
||||
}
|
||||
|
||||
function assetNameFromLocalPath(localPath: string) {
|
||||
const fileName = localPath.split(/[\\/]/u).filter(Boolean).pop() ?? '';
|
||||
return fileName.replace(/\.[^.]+$/u, '').trim() || '画布素材';
|
||||
}
|
||||
|
||||
function isExtendedArtMediaResource(resource: ProjectResource) {
|
||||
const mediaType = resource.mediaType.toLowerCase();
|
||||
return (
|
||||
@@ -1173,6 +1180,9 @@ export default function ProjectDevelopmentView({
|
||||
pendingResourceFocusRef.current = null;
|
||||
setHiddenCommittedResourceId(null);
|
||||
setAssetCanvasNotice('正在打开素材创作画布…');
|
||||
const sourceAsset = sourceAssetId
|
||||
? manifest.assets.find((asset) => asset.id === sourceAssetId)
|
||||
: null;
|
||||
void invoke<{ revision: number }>('get_local_game_project_revision', {
|
||||
projectPath,
|
||||
})
|
||||
@@ -1197,6 +1207,14 @@ export default function ProjectDevelopmentView({
|
||||
flowId,
|
||||
sessionId,
|
||||
expectedHostRevision: String(status.revision),
|
||||
initialAssetName:
|
||||
intent === 'refine' && sourceAsset
|
||||
? assetNameFromLocalPath(sourceAsset.localPath)
|
||||
: '画布素材',
|
||||
initialAssetKind:
|
||||
intent === 'refine' && sourceAsset
|
||||
? sourceAsset.kind
|
||||
: 'game-art',
|
||||
scope,
|
||||
host: createTauriImageCanvasHostAdapter({
|
||||
projectPath,
|
||||
@@ -1215,7 +1233,7 @@ export default function ProjectDevelopmentView({
|
||||
}
|
||||
});
|
||||
},
|
||||
[advanceFocusGeneration, manifest.projectId, projectPath],
|
||||
[advanceFocusGeneration, manifest.assets, manifest.projectId, projectPath],
|
||||
);
|
||||
|
||||
const cancelAssetCanvas = useCallback(() => {
|
||||
@@ -1557,6 +1575,8 @@ export default function ProjectDevelopmentView({
|
||||
scope={assetCanvasRoute.scope}
|
||||
sessionId={assetCanvasRoute.sessionId}
|
||||
expectedHostRevision={assetCanvasRoute.expectedHostRevision}
|
||||
initialAssetName={assetCanvasRoute.initialAssetName}
|
||||
initialAssetKind={assetCanvasRoute.initialAssetKind}
|
||||
onCancel={cancelAssetCanvas}
|
||||
onSaveAttempt={handleAssetCanvasSaveAttempt}
|
||||
onCommitted={handleAssetCommitted}
|
||||
|
||||
@@ -15,9 +15,24 @@ import {
|
||||
projectSupervisorChatRuntimeStatus,
|
||||
projectSupervisorResponseStreamIdentity,
|
||||
projectSupervisorVisibleConversationText,
|
||||
projectWorkspaceStatusForDisplay,
|
||||
submitProjectSupervisorRuntimeTask,
|
||||
} from '../src/features/agent-runtime/model';
|
||||
|
||||
describe('普通用户工作区状态', () => {
|
||||
test('用项目名称替代 Unix 和 Windows 绝对路径', () => {
|
||||
expect(projectWorkspaceStatusForDisplay('已打开:/tmp/authorized-game')).toBe(
|
||||
'已打开:authorized-game',
|
||||
);
|
||||
expect(
|
||||
projectWorkspaceStatusForDisplay('已打开:C:\\projects\\canvas-game'),
|
||||
).toBe('已打开:canvas-game');
|
||||
expect(projectWorkspaceStatusForDisplay('正在读取项目')).toBe(
|
||||
'正在读取项目',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Game Chat stream identity and source', () => {
|
||||
test('response stream identity binds run, slot, and revision', () => {
|
||||
const base: AgentRuntimeResponseStream = {
|
||||
|
||||
@@ -1825,7 +1825,7 @@ export function registerAgentRuntimeCommandTests() {
|
||||
submitChat('/project /tmp/authorized-game');
|
||||
fireEvent.click(screen.getByRole('button', { name: '确认' }));
|
||||
expect(
|
||||
await screen.findByText('已打开:/tmp/authorized-game'),
|
||||
await screen.findByText('已打开:authorized-game'),
|
||||
).not.toBeNull();
|
||||
|
||||
submitChat('/memory typo');
|
||||
|
||||
@@ -5475,7 +5475,7 @@ export function registerDeveloperToolsTests() {
|
||||
submitChat('/project /tmp/authorized-game');
|
||||
fireEvent.click(screen.getByRole('button', { name: '确认' }));
|
||||
expect(
|
||||
await screen.findByText('已打开:/tmp/authorized-game'),
|
||||
await screen.findByText('已打开:authorized-game'),
|
||||
).not.toBeNull();
|
||||
|
||||
submitChat('/audit');
|
||||
@@ -5603,7 +5603,7 @@ export function registerDeveloperToolsTests() {
|
||||
submitChat('/project /tmp/authorized-game');
|
||||
fireEvent.click(screen.getByRole('button', { name: '确认' }));
|
||||
expect(
|
||||
await screen.findByText('已打开:/tmp/authorized-game'),
|
||||
await screen.findByText('已打开:authorized-game'),
|
||||
).not.toBeNull();
|
||||
invoke.mockClear();
|
||||
|
||||
@@ -5686,7 +5686,7 @@ export function registerDeveloperToolsTests() {
|
||||
submitChat('/project /tmp/authorized-game');
|
||||
fireEvent.click(screen.getByRole('button', { name: '确认' }));
|
||||
expect(
|
||||
await screen.findByText('已打开:/tmp/authorized-game'),
|
||||
await screen.findByText('已打开:authorized-game'),
|
||||
).not.toBeNull();
|
||||
invoke.mockClear();
|
||||
|
||||
@@ -5800,7 +5800,7 @@ export function registerDeveloperToolsTests() {
|
||||
submitChat('/project /tmp/authorized-game');
|
||||
fireEvent.click(screen.getByRole('button', { name: '确认' }));
|
||||
expect(
|
||||
await screen.findByText('已打开:/tmp/authorized-game'),
|
||||
await screen.findByText('已打开:authorized-game'),
|
||||
).not.toBeNull();
|
||||
invoke.mockClear();
|
||||
|
||||
@@ -5898,7 +5898,7 @@ export function registerDeveloperToolsTests() {
|
||||
submitChat('/project /tmp/authorized-game');
|
||||
fireEvent.click(screen.getByRole('button', { name: '确认' }));
|
||||
expect(
|
||||
await screen.findByText('已打开:/tmp/authorized-game'),
|
||||
await screen.findByText('已打开:authorized-game'),
|
||||
).not.toBeNull();
|
||||
|
||||
submitChat('/audit');
|
||||
@@ -5960,7 +5960,7 @@ export function registerDeveloperToolsTests() {
|
||||
submitChat('/project /tmp/authorized-game');
|
||||
fireEvent.click(screen.getByRole('button', { name: '确认' }));
|
||||
expect(
|
||||
await screen.findByText('已打开:/tmp/authorized-game'),
|
||||
await screen.findByText('已打开:authorized-game'),
|
||||
).not.toBeNull();
|
||||
|
||||
submitChat('/audit');
|
||||
@@ -6011,7 +6011,7 @@ export function registerDeveloperToolsTests() {
|
||||
submitChat('/project /tmp/authorized-game');
|
||||
fireEvent.click(screen.getByRole('button', { name: '确认' }));
|
||||
expect(
|
||||
await screen.findByText('已打开:/tmp/authorized-game'),
|
||||
await screen.findByText('已打开:authorized-game'),
|
||||
).not.toBeNull();
|
||||
|
||||
submitChat('/audit');
|
||||
|
||||
@@ -92,6 +92,7 @@ import {
|
||||
GameChatReleaseApp,
|
||||
WorkspaceLauncher,
|
||||
} from '../../src/App';
|
||||
import { projectNameFromPath } from '../../src/features/agent-runtime/model';
|
||||
import ProjectDevelopmentView from '../../src/view/project-development';
|
||||
|
||||
const testAuthUser: AuthUser = {
|
||||
@@ -714,7 +715,9 @@ function createProjectSupervisorRuntimeHarness({
|
||||
async function openMainProject(projectPath: string) {
|
||||
submitChat(`/project ${projectPath}`);
|
||||
fireEvent.click(screen.getByRole('button', { name: '确认' }));
|
||||
expect(await screen.findByText(`已打开:${projectPath}`)).not.toBeNull();
|
||||
expect(
|
||||
await screen.findByText(`已打开:${projectNameFromPath(projectPath)}`),
|
||||
).not.toBeNull();
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -1192,7 +1192,7 @@ export function registerCanvasAssetTests() {
|
||||
submitChat('/project /tmp/authorized-game');
|
||||
fireEvent.click(screen.getByRole('button', { name: '确认' }));
|
||||
expect(
|
||||
await screen.findByText('已打开:/tmp/authorized-game'),
|
||||
await screen.findByText('已打开:authorized-game'),
|
||||
).not.toBeNull();
|
||||
|
||||
submitChat(
|
||||
@@ -1717,7 +1717,7 @@ export function registerCanvasAssetTests() {
|
||||
submitChat('/project /tmp/authorized-game');
|
||||
fireEvent.click(screen.getByRole('button', { name: '确认' }));
|
||||
expect(
|
||||
await screen.findByText('已打开:/tmp/authorized-game'),
|
||||
await screen.findByText('已打开:authorized-game'),
|
||||
).not.toBeNull();
|
||||
expect(await screen.findByText(/还没有最近一次 Agent run/)).not.toBeNull();
|
||||
|
||||
@@ -1821,7 +1821,7 @@ export function registerCanvasAssetTests() {
|
||||
submitChat('/project /tmp/authorized-game');
|
||||
fireEvent.click(screen.getByRole('button', { name: '确认' }));
|
||||
expect(
|
||||
await screen.findByText('已打开:/tmp/authorized-game'),
|
||||
await screen.findByText('已打开:authorized-game'),
|
||||
).not.toBeNull();
|
||||
|
||||
submitChat('/canvas bad\u0007canvas');
|
||||
@@ -2003,7 +2003,7 @@ export function registerCanvasAssetTests() {
|
||||
submitChat('/project /tmp/authorized-game');
|
||||
fireEvent.click(screen.getByRole('button', { name: '确认' }));
|
||||
expect(
|
||||
await screen.findByText('已打开:/tmp/authorized-game'),
|
||||
await screen.findByText('已打开:authorized-game'),
|
||||
).not.toBeNull();
|
||||
|
||||
submitChat('/sync-canvas-project canvas-project-1');
|
||||
@@ -2090,7 +2090,7 @@ export function registerCanvasAssetTests() {
|
||||
submitChat('/project /tmp/authorized-game');
|
||||
fireEvent.click(screen.getByRole('button', { name: '确认' }));
|
||||
expect(
|
||||
await screen.findByText('已打开:/tmp/authorized-game'),
|
||||
await screen.findByText('已打开:authorized-game'),
|
||||
).not.toBeNull();
|
||||
|
||||
submitChat('/generate-art 月光厨房弹幕主角');
|
||||
|
||||
@@ -241,7 +241,7 @@ export function registerProjectCommandTests() {
|
||||
submitChat('/project /tmp/authorized-game');
|
||||
fireEvent.click(screen.getByRole('button', { name: '确认' }));
|
||||
expect(
|
||||
await screen.findByText('已打开:/tmp/authorized-game'),
|
||||
await screen.findByText('已打开:authorized-game'),
|
||||
).not.toBeNull();
|
||||
|
||||
submitChat('/audit');
|
||||
@@ -2202,7 +2202,7 @@ export function registerProjectCommandTests() {
|
||||
fireEvent.click(screen.getByRole('button', { name: '初始化' }));
|
||||
fireEvent.click(screen.getByRole('button', { name: '确认' }));
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('已打开:/tmp/authorized-game')).not.toBeNull();
|
||||
expect(screen.getByText('已打开:authorized-game')).not.toBeNull();
|
||||
});
|
||||
|
||||
fireEvent.change(screen.getByLabelText('本地项目目录'), {
|
||||
@@ -2771,7 +2771,7 @@ export function registerProjectCommandTests() {
|
||||
fireEvent.click(screen.getByRole('button', { name: '初始化' }));
|
||||
fireEvent.click(screen.getByRole('button', { name: '确认' }));
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('已打开:/tmp/authorized-game')).not.toBeNull();
|
||||
expect(screen.getByText('已打开:authorized-game')).not.toBeNull();
|
||||
});
|
||||
|
||||
fireEvent.change(screen.getByLabelText('本地项目目录'), {
|
||||
@@ -3245,7 +3245,7 @@ export function registerProjectCommandTests() {
|
||||
fireEvent.click(screen.getByRole('button', { name: '初始化' }));
|
||||
fireEvent.click(screen.getByRole('button', { name: '确认' }));
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('已打开:/tmp/authorized-game')).not.toBeNull();
|
||||
expect(screen.getByText('已打开:authorized-game')).not.toBeNull();
|
||||
});
|
||||
|
||||
fireEvent.change(screen.getByLabelText('本地项目目录'), {
|
||||
@@ -3333,7 +3333,7 @@ export function registerProjectCommandTests() {
|
||||
fireEvent.click(screen.getByRole('button', { name: '初始化' }));
|
||||
fireEvent.click(screen.getByRole('button', { name: '确认' }));
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('已打开:/tmp/authorized-game')).not.toBeNull();
|
||||
expect(screen.getByText('已打开:authorized-game')).not.toBeNull();
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '静态入口自检' }));
|
||||
@@ -3448,7 +3448,7 @@ export function registerProjectCommandTests() {
|
||||
fireEvent.click(screen.getByRole('button', { name: '初始化' }));
|
||||
fireEvent.click(screen.getByRole('button', { name: '确认' }));
|
||||
expect(
|
||||
await screen.findByText('已打开:/tmp/authorized-game'),
|
||||
await screen.findByText('已打开:authorized-game'),
|
||||
).not.toBeNull();
|
||||
|
||||
fireEvent.click(logPanel.getByRole('button', { name: '命令日志' }));
|
||||
|
||||
@@ -416,7 +416,7 @@ export function registerProjectConversationTests() {
|
||||
fireEvent.click(screen.getByRole('button', { name: '确认' }));
|
||||
|
||||
expect(
|
||||
await screen.findByText('已打开:/tmp/authorized-game'),
|
||||
await screen.findByText('已打开:authorized-game'),
|
||||
).not.toBeNull();
|
||||
await waitFor(() => {
|
||||
expect(invoke).toHaveBeenCalledWith('read_project_permission_policy', {
|
||||
@@ -509,7 +509,7 @@ export function registerProjectConversationTests() {
|
||||
fireEvent.click(screen.getByRole('button', { name: '确认' }));
|
||||
|
||||
expect(await screen.findByText('受保护历史需求')).not.toBeNull();
|
||||
expect(screen.getByText('已打开:/tmp/authorized-game')).not.toBeNull();
|
||||
expect(screen.getByText('已打开:authorized-game')).not.toBeNull();
|
||||
expect(invoke).toHaveBeenCalledWith('read_local_conversation', {
|
||||
projectPath: '/tmp/authorized-game',
|
||||
agentId: null,
|
||||
@@ -787,7 +787,7 @@ export function registerProjectConversationTests() {
|
||||
expect(
|
||||
screen.queryByText('项目对话保存失败:conversation append failed once'),
|
||||
).toBeNull();
|
||||
expect(screen.getByText('已打开:/tmp/authorized-game')).not.toBeNull();
|
||||
expect(screen.getByText('已打开:authorized-game')).not.toBeNull();
|
||||
});
|
||||
|
||||
it('confirms before saving slash conversation when policy requires it', async () => {
|
||||
@@ -955,7 +955,7 @@ export function registerProjectConversationTests() {
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('conversation.write')).toBeNull();
|
||||
});
|
||||
expect(screen.getByText('已打开:/tmp/authorized-game')).not.toBeNull();
|
||||
expect(screen.getByText('已打开:authorized-game')).not.toBeNull();
|
||||
expect(invoke).not.toHaveBeenCalledWith(
|
||||
'append_local_conversation_message',
|
||||
expect.anything(),
|
||||
@@ -1233,7 +1233,7 @@ export function registerProjectConversationTests() {
|
||||
submitChat('/project /tmp/authorized-game');
|
||||
fireEvent.click(screen.getByRole('button', { name: '确认' }));
|
||||
expect(
|
||||
await screen.findByText('已打开:/tmp/authorized-game'),
|
||||
await screen.findByText('已打开:authorized-game'),
|
||||
).not.toBeNull();
|
||||
|
||||
expect(await screen.findByText('正在拆解创作方向')).not.toBeNull();
|
||||
@@ -1714,7 +1714,7 @@ export function registerProjectConversationTests() {
|
||||
window.__TAURI__ = { core: { invoke } };
|
||||
renderAppAt('/?main&projectPath=%2Ftmp%2Fauthorized-game');
|
||||
|
||||
await screen.findByText('已打开:/tmp/authorized-game');
|
||||
await screen.findByText('已打开:authorized-game');
|
||||
fireEvent.click(screen.getByRole('button', { name: /拆解创作方向/ }));
|
||||
const dialog = await screen.findByLabelText('Agent 对话');
|
||||
expect(
|
||||
@@ -2109,7 +2109,7 @@ export function registerProjectConversationTests() {
|
||||
window.__TAURI__ = { core: { invoke } };
|
||||
renderAppAt('/?main&projectPath=%2Ftmp%2Fauthorized-game');
|
||||
|
||||
await screen.findByText('已打开:/tmp/authorized-game');
|
||||
await screen.findByText('已打开:authorized-game');
|
||||
fireEvent.click(screen.getByRole('button', { name: /拆解创作方向/ }));
|
||||
const input = await screen.findByLabelText('Agent 对话内容');
|
||||
invoke.mockClear();
|
||||
@@ -2212,7 +2212,8 @@ export function registerProjectConversationTests() {
|
||||
window.__TAURI__ = { core: { invoke } };
|
||||
renderAppAt('/?main&projectPath=%2Ftmp%2Fauthorized-game');
|
||||
|
||||
await screen.findByText('/tmp/authorized-game');
|
||||
await screen.findByText('已打开:authorized-game');
|
||||
expect(screen.queryByText('/tmp/authorized-game')).toBeNull();
|
||||
fireEvent.click(screen.getByRole('button', { name: /拆解创作方向/ }));
|
||||
|
||||
const agentDialog = await screen.findByLabelText('Agent 对话');
|
||||
@@ -3126,7 +3127,7 @@ export function registerProjectConversationTests() {
|
||||
fireEvent.click(
|
||||
within(createA as HTMLElement).getByRole('button', { name: '确认' }),
|
||||
);
|
||||
expect(await screen.findByText('已打开:/tmp/project-a')).not.toBeNull();
|
||||
expect(await screen.findByText('已打开:project-a')).not.toBeNull();
|
||||
await waitFor(() => {
|
||||
expect(invoke).toHaveBeenCalledWith(
|
||||
'resume_game_creator_agent_runtime_tasks',
|
||||
@@ -3144,7 +3145,7 @@ export function registerProjectConversationTests() {
|
||||
fireEvent.click(
|
||||
within(createB as HTMLElement).getByRole('button', { name: '确认' }),
|
||||
);
|
||||
expect(await screen.findByText('已打开:/tmp/project-b')).not.toBeNull();
|
||||
expect(await screen.findByText('已打开:project-b')).not.toBeNull();
|
||||
|
||||
await act(async () => {
|
||||
resolveProjectAResume?.([]);
|
||||
@@ -3160,6 +3161,6 @@ export function registerProjectConversationTests() {
|
||||
'read_game_creator_agent_runtimes',
|
||||
{ projectPath: '/tmp/project-a' },
|
||||
);
|
||||
expect(screen.getByText('已打开:/tmp/project-b')).not.toBeNull();
|
||||
expect(screen.getByText('已打开:project-b')).not.toBeNull();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -8063,7 +8063,7 @@ export function registerProjectWorkbenchNavigationTests() {
|
||||
window.__TAURI__ = { core: { invoke } };
|
||||
renderAppAt('/?main&projectPath=%2Ftmp%2Fauthorized-game');
|
||||
|
||||
await screen.findByText('已打开:/tmp/authorized-game');
|
||||
await screen.findByText('已打开:authorized-game');
|
||||
const revealButtons = screen.getAllByRole('button', { name: '显示目录' });
|
||||
fireEvent.click(revealButtons[revealButtons.length - 1]);
|
||||
|
||||
@@ -8143,7 +8143,7 @@ export function registerProjectWorkbenchNavigationTests() {
|
||||
renderAppAt('/?main&projectPath=%2Ftmp%2Fauthorized-game');
|
||||
});
|
||||
|
||||
await screen.findByText('已打开:/tmp/authorized-game');
|
||||
await screen.findByText('已打开:authorized-game');
|
||||
invoke.mockClear();
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '文件' }));
|
||||
@@ -9086,7 +9086,7 @@ export function registerProjectAgentStatusTests() {
|
||||
window.__TAURI__ = { core: { invoke } };
|
||||
renderAppAt('/?main&projectPath=%2Ftmp%2Fauthorized-game');
|
||||
|
||||
await screen.findByText('已打开:/tmp/authorized-game');
|
||||
await screen.findByText('已打开:authorized-game');
|
||||
fireEvent.click(screen.getByRole('button', { name: /拆解创作方向/ }));
|
||||
expect(await screen.findByLabelText('Agent 对话')).not.toBeNull();
|
||||
expect(screen.getByLabelText('Agent 最近证据').textContent).toContain(
|
||||
|
||||
+2
-2
@@ -4,9 +4,9 @@ import type { PreviewShortcutInvoke } from './invoke-mock';
|
||||
export async function assertProjectAndDesignShortcutFlow(
|
||||
invoke: PreviewShortcutInvoke,
|
||||
) {
|
||||
await screen.findByText('已打开:/tmp/authorized-game');
|
||||
await screen.findByText('已打开:authorized-game');
|
||||
expect(screen.getByText('未命名游戏原型')).not.toBeNull();
|
||||
expect(screen.getByText('/tmp/authorized-game')).not.toBeNull();
|
||||
expect(screen.queryByText('/tmp/authorized-game')).toBeNull();
|
||||
expect(screen.getByText('preview: 未启动')).not.toBeNull();
|
||||
const projectSummary = screen.getByLabelText('项目摘要');
|
||||
expect(projectSummary.textContent).toContain('任务:已完成 0/');
|
||||
|
||||
@@ -132,7 +132,12 @@ function memoryHost(input?: {
|
||||
let hostRevision = 0;
|
||||
let eventListener: ((event: LocalAssetCommittedEvent) => void) | null = null;
|
||||
const updates: ImageCanvasDraftCanvas[] = [];
|
||||
const commits: Array<{ commitId: string; idempotencyKey: string }> = [];
|
||||
const commits: Array<{
|
||||
commitId: string;
|
||||
idempotencyKey: string;
|
||||
assetName: string;
|
||||
assetKind: string;
|
||||
}> = [];
|
||||
const generationCalls: Array<{
|
||||
intentId: string;
|
||||
generationId: string;
|
||||
@@ -317,6 +322,8 @@ function memoryHost(input?: {
|
||||
commits.push({
|
||||
commitId: commitInput.commitId,
|
||||
idempotencyKey: commitInput.idempotencyKey,
|
||||
assetName: commitInput.name,
|
||||
assetKind: commitInput.assetKind,
|
||||
});
|
||||
await input?.commitGate?.promise;
|
||||
hostRevision += 1;
|
||||
@@ -372,6 +379,7 @@ function renderSurface(
|
||||
onCommitted = vi.fn(),
|
||||
onSaveAttempt = vi.fn(),
|
||||
onCancel = vi.fn(),
|
||||
initialAsset?: { name: string; kind: string },
|
||||
) {
|
||||
return {
|
||||
onCommitted,
|
||||
@@ -384,6 +392,8 @@ function renderSurface(
|
||||
scope={canvasScope}
|
||||
sessionId="22222222-2222-4222-8222-222222222222"
|
||||
expectedHostRevision="0"
|
||||
initialAssetName={initialAsset?.name}
|
||||
initialAssetKind={initialAsset?.kind}
|
||||
onCancel={onCancel}
|
||||
onCommitted={onCommitted}
|
||||
onSaveAttempt={onSaveAttempt}
|
||||
@@ -463,6 +473,78 @@ describe('Tauri 素材创作无限画布独立 Surface', () => {
|
||||
expect(screen.getByRole('button', { name: '取消' })).toBeTruthy();
|
||||
});
|
||||
|
||||
it('保留可编辑名称,并把新建资源用途限制为中文固定选项', async () => {
|
||||
const memory = memoryHost();
|
||||
renderSurface(memory.host);
|
||||
await screen.findByText('画布可编辑');
|
||||
|
||||
const assetName = screen.getByRole('textbox', { name: '素材名称' });
|
||||
const assetKind = screen.getByRole('combobox', { name: '资源用途' });
|
||||
expect((assetName as HTMLInputElement).value).toBe('画布素材');
|
||||
expect((assetKind as HTMLSelectElement).value).toBe('game-art');
|
||||
expect((assetKind as HTMLSelectElement).disabled).toBe(false);
|
||||
expect(
|
||||
Array.from((assetKind as HTMLSelectElement).options).map(
|
||||
(option) => option.text,
|
||||
),
|
||||
).toEqual([
|
||||
'普通游戏美术',
|
||||
'统一视觉规范',
|
||||
'游戏界面原型',
|
||||
'核心美术图集',
|
||||
]);
|
||||
|
||||
fireEvent.change(assetName, { target: { value: '主界面草图' } });
|
||||
fireEvent.change(assetKind, { target: { value: 'ui-prototype' } });
|
||||
fireEvent.click(screen.getByRole('button', { name: '保存到项目' }));
|
||||
|
||||
await waitFor(() => expect(memory.commits).toHaveLength(1));
|
||||
expect(memory.commits[0]).toEqual(
|
||||
expect.objectContaining({
|
||||
assetName: '主界面草图',
|
||||
assetKind: 'ui-prototype',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('精修时继承源资源名称和用途,并锁定机器语义类型', async () => {
|
||||
const refineScope: ImageCanvasHostScope = {
|
||||
...scope,
|
||||
intent: 'refine',
|
||||
sourceAssetId: 'source-asset',
|
||||
};
|
||||
const memory = memoryHost({
|
||||
initialDraft: draftFixture(refineScope),
|
||||
});
|
||||
renderSurface(
|
||||
memory.host,
|
||||
refineScope,
|
||||
vi.fn(),
|
||||
vi.fn(),
|
||||
vi.fn(),
|
||||
{ name: '角色立绘', kind: 'illustration' },
|
||||
);
|
||||
await screen.findByText('画布可编辑');
|
||||
|
||||
const assetName = screen.getByRole('textbox', { name: '素材名称' });
|
||||
const assetKind = screen.getByRole('combobox', { name: '资源用途' });
|
||||
expect((assetName as HTMLInputElement).value).toBe('角色立绘');
|
||||
expect((assetName as HTMLInputElement).disabled).toBe(false);
|
||||
expect((assetKind as HTMLSelectElement).value).toBe('illustration');
|
||||
expect((assetKind as HTMLSelectElement).disabled).toBe(true);
|
||||
expect(screen.getByRole('option', { name: '原资源用途' })).toBeTruthy();
|
||||
|
||||
fireEvent.change(assetName, { target: { value: '角色立绘精修' } });
|
||||
fireEvent.click(screen.getByRole('button', { name: '保存到项目' }));
|
||||
await waitFor(() => expect(memory.commits).toHaveLength(1));
|
||||
expect(memory.commits[0]).toEqual(
|
||||
expect.objectContaining({
|
||||
assetName: '角色立绘精修',
|
||||
assetKind: 'illustration',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('让生成、失败、保存与取消区域使用内部滚动而不被固定高度裁剪', () => {
|
||||
const css = readFileSync(
|
||||
resolve(
|
||||
@@ -487,9 +569,18 @@ describe('Tauri 素材创作无限画布独立 Surface', () => {
|
||||
expect(css).toMatch(
|
||||
/\.asset-canvas-surface__savebar\s*\{[^}]*grid-template-columns:/s,
|
||||
);
|
||||
expect(css).toMatch(
|
||||
/\.asset-canvas-surface__toolbar-shell\s*\{[^}]*grid-template-columns:\s*minmax\(0, 1fr\)[^}]*grid-template-rows:\s*auto auto/s,
|
||||
);
|
||||
expect(css).toMatch(
|
||||
/\.asset-canvas-surface \.asset-canvas-surface__save\s*\{[^}]*min-width:\s*max-content[^}]*white-space:\s*nowrap/s,
|
||||
);
|
||||
expect(css).toMatch(
|
||||
/@media \(max-width: 760px\)[\s\S]*?\.asset-canvas-surface__save\s*\{[^}]*grid-column:\s*1 \/ -1[^}]*width:\s*100%/s,
|
||||
);
|
||||
expect(css).toMatch(
|
||||
/@container \(max-width: 620px\)[\s\S]*?\.asset-canvas-surface__save\s*\{[^}]*grid-column:\s*1 \/ -1[^}]*width:\s*100%/s,
|
||||
);
|
||||
});
|
||||
|
||||
it('取消时先把草稿标记为 cancelled,再返回资源总览', async () => {
|
||||
|
||||
@@ -21,6 +21,8 @@ vi.mock('../src/features/asset-canvas/AssetCanvasSurface', async () => {
|
||||
sourceAssetId: string | null;
|
||||
};
|
||||
sessionId: string;
|
||||
initialAssetName?: string;
|
||||
initialAssetKind?: string;
|
||||
onCancel?: () => void;
|
||||
onSaveAttempt?: (attempt: {
|
||||
saveAttemptId: string;
|
||||
@@ -37,7 +39,7 @@ vi.mock('../src/features/asset-canvas/AssetCanvasSurface', async () => {
|
||||
ReactModule.createElement(
|
||||
'span',
|
||||
null,
|
||||
`${props.scope.intent}:${props.scope.sourceAssetId ?? 'none'}`,
|
||||
`${props.scope.intent}:${props.scope.sourceAssetId ?? 'none'}:${props.initialAssetName ?? 'unset'}:${props.initialAssetKind ?? 'unset'}`,
|
||||
),
|
||||
ReactModule.createElement(
|
||||
'button',
|
||||
@@ -275,7 +277,7 @@ describe('project resource live canvas integration', () => {
|
||||
fireEvent.click(screen.getByRole('button', { name: '精修资源' }));
|
||||
expect(
|
||||
(await screen.findByLabelText('测试素材创作画布')).textContent,
|
||||
).toContain('refine:source-art');
|
||||
).toContain('refine:source-art:source-art:art-image');
|
||||
fireEvent.click(screen.getByRole('button', { name: '取消并返回' }));
|
||||
expect(
|
||||
await screen.findByRole('region', { name: /source-art\.png/ }),
|
||||
|
||||
@@ -94,6 +94,7 @@
|
||||
- 客户端 UI 对齐采用“同视觉、同组件、保留四区布局”,不复制主站整页布局。资源总览、运行视图和 Supervisor 对话继续是 Game Agent 工作台独有语义。
|
||||
- 客户端正式产品仍只按 `1280×800` 横屏合同交付;更窄浏览器样式只负责不崩溃和开发兼容,不改成移动端创作工作台。
|
||||
- 普通用户界面不默认展示内部错误码、开发者 API 配置或本机绝对路径;确需诊断的信息进入受控详情或开发模式,不与主要动作并列。
|
||||
- 素材画布的“素材名称”是用户可编辑的正式输出名称;“资源用途”是 manifest subtype,不向普通用户开放自由文本。新增资源默认“普通游戏美术”,可从普通游戏美术、统一视觉规范、游戏界面原型、核心美术图集四项中选择;精修资源继承源用途且不可改。导出格式继续限定 PNG/JPEG/WebP。工具动作与保存设置分层展示,“保存到项目”在 `1280×800` 和窄容器中都必须完整可见。
|
||||
|
||||
## 4. 工作台状态机
|
||||
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
- 决策:保留 Game Agent 左导航、中央主视窗、右侧 Supervisor 和底部 Agent Dock 四区布局;平台颜色、边框、文本和 surface 继续以 `packages/shared/src/theme.css` 为事实源,画布动作按钮、工具栏、分组与分隔符进入现有 `@genarrative/image-canvas-react`。主站和 Tauri 均直接消费同一 chrome,Tauri 宿主只保留业务接线和布局适配。
|
||||
- 依赖边界:共享组件只接受 React props、图标节点、短文案和事件,不读取账号、钱包、项目、生成、Tauri 或 HTTP。主站 `EditorIconButton` 只保留 Lucide/特殊浮层薄适配,主站业务 CSS 只保留定位和专属状态;Tauri 禁止导入网站完整 CSS。内部错误码、开发者 API 配置和本机绝对路径不作为普通用户默认视觉内容。
|
||||
- 分期:阶段一、二更新权威合同、抽取共享 chrome 并让主站消费;阶段三、四已统一中央素材画布、Supervisor 与 Agent Dock;阶段五在同一变更中补齐测试、真实视口测量和发现回归修复。
|
||||
- 保存字段:`name` 属于用户输出命名,继续可编辑;`assetKind` 属于机器 subtype,create 只允许 Runtime 权威目录中的 `game-art / icon-spec / ui-prototype / art-spritesheet` 并显示中文用途,refine 继承源 kind 且锁定。未知历史 kind 只按原值保存,不在精修中迁移或改写。工具与保存设置显式分为两行,主保存动作不可压缩。
|
||||
- 路径展示:业务内部继续持有和校验绝对项目路径,普通工作区状态只投影项目名称;显式目录选择、非空目录确认和开发诊断仍可按权限边界使用真实路径。该变化不修改项目身份或 Tauri command 参数。
|
||||
- 验证:共享 chrome 与 Tauri Surface 定向测试 `20/20` 通过,覆盖可访问性、pressed/expanded/disabled、工具组/分隔符、生成/失败/保存/取消和非裁剪 CSS;AppSurface 横屏合同 `1/1` 通过,锁定 Supervisor composer、Agent Dock 与 `100dvh` 两行工作台。客户端/全仓 typecheck、定向 ESLint、编码和 diff 检查通过。完整 AppSurface 本次运行是 `352/354` 通过:preview 权限用例单跑通过,属于全量异步顺序波动;仍失败的“画板 API Key”运行配置用例不涉及本次 diff,留在原业务范围处理。应用内浏览器 `1280×800` 实测 window/document/body client 与 scroll 均为 `1280×800`;真实入口没有登录会话时保持登录门禁,不增加测试绕过。
|
||||
- 关联:`docs/prd/【AI游戏创作】项目开发工作台PRD-2026-07-20.md`、`docs/technical/【技术方案】客户端素材创作无限画布阶段一合同-2026-08-05.md`、`packages/image-canvas-react/`。
|
||||
|
||||
|
||||
@@ -38,6 +38,14 @@
|
||||
- 验证:AppSurface CSS 合同检查 `100dvh`、Supervisor composer、Runtime 内滚动、Dock 常驻/可收缩;素材画布测试检查生成 dialog、operation card、状态栏和窄屏保存动作不会被裁剪。真实入口在 `1280×800` 测量 document/body client 与 scroll 一致;登录门禁不可为视觉测试绕过。
|
||||
- 关联:`apps/ai-game-creator-shell/src/styles.css`、`apps/ai-game-creator-shell/src/features/asset-canvas/assetCanvasSurface.css`、`apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts`、`apps/ai-game-creator-shell/tests/assetCanvasSurface.test.tsx`。
|
||||
|
||||
## 素材保存区不要把机器 subtype 当普通文本框(2026-08-06)
|
||||
|
||||
- 现象:保存区同时显示“画布素材”和裸 `asset` 文本框,普通用户无法判断两者用途;自由修改 kind 会形成无法稳定参与类型布局、Agent 合同和替换兼容性的 subtype。工具栏与保存设置挤在同一行时,主要保存按钮还会被压缩或裁切。
|
||||
- 原因:把 Host Port 的 `name / assetKind / mediaType` DTO 直接映射成同层输入控件,没有区分用户命名、机器分类和编码格式,也没有为中央区域的真实容器宽度保留主操作列。
|
||||
- 处理:名称保留编辑;create kind 使用 Runtime 权威四项目录和中文标签,refine 从源 manifest 继承并锁定,未知历史值只透传;格式继续使用有限枚举。工具动作和保存设置显式上下分行,保存列使用 `max-content + nowrap`,窄容器时按钮独占整行。普通工作区状态只显示项目名称,不把绝对路径作为默认辅助文案。
|
||||
- 验证:Surface 测试断言四项用途、默认值、精修未知 kind 锁定、最终 commit 参数和保存按钮 CSS;AppSurface 断言普通界面找不到绝对路径,内部 Tauri 调用仍使用原完整路径。
|
||||
- 关联:`apps/ai-game-creator-shell/src/features/asset-canvas/AssetCanvasSurface.tsx`、`apps/ai-game-creator-shell/src/features/asset-canvas/assetCanvasSurface.css`、`apps/ai-game-creator-shell/src/features/project-workspace/`。
|
||||
|
||||
## 正式素材提交不能把多文件写入或 Tauri 事件误当成一次原子动作
|
||||
|
||||
- 现象:图片已经落到 `assets/` 但 manifest 没有资产,或 manifest 已追加而 project revision/草稿仍是旧值;进程在 emit 前后退出后,用户重试又得到第二份图片、第二个 asset 或重复选中。
|
||||
|
||||
@@ -968,3 +968,4 @@ game-project/
|
||||
- 工作台仍是左导航、中央主视窗、右 Project Supervisor、底部 Agent Dock 四区。中央区使用主面板层级;Supervisor 的消息/Runtime 独立滚动且 composer 常驻;Dock 改为较轻的持续状态 surface 与可收缩卡片。桌面网格按 `100dvh` 将 Dock 固定在第二行,`≤760px` 只保留开发兼容堆叠。
|
||||
- AppSurface/CSS 合同锁定 `1280×800` 下的工作台无页面级溢出、Supervisor composer、主要保存动作和 Dock 可见;共享/Tauri 定向用例锁定按钮名称、工具组/分隔符、pressed/expanded/disabled 和生成/失败/保存/取消非裁剪。真实 Vite 页面测得 window/document/body client 与 scroll 均为 `1280×800`;无登录会话时按既有安全合同停在登录门禁,不增加或使用测试绕过。
|
||||
- 本次只调整 UI 和测试/文档,未改变图片生成请求、登录态、泥点计费、钱包刷新、Host Port、Tauri command/event、草稿 CAS、manifest/revision、幂等恢复、Runtime、审批、资源投影或保存事务。
|
||||
- 2026-08-06 视觉复核进一步锁定保存区语义:素材名称保留用户编辑;create 的资源用途改为四项中文固定选择并默认普通游戏美术;refine 从源 manifest 继承并锁定 kind,避免普通用户直接编辑内部 `assetKind` slug。顶部工具与保存设置显式分为两行,保存按钮不可压缩,窄容器独占整行;Supervisor 和普通聊天工作区状态把绝对路径收口为项目名称。
|
||||
|
||||
@@ -966,3 +966,5 @@ confirmation-required
|
||||
- 阶段四保留左导航、中央主视窗、右侧 Project Supervisor 和底部 Agent Dock 四区 DOM。中央壳使用主面板层级;Supervisor 降为协作面板,其消息和 Runtime 状态内部滚动、composer 固定在栏底;Dock 使用较轻 surface 和可收缩状态卡,不与中央主要操作争抢层级。
|
||||
- 桌面 `>760px` 统一使用 `100dvh` 的两行工作台网格,第一行是内部滚动的主视窗/Supervisor,第二行是常驻 Dock。正式 `1280×800` 基线下页面级尺寸合同为 window/document/body `1280×800`;无登录会话的真实 Vite 入口按安全合同停在登录页,不绕过门禁,工作台内部由 AppSurface、共享 chrome 和 CSS 合同测试提供可重复证据。
|
||||
- 阶段五定向测试覆盖共享 chrome 的 pressed、Tauri 实际消费的工具组/分隔符、按钮名称、expanded/disabled、生成确认、失败重试、保存/取消,以及生成 dialog、进度/失败卡、状态栏、Supervisor composer 和 Agent Dock 的非裁剪/可见性合同。生成请求、登录态、泥点计费、Host Port、草稿 CAS、manifest/revision、Runtime、审批和保存事务均未改变。
|
||||
- 保存设置的产品语义固定为“名称可编辑、用途受控、格式枚举”。create 默认 `game-art`,普通用户只从 `game-art / icon-spec / ui-prototype / art-spritesheet` 四个权威用途选择;界面显示中文名称,不暴露自由 slug 输入。refine 必须继承源 manifest asset 的 `kind` 并锁定,未知历史 kind 原值透传但只显示“原资源用途”,不得借精修改变 subtype。PNG/JPEG/WebP 继续是有限格式选项。
|
||||
- 工具动作与保存设置必须是显式上下两行,保存栅格把主按钮列固定为 `max-content` 且禁止换行;容器宽度不足时保存按钮独占整行。普通用户工作区状态只显示项目名称,不直接展示本机绝对项目路径;显式目录选择、权限确认或开发诊断不受该展示规则替代。
|
||||
|
||||
Reference in New Issue
Block a user