合并 修复 AGC 发布入口在 DirectProject 下的反馈与确认
Project CI / AI game creator shell Rust crates (push) Successful in 1m24s
Project CI / AI game creator shell Rust smoke (push) Successful in 1m53s
Project CI / Backend tests (push) Successful in 4m13s
Project CI / AI game creator shell Rust lane 1/2 (push) Failing after 6m31s
Project CI / Frontend tests (push) Successful in 2m39s
Project CI / AI game creator shell Rust lane 2/2 (push) Failing after 8m55s
Project CI / Native shell tests (push) Successful in 7m2s
Project CI / Repository checks (push) Successful in 2m35s
Project CI / AI game creator shell web tests (push) Successful in 1m28s
Project CI / AI game creator shell Rust crates (push) Successful in 1m24s
Project CI / AI game creator shell Rust smoke (push) Successful in 1m53s
Project CI / Backend tests (push) Successful in 4m13s
Project CI / AI game creator shell Rust lane 1/2 (push) Failing after 6m31s
Project CI / Frontend tests (push) Successful in 2m39s
Project CI / AI game creator shell Rust lane 2/2 (push) Failing after 8m55s
Project CI / Native shell tests (push) Successful in 7m2s
Project CI / Repository checks (push) Successful in 2m35s
Project CI / AI game creator shell web tests (push) Successful in 1m28s
- 发布动作即时回显权限检查与构建打包状态 - DirectProject 对话渲染项目权限确认卡片并回传确认与取消 - 权限拒绝、取消和查询失败统一回显到项目对话 - 新增发布反馈回归用例并记录排障口径
This commit is contained in:
@@ -1145,6 +1145,7 @@ export function App({
|
||||
...current,
|
||||
{ role: 'assistant', text: message },
|
||||
]);
|
||||
directProjectChatRef.current?.announce(message);
|
||||
return true;
|
||||
}
|
||||
if (!policyView.policy.confirmCommands.includes(commandId)) {
|
||||
@@ -1155,6 +1156,7 @@ export function App({
|
||||
...current,
|
||||
{ role: 'assistant', text: readyMessage },
|
||||
]);
|
||||
directProjectChatRef.current?.announce(readyMessage);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1180,6 +1182,7 @@ export function App({
|
||||
...current,
|
||||
{ role: 'assistant', text: message },
|
||||
]);
|
||||
directProjectChatRef.current?.announce(message);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
@@ -1229,7 +1232,11 @@ export function App({
|
||||
announcePublishMessage('先打开一个项目再发布');
|
||||
return;
|
||||
}
|
||||
// 权限查询也可能慢或挂住,先回一条即时反馈,别让按钮看起来没反应。
|
||||
announcePublishMessage('正在检查发布权限…');
|
||||
const runExport = async () => {
|
||||
// 导出可能包含构建步骤,再回一条即时反馈。
|
||||
announcePublishMessage('正在构建并打包试玩包,请稍候…');
|
||||
try {
|
||||
const result = await invoke<LocalProjectExportPackageResult>(
|
||||
'export_local_project_package',
|
||||
@@ -1251,16 +1258,26 @@ export function App({
|
||||
);
|
||||
}
|
||||
};
|
||||
const queued = await queueProjectPolicyConfirmationIfNeeded(
|
||||
invoke,
|
||||
'project.export_package',
|
||||
nextProjectPath,
|
||||
'导出试玩包并打开「发布到游戏广场」面板。',
|
||||
'导出试玩包需要确认,确认后继续。',
|
||||
() => void runExport(),
|
||||
);
|
||||
if (!queued) {
|
||||
await runExport();
|
||||
try {
|
||||
const queued = await queueProjectPolicyConfirmationIfNeeded(
|
||||
invoke,
|
||||
'project.export_package',
|
||||
nextProjectPath,
|
||||
'导出试玩包并打开「发布到游戏广场」面板。',
|
||||
'导出试玩包需要确认,确认后继续。',
|
||||
() => void runExport(),
|
||||
);
|
||||
if (!queued) {
|
||||
await runExport();
|
||||
}
|
||||
} catch (error) {
|
||||
// 权限查询失败也必须回话:onClick 的 Promise 没有 catch 时用户只会看到
|
||||
// 「点了没反应」,这里把它收敛成聊天里的可读错误。
|
||||
announcePublishMessage(
|
||||
`发布前权限检查失败:${
|
||||
error instanceof Error ? error.message : String(error)
|
||||
}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1368,6 +1385,7 @@ export function App({
|
||||
? '已取消对比项目 checkpoint'
|
||||
: '已取消导出本地试玩包';
|
||||
setWorkspaceStatus(status);
|
||||
directProjectChatRef.current?.announce(status);
|
||||
}
|
||||
if (pending.commandId === 'project.create') {
|
||||
setWorkspaceStatus('已取消');
|
||||
@@ -2320,6 +2338,9 @@ export function App({
|
||||
onRequestGamePublish={
|
||||
gamePublishAllowed ? requestGamePublish : undefined
|
||||
}
|
||||
onCancelConfirmation={cancelUiCommandConfirmation}
|
||||
onConfirmConfirmation={confirmUiCommand}
|
||||
pendingConfirmation={pendingUiConfirmation}
|
||||
projectPath={localProject?.projectPath ?? projectPath ?? null}
|
||||
ref={directProjectChatRef}
|
||||
/>
|
||||
|
||||
+27
@@ -10,6 +10,7 @@ import {
|
||||
|
||||
import { AGENT_CHAT_SCROLL_BOTTOM_THRESHOLD } from '../../../app/constants';
|
||||
import { claimInitialTurnForPage } from '../../../app/initialTurnClaims';
|
||||
import type { PendingUiConfirmation } from '../../../app/types';
|
||||
import { projectNameFromPath } from '../../../features/agent-runtime';
|
||||
import { directCodexUserItemFromContent } from '../../../features/project-workspace/resourceReferences';
|
||||
import { type ApprovalMode, approvalModeLabel } from '../approvalMode';
|
||||
@@ -80,6 +81,13 @@ export type DirectProjectChatViewProps = {
|
||||
ensureConversationWriteAllowed: DirectProjectConversationWriteGate;
|
||||
/** 发布入口:由工作台壳持有导出与发布面板,聊天只提供触发按钮。 */
|
||||
onRequestGamePublish?: () => void;
|
||||
/**
|
||||
* 工作台壳入队的项目权限确认:聊天只展示并转发确认/取消,
|
||||
* 策略读取、拒绝判定和确认后的动作仍由工作台壳持有。
|
||||
*/
|
||||
pendingConfirmation?: PendingUiConfirmation | null;
|
||||
onConfirmConfirmation?: () => void;
|
||||
onCancelConfirmation?: () => void;
|
||||
/** 工作台壳把运行/预览这类动作结果交给聊天的出口。 */
|
||||
ref?: Ref<DirectProjectChatHandle>;
|
||||
};
|
||||
@@ -95,6 +103,9 @@ function directInitialTurnText(content: readonly DirectCodexUserContentPart[]) {
|
||||
|
||||
export function DirectProjectChatView({
|
||||
onRequestGamePublish,
|
||||
onConfirmConfirmation,
|
||||
onCancelConfirmation,
|
||||
pendingConfirmation = null,
|
||||
projectPath,
|
||||
initialTurn = null,
|
||||
ensureConversationReadAllowed,
|
||||
@@ -242,6 +253,22 @@ export function DirectProjectChatView({
|
||||
onLoadEarlierHistory={() => void loadEarlierHistory()}
|
||||
onScroll={handleScroll}
|
||||
/>
|
||||
{pendingConfirmation &&
|
||||
onConfirmConfirmation &&
|
||||
onCancelConfirmation ? (
|
||||
<div className="pending-command">
|
||||
<span>
|
||||
{pendingConfirmation.commandId}
|
||||
<small>{pendingConfirmation.detail}</small>
|
||||
</span>
|
||||
<button type="button" onClick={onCancelConfirmation}>
|
||||
取消
|
||||
</button>
|
||||
<button type="button" onClick={onConfirmConfirmation}>
|
||||
确认
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
<DirectProjectComposer
|
||||
assets={assets}
|
||||
{...(versions ? { versions } : {})}
|
||||
|
||||
@@ -0,0 +1,272 @@
|
||||
// @vitest-environment jsdom
|
||||
/**
|
||||
* 客户端「发布」入口的可见反馈。
|
||||
*
|
||||
* DirectProject 项目不发工作台状态行,发布动作的提示必须回到项目对话;
|
||||
* 权限要求确认时也要在聊天里给出确认/取消,否则表现就是「点了没反应」。
|
||||
*/
|
||||
import { beforeEach, describe, it, vi } from 'vitest';
|
||||
|
||||
import type { GameCreationAppManifest } from '../../../packages/shared/src/contracts/gameCreationApp';
|
||||
import {
|
||||
App,
|
||||
createGameCreationAppManifest,
|
||||
createProjectChatRuntimeHarness,
|
||||
emptyProjectPolicy,
|
||||
expect,
|
||||
fireEvent,
|
||||
render,
|
||||
screen,
|
||||
waitFor,
|
||||
within,
|
||||
} from './appSurface/harness';
|
||||
|
||||
const readGamePublishAvailabilityMock = vi.hoisted(() =>
|
||||
vi.fn(async () => true),
|
||||
);
|
||||
|
||||
vi.mock('../src/services/gameDistributionPublish', async (importOriginal) => {
|
||||
const actual =
|
||||
await importOriginal<
|
||||
typeof import('../src/services/gameDistributionPublish')
|
||||
>();
|
||||
return {
|
||||
...actual,
|
||||
readGamePublishAvailability: readGamePublishAvailabilityMock,
|
||||
};
|
||||
});
|
||||
|
||||
const PROJECT_PATH = '/tmp/game-publish-feedback-project';
|
||||
const PROJECT_ID = 'game-publish-feedback-project';
|
||||
|
||||
function createFixtureManifest(): GameCreationAppManifest {
|
||||
return createGameCreationAppManifest(PROJECT_ID, '发布反馈项目');
|
||||
}
|
||||
|
||||
function installTauri(
|
||||
options: {
|
||||
exportPackage?: () => unknown;
|
||||
policy?: ReturnType<typeof emptyProjectPolicy>;
|
||||
readPolicy?: () => unknown;
|
||||
} = {},
|
||||
) {
|
||||
const manifest = createFixtureManifest();
|
||||
const chatHarness = createProjectChatRuntimeHarness({
|
||||
projectPath: PROJECT_PATH,
|
||||
});
|
||||
const invoke = vi.fn(
|
||||
async (command: string, args?: Record<string, unknown>) => {
|
||||
if (command === 'get_local_game_manifest') return manifest;
|
||||
if (command === 'get_local_game_project_revision') return { revision: 1 };
|
||||
if (command === 'read_project_permission_policy') {
|
||||
if (options.readPolicy) return options.readPolicy();
|
||||
return options.policy ?? emptyProjectPolicy();
|
||||
}
|
||||
if (command === 'export_local_project_package') {
|
||||
if (options.exportPackage) return options.exportPackage();
|
||||
return {
|
||||
projectPath: PROJECT_PATH,
|
||||
packagePath: `${PROJECT_PATH}/exports/game.zip`,
|
||||
packageRelativePath: 'exports/game.zip',
|
||||
fileCount: 1,
|
||||
totalBytes: 1,
|
||||
};
|
||||
}
|
||||
return chatHarness.invoke(command, args);
|
||||
},
|
||||
);
|
||||
window.__TAURI__ = {
|
||||
core: { invoke: invoke as never },
|
||||
event: { listen: chatHarness.listen as never },
|
||||
};
|
||||
return { invoke };
|
||||
}
|
||||
|
||||
function renderPublishProject() {
|
||||
return render(
|
||||
<App
|
||||
initialProjectManifest={createFixtureManifest()}
|
||||
initialProjectPath={PROJECT_PATH}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
|
||||
async function clickPublish() {
|
||||
const publish = await screen.findByRole('button', {
|
||||
name: '发布到游戏广场',
|
||||
});
|
||||
fireEvent.click(publish);
|
||||
return screen.findByLabelText('陶泥儿项目对话');
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
window.history.pushState({}, '', '/');
|
||||
readGamePublishAvailabilityMock.mockReset();
|
||||
readGamePublishAvailabilityMock.mockResolvedValue(true);
|
||||
});
|
||||
|
||||
function createExportPackageResult() {
|
||||
return {
|
||||
projectPath: PROJECT_PATH,
|
||||
packagePath: `${PROJECT_PATH}/exports/game.zip`,
|
||||
packageRelativePath: 'exports/game.zip',
|
||||
fileCount: 1,
|
||||
totalBytes: 1,
|
||||
};
|
||||
}
|
||||
|
||||
function createConfirmPolicy() {
|
||||
return {
|
||||
path: '.agent/policy.json',
|
||||
policy: {
|
||||
deniedCommands: [],
|
||||
confirmCommands: ['project.export_package'],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe('客户端发布入口的可见反馈', () => {
|
||||
it('导出成功时先回即时反馈,再回结果并打开发布面板', async () => {
|
||||
installTauri({ exportPackage: createExportPackageResult });
|
||||
renderPublishProject();
|
||||
|
||||
const surface = await clickPublish();
|
||||
await waitFor(() => {
|
||||
expect(surface.textContent ?? '').toContain('正在检查发布权限…');
|
||||
expect(surface.textContent ?? '').toContain(
|
||||
'正在构建并打包试玩包,请稍候…',
|
||||
);
|
||||
expect(surface.textContent ?? '').toContain(
|
||||
'已构建并打包试玩包:exports/game.zip',
|
||||
);
|
||||
});
|
||||
expect(
|
||||
await screen.findByRole('dialog', { name: '发布到游戏广场' }),
|
||||
).not.toBeNull();
|
||||
});
|
||||
|
||||
it('导出失败时把可读错误写回项目对话', async () => {
|
||||
installTauri({
|
||||
exportPackage: () => {
|
||||
throw new Error('导出试玩包前需要先生成 exports/README.md');
|
||||
},
|
||||
});
|
||||
renderPublishProject();
|
||||
|
||||
const surface = await clickPublish();
|
||||
await waitFor(() => {
|
||||
expect(surface.textContent ?? '').toContain(
|
||||
'导出试玩包前需要先生成 exports/README.md',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('策略要求确认时在 DirectProject 里展示确认卡片,确认后继续导出', async () => {
|
||||
const exportPackage = vi.fn(createExportPackageResult);
|
||||
installTauri({ exportPackage, policy: createConfirmPolicy() });
|
||||
renderPublishProject();
|
||||
|
||||
const publish = await screen.findByRole('button', {
|
||||
name: '发布到游戏广场',
|
||||
});
|
||||
fireEvent.click(publish);
|
||||
|
||||
const commandLabel = await screen.findByText('project.export_package');
|
||||
const card = commandLabel.closest('.pending-command');
|
||||
expect(card).not.toBeNull();
|
||||
expect(exportPackage).not.toHaveBeenCalled();
|
||||
expect(
|
||||
within(card as HTMLElement).getByText(
|
||||
'导出试玩包并打开「发布到游戏广场」面板。',
|
||||
),
|
||||
).not.toBeNull();
|
||||
|
||||
fireEvent.click(
|
||||
within(card as HTMLElement).getByRole('button', { name: '确认' }),
|
||||
);
|
||||
await waitFor(() => expect(exportPackage).toHaveBeenCalledTimes(1));
|
||||
const surface = await screen.findByLabelText('陶泥儿项目对话');
|
||||
expect(surface.textContent ?? '').toContain(
|
||||
'正在构建并打包试玩包,请稍候…',
|
||||
);
|
||||
expect(surface.textContent ?? '').toContain(
|
||||
'已构建并打包试玩包:exports/game.zip',
|
||||
);
|
||||
});
|
||||
|
||||
it('取消权限确认时把取消结果写回项目对话', async () => {
|
||||
const exportPackage = vi.fn(createExportPackageResult);
|
||||
installTauri({ exportPackage, policy: createConfirmPolicy() });
|
||||
renderPublishProject();
|
||||
|
||||
fireEvent.click(
|
||||
await screen.findByRole('button', { name: '发布到游戏广场' }),
|
||||
);
|
||||
const commandLabel = await screen.findByText('project.export_package');
|
||||
const card = commandLabel.closest('.pending-command');
|
||||
expect(card).not.toBeNull();
|
||||
fireEvent.click(
|
||||
within(card as HTMLElement).getByRole('button', { name: '取消' }),
|
||||
);
|
||||
|
||||
const surface = await screen.findByLabelText('陶泥儿项目对话');
|
||||
await waitFor(() => {
|
||||
expect(surface.textContent ?? '').toContain('已取消导出本地试玩包');
|
||||
});
|
||||
expect(exportPackage).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('确认时策略已改为拒绝,也要把拒绝原因写回项目对话', async () => {
|
||||
const exportPackage = vi.fn(createExportPackageResult);
|
||||
let denyOnNextPolicyRead = false;
|
||||
installTauri({
|
||||
exportPackage,
|
||||
readPolicy: () => {
|
||||
if (!denyOnNextPolicyRead) return createConfirmPolicy();
|
||||
return {
|
||||
path: '.agent/policy.json',
|
||||
policy: {
|
||||
deniedCommands: ['project.export_package'],
|
||||
confirmCommands: [],
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
renderPublishProject();
|
||||
|
||||
fireEvent.click(
|
||||
await screen.findByRole('button', { name: '发布到游戏广场' }),
|
||||
);
|
||||
const commandLabel = await screen.findByText('project.export_package');
|
||||
const card = commandLabel.closest('.pending-command');
|
||||
expect(card).not.toBeNull();
|
||||
denyOnNextPolicyRead = true;
|
||||
fireEvent.click(
|
||||
within(card as HTMLElement).getByRole('button', { name: '确认' }),
|
||||
);
|
||||
|
||||
const surface = await screen.findByLabelText('陶泥儿项目对话');
|
||||
await waitFor(() => {
|
||||
expect(surface.textContent ?? '').toContain(
|
||||
'项目权限策略拒绝执行:project.export_package',
|
||||
);
|
||||
});
|
||||
expect(exportPackage).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('权限查询失败时也在项目对话里回显错误', async () => {
|
||||
installTauri({
|
||||
readPolicy: () => {
|
||||
throw new Error('策略文件损坏');
|
||||
},
|
||||
});
|
||||
renderPublishProject();
|
||||
|
||||
const surface = await clickPublish();
|
||||
await waitFor(() => {
|
||||
expect(surface.textContent ?? '').toContain(
|
||||
'发布前权限检查失败:策略文件损坏',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -5805,6 +5805,14 @@ Cocos Creator 根目录由 `package.json.creator.version` 与普通 `assets/`
|
||||
- **处理(现行口径)**:① 产品行为保留“导出成功后自动打开面板”(一键发布入口),但受影响的用例必须先用 `findByRole('dialog', { name: '发布到游戏广场' })` 断言面板出现、点「关闭发布面板」再继续后续会话操作;② 给这类“新增自动弹窗”改流程时,先跑一遍相关 `appSurface` 用例,避免只跑新增用例;③ 排查同类“点了没反应”时,先看当前是否有焦点陷阱模态打开,而不是先怀疑事件绑定或状态。
|
||||
- **关联**:`apps/ai-game-creator-shell/src/App.tsx`(`setPublishPanelOpen(true)`)、`apps/ai-game-creator-shell/src/components/modal/ThemedModal.tsx`、`apps/ai-game-creator-shell/src/components/game-distribution/GameDistributionPublishPanel.tsx`、`apps/ai-game-creator-shell/tests/appSurface/project-preview/preview-shortcuts/assert-project-tools-and-preview.ts`。
|
||||
|
||||
## 2026-09-23 AGC 发布按钮点了没反应:DirectProject 不渲染 workspaceStatus,权限确认也没有挂载点
|
||||
|
||||
- **现象**:AGC 聊天头能看到「发布」按钮,点击后没有任何提示、面板也不出现;项目权限要求确认时同样静默,用户只能描述为「点了没反应」。
|
||||
- **原因**:普通项目固定走 `DirectProjectChatView`,它不渲染工作台状态行,也不渲染 `PlanningChatView` 里那块 `pending-command` 确认卡片;发布提示、权限确认和取消结果原先只写 `workspaceStatus` / `messages`。上游 `f502829fd` 已把发布提示同时 `announce` 回 DirectProject,但确认卡片仍未挂载到 DirectProject;权限查询抛错也没有 catch,确认时策略改为拒绝同样只写工作台消息。结果等待确认、命令被拒、确认后拒绝这几条路径仍表现为「点了没反应」。
|
||||
- **处理(现行口径)**:发布相关提示统一走 `announcePublishMessage`,同时写 `workspaceStatus` 和 `directProjectChatRef.current.announce`;`DirectProjectChatView` 增加 `pendingConfirmation` / `onConfirmConfirmation` / `onCancelConfirmation`,在对话列表与输入盒之间渲染与 `PlanningChatView` 同形的确认条;`requestGamePublish` 对权限查询整段兜底并把错误回显到聊天,点击后先回「正在检查发布权限…」,进入导出再回「正在构建并打包试玩包」。
|
||||
- **验证**:`npx vitest run apps/ai-game-creator-shell/tests/gamePublishFeedback.test.tsx`(正常发布 / 导出失败 / 确认继续 / 取消 / 确认时拒绝 / 权限查询失败 6 条)与 AGC 全量前端用例通过;`npm run ai-game-creator-shell:typecheck`、`npm run check:encoding`、`git diff --check` 通过。
|
||||
- **关联**:`apps/ai-game-creator-shell/src/App.tsx`、`apps/ai-game-creator-shell/src/view/project-development/chat/DirectProjectChatView.tsx`、`apps/ai-game-creator-shell/tests/gamePublishFeedback.test.tsx`。
|
||||
|
||||
## 2026-09-21 受控 Lexical 输入区的回写用被动 effect:滞后渲染的 props 会把用户草稿清空
|
||||
|
||||
- **现象**:DirectProject 输入盒里粘贴(或连续输入)长文本,提交时 `chat_with_game_creator_direct_codex` 根本没发出去,界面停在空输入盒;`chat-composer` 用例里表现为「队列/终止/语音追加」五条一起红,但手工操作只在快速输入后偶发。
|
||||
|
||||
Reference in New Issue
Block a user