Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1d6e7057bc | |||
| 6daff74c85 | |||
| ad49056d36 | |||
| 28af87e3ad | |||
| d994ccaa78 | |||
| 42f1109b90 | |||
| b772ec3efb | |||
| d014cb1d06 | |||
| e34ae89db3 | |||
| 88853d0a30 | |||
| fabc64df6c | |||
| 27803a6e78 | |||
| 828b3e1173 | |||
| deb60065a5 | |||
| 5c6a940b9c | |||
| 8df5ebff69 | |||
| 041b51adc1 | |||
| 5a31809e21 | |||
| 44bebae0a9 | |||
| 62793ac445 | |||
| f630c518f3 |
@@ -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"
|
||||
|
||||
|
||||
@@ -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 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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[];
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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
@@ -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,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
@@ -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
Reference in New Issue
Block a user