Merge branch 'master' into codex/game-agent-resource-workbench
This commit is contained in:
@@ -393,12 +393,23 @@ pub(crate) fn inspect_local_project_directory(
|
||||
is_godot_project: godot_project_root.is_some(),
|
||||
godot_project_root,
|
||||
project_name: game_creator_project_name(root),
|
||||
modified_at: project_directory_modified_at(root),
|
||||
manifest_error: game_creator_project_manifest_error(root),
|
||||
recent_run_status: recent_run_trace.as_ref().map(|trace| trace.status.clone()),
|
||||
recent_run_stop_reason: recent_run_trace.map(|trace| trace.stop_reason),
|
||||
})
|
||||
}
|
||||
|
||||
fn project_directory_modified_at(root: &Path) -> Option<u64> {
|
||||
root.metadata()
|
||||
.ok()?
|
||||
.modified()
|
||||
.ok()?
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.ok()
|
||||
.map(|duration| duration.as_millis().min(u128::from(u64::MAX)) as u64)
|
||||
}
|
||||
|
||||
pub(crate) fn is_game_creator_project_directory(root: &Path) -> bool {
|
||||
if !root.is_dir() {
|
||||
return false;
|
||||
|
||||
@@ -179,6 +179,7 @@ struct LocalProjectDirectoryStatus {
|
||||
is_godot_project: bool,
|
||||
godot_project_root: Option<String>,
|
||||
project_name: Option<String>,
|
||||
modified_at: Option<u64>,
|
||||
manifest_error: Option<String>,
|
||||
recent_run_status: Option<String>,
|
||||
recent_run_stop_reason: Option<String>,
|
||||
|
||||
@@ -1131,6 +1131,25 @@ fn validate_generation_draft_active(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_generation_draft_for_public_phase(
|
||||
ledger: &AssetCanvasGenerationLedger,
|
||||
draft: &AssetCanvasDraft,
|
||||
) -> Result<(), String> {
|
||||
if ledger.phase == GenerationLedgerPhase::AssetDurableCommitted
|
||||
&& draft.status == AssetCanvasDraftStatus::Committed
|
||||
{
|
||||
if draft.project_id != ledger.project_id
|
||||
|| draft.draft_id != ledger.draft_id
|
||||
|| draft.intent != ledger.intent
|
||||
|| draft.source_asset_id != ledger.source_asset_id
|
||||
{
|
||||
return Err("素材画布生成账本与草稿身份不一致".to_string());
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
validate_generation_draft_active(ledger, draft)
|
||||
}
|
||||
|
||||
fn ensure_generation_draft_active(
|
||||
root: &Path,
|
||||
ledger: &AssetCanvasGenerationLedger,
|
||||
@@ -1150,7 +1169,7 @@ fn upsert_public_generation_record(
|
||||
let _lock = acquire_asset_canvas_draft_lock(root)?;
|
||||
let mut draft = read_asset_canvas_draft_locked(root, &ledger.project_id, &ledger.draft_id)?
|
||||
.ok_or_else(|| "素材画布草稿不存在".to_string())?;
|
||||
validate_generation_draft_active(ledger, &draft)?;
|
||||
validate_generation_draft_for_public_phase(ledger, &draft)?;
|
||||
let now = asset_canvas_now();
|
||||
let output_asset_id = ledger
|
||||
.commit_result
|
||||
|
||||
@@ -1397,6 +1397,7 @@ fn project_directory_status_distinguishes_missing_file_and_dir() {
|
||||
is_godot_project: false,
|
||||
godot_project_root: None,
|
||||
project_name: None,
|
||||
modified_at: None,
|
||||
manifest_error: None,
|
||||
recent_run_status: None,
|
||||
recent_run_stop_reason: None,
|
||||
@@ -1412,6 +1413,7 @@ fn project_directory_status_distinguishes_missing_file_and_dir() {
|
||||
assert!(!file_status.is_godot_project);
|
||||
assert_eq!(file_status.godot_project_root, None);
|
||||
assert_eq!(file_status.project_name, None);
|
||||
assert!(file_status.modified_at.is_some());
|
||||
assert_eq!(file_status.manifest_error, None);
|
||||
assert_eq!(file_status.recent_run_status, None);
|
||||
fs::remove_file(&root).expect("remove file");
|
||||
|
||||
@@ -96,6 +96,7 @@ export interface LocalProjectDirectoryStatus {
|
||||
isGodotProject: boolean;
|
||||
godotProjectRoot: string | null;
|
||||
projectName: string | null;
|
||||
modifiedAt?: number | null;
|
||||
manifestError?: string | null;
|
||||
recentRunStatus: string | null;
|
||||
recentRunStopReason: string | null;
|
||||
|
||||
@@ -61,6 +61,7 @@ export type RecentProjectRow = {
|
||||
status: string;
|
||||
projectKind: 'web' | 'godot' | 'unknown';
|
||||
godotProjectRoot: string | null;
|
||||
modifiedAt: number | null;
|
||||
recentRunStatus: string | null;
|
||||
recentRunStopReason: string | null;
|
||||
canReveal: boolean;
|
||||
@@ -262,6 +263,7 @@ export function buildRecentProjectRows(
|
||||
? 'web'
|
||||
: 'unknown',
|
||||
godotProjectRoot: directoryStatus?.godotProjectRoot ?? null,
|
||||
modifiedAt: directoryStatus?.modifiedAt ?? null,
|
||||
recentRunStatus: directoryStatus?.recentRunStatus ?? null,
|
||||
recentRunStopReason: directoryStatus?.recentRunStopReason ?? null,
|
||||
canReveal,
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import {
|
||||
ArrowUp,
|
||||
FileText,
|
||||
FolderKanban,
|
||||
FolderOpen,
|
||||
Gamepad2,
|
||||
Image,
|
||||
type LucideIcon,
|
||||
Plus,
|
||||
Sparkles,
|
||||
} from 'lucide-react';
|
||||
import type { FormEvent } from 'react';
|
||||
@@ -65,9 +65,30 @@ export type HomeProjectRow = {
|
||||
path: string;
|
||||
name: string;
|
||||
status: string;
|
||||
projectKind: 'web' | 'godot' | 'unknown';
|
||||
modifiedAt: number | null;
|
||||
canOpen: boolean;
|
||||
};
|
||||
|
||||
function formatProjectUpdatedAt(modifiedAt: number | null) {
|
||||
if (!modifiedAt || !Number.isFinite(modifiedAt) || modifiedAt <= 0) {
|
||||
return '未记录';
|
||||
}
|
||||
const date = new Date(modifiedAt);
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
return `${year}/${month}/${day}`;
|
||||
}
|
||||
|
||||
function projectTypeLabel(projectKind: HomeProjectRow['projectKind']) {
|
||||
return projectKind === 'godot'
|
||||
? 'Godot 游戏'
|
||||
: projectKind === 'web'
|
||||
? 'Web 游戏'
|
||||
: '本地项目';
|
||||
}
|
||||
|
||||
type HomeViewProps = {
|
||||
hasPromo: boolean;
|
||||
status: string;
|
||||
@@ -143,26 +164,30 @@ export default function HomeView({
|
||||
return (
|
||||
<div className="platform-theme platform-theme--light min-h-screen bg-(image:--platform-body-fill) text-(--platform-text-strong)">
|
||||
<section
|
||||
className={`grid min-h-93.5 content-start justify-items-center gap-3.25 px-6 pb-4.5 pt-25.75 max-[760px]:min-h-0 max-[760px]:gap-4 max-[760px]:px-3.5 max-[760px]:pb-8.5 max-[760px]:pt-21 ${
|
||||
hasPromo ? 'pt-33.75 max-[760px]:pt-21' : ''
|
||||
className={`grid content-start justify-items-center gap-3.5 px-6 pb-4.5 pt-20 max-[760px]:gap-4 max-[760px]:px-3.5 max-[760px]:pb-8.5 max-[760px]:pt-21 ${
|
||||
hasPromo ? 'pt-28 max-[760px]:pt-21' : ''
|
||||
}`}
|
||||
aria-label="首页输入"
|
||||
>
|
||||
<div className="flex items-center gap-3 text-center max-[760px]:flex-col max-[760px]:gap-2.5">
|
||||
<span className="grid size-32 shrink-0 place-items-center rounded-full shadow-(--platform-profile-action-shadow)">
|
||||
<img src={BRAND_ICON} alt="logo" className="size-32" />
|
||||
<div className="flex w-[min(814px,calc(100vw-122px))] items-center gap-4 text-left max-[760px]:w-[min(100%,calc(100vw-76px))] max-[760px]:flex-col max-[760px]:items-start max-[760px]:gap-2.5">
|
||||
<span className="grid size-30 shrink-0 place-items-center rounded-full bg-(--platform-subpanel-fill) shadow-(--platform-profile-action-shadow) max-[760px]:size-27">
|
||||
<img
|
||||
src={BRAND_ICON}
|
||||
alt="logo"
|
||||
className="size-30 max-[760px]:size-27"
|
||||
/>
|
||||
</span>
|
||||
<div>
|
||||
<h1 className="m-0 text-[27px] font-[750] tracking-normal text-(--platform-brand-logo-title) max-[760px]:text-[22px]">
|
||||
<h1 className="m-0 text-[36px] font-[750] tracking-[-0.03em] text-(--platform-brand-logo-title) max-[760px]:text-[33px]">
|
||||
陶泥儿
|
||||
</h1>
|
||||
<p className="m-0 mt-3 text-[13px] text-(--platform-text-soft)">
|
||||
{activeCreationType.placeholder}
|
||||
<p className="m-0 mt-1.5 text-[19.5px] font-medium text-(--platform-warm-text)">
|
||||
你的游戏创作管家
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="flex w-[min(488px,calc(100vw-110px))] flex-wrap justify-center gap-2.25 max-[760px]:w-[min(100%,calc(100vw-76px))]"
|
||||
className="flex w-[min(814px,calc(100vw-122px))] flex-wrap justify-start gap-3 max-[760px]:w-[min(100%,calc(100vw-76px))]"
|
||||
role="group"
|
||||
aria-label="创作类型"
|
||||
>
|
||||
@@ -171,10 +196,10 @@ export default function HomeView({
|
||||
const isActive = item.creationType === homeCreationType;
|
||||
return (
|
||||
<button
|
||||
className={`inline-flex min-h-6.75 cursor-pointer items-center gap-1.25 rounded-full border px-3 text-[12px] ${
|
||||
className={`inline-flex min-h-7 cursor-pointer items-center gap-1.25 rounded-full border px-4 text-[12px] font-medium transition-colors ${
|
||||
isActive
|
||||
? 'border-(--platform-nav-active-border) bg-(image:--platform-nav-active-fill) text-(--platform-warm-text) shadow-(--platform-nav-active-shadow)'
|
||||
: 'border-(--platform-subpanel-border) bg-(image:--platform-subpanel-fill) text-(--platform-neutral-text)'
|
||||
? 'border-(--platform-button-primary-border) bg-(image:--platform-button-primary-fill) text-(--platform-button-primary-text) shadow-(--platform-nav-active-shadow)'
|
||||
: 'border-(--platform-subpanel-border) bg-(image:--platform-subpanel-fill) text-(--platform-neutral-text) hover:border-(--platform-nav-active-border) hover:text-(--platform-warm-text)'
|
||||
}`}
|
||||
type="button"
|
||||
key={item.creationType}
|
||||
@@ -187,11 +212,8 @@ export default function HomeView({
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<span className="text-[12px] text-(--platform-text-soft)">
|
||||
{status}
|
||||
</span>
|
||||
<form
|
||||
className="grid min-h-20 w-[min(1054px,calc(100vw-122px))] grid-rows-[minmax(42px,auto)_auto] rounded-[18px] border border-(--platform-surface-border) bg-(--platform-input-fill) px-3 pb-2 pt-3.25 shadow-(--platform-panel-shadow) focus-within:bg-(--platform-input-fill-focus) focus-within:ring-4 focus-within:ring-(--platform-input-focus-ring) max-[760px]:w-[min(100%,calc(100vw-76px))]"
|
||||
className="grid min-h-26 w-[min(814px,calc(100vw-122px))] grid-rows-[minmax(62px,auto)_auto] rounded-[16px] border border-(--platform-surface-border) bg-(--platform-input-fill) px-3.5 pb-2.5 pt-3.5 shadow-(--platform-panel-shadow) focus-within:bg-(--platform-input-fill-focus) focus-within:ring-4 focus-within:ring-(--platform-input-focus-ring) max-[760px]:w-[min(100%,calc(100vw-76px))]"
|
||||
onSubmit={handleHomeSubmit}
|
||||
>
|
||||
<RichInputArea
|
||||
@@ -205,20 +227,25 @@ export default function HomeView({
|
||||
<div className="grid grid-cols-[1fr_auto] items-center gap-2.5 text-[12px] text-(--platform-text-soft)">
|
||||
<UploadButton />
|
||||
<button
|
||||
className="grid size-6 cursor-pointer place-items-center rounded-full border-0 bg-(image:--platform-button-primary-fill) p-0 text-(--platform-button-primary-text) shadow-(--platform-profile-action-shadow) disabled:cursor-not-allowed disabled:opacity-55"
|
||||
className="grid size-7 cursor-pointer place-items-center rounded-full border-0 bg-(image:--platform-button-primary-fill) p-0 text-(--platform-button-primary-text) shadow-(--platform-profile-action-shadow) transition-transform hover:scale-105 disabled:cursor-not-allowed disabled:opacity-55"
|
||||
type="submit"
|
||||
aria-label="开启创作"
|
||||
disabled={homeCreationBusy}
|
||||
>
|
||||
<Plus size={16} aria-hidden="true" />
|
||||
<ArrowUp size={16} aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
</RichInputArea>
|
||||
</form>
|
||||
{status ? (
|
||||
<span className="w-[min(814px,calc(100vw-122px))] text-left text-[12px] text-(--platform-text-soft) max-[760px]:w-[min(100%,calc(100vw-76px))]">
|
||||
{status}
|
||||
</span>
|
||||
) : null}
|
||||
</section>
|
||||
|
||||
<section
|
||||
className="mx-auto grid w-[min(1054px,calc(100vw-122px))] gap-3.5 max-[760px]:w-[min(100%,calc(100vw-76px))]"
|
||||
className="mx-auto grid w-[min(814px,calc(100vw-122px))] gap-3.5 max-[760px]:w-[min(100%,calc(100vw-76px))]"
|
||||
aria-label="最近项目"
|
||||
>
|
||||
<header className="flex items-center justify-between gap-3">
|
||||
@@ -249,27 +276,37 @@ export default function HomeView({
|
||||
</div>
|
||||
</header>
|
||||
{recentProjectRows.length > 0 ? (
|
||||
<div className="grid grid-cols-5 gap-3 overflow-hidden max-[760px]:grid-cols-1 max-[760px]:overflow-visible">
|
||||
<div className="grid grid-cols-3 gap-4 overflow-hidden max-[760px]:grid-cols-1 max-[760px]:overflow-visible">
|
||||
{recentProjectRows.map((project) => (
|
||||
<article className="grid min-w-0" key={project.path}>
|
||||
<button
|
||||
className="grid h-auto w-full cursor-pointer content-start justify-items-start gap-1.5 overflow-hidden rounded-lg border border-(--platform-surface-border) bg-(image:--platform-subpanel-fill) p-3 text-left shadow-(--platform-panel-shadow) transition hover:border-(--platform-surface-hover-border) disabled:cursor-not-allowed disabled:opacity-[0.58] max-[760px]:h-33"
|
||||
className="grid min-h-34 w-full cursor-pointer grid-cols-[108px_minmax(0,1fr)] items-stretch gap-3 overflow-hidden rounded-xl border border-(--platform-surface-border) bg-(image:--platform-subpanel-fill) p-2 text-left shadow-(--platform-panel-shadow) transition hover:border-(--platform-surface-hover-border) hover:shadow-(--platform-profile-action-shadow) disabled:cursor-not-allowed disabled:opacity-[0.58] max-[760px]:grid-cols-[120px_minmax(0,1fr)]"
|
||||
type="button"
|
||||
disabled={!project.canOpen}
|
||||
onClick={() => onProjectOpen(project.path)}
|
||||
>
|
||||
<span className="grid size-10.5 place-items-center rounded-xl bg-(--platform-warm-bg) text-[16px] font-bold text-(--platform-warm-text)">
|
||||
<span className="grid min-h-29 place-items-center rounded-lg bg-[linear-gradient(145deg,#f9bf7d,#e96831_52%,#853c2c)] text-[27px] font-bold tracking-[-0.08em] text-white shadow-inner">
|
||||
{project.name.slice(0, 2)}
|
||||
</span>
|
||||
<strong className="min-w-0 overflow-hidden text-ellipsis whitespace-nowrap text-[12px] font-medium text-(--platform-text-strong)">
|
||||
{project.name}
|
||||
</strong>
|
||||
<small className="-mt-1.25 min-w-0 overflow-hidden text-ellipsis whitespace-nowrap text-[11px] text-(--platform-text-soft)">
|
||||
{project.path}
|
||||
</small>
|
||||
<small className="-mt-1.25 min-w-0 overflow-hidden text-ellipsis whitespace-nowrap text-[11px] text-(--platform-text-soft)">
|
||||
{project.status}
|
||||
</small>
|
||||
<span className="grid min-w-0 content-center gap-2.25 py-1">
|
||||
<strong className="min-w-0 overflow-hidden text-ellipsis whitespace-nowrap text-[14px] font-semibold leading-4.5 text-(--platform-text-strong)">
|
||||
{project.name}
|
||||
</strong>
|
||||
<small className="grid grid-cols-[36px_minmax(0,1fr)] gap-x-3 gap-y-1 text-[10px] leading-3.5 text-(--platform-text-soft)">
|
||||
<span className="whitespace-nowrap">类型</span>
|
||||
<span className="truncate whitespace-nowrap text-(--platform-neutral-text)">
|
||||
{projectTypeLabel(project.projectKind)}
|
||||
</span>
|
||||
<span className="whitespace-nowrap">更新时间</span>
|
||||
<span className="truncate whitespace-nowrap text-(--platform-neutral-text)">
|
||||
{formatProjectUpdatedAt(project.modifiedAt)}
|
||||
</span>
|
||||
<span className="whitespace-nowrap">进度</span>
|
||||
<span className="truncate whitespace-nowrap text-(--platform-warm-text)">
|
||||
{project.status}
|
||||
</span>
|
||||
</small>
|
||||
</span>
|
||||
</button>
|
||||
</article>
|
||||
))}
|
||||
@@ -296,29 +333,23 @@ export default function HomeView({
|
||||
</section>
|
||||
|
||||
<section
|
||||
className="mx-auto mt-5.5 grid w-[min(1054px,calc(100vw-122px))] gap-3.5 max-[760px]:w-[min(100%,calc(100vw-76px))]"
|
||||
className="mx-auto mt-5.5 grid w-[min(814px,calc(100vw-122px))] gap-3.5 max-[760px]:w-[min(100%,calc(100vw-76px))]"
|
||||
aria-label="灵感推荐"
|
||||
>
|
||||
<header className="flex items-center justify-start gap-3">
|
||||
<div className="grid gap-1">
|
||||
<span className="inline-flex items-center gap-1.5">
|
||||
<h2 className="m-0 text-[15px] text-(--platform-text-strong)">
|
||||
灵感推荐
|
||||
</h2>
|
||||
<Sparkles
|
||||
className="text-(--platform-warm-text)"
|
||||
size={15}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</span>
|
||||
<p className="mb-0 mt-1 text-[12px] text-(--platform-text-soft)">
|
||||
暂无灵感
|
||||
</p>
|
||||
</div>
|
||||
<span className="inline-flex items-baseline gap-1.5">
|
||||
<h2 className="m-0 text-[15px] text-(--platform-text-strong)">
|
||||
灵感推荐
|
||||
</h2>
|
||||
<Sparkles
|
||||
className="text-(--platform-warm-text)"
|
||||
size={15}
|
||||
aria-hidden="true"
|
||||
style={{ transform: 'translateY(1px)' }}
|
||||
/>
|
||||
</span>
|
||||
</header>
|
||||
<div className="grid min-h-24 place-items-center overflow-hidden rounded-lg border border-(--platform-surface-border) bg-(image:--platform-subpanel-fill) text-[11px] text-(--platform-text-soft)">
|
||||
暂无灵感
|
||||
</div>
|
||||
<div className="min-h-24 overflow-hidden rounded-lg border border-(--platform-surface-border) bg-(image:--platform-subpanel-fill)" />
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -26,12 +26,12 @@ import {
|
||||
} from './harness';
|
||||
|
||||
export function registerClientHomeTests() {
|
||||
it('keeps the empty inspiration section without reading the main-site showcase', async () => {
|
||||
it('keeps the empty inspiration module without requesting the retired feed', async () => {
|
||||
const fetchSpy = vi.spyOn(globalThis, 'fetch');
|
||||
renderLauncherAt('/?launcher');
|
||||
|
||||
const inspirationSection = screen.getByLabelText('灵感推荐');
|
||||
expect(within(inspirationSection).getAllByText('暂无灵感')).toHaveLength(2);
|
||||
expect(screen.getByLabelText('灵感推荐')).not.toBeNull();
|
||||
expect(screen.queryByText('暂无灵感')).toBeNull();
|
||||
await act(async () => {
|
||||
await Promise.resolve();
|
||||
});
|
||||
@@ -1316,19 +1316,22 @@ export function registerHomeProjectCreationTests() {
|
||||
expect(gameType.getAttribute('aria-pressed')).toBe('true');
|
||||
expect(artType.getAttribute('aria-pressed')).toBe('false');
|
||||
expect(documentType.getAttribute('aria-pressed')).toBe('false');
|
||||
expect(screen.getAllByText('今天想把什么灵感做成游戏')).toHaveLength(2);
|
||||
expect(screen.getByText('你的游戏创作管家')).not.toBeNull();
|
||||
expect(screen.getAllByText('今天想把什么灵感做成游戏')).toHaveLength(1);
|
||||
fireEvent.click(documentType);
|
||||
expect(documentType.getAttribute('aria-pressed')).toBe('true');
|
||||
expect(screen.getAllByText('今天有什么设计需要帮你整理')).toHaveLength(2);
|
||||
expect(screen.getByText('你的游戏创作管家')).not.toBeNull();
|
||||
expect(screen.getAllByText('今天有什么设计需要帮你整理')).toHaveLength(1);
|
||||
fireEvent.click(screen.getByRole('button', { name: '开启创作' }));
|
||||
expect(await screen.findByText('请输入方案需求或上传资料')).not.toBeNull();
|
||||
expect(screen.getByText('请输入方案需求或上传资料')).not.toBeNull();
|
||||
expect(invoke).not.toHaveBeenCalledWith(
|
||||
'create_automatic_local_game_project',
|
||||
);
|
||||
fireEvent.click(artType);
|
||||
expect(gameType.getAttribute('aria-pressed')).toBe('false');
|
||||
expect(artType.getAttribute('aria-pressed')).toBe('true');
|
||||
expect(screen.getAllByText('今天想做什么样的美术素材')).toHaveLength(2);
|
||||
expect(screen.getByText('你的游戏创作管家')).not.toBeNull();
|
||||
expect(screen.getAllByText('今天想做什么样的美术素材')).toHaveLength(1);
|
||||
|
||||
const promptInput = screen.getByLabelText('创作想法');
|
||||
nativeClipboardMock.text = '你好,今天多少号';
|
||||
|
||||
@@ -122,7 +122,7 @@
|
||||
### 3.9 项目管理页
|
||||
|
||||
- 项目页采用“标题与状态 + 本地搜索 + 打开项目 / 新建项目 + 紧凑项目表格”的桌面信息架构。可以参考成熟项目管理器的信息层级和密度,但不得复制 Unity 等外部产品的品牌、Logo、深色皮肤、专有图标、列名或文案;页面继续使用 Genarrative 平台主题和陶泥儿客户端壳。
|
||||
- 表格只展示现有权威数据:项目名称与工作区绝对路径、GameAgent / Godot 类型、目录 / manifest / 最近 Runtime 状态和行级操作。当前目录检查合同没有修改时间、编辑器版本、收藏或云状态,前端不得伪造这些列。
|
||||
- 表格只展示现有权威数据:项目名称与工作区绝对路径、GameAgent / Godot 类型、目录 / manifest / 最近 Runtime 状态和行级操作;项目管理表格不展示目录修改时间。首页“最近项目”卡片可使用目录检查返回的 `modifiedAt` 展示更新时间。编辑器版本、收藏或云状态仍不在当前目录检查合同内,前端不得伪造这些列。
|
||||
- 可打开项目的主行点击后进入项目;显示目录和从最近列表移除收进键盘可达的行尾更多菜单。Escape 关闭菜单并把焦点还给触发按钮;移除只修改本机 WebView 最近项目记录,不删除磁盘文件。
|
||||
- 搜索只在已加载项目行中匹配名称、路径、类型、Godot 相对根和状态,不改 localStorage、不触碰项目目录、不新增后端或 Tauri 命令。无匹配状态提供清除搜索;真正空状态仍只引导用户使用顶部打开或新建,不追加第三个目录选择入口。
|
||||
- 正式视觉验收只覆盖 `1280×720` 最小横屏和 `1280×800` 默认窗口:工具栏保持单行,表头与项目列对齐,项目列表内部滚动,document/body 不出现页面级横向或纵向溢出。截图必须使用含 Web、Godot 和无效状态的 populated fixture;视频必须演示搜索、清除、行尾菜单及可观察的项目操作结果,不能只录静止页面或无结果点击。
|
||||
|
||||
@@ -662,7 +662,9 @@ game-project/
|
||||
- Agent 状态列表从 `.agent/manifest.json` 的任务 / 角色清单、`.agent/run.latest.json` / `.agent/runs/<runId>.json` 的 step、taskGraph、passPlans、lifecycleStatus,以及 `read_game_creator_agent_runtimes` 批量读取的 `.agent/runtime/agents/<taskId>.json` 和最近任务派生;v1 不新增独立状态数据库,也不承诺完整后台 runner。
|
||||
- App 启动先检查平台登录态;登录后进入同一个客户端首页,不再有面向用户的启动器 / 主窗口切换概念。首页按 `做游戏` / `做素材` / `做方案` 保存 `game` / `art` / `doc` 初始意图,输入状态按文字与附件 token 的顺序保存,附件以文件名 token 内嵌在输入框中而非堆叠在下方;提交时才将 token 转为 LLM 可读的 `{1st attachment}` 引用。普通 `Enter` 在输入法组合态结束后自动在系统“文档/Genarrative GameAgent”下原子分配唯一工作区、初始化本地项目、导入附件,并把首条需求直接投递给 active `project-supervisor` Session 的后台 Runtime,随后写入最近项目并切到项目开发页;`Shift+Enter` 保留换行。Windows 使用系统 Documents 路径,Linux 使用 XDG Documents,macOS 使用用户 Documents;前端不得显示或持久化 `/tmp` 作为默认项目路径,精确 `/tmp/genarrative-ai-game-draft` 只允许由测试显式注入为 fixture。普通与 game-chat release 都必须在 Vite 生成最新 `frontendDist` 后、Tauri 嵌入资源前扫描实际构建产物;命中该旧 Linux 默认路径或无法安全遍历产物时构建失败,禁止复用 gitignored 的旧 `dist`。原“开启创作”加号按钮保持手动选择目录:点击后弹出原生目录选择,目标目录存在且非空时必须二次确认,再调用 `init_local_game_project` 完成同一后续链路。项目页和首页通用“打开项目”没有手填默认路径;picker 未携带已有工作区时由操作系统决定初始位置。缺少 active Session 时先创建并激活;成功后清空首页草稿,取消或创建失败时在首页回显状态,首页响应式断点与应用外壳统一为 `760px`。两条流程都不调用 `generate_local_game_draft`、`generate_platform_art_asset`、一次性 `chat_with_game_creator_agent` 或 legacy 项目对话 append。
|
||||
- 首页发送、项目组目录选择和本地文件选择必须使用 Tauri 非阻塞原生 picker,并把选择器绑定到当前 `client` 窗口;禁止在同步 command 中调用 `blocking_pick_folder` / `blocking_pick_file` 阻塞 WebView 事件循环。选择器打开期间保留首页草稿可编辑,取消后恢复“开启创作”按钮并回显“已取消”。
|
||||
- 当前尚未定义 GameAgent 独立的灵感数据源;首页保留“灵感推荐”区块并显示无数据状态,但不得展示或请求主站 `/creation` 的陶泥儿精选 `/api/editor/showcase/resources`。后续接入前必须先明确独立数据契约和交互验收。
|
||||
- 当前尚未定义 GameAgent 独立的灵感数据源;首页保留“灵感推荐”模块及空白内容容器,但不显示无数据文案,也不得展示或请求主站 `/creation` 的陶泥儿精选 `/api/editor/showcase/resources`。后续接入前必须先明确独立数据契约和交互验收。
|
||||
- 首页正式桌面版以约 `814px` 的居中内容栏组织品牌区、创作类型、输入框、最近项目与灵感推荐;品牌区和类型按钮在内容栏左缘对齐,输入框和下方模块仍保持整体居中。标题使用深色暖橙层级,固定副标题为“你的游戏创作管家”;选中的创作类型使用实心暖橙按钮,未选中项使用浅色描边,输入框使用更舒展的单行创作起始比例。
|
||||
- 最近项目固定展示至多三个横向信息卡:左侧为本地渐变封面占位,右侧只投影项目名称、项目类型、目录自身真实修改时间和已有最近运行状态。目录检查通过 `modifiedAt` 提供目录自身 mtime,不递归扫描项目文件;没有封面资产时不得假装读取了项目封面,也不新增封面持久化字段。
|
||||
- debug 构建启动后在用户 `client` 窗口之外额外打开 `developer` 窗口;该窗口用于开发者单独选择 Agent、管理对应 active/archived Session 并读取历史,用户消息和真实 Agent 回复只持久化到 `.agent/conversations/agents/<agentId>/` 下的规范 Session。普通用户窗口不得出现 `Agent 聊天` 导航、picker 或工具台入口。
|
||||
- 首页最近项目只展示最近 3 个有效项目;项目组页在同一窗口使用紧凑桌面项目表格管理最近项目。顶部工具栏只放本地搜索、“打开项目”“新建项目”,不常驻路径输入框、目录显示按钮或独立 Godot 入口。两个项目动作都先打开绑定当前 `client` 的非阻塞原生目录选择器:“打开项目”读取已有 GameAgent 项目,或自动识别根目录 / 一层直接子目录中的唯一 Godot 工程并导入;普通未初始化目录提示改用“新建项目”。“新建项目”在用户选择工作区根后沿用非空目录确认,不自动重建无效历史路径。项目表格只投影名称、路径、GameAgent / Godot 类型、目录 / manifest / Runtime 状态;可打开行点击进入项目,显示目录和移除最近记录收进行尾更多菜单。搜索仅过滤当前行,不修改 storage;空状态不追加第三个目录选择入口。正式验收只覆盖 `1280×720` 最小横屏和 `1280×800` 默认窗口,列表内部滚动且无页面级溢出。
|
||||
- 项目开发页保留左侧栏和顶部栏,顶部展示项目名、路径和最近 run 状态;中间只挂载 active `project-supervisor` Session 的正式对话面、Runtime 状态、确认/Needs input 和专业 Agent 协作只读状态,底部保留附件导入结果。真正的项目开发画布仍未落地;专业 Agent picker、完整计划和工具台继续留在开发入口。
|
||||
|
||||
@@ -14,13 +14,11 @@ const developmentWorkflowDocPath =
|
||||
'docs/project-memory/shared-memory/development-workflow.md';
|
||||
const decisionLogDocPath = 'docs/project-memory/shared-memory/decision-log.md';
|
||||
const rootPackageJson = JSON.parse(fs.readFileSync('package.json', 'utf8'));
|
||||
const mobileShellConfigCheckSource = fs.readFileSync(
|
||||
'apps/mobile-shell/scripts/check-config.mjs',
|
||||
'utf8',
|
||||
const mobileShellConfigCheckSource = normalizeSourceForGuardrail(
|
||||
fs.readFileSync('apps/mobile-shell/scripts/check-config.mjs', 'utf8'),
|
||||
);
|
||||
const desktopShellConfigCheckSource = fs.readFileSync(
|
||||
'apps/desktop-shell/scripts/check-config.mjs',
|
||||
'utf8',
|
||||
const desktopShellConfigCheckSource = normalizeSourceForGuardrail(
|
||||
fs.readFileSync('apps/desktop-shell/scripts/check-config.mjs', 'utf8'),
|
||||
);
|
||||
const aiGameCreatorShellAppSource = fs.readFileSync(
|
||||
'apps/ai-game-creator-shell/src/App.tsx',
|
||||
@@ -152,6 +150,14 @@ function readSourceTree(entryPath, extension) {
|
||||
return fs.readFileSync(entryPath, 'utf8');
|
||||
}
|
||||
|
||||
function normalizeSourceForGuardrail(source) {
|
||||
return source
|
||||
.replace(/\s+/g, ' ')
|
||||
.replace(/\s*([(),])\s*/g, '$1')
|
||||
.replace(/,\s*\)/g, ')')
|
||||
.trim();
|
||||
}
|
||||
|
||||
function assertRootNativeShellCheckScripts() {
|
||||
if (
|
||||
rootPackageJson.scripts?.['check:native-shells'] !==
|
||||
@@ -187,7 +193,11 @@ function assertNativeShellDependencyVersionGuardrails() {
|
||||
"'eas-cli': '^20.3.0'",
|
||||
"assertPackageLockVersion('apps/mobile-shell', 'eas-cli', '20.3.0')",
|
||||
]) {
|
||||
if (!mobileShellConfigCheckSource.includes(snippet)) {
|
||||
if (
|
||||
!mobileShellConfigCheckSource.includes(
|
||||
normalizeSourceForGuardrail(snippet),
|
||||
)
|
||||
) {
|
||||
throw new Error(
|
||||
`mobile shell dependency guardrail drifted: missing ${snippet}`,
|
||||
);
|
||||
@@ -211,7 +221,11 @@ function assertNativeShellDependencyVersionGuardrails() {
|
||||
"['tauri', '2.11.2']",
|
||||
'tauri-plugin-single-instance = { version = "2.4.2", features = ["deep-link"] }',
|
||||
]) {
|
||||
if (!desktopShellConfigCheckSource.includes(snippet)) {
|
||||
if (
|
||||
!desktopShellConfigCheckSource.includes(
|
||||
normalizeSourceForGuardrail(snippet),
|
||||
)
|
||||
) {
|
||||
throw new Error(
|
||||
`desktop shell dependency guardrail drifted: missing ${snippet}`,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user