Compare commits

..

21 Commits

Author SHA1 Message Date
k88936 1d6e7057bc 移除识别绑定旧计算命名
清理请求错误、结果校验和日志中的旧计算及联合文案
2026-09-05 15:39:17 +08:00
k88936 6daff74c85 更新并简化 UI 识别与绑定系统提示 2026-09-05 15:22:26 +08:00
k88936 ad49056d36 更新 UI workflow 阶段决策记录
移除历史阶段描述并记录现行阶段与无迁移约束
2026-09-05 14:14:37 +08:00
k88936 28af87e3ad 统一 UI workflow 阶段并拒绝未知值
现行阶段原地使用并移除旧阶段 fallback 与迁移兼容
2026-09-05 14:11:00 +08:00
k88936 d994ccaa78 更正独立素材上限语义
将128限制恢复为独立素材数量且不计入页面参考图
2026-09-05 13:33:52 +08:00
k88936 42f1109b90 限制联合图片上下文总量
将页面参考图与独立素材的单次请求总数限制为128张
2026-09-05 13:30:03 +08:00
k88936 b772ec3efb 记录联合识别重跑与阶段排名语义
说明重复识别会生成新结果并明确 workflow_stage_rank 的后端持久化边界
2026-09-05 13:25:38 +08:00
k88936 d014cb1d06 限制联合请求独立素材总量
将单次识别绑定请求的独立素材数量限制为128个
2026-09-05 13:24:58 +08:00
k88936 e34ae89db3 移除废弃的独立识别路径
让联合识别绑定成为 UI 编辑器唯一实现并清理旧状态投影
2026-09-05 13:11:38 +08:00
k88936 88853d0a30 清理联合识别后的节点选择
替换生成新树后清除已失效的旧节点选中状态
2026-09-05 13:09:40 +08:00
k88936 fabc64df6c 避免工作流渲染校验重复克隆状态
直接复用联合结果写入后的 State 完成组件校验
2026-09-05 13:08:50 +08:00
k88936 27803a6e78 明确联合结果按图片返回树
让提示与每张输入图一棵树的校验契约一致
2026-09-05 13:07:27 +08:00
k88936 828b3e1173 消除联合请求初始化崩溃路径
将 provider client 与配置 unwrap 改为显式错误
2026-09-05 13:02:38 +08:00
k88936 deb60065a5 校验联合结果素材引用
拒绝不存在的图片素材组件引用

拒绝不存在的字体绑定引用
2026-09-05 12:58:49 +08:00
k88936 5c6a940b9c 稳定联合识别图片上下文顺序
页面角色优先作为 ROOT

其余界面图按 ID 排序保持请求可复现
2026-09-05 12:49:56 +08:00
k88936 8df5ebff69 简化联合工作流动作状态
移除旧识别状态回退与重复分支

统一联合步骤的运行中和完成态显示
2026-09-05 12:47:43 +08:00
k88936 041b51adc1 归一化UI编辑器完成态入口
将旧 visual-binding 初始步骤归一到联合识别绑定步骤

简化工作流步骤索引计算
2026-09-05 12:46:55 +08:00
k88936 5a31809e21 修正联合概览状态定位
结构问题高亮 layout_status

组件问题高亮 components_status
2026-09-05 12:44:26 +08:00
k88936 44bebae0a9 统一UI编辑器联合计算入口
工作流导航收敛为参考分析与一键识别绑定两步

联合概览同时展示结构与组件状态
2026-09-05 12:01:54 +08:00
k88936 62793ac445 正式合并UI识别与组件绑定
删除旧 recognition 与 binding 命令及前端 DTO

新增 recognition_and_binding 单次 workflow 与联合概览

移除 debug 超时特判并直达 binding-ready
2026-09-05 11:55:47 +08:00
k88936 f630c518f3 重构UI工作流联合识别绑定契约
更新 UI workflow 以 recognition_and_binding 单次计算为正式识别路径

保留 merge 能力但移出联合识别绑定流程

声明联合提示词 placeholder 与 binding-ready 直达阶段
2026-09-05 11:07:42 +08:00
424 changed files with 39334 additions and 43355 deletions
-5
View File
@@ -8,11 +8,6 @@ LLM_BASE_URL="https://api.vectorengine.cn/v1"
# but it should not be relied on by browser code.
LLM_API_KEY=""
# Router account provisioning secret (server-side only). Prefer the protected
# file form in production; never expose either value to clients or commit it.
GENARRATIVE_LLM_ROUTER_PROVISIONING_SECRET=""
GENARRATIVE_LLM_ROUTER_PROVISIONING_SECRET_FILE=""
# Optional frontend override for the local proxy path.
VITE_LLM_PROXY_BASE_URL="/api/llm"
-2
View File
@@ -40,8 +40,6 @@ temp*build*/
/apps/ai-game-creator-shell/src-tauri/resources/codex/win-x64/codex-path/
/apps/ai-game-creator-shell/src-tauri/resources/codex/win-x64/codex-resources/
/apps/ai-game-creator-shell/src-tauri/resources/codex/win-x64/codex-package.json
/apps/ai-game-creator-shell/src-tauri/resources/plugins/
/plugins/agc-cocos-editor/native/payload/
/apps/ai-game-creator-shell/logs/
/apps/ai-game-creator-shell/.llm-drafts/
/apps/ai-game-creator-shell/game-creator.config.local.json
-50
View File
@@ -26,8 +26,6 @@ import type {
AdminErrorReportDetail,
AdminErrorReportEntry,
AdminErrorReportListResponse,
AdminExternalApiKeyListQuery,
AdminExternalApiKeyListResponse,
AdminFeatureGateConfigResponse,
AdminLoginResponse,
AdminMeResponse,
@@ -251,16 +249,6 @@ export function getAdminDatabaseTableRows(
);
}
export function getAdminExternalApiKeys(
token: string,
query: AdminExternalApiKeyListQuery = {},
) {
return request<AdminExternalApiKeyListResponse>(
`/admin/api/external-api-keys${buildExternalApiKeyQuery(query)}`,
{ token },
);
}
export function debugAdminHttp(token: string, payload: AdminDebugHttpRequest) {
return request<AdminDebugHttpResponse>('/admin/api/debug/http', {
method: 'POST',
@@ -940,28 +928,6 @@ function buildDatabaseTableRowsQuery(query: AdminDatabaseTableRowsQuery) {
return queryString ? `?${queryString}` : '';
}
function buildExternalApiKeyQuery(query: AdminExternalApiKeyListQuery) {
const params = new URLSearchParams();
appendQueryParam(params, 'ownerUserId', query.ownerUserId);
appendQueryParam(params, 'publicUserCode', query.publicUserCode);
appendQueryParam(params, 'keyId', query.keyId);
appendQueryParam(params, 'name', query.name);
appendQueryParam(params, 'keyPrefix', query.keyPrefix);
appendQueryParam(params, 'createdAfter', query.createdAfter);
appendQueryParam(params, 'createdBefore', query.createdBefore);
appendQueryParam(params, 'status', query.status);
if (typeof query.limit === 'number' && Number.isFinite(query.limit)) {
params.set('limit', String(Math.floor(query.limit)));
}
if (typeof query.offset === 'number' && Number.isFinite(query.offset)) {
params.set('offset', String(Math.floor(query.offset)));
}
appendQueryParam(params, 'sortColumn', query.sortColumn);
appendQueryParam(params, 'sortDirection', query.sortDirection);
const queryString = params.toString();
return queryString ? `?${queryString}` : '';
}
function buildEditorAssetListQuery(query: AdminEditorAssetListQuery) {
const params = new URLSearchParams();
appendQueryParam(params, 'cursor', query.cursor);
@@ -1072,19 +1038,3 @@ function buildAdminApiError(
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}
export function getAgcModelCatalog(token: string) {
return request<import('./adminApiTypes').AdminAgcModelCatalog>(
'/admin/api/agc-models',
{ token },
);
}
export function saveAgcModelCatalog(
token: string,
body: import('./adminApiTypes').AdminAgcModelCatalog,
) {
return request<import('./adminApiTypes').AdminAgcModelCatalog>(
'/admin/api/agc-models',
{ token, method: 'PUT', body },
);
}
-57
View File
@@ -275,51 +275,6 @@ export interface AdminDatabaseTableStatPayload {
errorMessage: string | null;
}
export interface AdminExternalApiKeyListQuery {
ownerUserId?: string;
publicUserCode?: string;
keyId?: string;
name?: string;
keyPrefix?: string;
createdAfter?: string;
createdBefore?: string;
status?: 'active' | 'revoked';
limit?: number;
offset?: number;
sortColumn?:
| 'keyId'
| 'ownerUserId'
| 'name'
| 'keyPrefix'
| 'createdAt'
| 'lastUsedAt'
| 'updatedAt';
sortDirection?: 'asc' | 'desc';
}
export interface AdminExternalApiKeyPayload {
keyId: string;
ownerUserId: string;
name: string;
keyPrefix: string;
scopes: string[];
createdAt: string;
lastUsedAt: string | null;
revokedAt: string | null;
updatedAt: string;
status: 'active' | 'revoked';
}
export interface AdminExternalApiKeyListResponse {
keys: AdminExternalApiKeyPayload[];
total: number;
limit: number;
offset: number;
scannedCount: number;
scanLimit: number;
scanLimitReached: boolean;
}
export interface AdminDebugHeaderInput {
name: string;
value: string;
@@ -998,15 +953,3 @@ export interface AdminRechargeRefundActionResponse {
export interface AdminWalletRestrictionResponse {
wallet: AdminProfileWalletPayload;
}
export interface AdminAgcModel {
id: string;
alias: string;
modelId: string;
enabled: boolean;
}
export interface AdminAgcModelCatalog {
revision: number;
defaultModelId: string;
models: AdminAgcModel[];
}
-4
View File
@@ -18,7 +18,6 @@ import {
setStoredAdminToken,
} from '../auth/adminAuthStore';
import { AdminAccountsPage } from '../pages/AdminAccountsPage';
import { AdminAgcModelsPage } from '../pages/AdminAgcModelsPage';
import { AdminDashboardPage } from '../pages/AdminDashboardPage';
import { AdminDatabaseTablesPage } from '../pages/AdminDatabaseTablesPage';
import { AdminDebugHttpPage } from '../pages/AdminDebugHttpPage';
@@ -290,9 +289,6 @@ export function AdminApp() {
onUnauthorized={handleUnauthorized}
/>
) : null}
{activeRouteId === 'agc-models' ? (
<AdminAgcModelsPage token={token} onUnauthorized={handleUnauthorized} />
) : null}
{activeRouteId === 'editor-showcase' ? (
<AdminEditorShowcaseReviewPage
token={token}
-1
View File
@@ -50,7 +50,6 @@ const routeIcons = {
'editor-showcase': Star,
'editor-assets': Images,
accounts: Users,
'agc-models': ListChecks,
} satisfies Record<AdminRouteId, typeof LayoutDashboard>;
export function AdminShell({
+1 -6
View File
@@ -16,13 +16,9 @@ export type AdminRouteId =
| 'editor-generation-pricing'
| 'editor-showcase'
| 'editor-assets'
| 'agc-models'
| 'accounts';
export type AdminTabPermission = Exclude<
AdminRouteId,
'accounts' | 'agc-models'
>;
export type AdminTabPermission = Exclude<AdminRouteId, 'accounts'>;
/** 后台导航项定义,hash 是浏览器地址栏和移动底栏共用入口。 */
export interface AdminRouteDefinition {
@@ -51,7 +47,6 @@ export const adminRoutes: AdminRouteDefinition[] = [
label: '模型定价',
hash: '#editor-generation-pricing',
},
{ id: 'agc-models', label: 'AGC 模型', hash: '#agc-models', ownerOnly: true },
{ id: 'editor-showcase', label: '精选审核', hash: '#editor-showcase' },
{ id: 'editor-assets', label: '素材查询', hash: '#editor-assets' },
{ id: 'accounts', label: '账号管理', hash: '#accounts', ownerOnly: true },
@@ -1,56 +0,0 @@
// @vitest-environment jsdom
import {
cleanup,
fireEvent,
render,
screen,
waitFor,
} from '@testing-library/react';
import { afterEach, expect, test, vi } from 'vitest';
import { getAgcModelCatalog, saveAgcModelCatalog } from '../api/adminApiClient';
import { AdminAgcModelsPage } from './AdminAgcModelsPage';
vi.mock('../api/adminApiClient', () => ({
getAgcModelCatalog: vi.fn(),
saveAgcModelCatalog: vi.fn(),
isAdminApiError: vi.fn(() => false),
formatAdminApiError: vi.fn(() => '保存失败'),
}));
vi.mock('../components/useAdminWriteConfirm', () => ({
useAdminWriteConfirm: () => ({
confirmWrite: async () => true,
confirmDialog: null,
}),
}));
afterEach(cleanup);
test('edits alias and upstream model without changing the stable identifier or revision', async () => {
const catalog = {
revision: 3,
defaultModelId: 'quality',
models: [
{ id: 'quality', alias: '高质量', modelId: 'gpt-6-astra', enabled: true },
],
};
vi.mocked(getAgcModelCatalog).mockResolvedValue(catalog);
vi.mocked(saveAgcModelCatalog).mockImplementation(async (_, input) => ({
...input,
revision: 4,
}));
render(<AdminAgcModelsPage token="test" onUnauthorized={vi.fn()} />);
await screen.findByDisplayValue('gpt-6-astra');
fireEvent.change(screen.getByLabelText('模型 1 别名'), {
target: { value: '精细创作' },
});
fireEvent.click(screen.getByRole('button', { name: '保存' }));
await waitFor(() =>
expect(saveAgcModelCatalog).toHaveBeenCalledWith('test', {
...catalog,
models: [{ ...catalog.models[0], alias: '精细创作' }],
}),
);
await waitFor(() => {
expect(screen.getAllByText('已保存').length).toBeGreaterThanOrEqual(2);
});
});
@@ -1,255 +0,0 @@
import { CircleHelp, Plus, RefreshCcw, Save, Trash2 } from 'lucide-react';
import { useEffect, useState } from 'react';
import { getAgcModelCatalog, saveAgcModelCatalog } from '../api/adminApiClient';
import type { AdminAgcModel, AdminAgcModelCatalog } from '../api/adminApiTypes';
import { useAdminWriteConfirm } from '../components/useAdminWriteConfirm';
import { handlePageError } from './pageUtils';
export function AdminAgcModelsPage({
token,
onUnauthorized,
}: {
token: string;
onUnauthorized: (message?: string) => void;
}) {
const [catalog, setCatalog] = useState<AdminAgcModelCatalog | null>(null);
const [busy, setBusy] = useState(false);
const [error, setError] = useState('');
const [saved, setSaved] = useState(false);
const { confirmWrite, confirmDialog } = useAdminWriteConfirm();
async function refresh() {
if (!token) return;
setBusy(true);
setError('');
setSaved(false);
try {
setCatalog(await getAgcModelCatalog(token));
} catch (error) {
handlePageError(error, onUnauthorized, setError);
} finally {
setBusy(false);
}
}
useEffect(() => {
void refresh();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [token]);
function update(id: string, patch: Partial<AdminAgcModel>) {
setSaved(false);
setCatalog(
(current) =>
current && {
...current,
models: current.models.map((model) =>
model.id === id ? { ...model, ...patch } : model,
),
},
);
}
async function save() {
if (!catalog || busy) return;
if (
!(await confirmWrite({
action: '保存 AGC 模型目录',
target: `${catalog.models.length} 个模型`,
}))
)
return;
setBusy(true);
setError('');
setSaved(false);
try {
setCatalog(await saveAgcModelCatalog(token, catalog));
setSaved(true);
} catch (error) {
handlePageError(error, onUnauthorized, setError);
} finally {
setBusy(false);
}
}
return (
<section className="admin-page admin-page-wide admin-agc-models">
<div className="admin-page-heading">
<div>
<h2>AGC </h2>
<p></p>
</div>
<span className="admin-agc-models-revision">
v{catalog?.revision ?? '-'}
</span>
</div>
<div className="admin-agc-models-summary">
<div>
<span></span>
<strong>
{catalog?.models.filter((model) => model.enabled).length ?? 0}
</strong>
</div>
<div>
<span></span>
<strong>
{catalog
? (catalog.models.find(
(model) => model.id === catalog.defaultModelId,
)?.alias ?? '未设置')
: '未设置'}
</strong>
</div>
<div>
<span></span>
<strong>{busy ? '处理中' : saved ? '已保存' : '待修改'}</strong>
</div>
</div>
<section className="admin-panel admin-agc-models-panel">
<div className="admin-panel-heading">
<div>
<h3></h3>
<span></span>
</div>
<CircleHelp size={17} aria-label="模型目录帮助" />
</div>
<div className="admin-agc-models-toolbar">
<button
type="button"
title="重新读取"
aria-label="重新读取模型"
disabled={busy}
onClick={() => void refresh()}
>
<RefreshCcw size={16} />
</button>
<button
type="button"
title="添加模型"
aria-label="添加模型"
disabled={busy || !catalog || catalog.models.length >= 32}
onClick={() => {
setSaved(false);
setCatalog(
(current) =>
current && {
...current,
models: [
...current.models,
{
id: crypto.randomUUID(),
alias: '',
modelId: '',
enabled: true,
},
],
},
);
}}
>
<Plus size={16} />
</button>
<button
type="button"
disabled={busy || !catalog}
onClick={() => void save()}
>
<Save size={16} />
</button>
</div>
{error ? <p role="alert">{error}</p> : null}
{saved ? <p role="status"></p> : null}
{busy ? <p role="status"></p> : null}
<div className="admin-table-wrap admin-agc-models-table">
<table className="admin-table admin-agc-models-table-grid">
<thead>
<tr>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
{catalog?.models.map((model, index) => (
<tr key={model.id}>
<td>
<input
aria-label={`模型 ${index + 1} 别名`}
maxLength={40}
required
value={model.alias}
disabled={busy}
onChange={(e) =>
update(model.id, { alias: e.target.value })
}
/>
</td>
<td>
<input
aria-label={`模型 ${index + 1} 实际模型名`}
maxLength={200}
required
value={model.modelId}
disabled={busy}
onChange={(e) =>
update(model.id, { modelId: e.target.value })
}
/>
</td>
<td>
<input
aria-label={`模型 ${index + 1} 启用`}
type="checkbox"
checked={model.enabled}
disabled={busy || model.id === catalog.defaultModelId}
onChange={(e) =>
update(model.id, { enabled: e.target.checked })
}
/>
</td>
<td>
<input
aria-label={`模型 ${index + 1} 默认`}
name="agc-default-model"
type="radio"
checked={model.id === catalog.defaultModelId}
disabled={busy || !model.enabled}
onChange={() => {
setSaved(false);
setCatalog({ ...catalog, defaultModelId: model.id });
}}
/>
</td>
<td>
<button
type="button"
title="删除模型"
aria-label={`删除模型 ${index + 1}`}
disabled={busy || model.id === catalog.defaultModelId}
onClick={() => {
setSaved(false);
setCatalog({
...catalog,
models: catalog.models.filter(
(candidate) => candidate.id !== model.id,
),
});
}}
>
<Trash2 size={16} />
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
</section>
{confirmDialog}
</section>
);
}
@@ -7,7 +7,6 @@ import { beforeEach, expect, test, vi } from 'vitest';
import {
getAdminDatabaseTableRows,
getAdminDatabaseTables,
getAdminExternalApiKeys,
} from '../api/adminApiClient';
import type { AdminDatabaseTableRowsResponse } from '../api/adminApiTypes';
import {
@@ -21,7 +20,6 @@ vi.mock('../api/adminApiClient', () => ({
),
getAdminDatabaseTableRows: vi.fn(),
getAdminDatabaseTables: vi.fn(),
getAdminExternalApiKeys: vi.fn(),
isAdminApiError: vi.fn(() => false),
}));
@@ -76,7 +74,6 @@ const referralRows = [
beforeEach(() => {
vi.clearAllMocks();
window.location.hash = '#tables?table=profile_referral_relation';
vi.mocked(getAdminExternalApiKeys).mockReset();
vi.mocked(getAdminDatabaseTables).mockResolvedValue({
fetchErrors: [],
tables: ['profile_referral_relation'],
@@ -95,55 +92,6 @@ beforeEach(() => {
});
});
test('external_api_key 使用专用安全查询且详情不展示原始 JSON', async () => {
const user = userEvent.setup();
window.location.hash = '#tables?table=external_api_key';
vi.mocked(getAdminDatabaseTables).mockResolvedValue({
fetchErrors: [],
tables: ['external_api_key'],
});
vi.mocked(getAdminExternalApiKeys).mockResolvedValue({
keys: [
{
keyId: 'external-api-key-1',
ownerUserId: 'user-1',
name: 'agc_auto_generate',
keyPrefix: 'tnr_sk_fixture',
scopes: ['llm:responses'],
createdAt: '2026-08-29T00:00:00Z',
lastUsedAt: null,
revokedAt: null,
updatedAt: '2026-08-29T00:00:00Z',
status: 'active',
},
],
total: 1,
limit: 100,
offset: 0,
scannedCount: 1,
scanLimit: 5000,
scanLimitReached: false,
});
render(
<AdminDatabaseTablesPage token="admin-token" onUnauthorized={vi.fn()} />,
);
await user.type(screen.getByPlaceholderText('精确 ownerUserId'), 'user-1');
await user.click(screen.getByRole('button', { name: '安全查询' }));
await waitFor(() => {
expect(getAdminExternalApiKeys).toHaveBeenLastCalledWith(
'admin-token',
expect.objectContaining({ ownerUserId: 'user-1' }),
);
});
expect(await screen.findByText('tnr_sk_fixture')).toBeTruthy();
await user.click(screen.getByRole('button', { name: '详情' }));
expect(screen.getByRole('dialog')).toBeTruthy();
expect(screen.queryByText('复制 JSON')).toBeNull();
expect(screen.queryByText('key_hash')).toBeNull();
});
test('后台表查询页通过页面级固定栏翻页并提示扫描结果可能不完整', async () => {
const user = userEvent.setup();
vi.mocked(getAdminDatabaseTableRows).mockResolvedValue({
File diff suppressed because it is too large Load Diff
+2 -137
View File
@@ -3093,140 +3093,5 @@ button:disabled {
background: var(--admin-surface, #fff);
}
.admin-detail-modal__panel header,
.admin-detail-modal__actions {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.admin-detail-modal__panel pre {
max-height: 360px;
overflow: auto;
white-space: pre-wrap;
background: #f8fafc;
padding: 12px;
border-radius: 8px;
}
.admin-agc-models {
min-width: 0;
}
.admin-agc-models-revision {
padding: 6px 10px;
border: 1px solid #e7d9cc;
border-radius: 999px;
background: #fffaf6;
color: #9a8170;
font-size: 12px;
font-variant-numeric: tabular-nums;
}
.admin-agc-models-summary {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 12px;
}
.admin-agc-models-summary > div {
display: grid;
gap: 6px;
padding: 16px 18px;
border: 1px solid #eadfd6;
border-radius: 10px;
background: #fffdfa;
}
.admin-agc-models-summary span,
.admin-agc-models-panel > .admin-panel-heading span {
color: #9a8170;
font-size: 12px;
}
.admin-agc-models-summary strong {
overflow: hidden;
color: #3d2a20;
font-size: 20px;
text-overflow: ellipsis;
white-space: nowrap;
}
.admin-agc-models-panel {
border: 1px solid #eadfd6;
border-radius: 10px;
background: #fffdfa;
box-shadow: 0 10px 30px rgb(78 48 28 / 6%);
}
.admin-agc-models-panel > .admin-panel-heading > div {
display: grid;
gap: 4px;
}
.admin-agc-models-panel > .admin-panel-heading > svg {
color: #b9947a;
}
.admin-agc-models-toolbar {
display: flex;
justify-content: flex-end;
gap: 8px;
padding: 4px 0 8px;
}
.admin-agc-models-toolbar button,
.admin-agc-models-table-grid button {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
min-height: 34px;
padding: 0 11px;
border: 1px solid #e2d3c7;
border-radius: 8px;
background: #fffaf6;
color: #684d3d;
font-size: 12px;
font-weight: 700;
cursor: pointer;
}
.admin-agc-models-toolbar button:hover,
.admin-agc-models-toolbar button:focus-visible,
.admin-agc-models-table-grid button:hover,
.admin-agc-models-table-grid button:focus-visible {
border-color: #c99d80;
background: #fff;
outline: none;
}
.admin-agc-models-toolbar button:last-child {
border-color: #a96442;
background: #a96442;
color: #fff;
}
.admin-agc-models-table-grid {
min-width: 720px;
}
.admin-agc-models-table-grid th {
padding-top: 12px;
padding-bottom: 12px;
background: #fcf7f2;
}
.admin-agc-models-table-grid td {
padding-top: 14px;
padding-bottom: 14px;
}
.admin-agc-models-table-grid td input:not([type]) {
width: 100%;
min-width: 180px;
box-sizing: border-box;
padding: 9px 10px;
border: 1px solid #e1d3c8;
border-radius: 7px;
background: #fff;
color: #3d2a20;
}
.admin-agc-models-table-grid td input:not([type]):focus-visible {
border-color: #b97854;
outline: none;
box-shadow: 0 0 0 3px rgb(185 120 84 / 14%);
}
.admin-agc-models-table-grid td:has(input[type='checkbox']),
.admin-agc-models-table-grid td:has(input[type='radio']) {
width: 72px;
text-align: center;
vertical-align: middle;
}
@media (max-width: 680px) {
.admin-agc-models-summary {
grid-template-columns: 1fr;
}
}
.admin-detail-modal__actions { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.admin-detail-modal__panel pre { max-height: 360px; overflow: auto; white-space: pre-wrap; background: #f8fafc; padding: 12px; border-radius: 8px; }
-4
View File
@@ -1,4 +0,0 @@
# resources/plugins 由 build.rs 从 plugins/ 复制生成,属于构建产物。
# 它在 dev 监听范围内,重新生成会让 Tauri dev 误判为源码改动而触发
# “构建 -> 监听 -> 再构建”的自触发循环。
resources/plugins/
@@ -1,14 +1,13 @@
{
"schemaVersion": "game-creator-config.v2",
"agentMode": "codex_app_server",
"llm": {
"apiKey": "",
"baseUrl": "https://dev.genarrative.world/gpt/v1",
"model": "gpt-6-astra",
"model": "gpt-5.6-sol",
"apiKind": "openai_responses",
"reasoningEffort": "max",
"stream": true,
"webSearchEnabled": true,
"webSearchEnabled": false,
"contextWindowTokens": 128000,
"autoCompactTokenLimit": 64000,
"toolOutputTokenLimit": 12000,
@@ -16,5 +15,8 @@
"maxRetries": 2,
"retryBackoffMs": 500
},
"agentLlm": {}
"agentLlm": {},
"planning": {
"capabilityEnabled": true
}
}
+2 -1
View File
@@ -19,6 +19,8 @@
"config": "node scripts/game-creator-config-wizard.mjs",
"test:chat": "node scripts/agent-swarm-test-chat.mjs --task \"制作一个可直接试玩的原创植物塔防小游戏:玩家选择并放置原创守卫阻挡敌人,完成波次后可以进入下一关并重新开始。主题、单位名称与视觉语言必须原创,不使用任何现有游戏角色、单位名、Logo 或受保护视觉语言。请自主完成正式产物、静态检查和双视口试玩验证。\" --no-open",
"test:chat:manual": "node scripts/agent-swarm-test-chat.mjs",
"test:plan": "node scripts/agent-swarm-test-chat.mjs --plan --task \"我想做一款原创横版像素解谜小游戏,主角是一个能操控自己影子的小机器人,影子可以变成平台和开关。请完成立项策划并给出 Fast GDD。主题、角色名与视觉语言必须原创,不使用任何现有游戏角色、名称、Logo 或受保护视觉语言。\"",
"test:plan:manual": "node scripts/agent-swarm-test-chat.mjs --plan",
"agent-run": "node scripts/run-cli-with-config.mjs --agent-run",
"agent-run:smoke": "node scripts/smoke-agent-run-local-provider.mjs",
"agent-runtime:real-e2e": "node scripts/agent-runtime-real-e2e.mjs",
@@ -51,7 +53,6 @@
"focus-trap-react": "^12.0.3",
"lexical": "^0.47.0",
"lucide-react": "^0.546.0",
"phaser": "^4.2.1",
"react": "^19.0.0",
"react-arborist": "^3.16.0",
"react-colorful": "^5.8.0",
@@ -1948,7 +1948,6 @@ async function runE2e(options) {
...process.env,
NO_COLOR: '1',
[platformSessionFixtureEnv]: fixturePath,
GENARRATIVE_AGC_DEBUG_PROVIDER_E2E: '1',
}),
);
childReport = parseChildReport(childResult);
@@ -773,8 +773,7 @@ export async function prepareIsolatedSuiteAppData({
isScopedAgentsSuite() ||
isProjectSkillSuite() ||
isParallelReadSuite() ||
isSupervisorSwarmSuite() ||
isSupervisorAutonomousPlayableLaneDefenseSuite()
isSupervisorSwarmSuite()
? 'private-copy'
: 'hardlink';
try {
@@ -797,7 +796,7 @@ export async function prepareIsolatedSuiteAppData({
storageMode === 'private-copy' &&
(linkedMetadata.dev !== source.metadata.dev ||
linkedMetadata.ino !== source.metadata.ino) &&
(process.platform === 'win32' || (linkedMetadata.mode & 0o077) === 0);
(linkedMetadata.mode & 0o077) === 0;
const hardlinkValid =
storageMode === 'hardlink' &&
linkedMetadata.dev === source.metadata.dev &&
@@ -1977,7 +1976,7 @@ export async function verifyIsolatedSuiteConfigLinksUnchanged() {
linkedMetadata.ino === link.linkedIno &&
(linkedMetadata.dev !== sourceMetadata.dev ||
linkedMetadata.ino !== sourceMetadata.ino) &&
(process.platform === 'win32' || (linkedMetadata.mode & 0o077) === 0)
(linkedMetadata.mode & 0o077) === 0
: linkedMetadata.dev === link.dev && linkedMetadata.ino === link.ino;
const sourceMetadataStable =
sourceMetadata.mode === link.sourceMode &&
@@ -36,6 +36,8 @@ export const ungeneratedGameEntryMarker =
'还没有生成游戏。回到聊天输入创意并确认生成后';
export const defaultRealSwarmTestTask =
'制作一个可直接试玩的原创植物塔防小游戏:玩家选择并放置原创守卫阻挡敌人,完成波次后可以进入下一关并重新开始。主题、单位名称与视觉语言必须原创,不使用任何现有游戏角色、单位名、Logo 或受保护视觉语言。请自主完成正式产物、静态检查和双视口试玩验证。';
export const defaultRealSwarmPlanTask =
'我想做一款原创横版像素解谜小游戏,主角是一个能操控自己影子的小机器人,影子可以变成平台和开关。请完成立项策划并给出 Fast GDD。主题、角色名与视觉语言必须原创,不使用任何现有游戏角色、名称、Logo 或受保护视觉语言。';
export const swarmTurnReportPrefix = '[turn.report] ';
export const swarmTurnReportSchema = 'game-creator-swarm-turn-report.v1';
@@ -138,10 +140,15 @@ export const usage = `用法:
--keep-project 保留自动创建的一次性项目
--no-open 手工模式启动预览但不自动打开浏览器
--task <需求> 通过 manual 入口非交互提交自定义需求
--timeout-minutes <分钟> 设置本次执行期限;自动任务默认 50 分钟,手工模式默认不限时
--plan 走「做方案」立项策划入口,不做游戏,不做产物验收和试玩
--timeout-minutes <分钟> 设置本次执行期限;自动任务默认 50 分钟,--plan 默认 6 分钟,手工模式默认不限时
--dry-run 只检查目录发现和项目准备,不启动 LLM
-h, --help 显示帮助
`;
环境变量:
AGC_PLAN_GDD_DECISION 审批卡自动应答动作,默认 approverevise/reject 必须
同时用 AGC_PLAN_GDD_COMMENT 给出真实修改意见
AGC_PLAN_GDD_COMMENT revise/reject 的意见原文`;
function readOptionValue(args, index, option) {
const value = args[index + 1]?.trim();
@@ -170,6 +177,7 @@ export function parseSwarmTestArguments(args) {
keepProject: false,
openBrowser: true,
task: null,
plan: false,
timeoutMinutes: null,
dryRun: false,
help: false,
@@ -194,6 +202,8 @@ export function parseSwarmTestArguments(args) {
if (task.length > 4_000) throw new Error('--task 不能超过 4000 字符');
options.task = task;
index += 1;
} else if (argument === '--plan') {
options.plan = true;
} else if (argument === '--timeout-minutes') {
if (options.timeoutMinutes !== null) {
throw new Error('--timeout-minutes 只能指定一次');
@@ -212,11 +222,16 @@ export function parseSwarmTestArguments(args) {
}
export function shouldStartPersistentPreview(options) {
return !options.task;
// 立项策划链路只出 GDD,没有可试玩产物,任何模式都不该起预览。
return !options.task && !options.plan;
}
export function resolveSwarmTestTimeoutMs(options) {
const minutes = options.timeoutMinutes ?? (options.task ? 50 : null);
// 立项策划的设计目标是五分钟出方案,给一分钟余量;再久就是卡住了,早失败
// 比让 harness 空等更有用。做游戏那条链路的 50 分钟不变。
const planMinutes = options.plan ? 6 : null;
const minutes =
options.timeoutMinutes ?? planMinutes ?? (options.task ? 50 : null);
return minutes === null ? null : minutes * 60_000;
}
@@ -964,12 +979,25 @@ export function swarmAutoPilotShouldCloseInput(output, promptsAfterSubmit) {
return swarmAutoPilotSitsAtPrompt(output) && promptsAfterSubmit >= 1;
}
// GDD 审批位不能等 CLI 退出之后再处理:Run 停在这里时状态是 waiting-for-user-input
// 而 swarm CLI 恰好把这个状态算作「本轮还在跑」,turn 永远不 settleCLI 也就永远
// 不退出。所以审批必须在 CLI 还活着的时候并发做完,让 Run 自己继续跑到收束。
// 这一句是 PlanGddCompletionBlockerKind::AwaitingApprovalDecision 专有的投影文案,
// 另外三个 blocked 子状态都不会打出它;即便认错了,真正的判据也是随后那次
// --plan-gdd-status,没有待决定审批时不会有任何写入。
const planGddApprovalWaitPattern = /等待 Fast GDD 审批决定/u;
export function swarmOutputAwaitsPlanGddApproval(line) {
return planGddApprovalWaitPattern.test(line);
}
async function runTaskCargo(
cliArguments,
task,
setActiveChild,
timeoutMs,
autoPilot = false,
onPlanGddApprovalWait = null,
) {
const child = spawnChild(cargoCommand, buildCargoCliArguments(cliArguments), {
stdio: ['pipe', 'pipe', 'inherit'],
@@ -981,6 +1009,25 @@ async function runTaskCargo(
let taskSubmitted = false;
let promptsSeen = 0;
let sittingAtPrompt = false;
let planGddApproval = null;
let planGddApprovalError = null;
let planGddApprovalStarted = false;
let planGddApprovalPromise = null;
const startPlanGddApproval = () => {
planGddApprovalStarted = true;
console.log(
`[自动审批] 检测到 Fast GDD 审批位,正在提交 ${resolvePlanGddAutoDecision().action}`,
);
planGddApprovalPromise = onPlanGddApprovalWait()
.then((value) => {
planGddApproval = value;
})
.catch((error) => {
planGddApprovalError = error;
// 审批没成的话 Run 会一直停在等待位,干等到超时只会把真正的原因埋掉。
void terminateChildTree(child).catch(() => {});
});
};
child.stdout.setEncoding('utf8');
child.stdout.on('data', (chunk) => {
process.stdout.write(chunk);
@@ -993,6 +1040,13 @@ async function runTaskCargo(
reportLines.push(normalizedLine);
settled = true;
}
if (
onPlanGddApprovalWait &&
!planGddApprovalStarted &&
swarmOutputAwaitsPlanGddApproval(normalizedLine)
) {
startPlanGddApproval();
}
}
if (!autoPilot || child.stdin.writableEnded) return;
const atPrompt = swarmAutoPilotSitsAtPrompt(pendingLine);
@@ -1031,9 +1085,12 @@ async function runTaskCargo(
if (normalizedPendingLine.startsWith(swarmTurnReportPrefix)) {
reportLines.push(normalizedPendingLine);
}
await planGddApprovalPromise;
if (planGddApprovalError) throw planGddApprovalError;
return {
...result,
turnReportOutput: reportLines.join('\n'),
planGddApproval,
};
} finally {
setActiveChild(null);
@@ -1723,6 +1780,196 @@ export async function validateSwarmProjectArtifacts(projectPath, options) {
return inspection;
}
// 这四条路径的权威定义都在 Rust 侧 `planning_storage.rs``PLAN_SESSION_PATH`、
// `PLAN_GDD_INDEX_PATH`、`PLAN_STORAGE_ROOT`、`PLAN_FAST_GDD_PATH`)。跨语言没有共享
// 常量的通道,改路径时要连同 `GddApprovalCard.tsx` 一起动。
export const planningOutputPaths = [
'.agent/planning/session.json',
'.agent/planning/index.json',
'.agent/planning/pending.json',
'game/fast_gdd.md',
];
export async function inspectPlanningOutputs(projectPath) {
const outputs = [];
for (const relativePath of planningOutputPaths) {
const absolutePath = path.join(projectPath, ...relativePath.split('/'));
const metadata = await lstat(absolutePath).catch((error) => {
if (error?.code === 'ENOENT') return null;
throw error;
});
outputs.push({
path: relativePath,
exists: Boolean(metadata?.isFile()),
bytes: metadata?.isFile() ? metadata.size : 0,
});
}
return outputs;
}
async function reportPlanningOutputs(projectPath) {
const outputs = await inspectPlanningOutputs(projectPath);
console.log('\n立项策划产物:');
for (const output of outputs) {
console.log(
output.exists
? ` [有] ${output.path}${output.bytes} 字节)`
: ` [无] ${output.path}`,
);
}
}
const planGddApprovalTimeoutMs = 60_000;
export const planGddStatusOutputPrefix = 'planGddStateJson=';
export const planGddDecisionOutputPrefix = 'planGddDecisionJson=';
function parsePrefixedJsonLine(output, prefix, label) {
const line = output
.split('\n')
.map((value) => (value.endsWith('\r') ? value.slice(0, -1) : value))
.find((value) => value.startsWith(prefix));
if (!line) throw new Error(`${label}缺少 ${prefix} 输出`);
try {
return JSON.parse(line.slice(prefix.length));
} catch (error) {
throw new Error(`解析${label}失败:${error.message}`);
}
}
export function parsePlanGddStatusOutput(output) {
return parsePrefixedJsonLine(
output,
planGddStatusOutputPrefix,
'Fast GDD 审批状态',
);
}
export function parsePlanGddDecisionOutput(output) {
return parsePrefixedJsonLine(
output,
planGddDecisionOutputPrefix,
'Fast GDD 审批回执',
);
}
// 审批卡是这条链路唯一的人类判据,所以自动应答默认只投 approve,且只在投影确实有
// 一张待决定审批时出手。revise/reject 需要一段真实的修改意见,让机器编一段等于把
// 判据换成噪声——所以那两条分支只在跑的人自己用 AGC_PLAN_GDD_COMMENT 给出意见时
// 才走。手工调 --plan-gdd-decide 也能达到同样效果,但那要求 plan 根 run 仍然活着,
// 而它恰好是本进程持有的 CLI 子进程。
export function planGddAutoApprovalIsPending(state) {
return Boolean(state?.pendingApproval);
}
export function resolvePlanGddAutoDecision(env = process.env) {
const action = (env.AGC_PLAN_GDD_DECISION ?? 'approve').trim();
if (!['approve', 'revise', 'reject'].includes(action)) {
throw new Error('AGC_PLAN_GDD_DECISION 只能是 approve / revise / reject');
}
const comment = (env.AGC_PLAN_GDD_COMMENT ?? '').trim();
if (action === 'approve') return { action, comment: null };
if (!comment) {
throw new Error(
`${action} 必须同时设 AGC_PLAN_GDD_COMMENT 提供真实修改意见`,
);
}
return { action, comment };
}
async function settlePlanGddApproval(
projectPath,
runtimeConfigPath,
setActiveChild,
) {
const readStatus = async () => {
const result = await runCapturedCargo(
['--config-dir', runtimeConfigPath, '--plan-gdd-status', projectPath],
setActiveChild,
{
timeoutMs: planGddApprovalTimeoutMs,
label: 'Fast GDD 审批状态查询',
},
);
if (result.code !== 0 || result.signal) {
throw new Error(
`读取 Fast GDD 审批状态失败:${result.stderr.trim() || result.stdout.trim()}`,
);
}
return parsePlanGddStatusOutput(result.stdout);
};
const before = await readStatus();
if (!planGddAutoApprovalIsPending(before)) {
return { decided: false, state: before };
}
const { action, comment } = resolvePlanGddAutoDecision();
const decision = await runCapturedCargo(
[
'--config-dir',
runtimeConfigPath,
'--plan-gdd-decide',
projectPath,
action,
...(comment === null ? [] : ['--stdin']),
],
setActiveChild,
{
timeoutMs: planGddApprovalTimeoutMs,
label: 'Fast GDD 审批决定',
stdin: comment,
},
);
if (decision.code !== 0 || decision.signal) {
throw new Error(
`提交 Fast GDD 审批决定失败:${decision.stderr.trim() || decision.stdout.trim()}`,
);
}
const receipt = parsePlanGddDecisionOutput(decision.stdout);
// 回执落盘和唤醒后台任务是两件事:decide 命令把唤醒失败降级成 recoveryPending
// 于是审批已经生效、Run 却仍停在 waiting-for-user-input。实测就是这样——只有
// 补一次 --agent-resume 才会重新起 turn。这是仓库自己给这个状态定义的恢复动作。
let recovered = false;
if (receipt.recoveryPending) {
const resume = await runCapturedCargo(
['--config-dir', runtimeConfigPath, '--agent-resume', projectPath],
setActiveChild,
{
timeoutMs: planGddApprovalTimeoutMs,
label: 'Fast GDD 审批后恢复后台任务',
},
);
if (resume.code !== 0 || resume.signal) {
throw new Error(
`审批已提交但恢复后台任务失败:${resume.stderr.trim() || resume.stdout.trim()}`,
);
}
recovered = true;
}
return { decided: true, receipt, recovered, state: await readStatus() };
}
async function reportPlanGddApproval(approval) {
const { state } = approval;
console.log('\nFast GDD 审批:');
if (!approval.decided) {
console.log(` [无待决定审批] 当前投影状态=${state.state}`);
return;
}
console.log(
` [已决定 ${approval.receipt.decisionRef.action}] outcome=${approval.receipt.outcome} v${approval.receipt.decisionRef.version} 投影状态=${state.state}`,
);
if (approval.recovered) {
console.log(
' [已恢复] 审批回执的 recoveryPending 由一次 --agent-resume 收口',
);
}
if (state.session) {
console.log(
` 澄清轮次=${state.session.clarificationRound} 返工深度=${state.session.repairDepth} phase=${state.session.phase}`,
);
}
}
export async function hasConfiguredEditorApiKey(configDir) {
let configured = false;
for (const fileName of [configFileName, localConfigFileName]) {
@@ -1875,7 +2122,13 @@ export async function runSwarmTestChat(options) {
const setActiveChild = (child) => {
activeChild = child;
};
// GDD 审批要和 swarm CLI 并发跑,两者不能共用 activeChild 这一个槽位:审批子进程
// 结束时的 setActiveChild(null) 会把 CLI 从槽里抹掉,Ctrl-C 就杀不到它了。
const concurrentChildren = new Set();
const setConcurrentChild = (child) => {
if (child) concurrentChildren.add(child);
else concurrentChildren.clear();
};
const stopRequested = () => receivedSignal !== null;
const handleSignal = (signal) => {
const repeatedSignal = receivedSignal !== null;
@@ -1953,10 +2206,11 @@ export async function runSwarmTestChat(options) {
);
}
console.log('LLM 配置已就绪。');
const requirementNoun = options.plan ? '立项策划需求' : '游戏需求';
console.log(
options.task
? '已提交一条非交互游戏需求,正在等待 Swarm 自主完成。\n'
: '输入一条游戏需求并回车;提交后按 Ctrl+D,让 Swarm 自主完成。\n',
? `已提交一条非交互${requirementNoun},正在等待 Swarm 自主完成。\n`
: `输入一条${requirementNoun}并回车;提交后按 Ctrl+D,让 Swarm 自主完成。\n`,
);
phase = 'chat';
@@ -1966,7 +2220,8 @@ export async function runSwarmTestChat(options) {
runtimeConfig.path,
'--swarm-chat',
'--init',
'--autonomous-game-build',
// 做方案链路只能跑 standard 档,后端对 plan + autonomous 是硬否决。
options.plan ? '--plan' : '--autonomous-game-build',
project.path,
];
let chat;
@@ -1979,6 +2234,15 @@ export async function runSwarmTestChat(options) {
timeoutDeadline === null
? null
: Math.max(1, timeoutDeadline - Date.now()),
options.plan,
options.plan
? () =>
settlePlanGddApproval(
project.path,
runtimeConfig.path,
setConcurrentChild,
)
: null,
)
: await runInteractiveCargo(chatArguments, setActiveChild);
} catch (error) {
@@ -1994,6 +2258,32 @@ export async function runSwarmTestChat(options) {
if (options.task) {
turnReport = parseSettledSwarmTurnReport(chat.turnReportOutput);
}
if (options.plan) {
// 立项策划不出游戏产物,正式验收在 GDD 审批卡上;这里只报告落盘情况,
// 是否收束已经由 CLI 的退出码判过了。
// 自动任务档的审批已经在 CLI 运行期间并发做完了;手工档(人自己敲 Ctrl+D
// 退出)没有那次触发,退出后补一次,没有待决定审批时它是只读的。
phase = 'plan-approval';
const approval =
chat.planGddApproval ??
(await settlePlanGddApproval(
project.path,
runtimeConfig.path,
setConcurrentChild,
));
if (receivedSignal) break session;
phase = 'plan-report';
await reportPlanGddApproval(approval);
await reportPlanningOutputs(project.path);
phase = 'complete';
console.log(
approval.decided
? '\n立项策划链路已收束:Fast GDD 已批准,策划产物见上方清单。'
: '\n立项策划链路已收束:Run 正常结束但没有待决定审批,策划产物见上方清单。',
);
break session;
}
phase = 'artifact-validation';
const requireEditorImages = await hasConfiguredEditorApiKey(
runtimeConfig.path,
);
@@ -107,26 +107,12 @@ const rustSharedContractSource = fs.readFileSync(
'utf8',
);
const allowedUncalledTauriCommands = [
'append_direct_project_conversation_message',
'chat_with_game_creator_agent',
'check_ui_editor_font_glyph_coverage',
'create_ui_design_resource',
'open_game_creator_launcher_window',
'open_game_creator_workspace_window',
'read_direct_project_conversation',
'stop_local_game_preview_if_matches',
'start_game_creator_external_mcp',
'stop_game_creator_external_mcp',
'list_agc_plugins',
'list_agc_extensions',
'refresh_agc_plugins',
'start_agc_plugin',
'stop_agc_plugin',
'reload_agc_plugin',
'call_agc_plugin',
'read_agc_plugin_panel',
'set_agc_plugin_project_path',
'set_agc_plugin_enabled',
];
const sourceExtensions = new Set([
'.json',
@@ -1295,7 +1281,7 @@ if (tauriConfig.identifier !== 'world.genarrative.ai-game-creator') {
throw new Error('AI game creator shell identifier drifted');
}
const expectedBundledWindowsResources = {
const expectedBundledCodexResources = {
'resources/codex/win-x64/bin/codex.exe': 'codex/win-x64/bin/codex.exe',
'resources/codex/win-x64/bin/codex-code-mode-host.exe':
'codex/win-x64/bin/codex-code-mode-host.exe',
@@ -1309,7 +1295,6 @@ const expectedBundledWindowsResources = {
'codex/win-x64/codex-package.json',
'resources/codex/win-x64/NOTICE.md': 'codex/win-x64/NOTICE.md',
'resources/codex/win-x64/manifest.json': 'codex/win-x64/manifest.json',
'resources/plugins': 'plugins',
};
if (tauriConfig.bundle?.resources !== undefined) {
throw new Error(
@@ -1318,8 +1303,8 @@ if (tauriConfig.bundle?.resources !== undefined) {
}
assert.deepEqual(
windowsTauriConfig.bundle?.resources,
expectedBundledWindowsResources,
'AI game creator shell Windows Tauri config must bundle the complete pinned Codex resource set and Cocos bridge payload directory',
expectedBundledCodexResources,
'AI game creator shell Windows Tauri config must bundle the complete pinned Codex resource set',
);
if (windowsTauriConfig.bundle?.useLocalToolsDir !== true) {
throw new Error(
@@ -1716,16 +1701,10 @@ if (
runtimeConfigSetupStart === -1 ||
runtimeConfigSetupEnd === -1 ||
!runtimeConfigSetupSource.includes('sanitize_diagnostic_message(') ||
!runtimeConfigSetupSource.includes('setup_log.fail(') ||
!runtimeConfigSetupSource.includes('append_bounded_diagnostic_line(') ||
!runtimeConfigSetupSource.includes(
'startup.appdata.configure.failed details={details}',
) ||
!tauriHandlerSource.includes('impl StartupLogSlot {') ||
!tauriHandlerSource.includes('append_bounded_diagnostic_line(&path, line)') ||
!tauriHandlerSource.includes(
'self.append(line);\n show_startup_error_dialog(self.path().as_deref());',
) ||
!tauriHandlerSource.includes('early_startup_log_path(')
)
) {
throw new Error(
'AI game creator setup must configure the runtime AppData directory and log sanitized setup failures',
@@ -1774,8 +1753,9 @@ for (const snippet of [
"'read_game_creator_app_config'",
"'write_game_creator_app_config'",
'aria-label="运行时配置"',
'陶泥儿智能创作(固定)',
'官方账号服务(固定)',
'LLM API Key',
'External Editor Base URL',
'External Editor API Key',
'runtime_config.save',
"'/run:运行自检,启动本地 HTTP 预览并载入客户端运行视图'",
"'activate_local_game_preview'",
@@ -26,7 +26,6 @@ const repositoryRoot = path.resolve(appRoot, '..', '..');
const cargoManifestPath = path.join(appRoot, 'src-tauri', 'Cargo.toml');
const defaultConfigPath = path.join(appRoot, configFileName);
const localConfigFileName = 'game-creator.config.local.json';
const gameCreatorConfigSchemaVersion = 'game-creator-config.v2';
const cargoCommand = process.platform === 'win32' ? 'cargo.exe' : 'cargo';
const npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm';
@@ -212,7 +211,6 @@ export function buildGameCreatorWizardConfig(existingConfig, llmInput) {
}
return {
...source,
schemaVersion: gameCreatorConfigSchemaVersion,
agentMode: 'provider',
llm: {
...previousLlm,
@@ -7,7 +7,6 @@ import path from 'node:path';
import { fileURLToPath } from 'node:url';
const appRoot = fileURLToPath(new URL('..', import.meta.url));
const inheritedChildEnvironment = { ...globalThis['process']['env'] };
const localConfigPath = path.join(appRoot, 'game-creator.config.local.json');
const projectRoot = path.join(
os.tmpdir(),
@@ -672,11 +671,6 @@ function runAgent() {
{
cwd: appRoot,
stdio: ['pipe', 'pipe', 'pipe'],
env: {
...inheritedChildEnvironment,
// 该 smoke 只使用一次性 loopback Provider;生产路由仍保持锁定。
GENARRATIVE_AGC_DEBUG_PROVIDER_E2E: '1',
},
},
);
let stdout = '';
@@ -722,7 +716,7 @@ function runAgent() {
stderr += chunk.toString();
});
child.on('error', reject);
child.on('close', (code, signal) => {
child.on('close', (code) => {
const output = `${stdout}${stderr}`;
if (previewReadError) {
reject(previewReadError);
@@ -740,24 +734,13 @@ function runAgent() {
previewDom,
});
} else {
reject(
new Error(
`agent run failed: exitCode=${code}, signal=${signal ?? 'none'}\n` +
`stderr tail (last 8000 characters):\n${stderr.slice(-8000)}\n` +
`stdout tail (last 4000 characters):\n${stdout.slice(-4000)}`,
),
);
reject(new Error(output || `agent run exited with ${code}`));
}
});
});
}
async function seedLocalAsset() {
await fs.mkdir(path.join(projectRoot, 'game'), { recursive: true });
await fs.writeFile(
path.join(projectRoot, 'game/index.html'),
'<!doctype html><html lang="zh-CN"><meta charset="UTF-8"><body>还没有生成游戏</body></html>',
);
await fs.mkdir(path.join(projectRoot, 'assets/uploads'), { recursive: true });
await fs.mkdir(path.join(projectRoot, '.agent'), { recursive: true });
await seedConversationContext();
@@ -1,16 +1,10 @@
import { spawn, spawnSync } from 'node:child_process';
import { spawn } from 'node:child_process';
import { existsSync, readdirSync, readFileSync } from 'node:fs';
import http from 'node:http';
import net from 'node:net';
import { resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import {
normalizeWindowsPath,
parseWindowsProcessSnapshot,
stopWindowsProcessTree,
stopWindowsWorktreeProcesses,
} from '../../../scripts/dev-windows-process.mjs';
import {
agcVitePortEnvKey,
readAgcDevEndpoint,
@@ -21,10 +15,6 @@ import {
const appRoot = fileURLToPath(new URL('..', import.meta.url));
const repoRoot = resolve(appRoot, '../..');
const devStackStatePath = resolve(repoRoot, '.app/dev-stack.json');
const apiServerExePath = resolve(
repoRoot,
'server-rs/target/debug/api-server.exe',
);
const defaultApiTarget =
process.env.RUST_SERVER_TARGET || 'http://127.0.0.1:8082';
const backendDatabase = 'genarrative-game-creator-dev';
@@ -137,217 +127,19 @@ function readBackendTargets({ requireAgcBackend = false } = {}) {
});
}
function readBackendServiceFailure(
state,
{
expectedDatabase = backendDatabase,
expectedSpacetimeDataDir = backendSpacetimeDataDir,
} = {},
) {
const targets = resolveBackendTargetsFromState(state, {
requireAgcBackend: true,
expectedDatabase,
expectedSpacetimeDataDir,
});
if (!targets.hasMatchingBackend) {
return null;
}
for (const serviceName of ['spacetime', 'api-server', 'bgfilter-worker']) {
const service = state?.services?.[serviceName];
if (service?.status !== 'failed') {
continue;
}
return {
serviceName,
failure: service.signal
? `signal=${service.signal}`
: `code=${service.exitCode ?? 1}`,
};
}
return null;
}
function urlPort(url) {
try {
const port = Number(new URL(url).port);
return Number.isInteger(port) && port > 0 ? port : 0;
} catch {
return 0;
}
}
// 读取端口当前真正的监听进程身份。返回 null 表示探测本身不可用(例如缺少
// Get-NetTCPConnection),此时调用方必须退化为旧行为,不能让本地启动直接失败。
function readWindowsPortOwnerIdentities(
ports,
{ spawnImpl = spawnSync, env = process.env } = {},
) {
const uniquePorts = [...new Set(ports.filter((port) => port > 0))];
if (uniquePorts.length === 0) {
return null;
}
const command = [
'$ErrorActionPreference = "SilentlyContinue"',
'$ports = ($env:GENARRATIVE_QUERY_PORTS -split ",") | Where-Object { $_ }',
'$result = @()',
'foreach ($port in $ports) {',
' $connection = Get-NetTCPConnection -State Listen -LocalPort ([int]$port) -ErrorAction SilentlyContinue | Select-Object -First 1',
' if (-not $connection) { continue }',
' $owner = Get-CimInstance Win32_Process -Filter ("ProcessId=" + $connection.OwningProcess) -ErrorAction SilentlyContinue',
' $result += [pscustomobject]@{ port = [int]$port; processId = [int]$connection.OwningProcess; name = $owner.Name; executablePath = $owner.ExecutablePath; commandLine = $owner.CommandLine }',
'}',
'ConvertTo-Json -InputObject @($result) -Compress',
].join('\n');
const result = spawnImpl(
'powershell.exe',
['-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command', command],
{
encoding: 'utf8',
env: { ...env, GENARRATIVE_QUERY_PORTS: uniquePorts.join(',') },
maxBuffer: 8 * 1024 * 1024,
},
);
if (result?.error || result?.status !== 0) {
return null;
}
const owners = new Map();
for (const entry of parseWindowsProcessSnapshot(result.stdout)) {
const port = Number(entry?.port);
if (Number.isInteger(port) && port > 0) {
owners.set(port, entry);
}
}
return owners;
}
function isWorktreeApiServerOwner(
owner,
{ expectedExePath = apiServerExePath } = {},
) {
if (!owner) {
return false;
}
const expected = normalizeWindowsPath(expectedExePath);
const actual = normalizeWindowsPath(owner.executablePath);
return Boolean(expected) && actual === expected;
}
function isWorktreeSpacetimeOwner(
owner,
{ expectedDataDir = backendSpacetimeDataDir } = {},
) {
if (!owner) {
return false;
}
const expected = normalizeWindowsPath(expectedDataDir);
if (!expected) {
return false;
}
const name = String(owner.name ?? '').toLowerCase();
if (!name.startsWith('spacetime')) {
return false;
}
return normalizeWindowsPath(owner.commandLine).includes(expected);
}
// 端口健康不代表后端属于当前工作树:上个工作树 Ctrl+C 残留的 api-server 仍会
// 应答 /healthz。复用前必须证明端口上的进程就是本工作树的可执行文件与数据目录。
function verifyAgcBackendOwnership({
apiUrl,
spacetimeUrl,
bgfilterWorkerUrl,
platform = process.platform,
expectedExePath = apiServerExePath,
expectedDataDir = backendSpacetimeDataDir,
readPortOwners = readWindowsPortOwnerIdentities,
} = {}) {
if (platform !== 'win32') {
return { ok: true, reason: 'platform-unsupported', owners: new Map() };
}
const ports = [
urlPort(apiUrl),
urlPort(bgfilterWorkerUrl),
urlPort(spacetimeUrl),
];
const owners = readPortOwners(ports);
if (!owners) {
return { ok: true, reason: 'owner-probe-unavailable', owners: new Map() };
}
const apiOwner = owners.get(urlPort(apiUrl));
if (!isWorktreeApiServerOwner(apiOwner, { expectedExePath })) {
return { ok: false, reason: 'api-server-owner-mismatch', owners, apiOwner };
}
const workerOwner = owners.get(urlPort(bgfilterWorkerUrl));
if (!isWorktreeApiServerOwner(workerOwner, { expectedExePath })) {
return {
ok: false,
reason: 'bgfilter-worker-owner-mismatch',
owners,
workerOwner,
};
}
const spacetimeOwner = owners.get(urlPort(spacetimeUrl));
if (!isWorktreeSpacetimeOwner(spacetimeOwner, { expectedDataDir })) {
return {
ok: false,
reason: 'spacetime-owner-mismatch',
owners,
spacetimeOwner,
};
}
return { ok: true, reason: 'owned', owners };
}
function formatOwnerLabel(owner) {
if (!owner) {
return '未知进程';
}
const pid = Number(owner.processId);
const label = owner.executablePath || owner.commandLine || owner.name || '';
return `${Number.isInteger(pid) ? `pid=${pid} ` : ''}${String(label).trim()}`.trim();
}
async function isBackendReady({
state = readJson(devStackStatePath),
isReady = isHttpReady,
verifyOwnership = verifyAgcBackendOwnership,
onOwnershipRejected = null,
} = {}) {
const { apiUrl, spacetimeUrl, bgfilterWorkerUrl, hasMatchingBackend } =
resolveBackendTargetsFromState(state, {
requireAgcBackend: true,
});
if (!hasMatchingBackend || !apiUrl || !spacetimeUrl || !bgfilterWorkerUrl) {
return false;
}
const ownership = await verifyOwnership({
apiUrl,
spacetimeUrl,
bgfilterWorkerUrl,
});
if (!ownership?.ok) {
onOwnershipRejected?.(ownership);
return false;
}
if (ownership.reason === 'owner-probe-unavailable') {
console.warn(
'[ai-game-creator-shell] 无法读取端口监听进程归属,本次按旧行为复用配套后端。',
);
}
return (
hasMatchingBackend &&
Boolean(apiUrl) &&
Boolean(spacetimeUrl) &&
Boolean(bgfilterWorkerUrl) &&
(await isReady(`${apiUrl}/healthz`)) &&
(await isReady(`${spacetimeUrl}/v1/ping`)) &&
(await isReady(`${bgfilterWorkerUrl}/readyz`))
@@ -436,10 +228,6 @@ function spawnChild(command, args, options, spawnImpl = spawn) {
const child = spawnImpl(command, args, {
...options,
shell: useShell,
// npm.cmd and the Windows shell otherwise create a visible console for
// every service in the dev stack. Their stdout/stderr is already inherited
// by the launcher, so no separate terminal window is useful.
windowsHide: process.platform === 'win32' ? true : options.windowsHide,
// POSIX 下让每个长驻服务拥有独立进程组,退出时可以连同 npm、
// Node、Cargo 及其子进程一起清理,避免残留订阅任务持续刷日志。
detached: isPosix,
@@ -664,18 +452,14 @@ async function terminateChildTree(
return { stopped: true, forced: false };
}
const result = await taskkillImpl(child.pid);
const taskkillStopped =
!result?.timedOut &&
!result?.error &&
[0, 128].includes(result?.code ?? 0);
if (taskkillStopped) {
return { stopped: true, forced: true, result };
}
// 包装层(cmd.exe / npm.cmd)先被 Ctrl+C 杀掉时 taskkill 拿不到活着的 PID
// 这里继续按记录下来的根 PID 遍历,尽量收掉更深的后端进程。
const treeStopped = stopWindowsProcessTree(child.pid);
return { stopped: treeStopped.length > 0, forced: true, result };
return {
stopped:
!result?.timedOut &&
!result?.error &&
[0, 128].includes(result?.code ?? 0),
forced: true,
result,
};
}
const processGroupId = childLifecycles.get(child)?.processGroupId;
@@ -721,43 +505,11 @@ async function terminateChildTree(
return { stopped, forced: true };
}
async function waitForBackendReady(
backendChild,
timeoutMs = 600_000,
{
checkBackendReady = (onOwnershipRejected) =>
isBackendReady({ onOwnershipRejected }),
readState = () => readJson(devStackStatePath),
resolveTargets = readBackendTargets,
} = {},
) {
const initialStateUpdatedAt = readState()?.updatedAt ?? '';
async function waitForBackendReady(backendChild, timeoutMs = 600_000) {
const startedAt = Date.now();
let lastOwnershipReason = '';
while (Date.now() - startedAt < timeoutMs) {
if (
await checkBackendReady((ownership) => {
if (ownership.reason === lastOwnershipReason) {
return;
}
lastOwnershipReason = ownership.reason;
// 本次自己拉起的后端如果归属校验一直不通过,必须把原因打出来,
// 否则只会表现为等待 600 秒后超时。
console.warn(
`[ai-game-creator-shell] 等待配套后端就绪时归属校验未通过(${ownership.reason}: ${formatOwnerLabel(ownership.apiOwner ?? ownership.spacetimeOwner ?? ownership.workerOwner)})。`,
);
})
) {
return resolveTargets();
}
const state = readState();
if ((state?.updatedAt ?? '') !== initialStateUpdatedAt) {
const serviceFailure = readBackendServiceFailure(state);
if (serviceFailure) {
throw new Error(
`配套后端启动失败: ${serviceFailure.serviceName} ${serviceFailure.failure}`,
);
}
if (await isBackendReady()) {
return readBackendTargets();
}
const failure = readChildFailure(backendChild);
if (failure) {
@@ -773,14 +525,7 @@ async function waitForBackendReady(
async function ensureBackend({
onBackendChild = () => {},
checkBackendReady = () =>
isBackendReady({
onOwnershipRejected(ownership) {
console.warn(
`[ai-game-creator-shell] 端口上的配套后端不属于当前工作树(${ownership.reason}: ${formatOwnerLabel(ownership.apiOwner ?? ownership.spacetimeOwner ?? ownership.workerOwner)}),改为启动本工作树自己的后端。`,
);
},
}),
checkBackendReady = isBackendReady,
resolveTargets = readBackendTargets,
spawnBackend = () =>
spawnChild(
@@ -795,7 +540,6 @@ async function ensureBackend({
backendDatabase,
'--spacetime-data-dir',
backendSpacetimeDataDir,
'--preserve-database',
'--no-interactive',
],
{ cwd: appRoot },
@@ -860,33 +604,15 @@ async function startVite(apiTarget, endpoint = readAgcDevEndpoint()) {
async function main() {
let backendChild = null;
let startedBackend = false;
let viteChild = null;
let shutdownSignal = '';
const signalHandlers = new Map();
// 只有本次会话真正拉起过配套后端时才做兜底清扫:复用别人后端时不能连带
// 杀掉对方的进程。dev.mjs 的清理依赖它的 shell 包装层仍然活着,而 Ctrl+C
// 往往先杀掉包装层,所以这里必须按本工作树 api-server.exe 的身份再收一次。
const sweepStartedBackend = () => {
if (!startedBackend || process.platform !== 'win32') {
return;
}
const stopped = stopWindowsWorktreeProcesses({ apiServerExePath });
if (stopped.length > 0) {
console.log(
`[ai-game-creator-shell] 已清理残留后端进程: ${stopped.join(', ')}`,
);
}
};
for (const signal of ['SIGINT', 'SIGTERM']) {
const handler = () => {
shutdownSignal = signal;
stopChild(viteChild, signal);
stopChild(backendChild, signal);
// 立刻清扫,避免外层 taskkill /F 抢在 finally 之前把本进程杀掉。
sweepStartedBackend();
};
signalHandlers.set(signal, handler);
process.on(signal, handler);
@@ -905,7 +631,6 @@ async function main() {
},
});
backendChild = backend.backendChild;
startedBackend = Boolean(backendChild);
if (shutdownSignal) {
throw new Error(`启动期收到 ${shutdownSignal},已停止配套后端`);
}
@@ -939,7 +664,6 @@ async function main() {
terminateChildTree(viteChild),
terminateChildTree(backendChild),
]);
sweepStartedBackend();
for (const [signal, handler] of signalHandlers) {
process.off(signal, handler);
}
@@ -956,25 +680,17 @@ function isDirectModuleExecution() {
export {
ensureBackend,
formatChildFailure,
formatOwnerLabel,
isAiGameCreatorServer,
isBackendReady,
isDirectModuleExecution,
isProcessGroupAlive,
isWorktreeApiServerOwner,
isWorktreeSpacetimeOwner,
preflightExistingVite,
readBackendServiceFailure,
readChildFailure,
readExistingViteServer,
readLinuxProcessGroupAlive,
readWindowsPortOwnerIdentities,
resolveBackendTargetsFromState,
runWindowsTaskkill,
spawnChild,
stopChild,
terminateChildTree,
verifyAgcBackendOwnership,
waitForBackendReady,
waitForChildTermination,
};
@@ -7,10 +7,7 @@ import {
withAgcDevEndpointEnv,
} from './dev-port.mjs';
import {
isAiGameCreatorServer,
preflightExistingVite,
readChildFailure,
readExistingViteServer,
spawnChild,
stopChild,
terminateChildTree,
@@ -23,9 +20,7 @@ const tauriCliPath = resolve(repoRoot, 'node_modules/@tauri-apps/cli/tauri.js');
function buildTauriArguments(argv, devUrl = readAgcDevEndpoint().url) {
const args = [...argv];
const configOverride = JSON.stringify({
build: { devUrl, beforeDevCommand: '' },
});
const configOverride = JSON.stringify({ build: { devUrl } });
const separatorIndex = args.indexOf('--');
if (separatorIndex < 0) {
return ['dev', ...args, '--config', configOverride];
@@ -43,28 +38,6 @@ function buildTauriArguments(argv, devUrl = readAgcDevEndpoint().url) {
];
}
// `agc_cocos_execute` 与 Cocos 编辑器适配器只在 `cocos-editor-execute` feature 下
// 注册。开发构建默认在 Windows 打开它,否则 Agent 的工具清单里根本没有该工具,
// 只能退化成改写脚本。可用 AGC_DEV_CARGO_FEATURES(逗号分隔)覆盖,传空串即关闭。
function readDevCargoFeatures(env = process.env) {
const override = env.AGC_DEV_CARGO_FEATURES;
if (override !== undefined) {
return override
.split(',')
.map((value) => value.trim())
.filter(Boolean);
}
return process.platform === 'win32' ? ['cocos-editor-execute'] : [];
}
function withDevCargoFeatures(argv, features = readDevCargoFeatures()) {
if (features.length === 0) return argv;
if (argv.some((value) => value === '--features' || value === '-f')) {
return argv;
}
return [`--features=${features.join(',')}`, ...argv];
}
function spawnTauriCli(argv, { env = process.env } = {}) {
return spawnChild(process.execPath, [tauriCliPath, ...argv], {
cwd: appRoot,
@@ -78,7 +51,6 @@ async function runTauriDev(
{
resolveDevEndpoint = resolveAgcDevEndpoint,
preflight = preflightExistingVite,
prepareFrontend = prepareFrontendDev,
spawnCli = spawnTauriCli,
waitForCli = waitForChildTermination,
terminateTree = terminateChildTree,
@@ -87,9 +59,10 @@ async function runTauriDev(
const endpoint = await resolveDevEndpoint();
await preflight({ endpoint });
let child = null;
let frontendChild = null;
const preparationAbort = new AbortController();
const tauriArguments = buildTauriArguments(argv, endpoint.url);
const child = spawnCli(tauriArguments, {
env: withAgcDevEndpointEnv(endpoint),
});
let resolveShutdown;
let shutdownSignal = '';
let repeatedSignal = false;
@@ -103,50 +76,21 @@ async function runTauriDev(
if (!shutdownSignal) {
shutdownSignal = signal;
stopChild(child, 'SIGTERM');
stopChild(frontendChild, 'SIGTERM');
preparationAbort.abort();
resolveShutdown(signal);
return;
}
repeatedSignal = true;
stopChild(child, 'SIGKILL');
stopChild(frontendChild, 'SIGKILL');
};
signalHandlers.set(signal, handler);
process.on(signal, handler);
}
try {
const preparation = prepareFrontend(endpoint, {
signal: preparationAbort.signal,
onChild(frontend) {
frontendChild = frontend;
},
});
const prepared = await Promise.race([
preparation.then(() => true),
shutdownRequested.then(() => false),
]);
if (!prepared || shutdownSignal) return 1;
const tauriArguments = buildTauriArguments(
withDevCargoFeatures(argv),
endpoint.url,
);
child = spawnCli(tauriArguments, {
env: withAgcDevEndpointEnv(endpoint),
});
const childResult = waitForCli(child);
const outcome = await Promise.race([
childResult.then((failure) => ({ type: 'exit', failure })),
shutdownRequested.then((signal) => ({ type: 'signal', signal })),
...(frontendChild
? [
waitForChildTermination(frontendChild).then((failure) => ({
type: 'frontend-exit',
failure,
})),
]
: []),
]);
const cleanup = await terminateTree(child, {
gracefulTimeoutMs: repeatedSignal ? 0 : 2500,
@@ -161,51 +105,15 @@ async function runTauriDev(
if (outcome.type === 'signal') {
return 1;
}
if (outcome.type === 'frontend-exit') return 1;
const { failure } = outcome;
return failure.type === 'error' || failure.signal ? 1 : (failure.code ?? 0);
} finally {
for (const [signal, handler] of signalHandlers) {
process.off(signal, handler);
}
preparationAbort.abort();
if (frontendChild) {
const cleanup = await terminateTree(frontendChild);
if (!cleanup.stopped) {
console.error('[ai-game-creator-shell] 配套开发服务未能完全停止。');
}
}
}
}
async function prepareFrontendDev(endpoint, { onChild, signal }) {
const frontend = spawnChild(
process.platform === 'win32' ? 'npm.cmd' : 'npm',
['run', 'agc:serve'],
{ cwd: repoRoot, env: withAgcDevEndpointEnv(endpoint) },
);
onChild(frontend);
console.log(
'[ai-game-creator-shell] 正在准备前端与配套后端,完成后启动 Tauri',
);
const deadline = Date.now() + 660_000;
while (Date.now() < deadline) {
signal.throwIfAborted();
const failure = readChildFailure(frontend);
if (failure) {
throw new Error(
`配套开发服务退出,前端未就绪:${failure.error?.message ?? failure.signal ?? failure.code}`,
);
}
if (isAiGameCreatorServer(await readExistingViteServer(endpoint))) return;
await Promise.race([
new Promise((resolveWait) => setTimeout(resolveWait, 1000)),
waitForChildTermination(frontend),
]);
}
throw new Error(`等待前端与配套后端就绪超时:${endpoint.url}`);
}
function isDirectModuleExecution() {
return Boolean(
process.argv[1] &&
@@ -218,7 +126,6 @@ export {
isDirectModuleExecution,
runTauriDev,
spawnTauriCli,
withDevCargoFeatures,
};
if (isDirectModuleExecution()) {
@@ -1,4 +0,0 @@
# resources/plugins 由 build.rs 从 plugins/ 复制生成,属于构建产物。
# 它在 dev 监听范围内,重新生成会让 Tauri dev 误判为源码改动而触发
# “构建 -> 监听 -> 再构建”的自触发循环。
resources/plugins/
-22
View File
@@ -733,18 +733,6 @@ dependencies = [
"error-code",
]
[[package]]
name = "cocos-editor-bridge"
version = "0.1.0"
dependencies = [
"cc",
"editor-adapter-api",
"serde",
"serde_json",
"sha2",
"windows-sys 0.61.2",
]
[[package]]
name = "combine"
version = "4.6.7"
@@ -1217,14 +1205,6 @@ version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555"
[[package]]
name = "editor-adapter-api"
version = "0.1.0"
dependencies = [
"serde",
"serde_json",
]
[[package]]
name = "either"
version = "1.16.0"
@@ -1729,8 +1709,6 @@ dependencies = [
"axum",
"base64 0.22.1",
"chromiumoxide",
"cocos-editor-bridge",
"editor-adapter-api",
"futures",
"getrandom 0.3.4",
"http",

Some files were not shown because too many files have changed in this diff Show More