恢复AGC首页三类创作入口
Project CI / Repository checks (push) Successful in 3m28s
Project CI / Backend tests (push) Successful in 4m3s
Project CI / Frontend tests (push) Successful in 4m6s
Project CI / Native shell tests (push) Failing after 12m2s

恢复做游戏、做素材、做方案的类型状态与分类提示

将受限 creationType 作为结构化首轮上下文传入项目内 Codex,保持用户原文不变

修正三项内置 Skill 指纹并补齐前端、Rust 与文档回归
This commit is contained in:
Git Hooks Test
2026-08-22 10:46:33 +08:00
parent 2d99a12196
commit d83551dd65
12 changed files with 332 additions and 33 deletions
@@ -1,6 +1,6 @@
{
"schemaVersion": "agc-skill-pack.v1",
"version": "2026-08-21.1",
"version": "2026-08-22.1",
"skills": [
{
"name": "agc-project-structure",
@@ -16,7 +16,7 @@
"agents/openai.yaml",
"references/structure-contract.md"
],
"sha256": "348e5e9fc7b2c628223caeb7fe9c0f15477cf97967b90777d3e0462589e13688"
"sha256": "69b46a2a7180a227dba587dc5eb4ce5b13fb70ab6de55420ee08133d07083225"
},
{
"name": "taonier-art-assets",
@@ -52,7 +52,7 @@
"agents/openai.yaml",
"references/game-quality-checklist.md"
],
"sha256": "4ecff75e30701d78d65522dd3218d2465b3114a3937c82d538bda1103efd58e4"
"sha256": "f897a38e89bfc576be9e91ed9bf89c25f54f700aa258039168ae2f7607f147df"
},
{
"name": "agc-browser-playtest",
@@ -87,7 +87,7 @@
"agents/openai.yaml",
"references/projection-contract.md"
],
"sha256": "e9da95c2f371620e045a3ab9f4721078c805e108263f479b4e01b1af96d462d3"
"sha256": "b7b4623032a3eafe55a74f8931c15b380392938985bce4126f16451a1b61de2a"
}
]
}
@@ -1552,6 +1552,38 @@ pub(crate) fn build_direct_codex_system_prompt(root: &Path) -> Result<String, St
.collect())
}
fn direct_creation_type_system_context(
creation_type: Option<&str>,
) -> Result<Option<String>, String> {
let Some(creation_type) = creation_type else {
return Ok(None);
};
let creation_type = creation_type.trim();
let label = match creation_type {
"game" => "做游戏",
"art" => "做素材",
"doc" => "做方案",
_ => return Err("创作类型只允许 game、art 或 doc".to_string()),
};
Ok(Some(format!(
"客户端结构化创作类型:{creation_type} / {label}。这是用户在首页显式选择的创作方向,不是 Runtime 模式、Provider 路由或固定工作流。请将它与用户原始消息一起理解,但不要改写用户原文、不要伪造额外用户消息,也不要因该类型启动 Supervisor、专业 Agent 或 harness。"
)))
}
pub(crate) fn build_direct_codex_system_prompt_with_creation_type(
root: &Path,
creation_type: Option<&str>,
) -> Result<String, String> {
let base_prompt = build_direct_codex_system_prompt(root)?;
let Some(creation_context) = direct_creation_type_system_context(creation_type)? else {
return Ok(base_prompt);
};
Ok(format!("{creation_context}\n{base_prompt}")
.chars()
.take(MAX_DIRECT_SYSTEM_PROMPT_CHARS)
.collect())
}
/// A home conversation deliberately has no project workspace. Keep its
/// instructions short, explicit, and free of project paths so a greeting or
/// general question cannot become an accidental game-generation request.
@@ -1686,6 +1718,14 @@ pub(crate) async fn run_direct_game_creator_home_turn(
pub(crate) async fn run_direct_game_creator_turn_at(
root: &Path,
prompt: &str,
) -> Result<String, String> {
run_direct_game_creator_turn_at_with_creation_type(root, prompt, None).await
}
pub(crate) async fn run_direct_game_creator_turn_at_with_creation_type(
root: &Path,
prompt: &str,
creation_type: Option<&str>,
) -> Result<String, String> {
if !root.is_absolute() || !root.is_dir() {
return Err("当前项目目录不存在或不是绝对路径".to_string());
@@ -1696,8 +1736,9 @@ pub(crate) async fn run_direct_game_creator_turn_at(
if prompt.is_empty() {
return Err("聊天内容不能为空".to_string());
}
direct_creation_type_system_context(creation_type)?;
emit_direct_game_creator_progress(root, "request.accepted", "已发送消息,正在等待陶泥儿回复");
match run_direct_game_creator_turn_inner(root, prompt).await {
match run_direct_game_creator_turn_inner(root, prompt, creation_type).await {
Ok(reply) => Ok(reply),
Err(failure) => Err(record_direct_codex_turn_failure(root, failure)),
}
@@ -1706,10 +1747,16 @@ pub(crate) async fn run_direct_game_creator_turn_at(
async fn run_direct_game_creator_turn_inner(
root: &Path,
prompt: &str,
creation_type: Option<&str>,
) -> Result<String, DirectCodexTurnFailure> {
run_direct_game_creator_turn_with(root, prompt, |system_prompt, user_prompt| async move {
direct_game_creator_codex_chat_at(root, system_prompt, user_prompt).await
})
run_direct_game_creator_turn_with_creation_type(
root,
prompt,
creation_type,
|system_prompt, user_prompt| async move {
direct_game_creator_codex_chat_at(root, system_prompt, user_prompt).await
},
)
.await
}
@@ -1724,15 +1771,29 @@ async fn run_direct_game_creator_turn_with<F, Fut>(
prompt: &str,
run_turn: F,
) -> Result<String, DirectCodexTurnFailure>
where
F: FnOnce(String, String) -> Fut,
Fut: Future<Output = Result<String, String>>,
{
run_direct_game_creator_turn_with_creation_type(root, prompt, None, run_turn).await
}
async fn run_direct_game_creator_turn_with_creation_type<F, Fut>(
root: &Path,
prompt: &str,
creation_type: Option<&str>,
run_turn: F,
) -> Result<String, DirectCodexTurnFailure>
where
F: FnOnce(String, String) -> Fut,
Fut: Future<Output = Result<String, String>>,
{
emit_direct_game_creator_progress(root, "codex.turn", "陶泥儿正在处理这条消息");
let previous_output_fingerprint = direct_codex_output_fingerprint(root);
let system_prompt = build_direct_codex_system_prompt(root).map_err(|error| {
DirectCodexTurnFailure::new(DirectCodexFailureStage::CodeGeneration, error)
})?;
let system_prompt = build_direct_codex_system_prompt_with_creation_type(root, creation_type)
.map_err(|error| {
DirectCodexTurnFailure::new(DirectCodexFailureStage::CodeGeneration, error)
})?;
let reply = run_turn(system_prompt, prompt.to_string())
.await
.map_err(|error| {
@@ -1951,8 +2012,14 @@ async fn run_direct_game_creator_turn_with_private_editor_credentials(
pub(crate) async fn chat_with_game_creator_direct_codex(
project_path: String,
prompt: String,
creation_type: Option<String>,
) -> Result<String, String> {
run_direct_game_creator_turn_at(Path::new(project_path.trim()), &prompt).await
run_direct_game_creator_turn_at_with_creation_type(
Path::new(project_path.trim()),
&prompt,
creation_type.as_deref(),
)
.await
}
#[tauri::command]
@@ -2147,6 +2214,56 @@ mod tests {
assert!(!prompt.contains("wechatpay"));
}
#[test]
fn direct_creation_type_is_a_bounded_structured_hint_not_user_prompt_text() {
for (creation_type, label) in [("game", "做游戏"), ("art", "做素材"), ("doc", "做方案")]
{
let prompt = build_direct_codex_system_prompt_with_creation_type(
Path::new("."),
Some(creation_type),
)
.expect("build creation type prompt");
assert!(prompt.contains("客户端结构化创作类型"));
assert!(prompt.contains(&format!("{creation_type} / {label}")));
assert!(prompt.contains("不是 Runtime 模式"));
assert!(prompt.contains("不要改写用户原文"));
assert!(prompt.chars().count() <= MAX_DIRECT_SYSTEM_PROMPT_CHARS);
}
let without_type =
build_direct_codex_system_prompt_with_creation_type(Path::new("."), None)
.expect("build prompt without creation type");
assert!(!without_type.contains("客户端结构化创作类型"));
assert!(build_direct_codex_system_prompt_with_creation_type(
Path::new("."),
Some("execute")
)
.is_err());
}
#[tokio::test]
async fn direct_creation_type_keeps_the_original_user_prompt_unchanged() {
let root = tempfile::tempdir().expect("temp dir");
init_local_game_project_at(root.path(), "direct-art-context", "素材上下文测试")
.expect("init project");
let reply = run_direct_game_creator_turn_with_creation_type(
root.path(),
"画一个橙色陶罐角色",
Some("art"),
|system, prompt| async move {
assert!(system.contains("art / 做素材"));
assert_eq!(prompt, "画一个橙色陶罐角色");
assert!(!prompt.contains("初始意图"));
Ok("已理解素材需求。".to_string())
},
)
.await
.expect("direct creation type turn");
assert_eq!(reply, "已理解素材需求。");
}
#[tokio::test]
async fn default_direct_turn_forwards_a_greeting_without_client_generation_workflow() {
let root = tempfile::tempdir().expect("temp dir");
+24 -5
View File
@@ -243,6 +243,7 @@ import {
SupervisorChatOnlyView,
} from './features/project-workspace/SupervisorChatOnlyView';
import { RuntimeConfigDialog } from './features/runtime-config/RuntimeConfigDialog';
import type { HomeCreationType } from './view/home';
import {
type ProjectAgentResultSummary,
type ProjectAgentRuntimeSummary,
@@ -615,6 +616,7 @@ type AppProps = {
directGameChatRuntime?: boolean;
allowAdvancedExternalEditorConfig?: boolean;
initialSupervisorMessage?: string;
initialCreationType?: HomeCreationType | null;
playRequest?: ProjectSupervisorComponentProps['playRequest'];
onPlayRequestHandled?: ProjectSupervisorComponentProps['onPlayRequestHandled'];
onManifestChange?: (
@@ -640,6 +642,7 @@ export function App({
directGameChatRuntime = false,
allowAdvancedExternalEditorConfig = false,
initialSupervisorMessage = '',
initialCreationType = null,
playRequest = null,
onPlayRequestHandled,
onManifestChange,
@@ -718,6 +721,7 @@ export function App({
const initialSupervisorMessageLatchRef = useRef({
projectPath: initialProjectPath,
prompt: initialSupervisorMessage.trim(),
creationType: initialCreationType,
});
const handledPlayRequestRef = useRef<string | null>(null);
@@ -990,7 +994,11 @@ export function App({
| null
>(null);
const executeChatAgentReplyRef = useRef<
(prompt: string, directConversationTurnId?: string) => Promise<void>
(
prompt: string,
directConversationTurnId?: string,
creationType?: HomeCreationType | null,
) => Promise<void>
>(async () => undefined);
const agentConversationSavingRef = useRef(false);
const agentConversationBackgroundBusyRef = useRef(false);
@@ -5921,6 +5929,7 @@ export function App({
async function executeChatAgentReply(
prompt: string,
directConversationTurnId?: string,
creationType?: HomeCreationType | null,
) {
// Product default: send the conversation directly to Codex app-server.
// The legacy Supervisor/harness path remains below for rollback and tests.
@@ -5963,12 +5972,20 @@ export function App({
setDirectCodexProgress('已发送消息,正在等待陶泥儿回复');
setProjectSupervisorRuntimeError('');
try {
const directTurnInput: {
projectPath: string;
prompt: string;
creationType?: HomeCreationType;
} = {
projectPath: directProjectPath,
prompt,
};
if (creationType) {
directTurnInput.creationType = creationType;
}
const reply = await directInvoke<string>(
'chat_with_game_creator_direct_codex',
{
projectPath: directProjectPath,
prompt,
},
directTurnInput,
);
if (localProjectPathRef.current === directProjectPath) {
setMessages((current) => [
@@ -6274,11 +6291,13 @@ export function App({
void executeChatAgentReplyRef.current(
latch.prompt,
directConversationTurnId,
latch.creationType,
);
}, [
chatAgentBusy,
directCodexProductRuntime,
gameChatOnly,
initialCreationType,
initialSupervisorMessage,
localProject,
]);
+2 -1
View File
@@ -7,7 +7,7 @@ import type {
GameCreationAppTaskStatus,
} from '../../../../packages/shared/src/contracts/gameCreationApp';
import type { ProfileRechargeCenterResponse } from '../../../../packages/shared/src/contracts/runtime';
import type { HomeDraft } from '../view/home';
import type { HomeCreationType, HomeDraft } from '../view/home';
export type RechargeContent = Omit<
ProfileRechargeCenterResponse,
@@ -58,6 +58,7 @@ export type LauncherProjectContext = {
projectKind: LocalProjectKind;
manifest: GameCreationAppManifest;
projectRevision: number | null;
creationType: HomeCreationType | null;
initialPrompt: string;
attachments: LauncherImportedAttachment[];
recentRunStatus: string | null;
@@ -320,6 +320,7 @@ export function WorkspaceLauncherShell({
initialProjectManifest={currentProjectContext.manifest}
initialProjectKind={currentProjectContext.projectKind}
initialSupervisorMessage={currentProjectContext.initialPrompt}
initialCreationType={currentProjectContext.creationType}
orchestrationMode="single-supervisor"
projectSupervisorOnly
playRequest={playRequest}
@@ -6,6 +6,7 @@ import type {
GameCreationAppPreviewState,
} from '../../../../../packages/shared/src/contracts/gameCreationApp';
import type { ChatMessage, LocalProjectDirectoryStatus } from '../../app/types';
import type { HomeCreationType } from '../../view/home';
import type { LauncherView } from '../../view/layout';
import type {
ProjectAgentResultSummary,
@@ -30,6 +31,7 @@ export type ProjectSupervisorComponentProps = {
initialProjectManifest?: GameCreationAppManifest;
initialProjectKind?: 'web' | 'godot';
initialSupervisorMessage?: string;
initialCreationType?: HomeCreationType | null;
orchestrationMode?: 'single-supervisor' | 'professional-dag';
projectSupervisorOnly?: boolean;
playRequest?: {
@@ -23,7 +23,11 @@ import type {
TauriInvoke,
UploadLocalAssetResult,
} from '../../app/types';
import type { HomeAttachmentDraft, HomeDraft } from '../../view/home';
import type {
HomeAttachmentDraft,
HomeCreationType,
HomeDraft,
} from '../../view/home';
import { useLauncherHomeDraftStore } from '../../view/home/useHomeDraftStore';
import type { LauncherView } from '../../view/layout';
import type {
@@ -156,6 +160,7 @@ export function useHomeProjectCreation({
async function enterCreatedHomeProject(
invoke: TauriInvoke,
result: InitLocalProjectResult,
creationType: HomeCreationType,
prompt: string,
attachments: HomeAttachmentDraft[],
) {
@@ -174,6 +179,7 @@ export function useHomeProjectCreation({
invoke,
result.projectPath,
),
creationType,
initialPrompt:
prompt.trim() ||
(attachments.length > 0
@@ -189,6 +195,7 @@ export function useHomeProjectCreation({
async function createHomeProjectFromDirectory(
nextProjectPath: string,
creationType: HomeCreationType,
prompt: string,
attachments: HomeAttachmentDraft[],
skipNonEmptyCheck = false,
@@ -213,7 +220,7 @@ export function useHomeProjectCreation({
setPendingNonEmptyProject({
kind: 'home-create',
projectPath: trimmedProjectPath,
draft: { prompt, attachments },
draft: { creationType, prompt, attachments },
});
setStatus('目标文件夹不是空的');
return '目标文件夹不是空的,请确认是否继续新建';
@@ -228,7 +235,13 @@ export function useHomeProjectCreation({
},
);
try {
await enterCreatedHomeProject(invoke, result, prompt, attachments);
await enterCreatedHomeProject(
invoke,
result,
creationType,
prompt,
attachments,
);
setStatus('已创建项目,正在开始智能创作');
} catch (error) {
throw new Error(
@@ -298,6 +311,7 @@ export function useHomeProjectCreation({
invoke,
result.projectPath,
),
creationType: null,
initialPrompt: '',
attachments: [],
recentRunStatus: null,
@@ -386,6 +400,7 @@ export function useHomeProjectCreation({
invoke,
trimmedProjectPath,
),
creationType: null,
initialPrompt: '',
attachments: [],
recentRunStatus: directoryStatus.recentRunStatus,
@@ -412,6 +427,7 @@ export function useHomeProjectCreation({
if (pendingProject.kind === 'home-create') {
await createHomeProjectFromDirectory(
pendingProject.projectPath,
pendingProject.draft.creationType,
pendingProject.draft.prompt,
pendingProject.draft.attachments,
true,
@@ -448,6 +464,7 @@ export function useHomeProjectCreation({
}
return createHomeProjectFromDirectory(
selectedPath,
draft.creationType,
draft.prompt,
draft.attachments,
);
@@ -466,6 +483,7 @@ export function useHomeProjectCreation({
await enterCreatedHomeProject(
invoke,
result,
draft.creationType,
draft.prompt,
draft.attachments,
);
@@ -1,4 +1,13 @@
import { FolderKanban, FolderOpen, Plus, Sparkles } from 'lucide-react';
import {
FileText,
FolderKanban,
FolderOpen,
Gamepad2,
Image,
type LucideIcon,
Plus,
Sparkles,
} from 'lucide-react';
import type { FormEvent } from 'react';
import { useRef, useState } from 'react';
@@ -8,9 +17,49 @@ import {
richTextToAttachments,
richTextToPrompt,
} from './components/RichInputArea/richTextToPrompt';
import { type HomeDraft, useLauncherHomeDraftStore } from './useHomeDraftStore';
import {
type HomeCreationType,
type HomeDraft,
useLauncherHomeDraftStore,
} from './useHomeDraftStore';
export type { HomeAttachmentDraft, HomeDraft } from './useHomeDraftStore';
export type {
HomeAttachmentDraft,
HomeCreationType,
HomeDraft,
} from './useHomeDraftStore';
type HomeCreationTypeItem = {
creationType: HomeCreationType;
label: string;
placeholder: string;
emptyPrompt: string;
icon: LucideIcon;
};
const HOME_CREATION_TYPE_ITEMS: readonly HomeCreationTypeItem[] = [
{
creationType: 'game',
label: '做游戏',
placeholder: '今天想把什么灵感做成游戏',
emptyPrompt: '请输入游戏灵感或上传参考素材',
icon: Gamepad2,
},
{
creationType: 'art',
label: '做素材',
placeholder: '今天想做什么样的美术素材',
emptyPrompt: '请输入素材需求或上传参考图',
icon: Image,
},
{
creationType: 'doc',
label: '做方案',
placeholder: '今天有什么设计需要帮你整理',
emptyPrompt: '请输入方案需求或上传资料',
icon: FileText,
},
];
export type HomeProjectRow = {
path: string;
@@ -40,12 +89,22 @@ export default function HomeView({
onProjectOpen,
onProjectPick,
}: HomeViewProps) {
const homeCreationType = useLauncherHomeDraftStore(
(state) => state.creationType,
);
const homeRichText = useLauncherHomeDraftStore((state) => state.draft);
const setHomeCreationType = useLauncherHomeDraftStore(
(state) => state.setCreationType,
);
const setHomeRichText = useLauncherHomeDraftStore(
(state) => state.setRichText,
);
const [homeCreationBusy, setHomeCreationBusy] = useState(false);
const homeCreationBusyRef = useRef(false);
const activeCreationType =
HOME_CREATION_TYPE_ITEMS.find(
(item) => item.creationType === homeCreationType,
) ?? HOME_CREATION_TYPE_ITEMS[0]!;
async function createFromHome() {
if (homeCreationBusyRef.current) {
@@ -54,7 +113,7 @@ export default function HomeView({
const referencedAttachments = richTextToAttachments(homeRichText);
const prompt = richTextToPrompt(homeRichText);
if (!prompt && referencedAttachments.length === 0) {
onStatusChange('请输入创作需求或上传参考附件');
onStatusChange(activeCreationType.emptyPrompt);
return;
}
homeCreationBusyRef.current = true;
@@ -63,6 +122,7 @@ export default function HomeView({
try {
onStatusChange(
await onCreateDraftAutomatically({
creationType: homeCreationType,
prompt,
attachments: referencedAttachments,
}),
@@ -97,10 +157,36 @@ export default function HomeView({
</h1>
<p className="m-0 mt-3 text-[13px] text-(--platform-text-soft)">
{activeCreationType.placeholder}
</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))]"
role="group"
aria-label="创作类型"
>
{HOME_CREATION_TYPE_ITEMS.map((item) => {
const CreationTypeIcon = item.icon;
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] ${
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)'
}`}
type="button"
key={item.creationType}
aria-pressed={isActive}
onClick={() => setHomeCreationType(item.creationType)}
>
<CreationTypeIcon size={14} aria-hidden="true" />
{item.label}
</button>
);
})}
</div>
<span className="text-[12px] text-(--platform-text-soft)">
{status}
</span>
@@ -110,7 +196,7 @@ export default function HomeView({
>
<RichInputArea
value={homeRichText}
placeholder="告诉陶泥儿你想创建什么"
placeholder={activeCreationType.placeholder}
onChange={setHomeRichText}
onEnter={() => {
void createFromHome();
@@ -8,24 +8,31 @@ export type HomeAttachmentDraft = {
export type Draft = EditorState | null;
export type HomeCreationType = 'game' | 'art' | 'doc';
export type HomeDraft = {
creationType: HomeCreationType;
prompt: string;
attachments: HomeAttachmentDraft[];
};
type UseHomeDraftStore = {
creationType: HomeCreationType;
draft: Draft;
setCreationType: (creationType: HomeCreationType) => void;
setRichText: (richText: Draft) => void;
reset: () => void;
};
const initialHomeDraft: Pick<UseHomeDraftStore, 'draft'> = {
const initialHomeDraft: Pick<UseHomeDraftStore, 'creationType' | 'draft'> = {
creationType: 'game',
draft: null,
};
// Keep the immutable Lexical snapshot available while the Home view is unmounted.
export const useLauncherHomeDraftStore = create<UseHomeDraftStore>((set) => ({
...initialHomeDraft,
setCreationType: (creationType) => set({ creationType }),
setRichText: (draft) => set({ draft: draft }),
reset: () => set(initialHomeDraft),
}));
@@ -1303,18 +1303,54 @@ export function registerHomeProjectCreationTests() {
};
renderLauncherAt('/?launcher', 'home', true);
const creationTypes = screen.getByRole('group', { name: '创作类型' });
const gameType = within(creationTypes).getByRole('button', {
name: '做游戏',
});
const artType = within(creationTypes).getByRole('button', {
name: '做素材',
});
const documentType = within(creationTypes).getByRole('button', {
name: '做方案',
});
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);
fireEvent.click(documentType);
expect(documentType.getAttribute('aria-pressed')).toBe('true');
expect(screen.getAllByText('今天有什么设计需要帮你整理')).toHaveLength(2);
fireEvent.click(screen.getByRole('button', { name: '开启创作' }));
expect(await screen.findByText('请输入方案需求或上传资料')).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);
const promptInput = screen.getByLabelText('创作想法');
nativeClipboardMock.text = '你好,今天多少号';
fireEvent.paste(promptInput);
await waitFor(() => {
expect(promptInput.textContent).toContain('你好,今天多少号');
});
fireEvent.click(screen.getByRole('button', { name: '开启创作' }));
fireEvent.keyDown(promptInput, {
key: 'Enter',
code: 'Enter',
shiftKey: true,
});
expect(invoke).not.toHaveBeenCalledWith(
'create_automatic_local_game_project',
);
fireEvent.keyDown(promptInput, { key: 'Enter', code: 'Enter' });
expect(await screen.findByLabelText('项目开发工作台')).not.toBeNull();
const projectConversation = screen.getByLabelText('陶泥儿项目对话');
await waitFor(() => {
expect(projectConversation.textContent).toContain('你好,今天多少号');
expect(projectConversation.textContent).not.toContain('初始意图');
expect(projectConversation.textContent).toContain(
'别这么骂自己,具体发生什么了?',
);
@@ -1327,12 +1363,21 @@ export function registerHomeProjectCreationTests() {
expect(invoke).toHaveBeenCalledWith('chat_with_game_creator_direct_codex', {
projectPath: automaticProjectPath,
prompt: '你好,今天多少号',
creationType: 'art',
});
expect(invoke).not.toHaveBeenCalledWith(
'chat_with_game_creator_home_direct_codex',
expect.anything(),
);
expect(screen.queryByLabelText('陶泥儿首页对话')).toBeNull();
fireEvent.click(screen.getByRole('button', { name: '首页' }));
const resetTypes = screen.getByRole('group', { name: '创作类型' });
expect(
within(resetTypes)
.getByRole('button', { name: '做游戏' })
.getAttribute('aria-pressed'),
).toBe('true');
});
it('imports home attachments into the automatic project before the project Codex turn', async () => {
@@ -1406,7 +1451,8 @@ export function registerHomeProjectCreationTests() {
});
expect(invoke).toHaveBeenCalledWith('chat_with_game_creator_direct_codex', {
projectPath: automaticProjectPath,
prompt: expect.stringContaining('按这个角色做游戏'),
prompt: '按这个角色做游戏',
creationType: 'game',
});
expect(invoke).not.toHaveBeenCalledWith(
'chat_with_game_creator_home_direct_codex',
@@ -127,15 +127,16 @@
### 收口契约
1. 首页保留一“陶泥儿”创作入口,不展示模式选择,也不在首页渲染用户或助手消息气泡。
1. 首页保留一“陶泥儿”创作入口,并恢复“做游戏 / 做素材 / 做方案”三个创作类型,默认为“做游戏”。这三项只是用户显式选择的创作方向,不是 Agent Runtime 模式、Provider 选择或旧 Supervisor 路由;首页仍不渲染用户或助手消息气泡。
2. 每次提交非空正文或附件时,客户端对且只对本次提交调用一次 `create_automatic_local_game_project`,在系统文档目录的 `Genarrative GameAgent/` 下分配唯一 `gameagent-*` 工作区;不弹目录选择器,也不复用最近项目。
3. 工作区初始化后先把附件导入该项目,再立即进入项目开发工作台;用户原始正文作为 `initialPrompt` 交给 project-bound direct Codex thread。意图理解、是否修改游戏以及后续试玩均在项目内完成,首页不运行 projectless Codex 对话。
3. 工作区初始化后先把附件导入该项目,再立即进入项目开发工作台;用户原始正文作为 `initialPrompt` 原样交给 project-bound direct Codex thread,选中的 `creationType=game|art|doc` 作为独立结构化首轮上下文传入。客户端不得把类型改写为“初始意图”文本、不得把它拼进用户消息,也不得据此切换 Runtime。意图理解、是否修改工程以及后续验收均由项目内同一 Codex 自主完成,首页不运行 projectless Codex 对话。
4. 项目工作台产品文案统一使用“陶泥儿”“智能创作”,不显示“项目总控”“自动执行”“Supervisor”或“专业 Agent”。客户端只做项目创建、附件导入和确定性投影,不恢复旧多 Agent Runtime。
5. 首页的创建状态只显示在主输入区;“最近项目”使用独立静态说明,不能复用“正在创建工作区 / 正在回复 / 已回复”等全局状态。
### 验收合同
- 普通文本、游戏需求和带附件需求都必须先创建项目,再在 `陶泥儿项目对话` 中出现同一条原始用户消息与 Codex 回复;`chat_with_game_creator_home_direct_codex` 不得暴露为首页 Tauri handler
- 三个创作类型必须在桌面与窄视口下可见、可键盘操作并具有明确的选中态;切换类型同步更新输入占位与空输入提示,一次创建收口后恢复默认“做游戏”
- 普通文本、三类创作需求和带附件需求都必须先创建项目,再在 `陶泥儿项目对话` 中出现同一条原始用户消息与 Codex 回复;首轮 direct command 必须同时携带原样 `prompt` 和受限 `creationType``chat_with_game_creator_home_direct_codex` 不得暴露为首页 Tauri handler。
- 连续点击或连续 Enter 只能创建一个工作区;创建进行中按钮禁用,但编辑器不得生成首页聊天气泡。
- 附件必须经 `upload_local_asset` 写入新项目后再交给项目 Codex;不得只把附件元数据留在首页,也不得把浏览器本地路径写入聊天正文。
@@ -1161,6 +1161,7 @@ game-project/
## 2026-08-20 Direct Codex 审核 Skill Pack 与受控工具内核
- 普通项目对话只由一个 project-bound Codex app-server thread 执行。客户端系统提示词只放最小工程合同、当前游戏源码有界快照、项目 prompts 和审核 Skill 索引;不再批量读取项目 `.codex/.agents/.hermes` Skill 正文,也不恢复 Supervisor、专业 Agent 或 harness。
- 首页恢复“做游戏 / 做素材 / 做方案”三个创作类型,默认“做游戏”。该选择与设置页的 Agent Runtime 模式无关;每次首页提交仍只自动创建一个新项目并进入项目工作台。用户正文原样进入项目对话,`game|art|doc` 仅作为受限结构化首轮上下文传给同一 Codex thread,不拼接“初始意图”文案、不产生首页对话、不切换 Provider 或恢复旧 Runtime 编排。
- `agc-skill-pack.v1` 只包含项目结构、陶泥儿美术、Web 游戏实现、真实浏览器试玩、客户端资源投影五项 Skill。清单记录用途、触发条件、所需工具、版本和内容 SHA-256;任何审核文件变化都必须同步重算对应清单指纹。客户端把审核文件安装到隔离目录后通过 app-server `skills/extraRoots/set + skills/list` 注册并复核,完整正文由 Codex 原生 Skill 机制按意图加载,一层引用只能经 `agc_read_skill_resource` 读取清单内 Markdown。引用路径按平台无关规则拒绝反斜杠、盘符、UNC、绝对路径和 `..`,不能依赖当前宿主的 `std::path` 语义判断其它平台路径。
- DirectProject 只连接客户端内置的 `agc_tools` STDIO MCP,工具固定为审核引用读取、标准陶泥儿美术准备和 desktop/mobile 浏览器试玩。MCP 进程只做协议;真实浏览器和付费 External v1 调用通过随机 loopback 地址回到客户端主进程,因此不复制 GUI 登录态、开发者 Key 或项目路径到模型上下文。三项工具固定自动批准,通用 shell、任意网络、多 Agent、插件和外部 MCP 继续关闭。
- 陶泥儿生成继续复用既有私有 Key、持久幂等账本、operation 恢复、来源/下载/PNG 解码和 manifest 登记。完整可信图集缺切片可以继续,固定四切片只是推荐路径;凭据失效、来源不明或结果未知时失败关闭,不能自动换 Key 或重新扣费。