Compare commits

..

22 Commits

Author SHA1 Message Date
kdletters 9b1e06b50a Merge branch 'master' into fix/codex-session-management
Project CI / Frontend tests (pull_request) Successful in 3m41s
Project CI / Backend tests (pull_request) Successful in 8m6s
Project CI / Repository checks (pull_request) Successful in 3m22s
Project CI / Native shell tests (pull_request) Successful in 21m49s
2026-09-03 19:30:13 +08:00
k88936 0b6efd98fe Merge branch 'master' into fix/codex-session-management
Project CI / Backend tests (pull_request) Successful in 7m57s
Project CI / Native shell tests (pull_request) Successful in 21m28s
Project CI / Repository checks (pull_request) Successful in 2m41s
Project CI / Frontend tests (pull_request) Successful in 2m57s
2026-09-03 16:08:17 +08:00
k88936 41b660bdc4 Merge branch 'master' into fix/codex-session-management
Project CI / Backend tests (pull_request) Successful in 7m28s
Project CI / Repository checks (pull_request) Failing after 1m8s
Project CI / Frontend tests (pull_request) Successful in 3m37s
Project CI / Native shell tests (pull_request) Failing after 18m11s
2026-09-03 13:31:41 +08:00
k88936 da1759322c 修复 DirectProject 回合上下文持久化
Project CI / Repository checks (pull_request) Failing after 13s
Project CI / Backend tests (pull_request) Failing after 9s
Project CI / Frontend tests (pull_request) Successful in 3m19s
Project CI / Native shell tests (pull_request) Has been cancelled
回合开始前幂等持久化规范化 user prompt
附件回合统一由 Rust 写入规范化历史,避免原始消息冲突
补充重启后多轮 user/assistant 历史恢复测试
同步 DirectProject replay 文档合同
2026-09-03 13:21:27 +08:00
k88936 ff1f77f88a Merge remote-tracking branch 'origin/master' into fix/codex-session-management
Project CI / Backend tests (pull_request) Successful in 6m8s
Project CI / Repository checks (pull_request) Successful in 2m55s
Project CI / Frontend tests (pull_request) Successful in 3m37s
Project CI / Native shell tests (pull_request) Successful in 17m5s
# Conflicts:
#	docs/project-memory/shared-memory/decision-log.md
2026-09-03 11:51:28 +08:00
k88936 e73d04db00 补充 Direct replay 有界历史设计
Project CI / Repository checks (pull_request) Failing after 13s
Project CI / Backend tests (pull_request) Failing after 10s
Project CI / Frontend tests (pull_request) Successful in 2m45s
Project CI / Native shell tests (pull_request) Successful in 19m23s
记录不可变 project.jsonl 与滑动窗口恢复约定

说明 system 标记、预算边界和超限失败语义
2026-09-03 11:44:38 +08:00
k88936 8a679d7011 实现 Direct replay 有界滑动窗口
DirectProject replay 按上下文预算保留最新连续历史

超预算时仅派生裁剪 prompt,不修改 project.jsonl

补充超大历史与当前请求超限测试
2026-09-03 11:44:29 +08:00
k88936 456762f569 Merge remote-tracking branch 'origin/fix/codex-session-management' into fix/codex-session-management 2026-09-03 10:44:36 +08:00
k88936 48a2723527 支持自定义Direct历史根路径 2026-09-03 10:42:29 +08:00
k88936 1132fe5325 Merge branch 'master' into fix/codex-session-management
Project CI / Repository checks (pull_request) Successful in 2m42s
Project CI / Frontend tests (pull_request) Successful in 3m11s
Project CI / Backend tests (pull_request) Successful in 5m58s
Project CI / Native shell tests (pull_request) Successful in 17m9s
2026-09-02 20:21:20 +08:00
k88936 ea3c34beef Merge remote-tracking branch 'origin/fix/codex-session-management' into fix/codex-session-management
Project CI / Backend tests (pull_request) Failing after 9s
Project CI / Repository checks (pull_request) Failing after 15s
Project CI / Frontend tests (pull_request) Successful in 3m0s
Project CI / Native shell tests (pull_request) Successful in 18m39s
# Conflicts:
#	docs/project-memory/shared-memory/decision-log.md
2026-09-02 20:16:30 +08:00
k88936 877724d7bc 保持Direct观察器调用接口稳定
保留原有run_turn_with_direct_observer参数形状。

将历史回放参数收口到内部专用入口。
2026-09-02 20:14:53 +08:00
k88936 ae8d51500b 记录Direct回放有意设计取舍
在partial持久化处说明普通assistant与中断标记语义。

在技术方案和决策日志记录全量回放与简单前缀的设计原因。
2026-09-02 20:12:56 +08:00
k88936 8ed2f42b52 原子化Direct线程与历史回放判定
让thread_for在同一锁内返回线程是否新建。

仅新建Direct线程时读取project.jsonl并注入历史。

保持已有线程只发送当前用户消息。
2026-09-02 20:12:41 +08:00
k88936 300ea76780 Merge branch 'master' into fix/codex-session-management
Project CI / Repository checks (pull_request) Successful in 2m26s
Project CI / Frontend tests (pull_request) Successful in 3m8s
Project CI / Backend tests (pull_request) Successful in 7m12s
Project CI / Native shell tests (pull_request) Failing after 12m58s
2026-09-02 20:03:19 +08:00
k88936 b136a7c346 删除Direct打开项目自动重发测试
Project CI / Repository checks (pull_request) Failing after 10s
Project CI / Backend tests (pull_request) Failing after 10s
Project CI / Frontend tests (pull_request) Has been cancelled
Project CI / Native shell tests (pull_request) Has been cancelled
移除已废弃的hydrated Direct回合自动恢复场景。

保留当前新发送与历史回放语义的测试覆盖。
2026-09-02 19:58:00 +08:00
k88936 b1a9a296e8 恢复Direct失败态临时回复清理
失败事件到达时清空界面中的临时回复。

保留ref中的partial供中断持久化流程使用。
2026-09-02 19:58:00 +08:00
k88936 75875a0fad 删除失效的Direct重试单测
移除已删除helper的测试导入。

删除已废弃的user-only自动重发用例。
2026-09-02 19:58:00 +08:00
k88936 1b12b7b98e 限制Direct历史回放到新线程
保留进程内thread的正常连续对话
补充中断恢复与线程重建文档
2026-09-02 19:58:00 +08:00
k88936 bb51c2716b 修正Direct聊天中断上下文恢复
删除打开项目自动重发旧消息
持久化partial并在新消息前回放完整历史
2026-09-02 19:58:00 +08:00
k88936 9e8b7d880e 让Direct Codex请求携带项目聊天历史
读取project.jsonl完整user assistant tool记录
新请求覆盖当前消息并保留ephemeral线程
2026-09-02 19:57:59 +08:00
k88936 609efe805c 补充DirectProject会话恢复约定
明确ephemeral线程与AGC聊天事实源
记录partial中断标记和取消自动重发
2026-09-02 17:47:32 +08:00
178 changed files with 1973 additions and 15368 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"
-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; }
@@ -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,
+1 -2
View File
@@ -1,7 +1,7 @@
{
"name": "@genarrative/ai-game-creator-shell",
"private": true,
"version": "0.1.19",
"version": "0.1.12",
"type": "module",
"scripts": {
"dev": "node scripts/start-tauri-dev.mjs",
@@ -9,7 +9,6 @@
"dev-stack": "node scripts/start-dev-stack.mjs",
"build": "node scripts/build-release.mjs",
"release:upload": "node scripts/release-upload.mjs",
"bump-version": "node scripts/bump-version.mjs",
"skill-pack:check": "node scripts/check-skill-pack.mjs",
"skill-pack:sync": "node scripts/check-skill-pack.mjs --write",
"skill-pack:test": "node --test scripts/check-skill-pack.test.mjs",
@@ -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 &&
@@ -5,7 +5,6 @@ import path from 'node:path';
import { fileURLToPath } from 'node:url';
const appRoot = fileURLToPath(new URL('..', import.meta.url));
const repoRoot = path.resolve(appRoot, '../..');
const defaultReleaseTarget = 'x86_64-pc-windows-msvc';
const releaseTarget =
process.env.AGC_BUILD_TARGET?.trim() || defaultReleaseTarget;
@@ -50,23 +49,16 @@ function parseVersion(value, label) {
return value;
}
export function bumpVersion(current, target = 'patch') {
const [major, minor, patch] =
parseVersion(current, '当前版本').split('.').map(Number);
if (target === 'major') return `${major + 1}.0.0`;
if (target === 'minor') return `${major}.${minor + 1}.0`;
if (target === 'patch' || target == null) {
if (patch === Number.MAX_SAFE_INTEGER) {
throw new Error(`版本号 patch 已达到上限:${current}`);
}
return `${major}.${minor}.${patch + 1}`;
export function nextPatchVersion(localVersion, remoteVersion) {
const local = parseVersion(localVersion, '本地版本');
const remote =
remoteVersion == null ? null : parseVersion(remoteVersion, 'OSS版本');
const base = remote && compareVersions(remote, local) > 0 ? remote : local;
const [major, minor, patch] = base.split('.').map(Number);
if (patch === Number.MAX_SAFE_INTEGER) {
throw new Error(`版本号 patch 已达到上限:${base}`);
}
if (typeof target === 'string' && /^\d+\.\d+\.\d+$/u.test(target)) {
return parseVersion(target, '指定版本');
}
throw new Error(
`无法识别的版本目标:${String(target)}(支持 patch / minor / major / 明确的三段版本号)`,
);
return `${major}.${minor}.${patch + 1}`;
}
async function readRemoteVersion() {
@@ -96,52 +88,14 @@ function replaceVersionLine(source, version, pattern, label) {
return source.replace(pattern, `$1${version}$3`);
}
const versionFileSources = [
{
file: packageJsonPath,
pattern: /("version"\s*:\s*")([^"]+)(")/u,
label: 'package.json',
},
{
file: rootPackageLockPath,
pattern: /("apps\/ai-game-creator-shell"\s*:\s*\{\s*\n\s*"name"\s*:\s*"@genarrative\/ai-game-creator-shell"\s*,\s*\n\s*"version"\s*:\s*")([^"]+)(")/u,
label: 'package-lock.json',
},
{
file: tauriConfigPath,
pattern: /("version"\s*:\s*")([^"]+)(")/u,
label: 'tauri.conf.json',
},
{
file: cargoManifestPath,
pattern: /(^\[package\][\s\S]*?^version\s*=\s*")([^"]+)(")/mu,
label: 'Cargo.toml',
},
{
file: cargoLockPath,
pattern: /(^name\s*=\s*"genarrative-ai-game-creator-shell"\s*\nversion\s*=\s*")([^"]+)(")/mu,
label: 'Cargo.lock',
},
];
export async function prepareReleaseVersion() {
const localVersion = parseVersion(readPackageJson().version, '本地版本');
const remoteVersion = await readRemoteVersion();
const requestedVersion = process.env.AGC_RELEASE_VERSION?.trim();
const nextVersion = requestedVersion
? parseVersion(requestedVersion, '指定版本')
: nextPatchVersion(localVersion, remoteVersion);
export function readLocalVersion() {
return parseVersion(readPackageJson().version, '本地版本');
}
export function validateVersionConsistency(expectedVersion) {
for (const source of versionFileSources) {
const match = fs.readFileSync(source.file, 'utf8').match(source.pattern);
const actual = match ? match[2].trim() : '<未找到>';
if (actual !== expectedVersion) {
throw new Error(
`AGC 版本号不一致:${source.label} 应为 ${expectedVersion},实际 ${actual}`,
);
}
}
}
export function writeVersionFiles(version) {
const nextVersion = parseVersion(version, '目标版本');
const packageSource = fs.readFileSync(packageJsonPath, 'utf8');
fs.writeFileSync(
packageJsonPath,
@@ -197,47 +151,14 @@ export function writeVersionFiles(version) {
),
);
console.log(
requestedVersion
? `[ai-game-creator-shell] 使用指定版本 ${nextVersion}(本地 ${localVersion} / OSS ${remoteVersion ?? '不存在'}`
: `[ai-game-creator-shell] 版本 ${localVersion} / OSS ${remoteVersion ?? '不存在'} -> ${nextVersion}`,
);
return nextVersion;
}
export function assertVersionCommitted() {
const repoPaths = versionFileSources.map((source) =>
path.relative(repoRoot, source.file).replaceAll('\\', '/'),
);
const changed =
spawnSync(
'git',
['diff', '--quiet', 'HEAD', '--', ...repoPaths],
{ cwd: repoRoot },
).status === 1;
if (changed) {
throw new Error(
'版本文件相对 HEAD 存在未提交改动,请先提交后再发布:运行 npm --prefix apps/ai-game-creator-shell run bump-version -- --commit',
);
}
}
export function prepareReleaseVersion() {
const localVersion = readLocalVersion();
validateVersionConsistency(localVersion);
console.log(
`[ai-game-creator-shell] 使用仓库版本 ${localVersion}(不再自动递增或自由指定版本)`,
);
return localVersion;
}
export async function assertVersionNotBelowOss(version) {
const remoteVersion = await readRemoteVersion();
if (remoteVersion && compareVersions(version, remoteVersion) < 0) {
throw new Error(
`仓库版本 ${version} 低于线上 OSS 版本 ${remoteVersion}。请先运行 npm --prefix apps/ai-game-creator-shell run bump-version 提升版本后再发布。`,
);
}
console.log(
`[ai-game-creator-shell] 线上 OSS 版本 ${remoteVersion ?? '不存在'},发布版本 ${version}`,
);
}
export function runTauriBuild(args = []) {
const noBundle = args.includes('--no-bundle');
const hasTarget = args.includes('--target');
@@ -1,19 +1,16 @@
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { test } from 'node:test';
import { fileURLToPath } from 'node:url';
import {
bumpVersion,
compareVersions,
createUpdateManifest,
nextPatchVersion,
selectReleaseArtifact,
} from './build-release.mjs';
test('selects an explicit release artifact when configured', () => {
const artifactPath = fileURLToPath(
new URL('../package.json', import.meta.url),
);
const artifactPath = new URL('../package.json', import.meta.url).pathname;
const previous = process.env.AGC_UPDATE_ARTIFACT;
process.env.AGC_UPDATE_ARTIFACT = artifactPath;
try {
@@ -33,7 +30,7 @@ test('does not select unsupported files', () => {
test('manifest contains version, download URL and integrity fields', () => {
const manifest = createUpdateManifest(
fileURLToPath(new URL('../package.json', import.meta.url)),
new URL('../package.json', import.meta.url).pathname,
);
assert.match(manifest.version, /^\d+\.\d+\.\d+$/u);
assert.match(
@@ -49,7 +46,7 @@ test('manifest preserves multiline release notes', () => {
process.env.AGC_UPDATE_RELEASE_NOTES = '第一行\n第二行\r\n第三行';
try {
const manifest = createUpdateManifest(
fileURLToPath(new URL('../package.json', import.meta.url)),
new URL('../package.json', import.meta.url).pathname,
);
assert.equal(manifest.releaseNotes, '第一行\n第二行\r\n第三行');
} finally {
@@ -58,14 +55,11 @@ test('manifest preserves multiline release notes', () => {
}
});
test('bump version follows the requested target', () => {
test('next release version follows the higher local or OSS version', () => {
assert.equal(compareVersions('0.1.15', '0.1.12'), 1);
assert.equal(compareVersions('0.1.12', '0.1.12'), 0);
assert.equal(bumpVersion('0.1.19', 'patch'), '0.1.20');
assert.equal(bumpVersion('0.1.19'), '0.1.20');
assert.equal(bumpVersion('0.1.19', 'minor'), '0.2.0');
assert.equal(bumpVersion('0.1.19', 'major'), '1.0.0');
assert.equal(bumpVersion('0.1.12', '0.1.25'), '0.1.25');
assert.equal(nextPatchVersion('0.1.12', '0.1.15'), '0.1.16');
assert.equal(nextPatchVersion('0.1.18', '0.1.15'), '0.1.19');
assert.equal(nextPatchVersion('0.1.12', null), '0.1.13');
});
test('release upload forces overwrite for versioned artifact and latest pointer', () => {
@@ -78,12 +72,3 @@ test('release upload forces overwrite for versioned artifact and latest pointer'
2,
);
});
test('release upload verifies the version is committed and not below OSS', () => {
const source = readFileSync(
new URL('./release-upload.mjs', import.meta.url),
'utf8',
);
assert.match(source, /assertVersionCommitted\(\)/u);
assert.match(source, /assertVersionNotBelowOss\(/u);
});
@@ -1,121 +0,0 @@
import { spawnSync } from 'node:child_process';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const appRoot = fileURLToPath(new URL('..', import.meta.url));
const repoRoot = path.resolve(appRoot, '../..');
const {
bumpVersion,
readLocalVersion,
writeVersionFiles,
} = await import('./build-release.mjs');
const versionFiles = [
'apps/ai-game-creator-shell/package.json',
'package-lock.json',
'apps/ai-game-creator-shell/src-tauri/tauri.conf.json',
'apps/ai-game-creator-shell/src-tauri/Cargo.toml',
'apps/ai-game-creator-shell/src-tauri/Cargo.lock',
];
function parseArgs(argv) {
const args = argv.slice(2);
const explicitIndex = args.indexOf('--version');
const explicit = explicitIndex >= 0 ? args[explicitIndex + 1] : null;
const target =
explicit ??
args.find((arg) => arg === 'patch' || arg === 'minor' || arg === 'major') ??
'patch';
return { target, commit: args.includes('--commit') };
}
function runGit(args) {
const result = spawnSync('git', args, { cwd: repoRoot, stdio: 'inherit' });
if (result.error) throw result.error;
return result.status ?? 1;
}
function hasUncommittedVersionChanges() {
return (
spawnSync('git', ['diff', '--quiet', '--', ...versionFiles], {
cwd: repoRoot,
}).status === 1
);
}
function otherStagedFiles() {
const result = spawnSync(
'git',
['diff', '--cached', '--name-only'],
{ cwd: repoRoot, encoding: 'utf8' },
);
if (result.status !== 0) {
throw new Error('无法读取已暂存文件列表');
}
const versionSet = new Set(versionFiles);
return result.stdout
.split('\n')
.map((file) => file.trim())
.filter(Boolean)
.filter((file) => !versionSet.has(file));
}
const { target, commit } = parseArgs(process.argv);
// 提交前先确认没有其他已暂存改动,避免把无关改动一并提交;若存在则在改动任何文件前中止。
if (commit) {
const others = otherStagedFiles();
if (others.length > 0) {
console.error(
`[bump-version] 检测到其他已暂存改动,为避免误提交,请先单独处理后再运行 --commit:\n ${others.join('\n ')}`,
);
console.error(
'提示:用 git restore --staged <file> 取消暂存,或先提交这些改动。',
);
process.exit(1);
}
}
const current = readLocalVersion();
// 若版本文件已带着一次未提交的提升(例如先默认跑过一次),再 --commit 时不再重复递增,直接提交现有改动。
const next =
commit && hasUncommittedVersionChanges()
? current
: bumpVersion(current, target);
writeVersionFiles(next);
if (!commit) {
console.log(
`[bump-version] 版本 ${current} -> ${next}(已写入版本文件,未创建提交;如需提交加 --commit)`,
);
process.exit(0);
}
const addStatus = runGit(['add', ...versionFiles]);
if (addStatus !== 0) process.exit(addStatus);
const hasDiff =
spawnSync('git', ['diff', '--cached', '--quiet'], {
cwd: repoRoot,
}).status === 1;
if (!hasDiff) {
console.log(`[bump-version] 版本未变化(已是 ${current}),未创建提交`);
process.exit(0);
}
const commitStatus = runGit([
'commit',
'-m',
`提升 AGC 版本至 ${next}`,
'-m',
`- AGC 客户端版本提升至 ${next}`,
'-m',
'- 同步更新 package.json、根 package-lock.json、tauri.conf.json、Cargo.toml、Cargo.lock 中的 AGC 包条目',
]);
if (commitStatus !== 0) process.exit(commitStatus);
console.log(`[bump-version] 已提交版本 ${next}(本地提交,未推送)`);
@@ -1753,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,
@@ -9,13 +9,8 @@ if (!/^[a-z0-9][a-z0-9.-]{1,62}$/u.test(bucket) || /[\r\n\0]/u.test(endpoint)) {
}
process.env.AGC_UPDATE_OSS_BASE_URL ||= `https://${bucket}.${endpoint}/agc`;
const {
assertVersionCommitted,
assertVersionNotBelowOss,
generateUpdateManifest,
prepareReleaseVersion,
runTauriBuild,
} = await import('./build-release.mjs');
const { generateUpdateManifest, prepareReleaseVersion, runTauriBuild } =
await import('./build-release.mjs');
function runOssutil(args) {
const binary = process.env.OSSUTIL_BIN?.trim() || 'ossutil';
@@ -41,9 +36,7 @@ function runOssutil(args) {
if (result.status !== 0) process.exit(result.status ?? 1);
}
const releaseVersion = await prepareReleaseVersion();
assertVersionCommitted();
await assertVersionNotBelowOss(releaseVersion);
await prepareReleaseVersion();
runTauriBuild([]);
const { artifact, manifestPath, manifest } = generateUpdateManifest();
const artifactKey = `agc/${manifest.version}/${path.basename(artifact)}`;
@@ -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 = '';
@@ -540,7 +540,6 @@ async function ensureBackend({
backendDatabase,
'--spacetime-data-dir',
backendSpacetimeDataDir,
'--preserve-database',
'--no-interactive',
],
{ cwd: appRoot },
@@ -681,13 +680,11 @@ function isDirectModuleExecution() {
export {
ensureBackend,
formatChildFailure,
isAiGameCreatorServer,
isBackendReady,
isDirectModuleExecution,
isProcessGroupAlive,
preflightExistingVite,
readChildFailure,
readExistingViteServer,
readLinuxProcessGroupAlive,
resolveBackendTargetsFromState,
runWindowsTaskkill,
@@ -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];
@@ -56,7 +51,6 @@ async function runTauriDev(
{
resolveDevEndpoint = resolveAgcDevEndpoint,
preflight = preflightExistingVite,
prepareFrontend = prepareFrontendDev,
spawnCli = spawnTauriCli,
waitForCli = waitForChildTermination,
terminateTree = terminateChildTree,
@@ -65,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;
@@ -81,47 +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(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,
@@ -136,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] &&
+1 -1
View File
@@ -1703,7 +1703,7 @@ dependencies = [
[[package]]
name = "genarrative-ai-game-creator-shell"
version = "0.1.19"
version = "0.1.12"
dependencies = [
"agent-runtime-core",
"axum",

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