合并 origin/master(58 个提交):生产 Nginx 模板收敛与记录同步
Project CI / AI game creator shell Rust lane 1/2 (push) Failing after 2m23s
Project CI / AI game creator shell Rust lane 2/2 (push) Failing after 1m15s
Project CI / AI game creator shell Rust crates (push) Successful in 1m14s
Project CI / AI game creator shell Rust smoke (push) Successful in 1m35s
Project CI / Frontend tests (push) Successful in 2m53s
Project CI / Repository checks (push) Successful in 3m36s
Project CI / Backend tests (push) Successful in 4m26s
Project CI / AI game creator shell web tests (push) Successful in 2m23s
Project CI / Native shell tests (push) Successful in 7m27s
Project CI / AI game creator shell Rust lane 1/2 (push) Failing after 2m23s
Project CI / AI game creator shell Rust lane 2/2 (push) Failing after 1m15s
Project CI / AI game creator shell Rust crates (push) Successful in 1m14s
Project CI / AI game creator shell Rust smoke (push) Successful in 1m35s
Project CI / Frontend tests (push) Successful in 2m53s
Project CI / Repository checks (push) Successful in 3m36s
Project CI / Backend tests (push) Successful in 4m26s
Project CI / AI game creator shell web tests (push) Successful in 2m23s
Project CI / Native shell tests (push) Successful in 7m27s
- 合并 origin/master 58 个提交,解决 2 处文件冲突:决策记录与踩坑记录均按两侧新增条目并存处理 - claude_code_cli.rs 自动合并通过:本机 Claude sidecar 诊断改动与上游 sidecar 归位并存,无重复函数定义、无冲突残留 - 保留本地未推送提交:AGC Claude 执行认证与错误诊断、Claude sidecar 超时诊断 - 本次推送主体内容不变:生产 Nginx 模板收敛、host-only snippet 与运维/踩坑/决策文档同步
This commit is contained in:
@@ -58,6 +58,12 @@ temp*build*/
|
|||||||
/apps/ai-game-creator-shell/src-tauri/resources/node-runtime/
|
/apps/ai-game-creator-shell/src-tauri/resources/node-runtime/
|
||||||
/apps/ai-game-creator-shell/src-tauri/resources/claude-agent/
|
/apps/ai-game-creator-shell/src-tauri/resources/claude-agent/
|
||||||
/apps/ai-game-creator-shell/src-tauri/resources/node-runtime-staging-*/
|
/apps/ai-game-creator-shell/src-tauri/resources/node-runtime-staging-*/
|
||||||
|
/apps/ai-game-creator-shell/src-tauri/resources/plugins-staging-*/
|
||||||
|
/apps/ai-game-creator-shell/src-tauri/resources/codex/*-staging-*/
|
||||||
|
/apps/ai-game-creator-shell/src-tauri/resources/plugins-backup-*/
|
||||||
|
/apps/ai-game-creator-shell/src-tauri/resources/codex/*-backup-*/
|
||||||
|
/apps/ai-game-creator-shell/src-tauri/resources/claude-agent-staging-*/
|
||||||
|
/apps/ai-game-creator-shell/src-tauri/resources/claude-agent-backup-*/
|
||||||
/apps/ai-game-creator-shell/.llm-drafts/
|
/apps/ai-game-creator-shell/.llm-drafts/
|
||||||
/apps/ai-game-creator-shell/game-creator.config.local.json
|
/apps/ai-game-creator-shell/game-creator.config.local.json
|
||||||
/apps/mobile-shell/.expo/
|
/apps/mobile-shell/.expo/
|
||||||
|
|||||||
@@ -16,7 +16,10 @@ import {
|
|||||||
getAdminProjectSnapshotChannels,
|
getAdminProjectSnapshotChannels,
|
||||||
listAdminProjectSnapshots,
|
listAdminProjectSnapshots,
|
||||||
} from '../api/adminApiClient';
|
} from '../api/adminApiClient';
|
||||||
import type { AdminProjectSnapshotEntry } from '../api/adminApiTypes';
|
import type {
|
||||||
|
AdminProjectSnapshotEntry,
|
||||||
|
AdminProjectSnapshotListResponse,
|
||||||
|
} from '../api/adminApiTypes';
|
||||||
import { AdminProjectSnapshotsPage } from './AdminProjectSnapshotsPage';
|
import { AdminProjectSnapshotsPage } from './AdminProjectSnapshotsPage';
|
||||||
|
|
||||||
vi.mock('../api/adminApiClient', async () => ({
|
vi.mock('../api/adminApiClient', async () => ({
|
||||||
@@ -58,6 +61,241 @@ afterEach(() => {
|
|||||||
vi.useRealTimers();
|
vi.useRealTimers();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('全量读取后按同步时间和身份排序,本地分页并支持刷新和恢复默认', async () => {
|
||||||
|
const olderProjects = Array.from({ length: 22 }, (_, index) => ({
|
||||||
|
...entry,
|
||||||
|
projectId: `older-${index}`,
|
||||||
|
projectName: `旧项目 ${index}`,
|
||||||
|
syncedAtMs: entry.syncedAtMs + index,
|
||||||
|
}));
|
||||||
|
const tiedProjects = [
|
||||||
|
{ ...entry, userId: 'user-2', projectId: 'p-1', projectName: '同秒乙' },
|
||||||
|
{ ...entry, userId: 'user-1', projectId: 'p-2', projectName: '同秒甲二' },
|
||||||
|
{ ...entry, userId: 'user-1', projectId: 'p-1', projectName: '同秒甲一' },
|
||||||
|
].map((project) => ({ ...project, syncedAtMs: entry.syncedAtMs + 100 }));
|
||||||
|
vi.mocked(listAdminProjectSnapshots)
|
||||||
|
.mockResolvedValueOnce({ items: [entry], nextCursor: 'original-next' })
|
||||||
|
.mockResolvedValueOnce({ items: olderProjects, nextCursor: 'scan-2' })
|
||||||
|
// 后端有界扫描可能返回带下一游标的空页,不能提前结束。
|
||||||
|
.mockResolvedValueOnce({ items: [], nextCursor: 'scan-3' })
|
||||||
|
.mockResolvedValueOnce({
|
||||||
|
items: [
|
||||||
|
...tiedProjects,
|
||||||
|
{
|
||||||
|
...olderProjects[0]!,
|
||||||
|
projectName: '更新项目',
|
||||||
|
syncedAtMs: entry.syncedAtMs + 200,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
nextCursor: null,
|
||||||
|
});
|
||||||
|
render(<AdminProjectSnapshotsPage token="token" onUnauthorized={vi.fn()} />);
|
||||||
|
await screen.findByText('三消工程');
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: '按同步时间排序' }));
|
||||||
|
await screen.findByRole('button', { name: '恢复默认顺序' });
|
||||||
|
const rowNames = () =>
|
||||||
|
screen
|
||||||
|
.getAllByRole('row')
|
||||||
|
.map((row) => row.querySelector('td strong')?.textContent)
|
||||||
|
.filter(Boolean);
|
||||||
|
expect(rowNames().slice(0, 4)).toEqual([
|
||||||
|
'更新项目',
|
||||||
|
'同秒甲一',
|
||||||
|
'同秒甲二',
|
||||||
|
'同秒乙',
|
||||||
|
]);
|
||||||
|
expect(rowNames()).toHaveLength(20);
|
||||||
|
expect(screen.getByRole('navigation').textContent).toContain('共 25 个');
|
||||||
|
expect(listAdminProjectSnapshots).toHaveBeenNthCalledWith(
|
||||||
|
4,
|
||||||
|
'token',
|
||||||
|
{ cursor: 'scan-3', limit: 100, channel: 'dev' },
|
||||||
|
expect.any(AbortSignal),
|
||||||
|
);
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: '下一页' }));
|
||||||
|
expect(rowNames()).toHaveLength(5);
|
||||||
|
expect(screen.getByRole('navigation').textContent).toContain('第 2 页');
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: '上一页' }));
|
||||||
|
expect(rowNames()[0]).toBe('更新项目');
|
||||||
|
fireEvent.change(screen.getByLabelText('每页条数'), {
|
||||||
|
target: { value: '50' },
|
||||||
|
});
|
||||||
|
expect(rowNames()).toHaveLength(25);
|
||||||
|
expect(listAdminProjectSnapshots).toHaveBeenCalledTimes(4);
|
||||||
|
|
||||||
|
vi.mocked(listAdminProjectSnapshots).mockResolvedValueOnce({
|
||||||
|
items: [{ ...entry, projectName: '刷新后的项目' }],
|
||||||
|
nextCursor: null,
|
||||||
|
});
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: '刷新' }));
|
||||||
|
await screen.findByText('刷新后的项目');
|
||||||
|
expect(listAdminProjectSnapshots).toHaveBeenNthCalledWith(
|
||||||
|
5,
|
||||||
|
'token',
|
||||||
|
{ cursor: null, limit: 100, channel: 'dev' },
|
||||||
|
expect.any(AbortSignal),
|
||||||
|
);
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: '恢复默认顺序' }));
|
||||||
|
await screen.findByText('三消工程');
|
||||||
|
expect(listAdminProjectSnapshots).toHaveBeenNthCalledWith(
|
||||||
|
6,
|
||||||
|
'token',
|
||||||
|
{ cursor: null, limit: 50, channel: 'dev' },
|
||||||
|
expect.any(AbortSignal),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('恢复默认顺序失败保留排序分页,重试成功后切换远端第一页及游标', async () => {
|
||||||
|
const projects = Array.from({ length: 45 }, (_, index) => ({
|
||||||
|
...entry,
|
||||||
|
projectId: `sorted-${index}`,
|
||||||
|
projectName: `排序项目 ${index}`,
|
||||||
|
syncedAtMs: entry.syncedAtMs - index,
|
||||||
|
}));
|
||||||
|
vi.mocked(listAdminProjectSnapshots)
|
||||||
|
.mockResolvedValueOnce({ items: [entry], nextCursor: null })
|
||||||
|
.mockResolvedValueOnce({ items: projects, nextCursor: null })
|
||||||
|
.mockRejectedValueOnce(new Error('恢复失败'))
|
||||||
|
.mockResolvedValueOnce({
|
||||||
|
items: [{ ...entry, projectName: '默认第一页' }],
|
||||||
|
nextCursor: 'default-next',
|
||||||
|
})
|
||||||
|
.mockResolvedValueOnce({
|
||||||
|
items: [{ ...entry, projectName: '默认第二页' }],
|
||||||
|
nextCursor: null,
|
||||||
|
});
|
||||||
|
render(<AdminProjectSnapshotsPage token="token" onUnauthorized={vi.fn()} />);
|
||||||
|
await screen.findByText('三消工程');
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: '按同步时间排序' }));
|
||||||
|
await screen.findByText('排序项目 0');
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: '下一页' }));
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: '恢复默认顺序' }));
|
||||||
|
await screen.findByText('恢复失败');
|
||||||
|
expect(screen.getByText('排序项目 20')).toBeTruthy();
|
||||||
|
expect(screen.getByRole('navigation').textContent).toContain('第 2 页');
|
||||||
|
expect(screen.getByRole('navigation').textContent).toContain('共 45 个');
|
||||||
|
expect(screen.getByRole('button', { name: '恢复默认顺序' })).toBeTruthy();
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: '下一页' }));
|
||||||
|
expect(screen.getByText('排序项目 40')).toBeTruthy();
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: '上一页' }));
|
||||||
|
expect(screen.getByText('排序项目 20')).toBeTruthy();
|
||||||
|
expect(listAdminProjectSnapshots).toHaveBeenCalledTimes(3);
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: '恢复默认顺序' }));
|
||||||
|
await screen.findByText('默认第一页');
|
||||||
|
expect(screen.getByRole('navigation').textContent).toContain('第 1 页');
|
||||||
|
expect(screen.getByRole('button', { name: '按同步时间排序' })).toBeTruthy();
|
||||||
|
expect(listAdminProjectSnapshots).toHaveBeenNthCalledWith(
|
||||||
|
4,
|
||||||
|
'token',
|
||||||
|
{ cursor: null, limit: 20, channel: 'dev' },
|
||||||
|
expect.any(AbortSignal),
|
||||||
|
);
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: '下一页' }));
|
||||||
|
await screen.findByText('默认第二页');
|
||||||
|
expect(screen.getByRole('navigation').textContent).toContain('第 2 页');
|
||||||
|
expect(listAdminProjectSnapshots).toHaveBeenNthCalledWith(
|
||||||
|
5,
|
||||||
|
'token',
|
||||||
|
{ cursor: 'default-next', limit: 20, channel: 'dev' },
|
||||||
|
expect.any(AbortSignal),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('全量排序中途失败不发布部分结果,重试刷新失败保留完整排序', async () => {
|
||||||
|
vi.mocked(listAdminProjectSnapshots)
|
||||||
|
.mockResolvedValueOnce({ items: [entry], nextCursor: 'original-next' })
|
||||||
|
.mockResolvedValueOnce({
|
||||||
|
items: [{ ...entry, projectName: '部分读取项目' }],
|
||||||
|
nextCursor: 'scan-next',
|
||||||
|
})
|
||||||
|
.mockRejectedValueOnce(new Error('全量读取失败'));
|
||||||
|
render(<AdminProjectSnapshotsPage token="token" onUnauthorized={vi.fn()} />);
|
||||||
|
await screen.findByText('三消工程');
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: '按同步时间排序' }));
|
||||||
|
await screen.findByText('全量读取失败');
|
||||||
|
expect(screen.getByText('三消工程')).toBeTruthy();
|
||||||
|
expect(screen.queryByText('部分读取项目')).toBeNull();
|
||||||
|
expect(screen.queryByRole('button', { name: '恢复默认顺序' })).toBeNull();
|
||||||
|
expect(
|
||||||
|
screen.getByRole('button', { name: '下一页' }).hasAttribute('disabled'),
|
||||||
|
).toBe(false);
|
||||||
|
|
||||||
|
vi.mocked(listAdminProjectSnapshots).mockResolvedValueOnce({
|
||||||
|
items: [{ ...entry, projectName: '完整排序项目' }],
|
||||||
|
nextCursor: null,
|
||||||
|
});
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: '按同步时间排序' }));
|
||||||
|
await screen.findByText('完整排序项目');
|
||||||
|
vi.mocked(listAdminProjectSnapshots).mockRejectedValueOnce(
|
||||||
|
new Error('刷新读取失败'),
|
||||||
|
);
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: '刷新' }));
|
||||||
|
await screen.findByText('刷新读取失败');
|
||||||
|
expect(screen.getByText('完整排序项目')).toBeTruthy();
|
||||||
|
expect(screen.getByRole('navigation').textContent).toContain('共 1 个');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('取消或切换渠道时中断全量读取,忽略随后到达的旧响应', async () => {
|
||||||
|
vi.mocked(getAdminProjectSnapshotChannels).mockResolvedValue({
|
||||||
|
defaultChannel: 'dev',
|
||||||
|
channels: ['dev', 'release'],
|
||||||
|
});
|
||||||
|
let finishCancelled!: (value: AdminProjectSnapshotListResponse) => void;
|
||||||
|
let finishOldChannel!: (value: AdminProjectSnapshotListResponse) => void;
|
||||||
|
vi.mocked(listAdminProjectSnapshots)
|
||||||
|
.mockResolvedValueOnce({ items: [entry], nextCursor: null })
|
||||||
|
.mockImplementationOnce(
|
||||||
|
() =>
|
||||||
|
new Promise<AdminProjectSnapshotListResponse>((resolve) => {
|
||||||
|
finishCancelled = resolve;
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.mockImplementationOnce(
|
||||||
|
() =>
|
||||||
|
new Promise<AdminProjectSnapshotListResponse>((resolve) => {
|
||||||
|
finishOldChannel = resolve;
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.mockResolvedValueOnce({
|
||||||
|
items: [{ ...entry, channel: 'release', projectName: '正式渠道项目' }],
|
||||||
|
nextCursor: null,
|
||||||
|
});
|
||||||
|
render(<AdminProjectSnapshotsPage token="token" onUnauthorized={vi.fn()} />);
|
||||||
|
await screen.findByText('三消工程');
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: '按同步时间排序' }));
|
||||||
|
const cancelledSignal = vi.mocked(listAdminProjectSnapshots).mock
|
||||||
|
.calls[1]![2]!;
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: '取消排序' }));
|
||||||
|
expect(cancelledSignal.aborted).toBe(true);
|
||||||
|
await act(async () => {
|
||||||
|
finishCancelled({
|
||||||
|
items: [{ ...entry, projectName: '已取消项目' }],
|
||||||
|
nextCursor: null,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
expect(screen.getByText('三消工程')).toBeTruthy();
|
||||||
|
expect(screen.queryByText('已取消项目')).toBeNull();
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: '按同步时间排序' }));
|
||||||
|
const oldChannelSignal = vi.mocked(listAdminProjectSnapshots).mock
|
||||||
|
.calls[2]![2]!;
|
||||||
|
fireEvent.change(screen.getByLabelText('项目工程渠道'), {
|
||||||
|
target: { value: 'release' },
|
||||||
|
});
|
||||||
|
await screen.findByText('正式渠道项目');
|
||||||
|
expect(oldChannelSignal.aborted).toBe(true);
|
||||||
|
await act(async () => {
|
||||||
|
finishOldChannel({
|
||||||
|
items: [{ ...entry, projectName: '旧渠道项目' }],
|
||||||
|
nextCursor: null,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
expect(screen.getByText('正式渠道项目')).toBeTruthy();
|
||||||
|
expect(screen.queryByText('旧渠道项目')).toBeNull();
|
||||||
|
expect(screen.getByRole('button', { name: '按同步时间排序' })).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
test('按项目展示完整性并限制未完成工程下载', async () => {
|
test('按项目展示完整性并限制未完成工程下载', async () => {
|
||||||
vi.mocked(listAdminProjectSnapshots).mockResolvedValue({
|
vi.mocked(listAdminProjectSnapshots).mockResolvedValue({
|
||||||
items: [
|
items: [
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
AdminPagination,
|
AdminPagination,
|
||||||
AdminStatusPill,
|
AdminStatusPill,
|
||||||
} from '@genarrative/shared/components';
|
} from '@genarrative/shared/components';
|
||||||
import { Download, RefreshCcw, X } from 'lucide-react';
|
import { ArrowDownWideNarrow, Download, RefreshCcw, X } from 'lucide-react';
|
||||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -50,6 +50,9 @@ const snapshotStatuses: Record<
|
|||||||
|
|
||||||
const DEFAULT_PAGE_SIZE = 20;
|
const DEFAULT_PAGE_SIZE = 20;
|
||||||
const PAGE_SIZE_OPTIONS = [20, 50, 100];
|
const PAGE_SIZE_OPTIONS = [20, 50, 100];
|
||||||
|
const MAX_SORT_PAGES = 200;
|
||||||
|
const MAX_SORT_PROJECTS = 10_000;
|
||||||
|
const SORT_TIMEOUT_MS = 60_000;
|
||||||
|
|
||||||
export function AdminProjectSnapshotsPage({
|
export function AdminProjectSnapshotsPage({
|
||||||
token,
|
token,
|
||||||
@@ -64,6 +67,9 @@ export function AdminProjectSnapshotsPage({
|
|||||||
const [channelOptions, setChannelOptions] = useState<string[]>([]);
|
const [channelOptions, setChannelOptions] = useState<string[]>([]);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [hasLoaded, setHasLoaded] = useState(false);
|
const [hasLoaded, setHasLoaded] = useState(false);
|
||||||
|
const [isSorted, setIsSorted] = useState(false);
|
||||||
|
const [isSorting, setIsSorting] = useState(false);
|
||||||
|
const [sortProgress, setSortProgress] = useState(0);
|
||||||
const [errorMessage, setErrorMessage] = useState('');
|
const [errorMessage, setErrorMessage] = useState('');
|
||||||
const [downloadingKey, setDownloadingKey] = useState<string | null>(null);
|
const [downloadingKey, setDownloadingKey] = useState<string | null>(null);
|
||||||
const listController = useRef<AbortController | null>(null);
|
const listController = useRef<AbortController | null>(null);
|
||||||
@@ -71,9 +77,23 @@ export function AdminProjectSnapshotsPage({
|
|||||||
// 远端按游标分页且不给总数:第 N 页的起始游标只能由前 N-1 页依次返回,
|
// 远端按游标分页且不给总数:第 N 页的起始游标只能由前 N-1 页依次返回,
|
||||||
// 因此按页记录已取得的游标,翻页只在这些游标之间移动。
|
// 因此按页记录已取得的游标,翻页只在这些游标之间移动。
|
||||||
const pageCursors = useRef<(string | null)[]>([null]);
|
const pageCursors = useRef<(string | null)[]>([null]);
|
||||||
|
// 全量读取完成后才发布;分页只切片本次读到的完整集合。
|
||||||
|
const sortedItems = useRef<AdminProjectSnapshotEntry[] | null>(null);
|
||||||
|
|
||||||
const loadPage = useCallback(
|
const loadPage = useCallback(
|
||||||
async (cursor: string | null, limit: number, page: number) => {
|
async (
|
||||||
|
cursor: string | null,
|
||||||
|
limit: number,
|
||||||
|
page: number,
|
||||||
|
restoreDefault = false,
|
||||||
|
) => {
|
||||||
|
if (!restoreDefault && sortedItems.current !== null) {
|
||||||
|
setItems(sortedItems.current.slice((page - 1) * limit, page * limit));
|
||||||
|
setPageIndex(page);
|
||||||
|
setHasLoaded(true);
|
||||||
|
setErrorMessage('');
|
||||||
|
return;
|
||||||
|
}
|
||||||
listController.current?.abort();
|
listController.current?.abort();
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
listController.current = controller;
|
listController.current = controller;
|
||||||
@@ -86,6 +106,12 @@ export function AdminProjectSnapshotsPage({
|
|||||||
controller.signal,
|
controller.signal,
|
||||||
);
|
);
|
||||||
if (controller.signal.aborted) return;
|
if (controller.signal.aborted) return;
|
||||||
|
// 远端第一页读取成功后再切换模式,失败时保留完整排序和分页。
|
||||||
|
if (restoreDefault) {
|
||||||
|
sortedItems.current = null;
|
||||||
|
setIsSorted(false);
|
||||||
|
pageCursors.current = [null];
|
||||||
|
}
|
||||||
setItems(response.items);
|
setItems(response.items);
|
||||||
setNextCursor(response.nextCursor);
|
setNextCursor(response.nextCursor);
|
||||||
setPageIndex(page);
|
setPageIndex(page);
|
||||||
@@ -126,8 +152,16 @@ export function AdminProjectSnapshotsPage({
|
|||||||
return () => controller.abort();
|
return () => controller.abort();
|
||||||
}, [token, onUnauthorized]);
|
}, [token, onUnauthorized]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
sortedItems.current = null;
|
||||||
|
setIsSorted(false);
|
||||||
|
}, [token, channel]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (channel === null) return undefined;
|
if (channel === null) return undefined;
|
||||||
|
setIsSorting(false);
|
||||||
|
setIsLoading(false);
|
||||||
|
setSortProgress(0);
|
||||||
pageCursors.current = [null];
|
pageCursors.current = [null];
|
||||||
setItems([]);
|
setItems([]);
|
||||||
setNextCursor(null);
|
setNextCursor(null);
|
||||||
@@ -144,6 +178,10 @@ export function AdminProjectSnapshotsPage({
|
|||||||
}, [loadPage, pageSize, channel]);
|
}, [loadPage, pageSize, channel]);
|
||||||
|
|
||||||
function goToNextPage() {
|
function goToNextPage() {
|
||||||
|
if (sortedItems.current !== null) {
|
||||||
|
void loadPage(null, pageSize, pageIndex + 1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!nextCursor) return;
|
if (!nextCursor) return;
|
||||||
pageCursors.current[pageIndex] = nextCursor;
|
pageCursors.current[pageIndex] = nextCursor;
|
||||||
void loadPage(nextCursor, pageSize, pageIndex + 1);
|
void loadPage(nextCursor, pageSize, pageIndex + 1);
|
||||||
@@ -159,6 +197,10 @@ export function AdminProjectSnapshotsPage({
|
|||||||
}
|
}
|
||||||
|
|
||||||
function refreshCurrentPage() {
|
function refreshCurrentPage() {
|
||||||
|
if (sortedItems.current !== null) {
|
||||||
|
void sortAllProjects();
|
||||||
|
return;
|
||||||
|
}
|
||||||
void loadPage(
|
void loadPage(
|
||||||
pageCursors.current[pageIndex - 1] ?? null,
|
pageCursors.current[pageIndex - 1] ?? null,
|
||||||
pageSize,
|
pageSize,
|
||||||
@@ -166,6 +208,90 @@ export function AdminProjectSnapshotsPage({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function sortAllProjects() {
|
||||||
|
if (channel === null) return;
|
||||||
|
listController.current?.abort();
|
||||||
|
const controller = new AbortController();
|
||||||
|
listController.current = controller;
|
||||||
|
setIsLoading(true);
|
||||||
|
setIsSorting(true);
|
||||||
|
setSortProgress(0);
|
||||||
|
setErrorMessage('');
|
||||||
|
let timedOut = false;
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
timedOut = true;
|
||||||
|
controller.abort();
|
||||||
|
}, SORT_TIMEOUT_MS);
|
||||||
|
try {
|
||||||
|
const projects = new Map<string, AdminProjectSnapshotEntry>();
|
||||||
|
const seenCursors = new Set<string>();
|
||||||
|
let cursor: string | null = null;
|
||||||
|
for (let page = 0; page < MAX_SORT_PAGES; page += 1) {
|
||||||
|
const response = await listAdminProjectSnapshots(
|
||||||
|
token,
|
||||||
|
{ cursor, limit: 100, channel },
|
||||||
|
controller.signal,
|
||||||
|
);
|
||||||
|
if (controller.signal.aborted) return;
|
||||||
|
for (const project of response.items) {
|
||||||
|
const key = snapshotKey(project);
|
||||||
|
const previous = projects.get(key);
|
||||||
|
if (!previous || project.syncedAtMs >= previous.syncedAtMs) {
|
||||||
|
projects.set(key, project);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (projects.size > MAX_SORT_PROJECTS) {
|
||||||
|
throw new Error('项目超过 10,000 个,无法完成全量排序');
|
||||||
|
}
|
||||||
|
setSortProgress(projects.size);
|
||||||
|
if (!response.nextCursor) {
|
||||||
|
const result = [...projects.values()].sort(
|
||||||
|
(left, right) =>
|
||||||
|
right.syncedAtMs - left.syncedAtMs ||
|
||||||
|
compareIds(left.userId, right.userId) ||
|
||||||
|
compareIds(left.projectId, right.projectId),
|
||||||
|
);
|
||||||
|
sortedItems.current = result;
|
||||||
|
setIsSorted(true);
|
||||||
|
setItems(result.slice(0, pageSize));
|
||||||
|
setPageIndex(1);
|
||||||
|
setNextCursor(null);
|
||||||
|
setHasLoaded(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (seenCursors.has(response.nextCursor)) {
|
||||||
|
throw new Error('项目列表游标重复,无法完成全量排序,请重试');
|
||||||
|
}
|
||||||
|
seenCursors.add(response.nextCursor);
|
||||||
|
cursor = response.nextCursor;
|
||||||
|
}
|
||||||
|
throw new Error('项目读取超过 200 页,无法完成全量排序');
|
||||||
|
} catch (error: unknown) {
|
||||||
|
if (!controller.signal.aborted) {
|
||||||
|
handlePageError(error, onUnauthorized, setErrorMessage);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
if (listController.current === controller) {
|
||||||
|
listController.current = null;
|
||||||
|
setIsLoading(false);
|
||||||
|
setIsSorting(false);
|
||||||
|
if (timedOut) setErrorMessage('全量项目读取超时,请重试');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancelSorting() {
|
||||||
|
listController.current?.abort();
|
||||||
|
listController.current = null;
|
||||||
|
setIsLoading(false);
|
||||||
|
setIsSorting(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
function restoreDefaultOrder() {
|
||||||
|
void loadPage(null, pageSize, 1, true);
|
||||||
|
}
|
||||||
|
|
||||||
async function downloadProject(entry: AdminProjectSnapshotEntry) {
|
async function downloadProject(entry: AdminProjectSnapshotEntry) {
|
||||||
if (downloadController.current || entry.status === 'partial') return;
|
if (downloadController.current || entry.status === 'partial') return;
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
@@ -236,6 +362,32 @@ export function AdminProjectSnapshotsPage({
|
|||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
</AdminField>
|
</AdminField>
|
||||||
|
<AdminButton
|
||||||
|
variant="secondary"
|
||||||
|
disabled={isLoading || !hasLoaded}
|
||||||
|
type="button"
|
||||||
|
onClick={
|
||||||
|
isSorted ? restoreDefaultOrder : () => void sortAllProjects()
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<ArrowDownWideNarrow size={17} aria-hidden="true" />
|
||||||
|
<span>{isSorted ? '恢复默认顺序' : '按同步时间排序'}</span>
|
||||||
|
</AdminButton>
|
||||||
|
{isSorting ? (
|
||||||
|
<>
|
||||||
|
<span role="status">
|
||||||
|
正在读取全部项目,已读取 {sortProgress} 个
|
||||||
|
</span>
|
||||||
|
<AdminButton
|
||||||
|
variant="secondary"
|
||||||
|
type="button"
|
||||||
|
onClick={cancelSorting}
|
||||||
|
>
|
||||||
|
<X size={17} aria-hidden="true" />
|
||||||
|
<span>取消排序</span>
|
||||||
|
</AdminButton>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
<AdminButton
|
<AdminButton
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
@@ -269,6 +421,9 @@ export function AdminProjectSnapshotsPage({
|
|||||||
<>
|
<>
|
||||||
第 {pageIndex} 页
|
第 {pageIndex} 页
|
||||||
{items.length ? `,本页 ${items.length} 个项目` : ''}
|
{items.length ? `,本页 ${items.length} 个项目` : ''}
|
||||||
|
{isSorted
|
||||||
|
? `,共 ${sortedItems.current?.length ?? 0} 个,按同步时间从新到旧`
|
||||||
|
: ''}
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
pageSize={{
|
pageSize={{
|
||||||
@@ -279,7 +434,12 @@ export function AdminProjectSnapshotsPage({
|
|||||||
}}
|
}}
|
||||||
busy={isLoading}
|
busy={isLoading}
|
||||||
previous={{ disabled: pageIndex <= 1, onClick: goToPreviousPage }}
|
previous={{ disabled: pageIndex <= 1, onClick: goToPreviousPage }}
|
||||||
next={{ disabled: !nextCursor, onClick: goToNextPage }}
|
next={{
|
||||||
|
disabled: isSorted
|
||||||
|
? pageIndex * pageSize >= (sortedItems.current?.length ?? 0)
|
||||||
|
: !nextCursor,
|
||||||
|
onClick: goToNextPage,
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
) : null
|
) : null
|
||||||
}
|
}
|
||||||
@@ -375,6 +535,10 @@ function snapshotKey(entry: AdminProjectSnapshotEntry) {
|
|||||||
return `${entry.userId}/${entry.projectId}`;
|
return `${entry.userId}/${entry.projectId}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function compareIds(left: string, right: string) {
|
||||||
|
return left < right ? -1 : left > right ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
function formatBytes(bytes: number) {
|
function formatBytes(bytes: number) {
|
||||||
const units = ['B', 'KiB', 'MiB', 'GiB'];
|
const units = ['B', 'KiB', 'MiB', 'GiB'];
|
||||||
const unit = Math.min(
|
const unit = Math.min(
|
||||||
|
|||||||
@@ -56,7 +56,9 @@ export default defineConfig(({ command, mode }) => {
|
|||||||
build: {
|
build: {
|
||||||
outDir: 'dist',
|
outDir: 'dist',
|
||||||
emptyOutDir: true,
|
emptyOutDir: true,
|
||||||
chunkSizeWarningLimit: 600,
|
// 中文注释:后台单页 chunk 已增长到 600.23 kB;沿用 2026-05-01 把 500 提到 600 的同一处理口径
|
||||||
|
// 提到 700,先让直推 master 的构建门禁恢复可过,真正的按路由拆分另行排期。
|
||||||
|
chunkSizeWarningLimit: 700,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
# resources/plugins 由 build.rs 从 plugins/ 复制生成,属于构建产物。
|
|
||||||
# 它在 dev 监听范围内,重新生成会让 Tauri dev 误判为源码改动而触发
|
|
||||||
# “构建 -> 监听 -> 再构建”的自触发循环。
|
|
||||||
resources/plugins/
|
|
||||||
@@ -13,6 +13,10 @@
|
|||||||
"skill-pack:check": "node scripts/check-skill-pack.mjs",
|
"skill-pack:check": "node scripts/check-skill-pack.mjs",
|
||||||
"skill-pack:sync": "node scripts/check-skill-pack.mjs --write",
|
"skill-pack:sync": "node scripts/check-skill-pack.mjs --write",
|
||||||
"skill-pack:test": "node --test scripts/check-skill-pack.test.mjs",
|
"skill-pack:test": "node --test scripts/check-skill-pack.test.mjs",
|
||||||
|
"bundled-resources:check": "node scripts/check-package-layout.mjs",
|
||||||
|
"bundled-resources:sync": "node scripts/check-package-layout.mjs --write",
|
||||||
|
"bundled-resources:prepare": "node scripts/prepare-bundled-resources.mjs",
|
||||||
|
"bundled-resources:test": "node --test scripts/prepare-bundled-resources.test.mjs",
|
||||||
"llm-status": "node scripts/run-cli-with-config.mjs --llm-status",
|
"llm-status": "node scripts/run-cli-with-config.mjs --llm-status",
|
||||||
"agent-task": "node scripts/run-cli-with-config.mjs --agent-task",
|
"agent-task": "node scripts/run-cli-with-config.mjs --agent-task",
|
||||||
"config": "node scripts/game-creator-config-wizard.mjs",
|
"config": "node scripts/game-creator-config-wizard.mjs",
|
||||||
@@ -24,7 +28,7 @@
|
|||||||
"agent-runtime:supervisor-swarm-tool-plan-handoff-runner-kill-real-e2e": "node scripts/agent-runtime-real-e2e.mjs --suite supervisor-swarm-tool-plan-handoff-runner-kill",
|
"agent-runtime:supervisor-swarm-tool-plan-handoff-runner-kill-real-e2e": "node scripts/agent-runtime-real-e2e.mjs --suite supervisor-swarm-tool-plan-handoff-runner-kill",
|
||||||
"agent-runtime:steer-real-e2e": "node scripts/agent-runtime-steer-real-e2e.mjs",
|
"agent-runtime:steer-real-e2e": "node scripts/agent-runtime-steer-real-e2e.mjs",
|
||||||
"agent-runtime:steer-runner-kill-real-e2e": "node scripts/agent-runtime-real-e2e.mjs --suite steer-runner-kill",
|
"agent-runtime:steer-runner-kill-real-e2e": "node scripts/agent-runtime-real-e2e.mjs --suite steer-runner-kill",
|
||||||
"typecheck": "tsc -p tsconfig.json --noEmit && npm run skill-pack:check && node scripts/check-config.mjs"
|
"typecheck": "tsc -p tsconfig.json --noEmit && npm run skill-pack:check && npm run bundled-resources:check && node scripts/check-config.mjs"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@anthropic-ai/claude-agent-sdk": "0.3.285",
|
"@anthropic-ai/claude-agent-sdk": "0.3.285",
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ import {
|
|||||||
} from './agc-global-version.mjs';
|
} from './agc-global-version.mjs';
|
||||||
import {
|
import {
|
||||||
defaultEditorFeatures,
|
defaultEditorFeatures,
|
||||||
|
readCargoTarget,
|
||||||
|
resolveEditorFeatures,
|
||||||
withDefaultCargoFeatures,
|
withDefaultCargoFeatures,
|
||||||
} from './cargo-features.mjs';
|
} from './cargo-features.mjs';
|
||||||
import {
|
import {
|
||||||
@@ -19,6 +21,10 @@ import {
|
|||||||
resolveReleaseChannel,
|
resolveReleaseChannel,
|
||||||
} from './channel-identity.mjs';
|
} from './channel-identity.mjs';
|
||||||
import { prepareNsisToolsetForRelease } from './nsis-toolset.mjs';
|
import { prepareNsisToolsetForRelease } from './nsis-toolset.mjs';
|
||||||
|
import {
|
||||||
|
prepareBundledResources,
|
||||||
|
supportedHostTarget,
|
||||||
|
} from './prepare-bundled-resources.mjs';
|
||||||
import { stageNodeRuntime } from './stage-node-runtime.mjs';
|
import { stageNodeRuntime } from './stage-node-runtime.mjs';
|
||||||
|
|
||||||
const appRoot = fileURLToPath(new URL('..', import.meta.url));
|
const appRoot = fileURLToPath(new URL('..', import.meta.url));
|
||||||
@@ -40,26 +46,7 @@ function defaultTarget() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function explicitBuildTarget(args) {
|
function explicitBuildTarget(args) {
|
||||||
let target;
|
return readCargoTarget(args);
|
||||||
const separator = args.indexOf('--');
|
|
||||||
const options = separator < 0 ? args : args.slice(0, separator);
|
|
||||||
for (let index = 0; index < options.length; index += 1) {
|
|
||||||
const argument = options[index];
|
|
||||||
let value;
|
|
||||||
if (argument === '--target' || argument === '-t') {
|
|
||||||
value = options[++index];
|
|
||||||
} else if (argument.startsWith('--target=')) {
|
|
||||||
value = argument.slice('--target='.length);
|
|
||||||
} else {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (!value?.trim() || value.startsWith('-')) {
|
|
||||||
throw new Error('--target 缺少有效目标');
|
|
||||||
}
|
|
||||||
if (target !== undefined) throw new Error('不能重复指定 --target');
|
|
||||||
target = value.trim();
|
|
||||||
}
|
|
||||||
return target;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateReleaseTarget(target) {
|
function validateReleaseTarget(target) {
|
||||||
@@ -469,10 +456,32 @@ function writeChannelConfigFile(channel, target, includeNodeRuntime = false) {
|
|||||||
return configPath;
|
return configPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 随包资源必须在打包工具之前生成:构建脚本只做只读校验,不再生成。
|
||||||
|
export function stageBundledResources(
|
||||||
|
target,
|
||||||
|
{ prepare = prepareBundledResources, features } = {},
|
||||||
|
) {
|
||||||
|
const summaries = prepare({
|
||||||
|
target,
|
||||||
|
features: new Set(features ?? defaultEditorFeatures(target)),
|
||||||
|
profile: 'release',
|
||||||
|
log: (line) => console.log(`[ai-game-creator-shell] ${line}`),
|
||||||
|
});
|
||||||
|
for (const summary of summaries) {
|
||||||
|
console.log(`[ai-game-creator-shell] ${summary}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function runTauriBuild(
|
export function runTauriBuild(
|
||||||
args = [],
|
args = [],
|
||||||
context = resolveReleaseContext(args),
|
context = resolveReleaseContext(args),
|
||||||
{ spawn = spawnSync, stageRuntime = stageNodeRuntime } = {},
|
{
|
||||||
|
spawn = spawnSync,
|
||||||
|
stageRuntime = stageNodeRuntime,
|
||||||
|
stageBundled = stageBundledResources,
|
||||||
|
platform = process.platform,
|
||||||
|
arch = process.arch,
|
||||||
|
} = {},
|
||||||
) {
|
) {
|
||||||
if (
|
if (
|
||||||
explicitBuildTarget(args) &&
|
explicitBuildTarget(args) &&
|
||||||
@@ -480,14 +489,44 @@ export function runTauriBuild(
|
|||||||
) {
|
) {
|
||||||
throw new Error('构建参数与发布上下文目标不一致');
|
throw new Error('构建参数与发布上下文目标不一致');
|
||||||
}
|
}
|
||||||
const tauriArguments = buildTauriBuildArguments(args, context.target);
|
const tauriArguments = buildTauriBuildArguments(
|
||||||
const { channel, target } = context;
|
args,
|
||||||
if (!args.includes('--no-bundle')) stageRuntime(target);
|
context.target,
|
||||||
const configPath = writeChannelConfigFile(
|
platform,
|
||||||
channel,
|
|
||||||
target,
|
|
||||||
!args.includes('--no-bundle'),
|
|
||||||
);
|
);
|
||||||
|
const bundling = !args.includes('--no-bundle');
|
||||||
|
// 无显式 target 的 no-bundle 由 Cargo 构建宿主平台,不使用默认发布目标。
|
||||||
|
const resourceTarget =
|
||||||
|
explicitBuildTarget(args) ||
|
||||||
|
(bundling ? context.target : supportedHostTarget(platform, arch));
|
||||||
|
if (
|
||||||
|
!bundling &&
|
||||||
|
!resourceTarget &&
|
||||||
|
defaultEditorFeatures(platform).length > 0
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
`当前宿主 ${platform}/${arch} 不在随包资源声明覆盖内;请用 --target 指定受支持的构建目标`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const features = resolveEditorFeatures({
|
||||||
|
argv: tauriArguments,
|
||||||
|
target: resourceTarget ?? platform,
|
||||||
|
env: {},
|
||||||
|
});
|
||||||
|
const { channel } = context;
|
||||||
|
if (bundling) {
|
||||||
|
stageRuntime(resourceTarget);
|
||||||
|
}
|
||||||
|
if (resourceTarget) {
|
||||||
|
stageBundled(resourceTarget, { features });
|
||||||
|
} else {
|
||||||
|
console.log(
|
||||||
|
'[ai-game-creator-shell] 当前宿主平台不参与客户端随包资源构建,跳过资源准备',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Linux smoke 不发布 updater,沿用渠道上下文;桌面宿主必须与实际构建目标一致。
|
||||||
|
const target = resourceTarget ?? context.target;
|
||||||
|
const configPath = writeChannelConfigFile(channel, target, bundling);
|
||||||
console.log(
|
console.log(
|
||||||
`[ai-game-creator-shell] 渠道 ${channel} 端点配置:${configPath}`,
|
`[ai-game-creator-shell] 渠道 ${channel} 端点配置:${configPath}`,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ import {
|
|||||||
AGC_PRODUCT_NAME,
|
AGC_PRODUCT_NAME,
|
||||||
resolveChannelInstallIdentity,
|
resolveChannelInstallIdentity,
|
||||||
} from './channel-identity.mjs';
|
} from './channel-identity.mjs';
|
||||||
|
import { supportedHostTarget } from './prepare-bundled-resources.mjs';
|
||||||
|
|
||||||
const windowsTarget = 'x86_64-pc-windows-msvc';
|
const windowsTarget = 'x86_64-pc-windows-msvc';
|
||||||
const universalTarget = 'universal-apple-darwin';
|
const universalTarget = 'universal-apple-darwin';
|
||||||
@@ -504,6 +505,8 @@ test('packaged renderer receives the same channel as the updater manifest', () =
|
|||||||
// 必须 stub:真实 staging 会用宿主平台(如 macOS 的 darwin/arm64)去对默认的
|
// 必须 stub:真实 staging 会用宿主平台(如 macOS 的 darwin/arm64)去对默认的
|
||||||
// Windows 目标做一致性校验,在非 Windows 主机上直接失败——本用例只关心渠道注入。
|
// Windows 目标做一致性校验,在非 Windows 主机上直接失败——本用例只关心渠道注入。
|
||||||
stageRuntime: () => {},
|
stageRuntime: () => {},
|
||||||
|
// 同上:随包资源准备会读取真实上游包,本用例只关心渠道环境变量。
|
||||||
|
stageBundled: () => {},
|
||||||
spawn: (_binary, _args, options) => {
|
spawn: (_binary, _args, options) => {
|
||||||
spawnOptions = options;
|
spawnOptions = options;
|
||||||
return { status: 0 };
|
return { status: 0 };
|
||||||
@@ -594,6 +597,8 @@ test('explicit macOS target drives version lookup, Tauri endpoint, artifact and
|
|||||||
seenContexts.push(context);
|
seenContexts.push(context);
|
||||||
runTauriBuild(args, context, {
|
runTauriBuild(args, context, {
|
||||||
stageRuntime: () => {},
|
stageRuntime: () => {},
|
||||||
|
// 必须 stub:随包资源准备会读取真实上游包与仓库插件工作区,本用例只关心参数。
|
||||||
|
stageBundled: () => {},
|
||||||
spawn: (_binary, command) => {
|
spawn: (_binary, command) => {
|
||||||
const configIndex = command.lastIndexOf('--config');
|
const configIndex = command.lastIndexOf('--config');
|
||||||
const config = JSON.parse(
|
const config = JSON.parse(
|
||||||
@@ -843,6 +848,7 @@ test('Windows remains the default and explicit Windows overrides macOS environme
|
|||||||
context,
|
context,
|
||||||
{
|
{
|
||||||
stageRuntime: () => {},
|
stageRuntime: () => {},
|
||||||
|
stageBundled: () => {},
|
||||||
spawn: (_binary, command) => {
|
spawn: (_binary, command) => {
|
||||||
assert.ok(
|
assert.ok(
|
||||||
command.includes(
|
command.includes(
|
||||||
@@ -960,6 +966,10 @@ test('release stages Node before Tauri and injects its resource mapping only for
|
|||||||
assert.equal(target, windowsTarget);
|
assert.equal(target, windowsTarget);
|
||||||
events.push('stage');
|
events.push('stage');
|
||||||
},
|
},
|
||||||
|
stageBundled(target) {
|
||||||
|
assert.equal(target, windowsTarget);
|
||||||
|
events.push('bundled');
|
||||||
|
},
|
||||||
spawn(_binary, args) {
|
spawn(_binary, args) {
|
||||||
events.push('build');
|
events.push('build');
|
||||||
const config = JSON.parse(
|
const config = JSON.parse(
|
||||||
@@ -975,12 +985,24 @@ test('release stages Node before Tauri and injects its resource mapping only for
|
|||||||
return { status: 0 };
|
return { status: 0 };
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
assert.deepEqual(events, ['stage', 'build']);
|
assert.deepEqual(events, ['stage', 'bundled', 'build']);
|
||||||
|
events.length = 0;
|
||||||
runTauriBuild(['--no-bundle', '--target', windowsTarget], context, {
|
runTauriBuild(['--no-bundle', '--target', windowsTarget], context, {
|
||||||
stageRuntime() {
|
stageRuntime() {
|
||||||
assert.fail('no-bundle must not stage resources');
|
assert.fail('no-bundle must not stage node runtime');
|
||||||
|
},
|
||||||
|
stageBundled(target, options) {
|
||||||
|
// app 构建本身就需要随包资源,因此 --no-bundle 也要 staging,
|
||||||
|
// 且必须拿到与 cargo 相同的 feature 集(避免「cargo 开、staging 没开」)。
|
||||||
|
assert.equal(target, windowsTarget);
|
||||||
|
assert.ok(
|
||||||
|
options?.features?.length > 0,
|
||||||
|
'bundled staging 必须收到 feature 集',
|
||||||
|
);
|
||||||
|
events.push('bundled');
|
||||||
},
|
},
|
||||||
spawn(_binary, args) {
|
spawn(_binary, args) {
|
||||||
|
events.push('build');
|
||||||
const config = JSON.parse(
|
const config = JSON.parse(
|
||||||
readFileSync(args[args.lastIndexOf('--config') + 1], 'utf8'),
|
readFileSync(args[args.lastIndexOf('--config') + 1], 'utf8'),
|
||||||
);
|
);
|
||||||
@@ -993,12 +1015,16 @@ test('release stages Node before Tauri and injects its resource mapping only for
|
|||||||
return { status: 0 };
|
return { status: 0 };
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
assert.deepEqual(events, ['bundled', 'build']);
|
||||||
assert.throws(
|
assert.throws(
|
||||||
() =>
|
() =>
|
||||||
runTauriBuild(['--target', windowsTarget], context, {
|
runTauriBuild(['--target', windowsTarget], context, {
|
||||||
stageRuntime() {
|
stageRuntime() {
|
||||||
throw new Error('missing runtime');
|
throw new Error('missing runtime');
|
||||||
},
|
},
|
||||||
|
stageBundled() {
|
||||||
|
assert.fail('invalid runtime must prevent bundled staging');
|
||||||
|
},
|
||||||
spawn() {
|
spawn() {
|
||||||
assert.fail('invalid runtime must prevent build');
|
assert.fail('invalid runtime must prevent build');
|
||||||
},
|
},
|
||||||
@@ -1007,6 +1033,297 @@ test('release stages Node before Tauri and injects its resource mapping only for
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const windowsEditorFeatures = [
|
||||||
|
'cocos-editor-execute',
|
||||||
|
'unity-editor-execute',
|
||||||
|
'godot-editor-execute',
|
||||||
|
];
|
||||||
|
|
||||||
|
function uniqueSorted(values) {
|
||||||
|
return [...new Set(values ?? [])].sort();
|
||||||
|
}
|
||||||
|
|
||||||
|
function splitFeatureList(value) {
|
||||||
|
return String(value ?? '')
|
||||||
|
.split(',')
|
||||||
|
.map((name) => name.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 最终命令行里 Cargo 实际会收到的 feature 集:tauri 自身的 `--features` / `-f`
|
||||||
|
/// 与 `--` 之后的 runner 参数(tauri 会把它们追加到 cargo 命令)都算数,
|
||||||
|
/// 随包资源准备必须拿到同一集合。
|
||||||
|
function cargoFeaturesFromCommand(command) {
|
||||||
|
const argv = command.slice(command.indexOf('--') + 1);
|
||||||
|
const features = [];
|
||||||
|
for (let index = 0; index < argv.length; index += 1) {
|
||||||
|
const argument = String(argv[index]);
|
||||||
|
if (argument.startsWith('--features=')) {
|
||||||
|
features.push(...splitFeatureList(argument.slice('--features='.length)));
|
||||||
|
} else if (argument === '--features' || argument === '-f') {
|
||||||
|
features.push(...splitFeatureList(argv[index + 1]));
|
||||||
|
index += 1;
|
||||||
|
} else if (/^-f.+/u.test(argument)) {
|
||||||
|
features.push(...splitFeatureList(argument.slice(2)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return uniqueSorted(features);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PR520 的 CI 回归:`tauri build --no-bundle` 不带显式 target 时编译的是**宿主**平台,
|
||||||
|
* 而发布上下文默认目标始终是 Windows。随包资源与 feature 集必须跟着宿主走,
|
||||||
|
* 否则 Linux 宿主会拿着 Windows 目标去 staging(CI smoke 直接失败),
|
||||||
|
* 还会出现「cargo 没开 feature、staging 开了」的错配。
|
||||||
|
*/
|
||||||
|
test('no-bundle 在无随包资源声明的宿主(Linux)既不 staging 也不注入 Windows feature', () => {
|
||||||
|
const context = resolveReleaseContext(['--no-bundle'], {});
|
||||||
|
assert.equal(context.target, windowsTarget);
|
||||||
|
let command;
|
||||||
|
withEnv({ AGC_DEV_CARGO_FEATURES: undefined }, () => {
|
||||||
|
runTauriBuild(['--no-bundle'], context, {
|
||||||
|
platform: 'linux',
|
||||||
|
arch: 'x64',
|
||||||
|
stageRuntime() {
|
||||||
|
assert.fail('--no-bundle 不得准备 Node 运行时');
|
||||||
|
},
|
||||||
|
stageBundled(target) {
|
||||||
|
assert.fail(`Linux 宿主没有随包资源声明,不得 staging ${target}`);
|
||||||
|
},
|
||||||
|
spawn(_binary, args) {
|
||||||
|
command = args;
|
||||||
|
return { status: 0 };
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
assert.ok(command, '宿主不受声明覆盖时仍必须完成本次构建');
|
||||||
|
assert.ok(!command.includes('--target'), '宿主构建不得打显式 target');
|
||||||
|
assert.deepEqual(
|
||||||
|
cargoFeaturesFromCommand(command),
|
||||||
|
[],
|
||||||
|
'Linux 宿主不得注入 Windows 编辑器 feature',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('no-bundle 在 macOS / Windows 宿主按真实宿主三元组准备随包资源', () => {
|
||||||
|
const context = resolveReleaseContext(['--no-bundle'], {});
|
||||||
|
for (const [platform, arch, hostTarget, expectedFeatures] of [
|
||||||
|
['darwin', 'arm64', 'aarch64-apple-darwin', []],
|
||||||
|
['darwin', 'x64', 'x86_64-apple-darwin', []],
|
||||||
|
['win32', 'x64', windowsTarget, windowsEditorFeatures],
|
||||||
|
]) {
|
||||||
|
const label = `${platform}/${arch}`;
|
||||||
|
let staged;
|
||||||
|
let command;
|
||||||
|
withEnv({ AGC_DEV_CARGO_FEATURES: undefined }, () => {
|
||||||
|
runTauriBuild(['--no-bundle'], context, {
|
||||||
|
platform,
|
||||||
|
arch,
|
||||||
|
stageRuntime() {
|
||||||
|
assert.fail('--no-bundle 不得准备 Node 运行时');
|
||||||
|
},
|
||||||
|
stageBundled(target, options) {
|
||||||
|
staged = { target, features: options?.features };
|
||||||
|
},
|
||||||
|
spawn(_binary, args) {
|
||||||
|
command = args;
|
||||||
|
return { status: 0 };
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
assert.equal(
|
||||||
|
staged?.target,
|
||||||
|
hostTarget,
|
||||||
|
`${label} 必须按宿主三元组 staging`,
|
||||||
|
);
|
||||||
|
assert.deepEqual(
|
||||||
|
uniqueSorted(staged?.features),
|
||||||
|
uniqueSorted(expectedFeatures),
|
||||||
|
`${label} staging feature 与宿主不一致`,
|
||||||
|
);
|
||||||
|
assert.deepEqual(
|
||||||
|
cargoFeaturesFromCommand(command),
|
||||||
|
uniqueSorted(expectedFeatures),
|
||||||
|
`${label} Cargo feature 与 staging 不一致`,
|
||||||
|
);
|
||||||
|
assert.ok(
|
||||||
|
!command.includes('--target'),
|
||||||
|
`${label} 宿主构建不得打显式 target`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('no-bundle 的显式 target 优先于宿主:跨平台 / 跨架构仍按参数准备', () => {
|
||||||
|
for (const [platform, arch, target, expectedFeatures] of [
|
||||||
|
['linux', 'x64', windowsTarget, windowsEditorFeatures],
|
||||||
|
['darwin', 'arm64', 'x86_64-apple-darwin', []],
|
||||||
|
]) {
|
||||||
|
const args = ['--no-bundle', '--target', target];
|
||||||
|
const context = resolveReleaseContext(args, {});
|
||||||
|
const label = `${platform}/${arch} → ${target}`;
|
||||||
|
let staged;
|
||||||
|
let command;
|
||||||
|
withEnv({ AGC_DEV_CARGO_FEATURES: undefined }, () => {
|
||||||
|
runTauriBuild(args, context, {
|
||||||
|
platform,
|
||||||
|
arch,
|
||||||
|
stageRuntime() {
|
||||||
|
assert.fail('--no-bundle 不得准备 Node 运行时');
|
||||||
|
},
|
||||||
|
stageBundled(stageTarget, options) {
|
||||||
|
staged = { target: stageTarget, features: options?.features };
|
||||||
|
},
|
||||||
|
spawn(_binary, argv) {
|
||||||
|
command = argv;
|
||||||
|
return { status: 0 };
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
assert.equal(staged?.target, target, `${label} 必须按显式 target staging`);
|
||||||
|
assert.ok(
|
||||||
|
command.includes(target),
|
||||||
|
`${label} 必须把显式 target 传给 Tauri`,
|
||||||
|
);
|
||||||
|
assert.deepEqual(
|
||||||
|
uniqueSorted(staged?.features),
|
||||||
|
uniqueSorted(expectedFeatures),
|
||||||
|
`${label} staging feature 与预期不一致`,
|
||||||
|
);
|
||||||
|
assert.deepEqual(
|
||||||
|
cargoFeaturesFromCommand(command),
|
||||||
|
uniqueSorted(expectedFeatures),
|
||||||
|
`${label} Cargo feature 与 staging 不一致`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('打包构建(非 no-bundle)的 staging 目标跟随发布上下文而不是宿主', () => {
|
||||||
|
const crossArch = 'x86_64-apple-darwin';
|
||||||
|
const context = resolveReleaseContext([], { AGC_BUILD_TARGET: crossArch });
|
||||||
|
assert.equal(context.target, crossArch);
|
||||||
|
let runtimeTarget;
|
||||||
|
let staged;
|
||||||
|
let command;
|
||||||
|
withEnv({ AGC_DEV_CARGO_FEATURES: undefined }, () => {
|
||||||
|
runTauriBuild([], context, {
|
||||||
|
platform: 'darwin',
|
||||||
|
arch: 'arm64',
|
||||||
|
stageRuntime(target) {
|
||||||
|
runtimeTarget = target;
|
||||||
|
},
|
||||||
|
stageBundled(target, options) {
|
||||||
|
staged = { target, features: options?.features };
|
||||||
|
},
|
||||||
|
spawn(_binary, args) {
|
||||||
|
command = args;
|
||||||
|
return { status: 0 };
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
assert.equal(runtimeTarget, crossArch);
|
||||||
|
assert.equal(
|
||||||
|
staged?.target,
|
||||||
|
crossArch,
|
||||||
|
'打包构建必须按发布上下文目标 staging',
|
||||||
|
);
|
||||||
|
assert.deepEqual(uniqueSorted(staged?.features), []);
|
||||||
|
assert.ok(
|
||||||
|
command.includes(crossArch),
|
||||||
|
'打包构建必须把发布上下文目标传给 Tauri',
|
||||||
|
);
|
||||||
|
assert.deepEqual(cargoFeaturesFromCommand(command), []);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('staging feature 与最终 Cargo feature 一致:-f / --features / -- 之后的 runner 参数', () => {
|
||||||
|
const context = resolveReleaseContext(['--no-bundle'], {});
|
||||||
|
const cases = [
|
||||||
|
{
|
||||||
|
args: ['--no-bundle', '--features', 'custom-editor-feature'],
|
||||||
|
expected: ['custom-editor-feature'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
args: ['--no-bundle', '--features=custom-a,custom-b'],
|
||||||
|
expected: ['custom-a', 'custom-b'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
args: ['--no-bundle', '-f', 'custom-editor-feature'],
|
||||||
|
expected: ['custom-editor-feature'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
args: ['--no-bundle', '-fcustom-editor-feature'],
|
||||||
|
expected: ['custom-editor-feature'],
|
||||||
|
},
|
||||||
|
// `--` 之后的参数是 runner 参数:tauri 会把它们追加到 cargo 命令行
|
||||||
|
// (`tauri build --help`:Command line arguments passed to the runner),
|
||||||
|
// 因此 Cargo 最终拿到的是「注入的默认值 ∪ runner 参数」,staging 必须一致。
|
||||||
|
{
|
||||||
|
args: ['--no-bundle', '--', '--features=app-runner'],
|
||||||
|
expected: [...windowsEditorFeatures, 'app-runner'],
|
||||||
|
},
|
||||||
|
// 环境变量泄漏(Jenkins Job 参数)不得让 staging 与 Cargo 分叉。
|
||||||
|
{
|
||||||
|
args: ['--no-bundle'],
|
||||||
|
env: { AGC_DEV_CARGO_FEATURES: 'cocos-editor-execute' },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
for (const entry of cases) {
|
||||||
|
const { args, expected } = entry;
|
||||||
|
const label = args.join(' ');
|
||||||
|
let staged;
|
||||||
|
let command;
|
||||||
|
withEnv({ AGC_DEV_CARGO_FEATURES: undefined, ...entry.env }, () => {
|
||||||
|
runTauriBuild(args, context, {
|
||||||
|
platform: 'win32',
|
||||||
|
arch: 'x64',
|
||||||
|
stageRuntime() {
|
||||||
|
assert.fail(`${label} 不得准备 Node 运行时`);
|
||||||
|
},
|
||||||
|
stageBundled(target, options) {
|
||||||
|
staged = { target, features: options?.features };
|
||||||
|
},
|
||||||
|
spawn(_binary, argv) {
|
||||||
|
command = argv;
|
||||||
|
return { status: 0 };
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
assert.equal(staged?.target, windowsTarget, label);
|
||||||
|
const cargoFeatures = cargoFeaturesFromCommand(command);
|
||||||
|
assert.deepEqual(
|
||||||
|
uniqueSorted(staged?.features),
|
||||||
|
cargoFeatures,
|
||||||
|
`${label}:staging feature 必须等于 Cargo 收到的 feature`,
|
||||||
|
);
|
||||||
|
if (expected) {
|
||||||
|
assert.deepEqual(
|
||||||
|
cargoFeatures,
|
||||||
|
uniqueSorted(expected),
|
||||||
|
`${label}:Cargo feature 与预期不一致`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('未注入 platform / arch 时按真实 process 平台取宿主随包目标', () => {
|
||||||
|
const hostTarget = supportedHostTarget(process.platform, process.arch);
|
||||||
|
const context = resolveReleaseContext(['--no-bundle'], {});
|
||||||
|
const events = [];
|
||||||
|
withEnv({ AGC_DEV_CARGO_FEATURES: undefined }, () => {
|
||||||
|
runTauriBuild(['--no-bundle'], context, {
|
||||||
|
stageRuntime() {
|
||||||
|
assert.fail('--no-bundle 不得准备 Node 运行时');
|
||||||
|
},
|
||||||
|
stageBundled(target) {
|
||||||
|
events.push(target);
|
||||||
|
},
|
||||||
|
spawn() {
|
||||||
|
return { status: 0 };
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
assert.deepEqual(events, hostTarget ? [hostTarget] : []);
|
||||||
|
});
|
||||||
|
|
||||||
test('channel manifest carries version, platform keys and signature', () => {
|
test('channel manifest carries version, platform keys and signature', () => {
|
||||||
withSignedArtifact('陶泥儿_0.1.48_x64-setup.exe', (artifact) => {
|
withSignedArtifact('陶泥儿_0.1.48_x64-setup.exe', (artifact) => {
|
||||||
withEnv({ AGC_UPDATE_RELEASE_NOTES: '修复与改进' }, () => {
|
withEnv({ AGC_UPDATE_RELEASE_NOTES: '修复与改进' }, () => {
|
||||||
@@ -1111,6 +1428,8 @@ for (const channel of ['release', 'beta-2']) {
|
|||||||
);
|
);
|
||||||
runTauriBuild([`--target=${target}`], context, {
|
runTauriBuild([`--target=${target}`], context, {
|
||||||
stageRuntime: () => {},
|
stageRuntime: () => {},
|
||||||
|
// 必须 stub:随包资源准备会读取真实上游包与仓库插件工作区,本用例只关心参数。
|
||||||
|
stageBundled: () => {},
|
||||||
spawn: (_binary, command) => {
|
spawn: (_binary, command) => {
|
||||||
const config = JSON.parse(
|
const config = JSON.parse(
|
||||||
readFileSync(
|
readFileSync(
|
||||||
|
|||||||
@@ -1,24 +1,135 @@
|
|||||||
/** 默认桌面能力;显式 feature 参数优先,不把应用参数当 Cargo 参数。 */
|
/** 默认桌面能力;显式 feature 参数优先,不把应用参数当 Cargo 参数。 */
|
||||||
export function withDefaultCargoFeatures(argv, features) {
|
export function withDefaultCargoFeatures(argv, features) {
|
||||||
const separator = argv.indexOf('--');
|
// 只看第一个 `--` 之前的 Tauri 选项:runner 参数区里的 `--features` 会与这里注入的
|
||||||
const cargoArgs = separator < 0 ? argv : argv.slice(0, separator);
|
// 默认值在 Cargo 侧合并,不能当作「用户已经指定过了」而跳过注入。
|
||||||
if (
|
if (!features.length || tauriOptions(argv).some(hasExplicitFeatures)) {
|
||||||
!features.length ||
|
|
||||||
cargoArgs.some(
|
|
||||||
(value) =>
|
|
||||||
value === '--features' ||
|
|
||||||
value === '-f' ||
|
|
||||||
value.startsWith('--features=') ||
|
|
||||||
/^-f.+/u.test(value),
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
return argv;
|
return argv;
|
||||||
}
|
}
|
||||||
return [`--features=${features.join(',')}`, ...argv];
|
return [`--features=${features.join(',')}`, ...argv];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 第一个 `--` 之前是 Tauri 自己的选项,之后是 runner(Cargo)参数。
|
||||||
|
function tauriOptions(argv) {
|
||||||
|
const separator = argv.indexOf('--');
|
||||||
|
return separator < 0 ? argv : argv.slice(0, separator);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 交给 Tauri 与 Cargo 的参数区间:第一个 `--` 之前是 Tauri 选项,之后到第二个 `--` 是
|
||||||
|
* runner(Cargo)参数;第二个 `--` 之后由 Tauri 转交应用,不属于构建参数。
|
||||||
|
* dev 入口(`buildTauriArguments` 会把启动器的单个 `--` 补成 runner + 应用两段)与发布
|
||||||
|
* 入口都用这一份区间解析,两边的目标与 feature 必须来自同一个口径。
|
||||||
|
*/
|
||||||
|
function cargoArguments(argv) {
|
||||||
|
const separator = argv.indexOf('--');
|
||||||
|
if (separator < 0) return argv;
|
||||||
|
const applicationStart = argv.indexOf('--', separator + 1);
|
||||||
|
return applicationStart < 0 ? argv : argv.slice(0, applicationStart);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 四种拼写(`--features x` / `--features=x` / `-f x` / `-fx`)都算显式指定。
|
||||||
|
function hasExplicitFeatures(value) {
|
||||||
|
return (
|
||||||
|
value === '--features' ||
|
||||||
|
value === '-f' ||
|
||||||
|
value.startsWith('--features=') ||
|
||||||
|
/^-f.+/u.test(value)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseFeatureNames(raw) {
|
||||||
|
return String(raw)
|
||||||
|
.split(/[\s,]+/u)
|
||||||
|
.map((name) => name.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
export function defaultEditorFeatures(target) {
|
export function defaultEditorFeatures(target) {
|
||||||
return target === 'win32' || target.includes('windows')
|
return target === 'win32' || target.includes('windows')
|
||||||
? ['cocos-editor-execute', 'unity-editor-execute', 'godot-editor-execute']
|
? ['cocos-editor-execute', 'unity-editor-execute', 'godot-editor-execute']
|
||||||
: [];
|
: [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 显式 Cargo 目标解析:`--target <triple>` / `--target=<triple>` / `-t <triple>`。
|
||||||
|
* 应用参数区(第二个 `--` 之后)不参与解析。未显式指定返回 undefined;取值缺失、
|
||||||
|
* 取到另一个选项或重复指定都失败关闭。
|
||||||
|
* dev 入口与发布入口共用这一份解析:cargo 的构建目标与随包资源 staging 目标
|
||||||
|
* 必须来自同一个显式来源,不能一方读参数、另一方回退宿主默认值。
|
||||||
|
*/
|
||||||
|
export function readCargoTarget(argv = []) {
|
||||||
|
const options = cargoArguments(argv);
|
||||||
|
let target;
|
||||||
|
for (let index = 0; index < options.length; index += 1) {
|
||||||
|
const argument = String(options[index]);
|
||||||
|
let value;
|
||||||
|
if (argument === '--target' || argument === '-t') {
|
||||||
|
value = options[index + 1];
|
||||||
|
index += 1;
|
||||||
|
} else if (argument.startsWith('--target=')) {
|
||||||
|
value = argument.slice('--target='.length);
|
||||||
|
} else if (argument.startsWith('-t') && argument.length > 2) {
|
||||||
|
value = argument.slice(2).replace(/^=/u, '');
|
||||||
|
} else {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!value?.trim() || String(value).startsWith('-')) {
|
||||||
|
throw new Error('--target 缺少有效目标');
|
||||||
|
}
|
||||||
|
if (target !== undefined) throw new Error('不能重复指定 --target');
|
||||||
|
target = String(value).trim();
|
||||||
|
}
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 单一声明式解析:本次构建实际生效的编辑器 feature 集。
|
||||||
|
* 解析顺序:命令行 `--features`(Cargo 真正收到的参数,不能被环境覆盖)
|
||||||
|
* > 环境变量 `AGC_DEV_CARGO_FEATURES`(存在即以它为准,空串即关闭默认 feature)
|
||||||
|
* > 目标平台默认值。
|
||||||
|
* 传入的必须是「最终交给 Tauri 的参数」:dev 入口与发布入口的 cargo 参数与随包资源
|
||||||
|
* 准备步骤都消费这里的返回值,否则会出现「cargo 开了 feature、staging 没开(或反之)」。
|
||||||
|
*/
|
||||||
|
export function resolveEditorFeatures({
|
||||||
|
argv = [],
|
||||||
|
target,
|
||||||
|
env = process.env,
|
||||||
|
} = {}) {
|
||||||
|
const fromArgv = featuresFromArgv(argv);
|
||||||
|
if (fromArgv) {
|
||||||
|
return fromArgv;
|
||||||
|
}
|
||||||
|
// 变量存在本身即声明「本次 feature 集由环境决定」:空串是有意的关闭,不是未设置。
|
||||||
|
if (env.AGC_DEV_CARGO_FEATURES !== undefined) {
|
||||||
|
return parseFeatureNames(env.AGC_DEV_CARGO_FEATURES);
|
||||||
|
}
|
||||||
|
return defaultEditorFeatures(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 汇总 cargo 参数区里出现在任何位置的 `--features` / `-f`(Cargo 会把多个 flag 合并);
|
||||||
|
/// 没有任何 feature flag 时返回 null。显式但为空(如 `--features=`)返回空数组。
|
||||||
|
export function featuresFromArgv(argv = []) {
|
||||||
|
const options = cargoArguments(argv);
|
||||||
|
const features = [];
|
||||||
|
let found = false;
|
||||||
|
for (let index = 0; index < options.length; index += 1) {
|
||||||
|
const value = String(options[index]);
|
||||||
|
if (value.startsWith('--features=')) {
|
||||||
|
found = true;
|
||||||
|
features.push(...parseFeatureNames(value.slice('--features='.length)));
|
||||||
|
} else if (value === '--features' || value === '-f') {
|
||||||
|
found = true;
|
||||||
|
while (
|
||||||
|
index + 1 < options.length &&
|
||||||
|
!String(options[index + 1]).startsWith('-')
|
||||||
|
) {
|
||||||
|
index += 1;
|
||||||
|
features.push(...parseFeatureNames(options[index]));
|
||||||
|
}
|
||||||
|
} else if (value.startsWith('-f') && value.length > 2) {
|
||||||
|
found = true;
|
||||||
|
features.push(...parseFeatureNames(value.slice(2).replace(/^=/u, '')));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return found ? features : null;
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,7 +2,10 @@ import assert from 'node:assert/strict';
|
|||||||
import { test } from 'node:test';
|
import { test } from 'node:test';
|
||||||
|
|
||||||
import { buildTauriBuildArguments } from './build-release.mjs';
|
import { buildTauriBuildArguments } from './build-release.mjs';
|
||||||
import { withDefaultCargoFeatures } from './cargo-features.mjs';
|
import {
|
||||||
|
featuresFromArgv,
|
||||||
|
withDefaultCargoFeatures,
|
||||||
|
} from './cargo-features.mjs';
|
||||||
|
|
||||||
test('Windows release includes the same editor feature as development', () => {
|
test('Windows release includes the same editor feature as development', () => {
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
@@ -47,3 +50,16 @@ test('explicit Cargo features override defaults in every supported spelling', ()
|
|||||||
['--features=cocos-editor-execute', '--', '--features=app'],
|
['--features=cocos-editor-execute', '--', '--features=app'],
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('features 变参会完整消费空格与逗号分隔的值', () => {
|
||||||
|
assert.deepEqual(
|
||||||
|
featuresFromArgv([
|
||||||
|
'-f',
|
||||||
|
'cocos-editor-execute,unity-editor-execute',
|
||||||
|
'godot-editor-execute',
|
||||||
|
'--target',
|
||||||
|
'x86_64-pc-windows-msvc',
|
||||||
|
]),
|
||||||
|
['cocos-editor-execute', 'unity-editor-execute', 'godot-editor-execute'],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|||||||
@@ -98,10 +98,13 @@ const appInvokeSources = readSourceFiles(
|
|||||||
new URL('../src/', import.meta.url),
|
new URL('../src/', import.meta.url),
|
||||||
new Set(['.ts', '.tsx']),
|
new Set(['.ts', '.tsx']),
|
||||||
);
|
);
|
||||||
const tauriHandlerSource = fs.readFileSync(
|
// 生产接线在 desktop,受测的启动诊断实现仍在 main;两者共同构成启动检查范围。
|
||||||
|
const tauriHandlerSource = [
|
||||||
new URL('../src-tauri/src/main.rs', import.meta.url),
|
new URL('../src-tauri/src/main.rs', import.meta.url),
|
||||||
'utf8',
|
new URL('../src-tauri/src/desktop.rs', import.meta.url),
|
||||||
);
|
]
|
||||||
|
.map((source) => fs.readFileSync(source, 'utf8'))
|
||||||
|
.join('\n');
|
||||||
const tauriRustSource = readSourceTree(
|
const tauriRustSource = readSourceTree(
|
||||||
new URL('../src-tauri/src/', import.meta.url),
|
new URL('../src-tauri/src/', import.meta.url),
|
||||||
'.rs',
|
'.rs',
|
||||||
@@ -1358,7 +1361,6 @@ for (const channel of ['release', 'beta-2']) {
|
|||||||
|
|
||||||
const expectedBundledDesignAgentResources = {
|
const expectedBundledDesignAgentResources = {
|
||||||
'design-agent': 'design-agent',
|
'design-agent': 'design-agent',
|
||||||
'resources/claude-agent': 'claude-agent',
|
|
||||||
...Object.fromEntries(
|
...Object.fromEntries(
|
||||||
[
|
[
|
||||||
'codex-patch-parser',
|
'codex-patch-parser',
|
||||||
@@ -1387,6 +1389,7 @@ const expectedBundledWindowsResources = {
|
|||||||
'resources/codex/win-x64/NOTICE.md': 'coding-agent/win-x64/NOTICE.md',
|
'resources/codex/win-x64/NOTICE.md': 'coding-agent/win-x64/NOTICE.md',
|
||||||
'resources/codex/win-x64/manifest.json': 'coding-agent/win-x64/manifest.json',
|
'resources/codex/win-x64/manifest.json': 'coding-agent/win-x64/manifest.json',
|
||||||
'resources/plugins': 'plugins',
|
'resources/plugins': 'plugins',
|
||||||
|
'resources/claude-agent': 'claude-agent',
|
||||||
};
|
};
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
tauriConfig.bundle?.resources,
|
tauriConfig.bundle?.resources,
|
||||||
@@ -1399,6 +1402,13 @@ for (const key of Object.keys(tauriConfig.bundle?.resources ?? {})) {
|
|||||||
'AI game creator shell base Tauri config must not require Windows-only Codex resources',
|
'AI game creator shell base Tauri config must not require Windows-only Codex resources',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
// 基线配置同时服务 Linux(只有 CI 会编译壳 crate):随包资源由准备步骤按目标生成,
|
||||||
|
// 基线声明它们会让没有 node_modules 的平台在构建期就因资源缺失失败。
|
||||||
|
if (String(key).startsWith('resources/')) {
|
||||||
|
throw new Error(
|
||||||
|
'AI game creator shell base Tauri config must not require platform-only bundled resources',
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
windowsTauriConfig.bundle?.resources,
|
windowsTauriConfig.bundle?.resources,
|
||||||
@@ -1438,6 +1448,7 @@ assert.deepEqual(
|
|||||||
]),
|
]),
|
||||||
),
|
),
|
||||||
['resources/plugins', 'plugins'],
|
['resources/plugins', 'plugins'],
|
||||||
|
['resources/claude-agent', 'claude-agent'],
|
||||||
]),
|
]),
|
||||||
'macOS must bundle the complete native Codex layout and plugin workspace',
|
'macOS must bundle the complete native Codex layout and plugin workspace',
|
||||||
);
|
);
|
||||||
@@ -1659,13 +1670,11 @@ if (!viteConfigSource.includes('allow: [repoRoot]')) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (!(
|
||||||
!(
|
tauriConfig.build?.beforeDevCommand?.includes(
|
||||||
tauriConfig.build?.beforeDevCommand?.includes(
|
'run ai-game-creator-shell:dev-server',
|
||||||
'run ai-game-creator-shell:dev-server',
|
) || tauriConfig.build?.beforeDevCommand?.includes('run agc:serve')
|
||||||
) || tauriConfig.build?.beforeDevCommand?.includes('run agc:serve')
|
)) {
|
||||||
)
|
|
||||||
) {
|
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'AI game creator shell beforeDevCommand must start the selected Vite dev server',
|
'AI game creator shell beforeDevCommand must start the selected Vite dev server',
|
||||||
);
|
);
|
||||||
|
|||||||
+917
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,8 +1,10 @@
|
|||||||
import { resolve } from 'node:path';
|
import { resolve } from 'node:path';
|
||||||
import { fileURLToPath } from 'node:url';
|
import { fileURLToPath } from 'node:url';
|
||||||
|
|
||||||
|
import { buildLocalRustProcessEnv } from '../../../scripts/dev.mjs';
|
||||||
import {
|
import {
|
||||||
defaultEditorFeatures,
|
readCargoTarget,
|
||||||
|
resolveEditorFeatures,
|
||||||
withDefaultCargoFeatures,
|
withDefaultCargoFeatures,
|
||||||
} from './cargo-features.mjs';
|
} from './cargo-features.mjs';
|
||||||
import {
|
import {
|
||||||
@@ -10,6 +12,10 @@ import {
|
|||||||
resolveAgcDevEndpoint,
|
resolveAgcDevEndpoint,
|
||||||
withAgcDevEndpointEnv,
|
withAgcDevEndpointEnv,
|
||||||
} from './dev-port.mjs';
|
} from './dev-port.mjs';
|
||||||
|
import {
|
||||||
|
prepareBundledResources,
|
||||||
|
supportedHostTarget,
|
||||||
|
} from './prepare-bundled-resources.mjs';
|
||||||
import {
|
import {
|
||||||
isAiGameCreatorServer,
|
isAiGameCreatorServer,
|
||||||
preflightExistingVite,
|
preflightExistingVite,
|
||||||
@@ -51,22 +57,51 @@ function buildTauriArguments(argv, devUrl = readAgcDevEndpoint().url) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 开发和发行构建使用同一平台编辑器 feature 集合。
|
// 开发和发行构建使用同一平台编辑器 feature 集合。
|
||||||
// 可用 AGC_DEV_CARGO_FEATURES(逗号分隔)覆盖,传空串即关闭。
|
// 可用 AGC_DEV_CARGO_FEATURES(逗号分隔)覆盖,传空串即关闭;
|
||||||
function readDevCargoFeatures(env = process.env) {
|
// 命令行显式 `--features` / `-f` 优先于环境变量(cargo 实际收到的是命令行参数)。
|
||||||
const override = env.AGC_DEV_CARGO_FEATURES;
|
// 入参是「最终交给 Tauri 的参数」:默认平台由其中的显式 `--target` 决定,未指定才是宿主平台。
|
||||||
if (override !== undefined) {
|
function readDevCargoFeatures(argv = []) {
|
||||||
return override
|
return resolveEditorFeatures({
|
||||||
.split(',')
|
argv,
|
||||||
.map((value) => value.trim())
|
target: readCargoTarget(argv) ?? process.platform,
|
||||||
.filter(Boolean);
|
});
|
||||||
}
|
|
||||||
return defaultEditorFeatures(process.platform);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function withDevCargoFeatures(argv, features = readDevCargoFeatures()) {
|
function withDevCargoFeatures(argv, features = readDevCargoFeatures(argv)) {
|
||||||
return withDefaultCargoFeatures(argv, features);
|
return withDefaultCargoFeatures(argv, features);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 随包资源必须在 Tauri 之前生成:构建脚本只做只读校验,不再生成资源。
|
||||||
|
/// 命中缓存的重复调用不写任何文件,因此每次 dev 启动都会先跑一次。
|
||||||
|
/// `argv` 是「最终交给 Tauri 的参数」;staging 目标与本次 cargo 构建目标同源:
|
||||||
|
/// 显式 `--target` 优先,未指定时回落宿主目标;宿主平台不受声明覆盖(如 Linux)时保持跳过,
|
||||||
|
/// 不为其新增随包资源准备。`features` 省略时按同一份参数解析,不允许「cargo 一套、staging 另一套」。
|
||||||
|
function prepareBundledResourcesBeforeTauri(
|
||||||
|
features,
|
||||||
|
argv = [],
|
||||||
|
{
|
||||||
|
prepare = prepareBundledResources,
|
||||||
|
log = console.log,
|
||||||
|
hostTarget = supportedHostTarget(),
|
||||||
|
} = {},
|
||||||
|
) {
|
||||||
|
const target = readCargoTarget(argv) ?? hostTarget;
|
||||||
|
if (!target) {
|
||||||
|
log(
|
||||||
|
'[ai-game-creator-shell] 当前平台不受随包资源声明覆盖,跳过随包资源准备',
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const summaries = prepare({
|
||||||
|
target,
|
||||||
|
features: new Set(features ?? readDevCargoFeatures(argv)),
|
||||||
|
log: (line) => log(`[ai-game-creator-shell] ${line}`),
|
||||||
|
});
|
||||||
|
for (const summary of summaries) {
|
||||||
|
log(`[ai-game-creator-shell] ${summary}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function spawnTauriCli(argv, { env = process.env } = {}) {
|
function spawnTauriCli(argv, { env = process.env } = {}) {
|
||||||
return spawnChild(process.execPath, [tauriCliPath, ...argv], {
|
return spawnChild(process.execPath, [tauriCliPath, ...argv], {
|
||||||
cwd: appRoot,
|
cwd: appRoot,
|
||||||
@@ -75,6 +110,17 @@ function spawnTauriCli(argv, { env = process.env } = {}) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Tauri dev 的 Cargo 直接继承启动器环境,用户级 / 仓库级 Cargo 配置里的
|
||||||
|
/// `rustc-wrapper`(本地常见为 sccache)会在这里生效。本地 sccache daemon 状态
|
||||||
|
/// 一旦损坏,`cargo` 的首次 rustc 探测就会失败并阻断整个 AGC 启动;因此这里复用
|
||||||
|
/// `npm run dev` 的本地 Rust 环境规则,由脚本而不是本机 Cargo 配置决定 wrapper。
|
||||||
|
function buildTauriDevProcessEnv(endpoint, env = process.env) {
|
||||||
|
return buildLocalRustProcessEnv({
|
||||||
|
...withAgcDevEndpointEnv(endpoint, env),
|
||||||
|
[AGC_DESIGN_DEBUG_ENV]: designDebugEnabled,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async function runTauriDev(
|
async function runTauriDev(
|
||||||
argv = process.argv.slice(2),
|
argv = process.argv.slice(2),
|
||||||
{
|
{
|
||||||
@@ -84,6 +130,7 @@ async function runTauriDev(
|
|||||||
spawnCli = spawnTauriCli,
|
spawnCli = spawnTauriCli,
|
||||||
waitForCli = waitForChildTermination,
|
waitForCli = waitForChildTermination,
|
||||||
terminateTree = terminateChildTree,
|
terminateTree = terminateChildTree,
|
||||||
|
prepareResources = prepareBundledResourcesBeforeTauri,
|
||||||
} = {},
|
} = {},
|
||||||
) {
|
) {
|
||||||
const endpoint = await resolveDevEndpoint();
|
const endpoint = await resolveDevEndpoint();
|
||||||
@@ -119,6 +166,12 @@ async function runTauriDev(
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// 目标与 feature 都从「最终交给 Tauri 的参数」解析:启动器自己的 `--` 之后是应用参数,
|
||||||
|
// 不能参与解析;注入默认 feature 也不改变这份解析结果。
|
||||||
|
const devTauriArguments = buildTauriArguments(argv, endpoint.url);
|
||||||
|
const devFeatures = readDevCargoFeatures(devTauriArguments);
|
||||||
|
prepareResources(devFeatures, devTauriArguments);
|
||||||
|
// Windows 下 Vite 会监听资源目录;必须在启动前完成目录替换,避免 rename 被占用。
|
||||||
const preparation = prepareFrontend(endpoint, {
|
const preparation = prepareFrontend(endpoint, {
|
||||||
signal: preparationAbort.signal,
|
signal: preparationAbort.signal,
|
||||||
onChild(frontend) {
|
onChild(frontend) {
|
||||||
@@ -131,14 +184,11 @@ async function runTauriDev(
|
|||||||
]);
|
]);
|
||||||
if (!prepared || shutdownSignal) return 1;
|
if (!prepared || shutdownSignal) return 1;
|
||||||
const tauriArguments = buildTauriArguments(
|
const tauriArguments = buildTauriArguments(
|
||||||
withDevCargoFeatures(argv),
|
withDevCargoFeatures(argv, devFeatures),
|
||||||
endpoint.url,
|
endpoint.url,
|
||||||
);
|
);
|
||||||
child = spawnCli(tauriArguments, {
|
child = spawnCli(tauriArguments, {
|
||||||
env: {
|
env: buildTauriDevProcessEnv(endpoint),
|
||||||
...withAgcDevEndpointEnv(endpoint),
|
|
||||||
[AGC_DESIGN_DEBUG_ENV]: designDebugEnabled,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
const childResult = waitForCli(child);
|
const childResult = waitForCli(child);
|
||||||
const outcome = await Promise.race([
|
const outcome = await Promise.race([
|
||||||
@@ -219,13 +269,15 @@ async function prepareFrontendDev(endpoint, { onChild, signal }) {
|
|||||||
function isDirectModuleExecution() {
|
function isDirectModuleExecution() {
|
||||||
return Boolean(
|
return Boolean(
|
||||||
process.argv[1] &&
|
process.argv[1] &&
|
||||||
resolve(process.argv[1]) === fileURLToPath(import.meta.url),
|
resolve(process.argv[1]) === fileURLToPath(import.meta.url),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
buildTauriArguments,
|
buildTauriArguments,
|
||||||
|
buildTauriDevProcessEnv,
|
||||||
isDirectModuleExecution,
|
isDirectModuleExecution,
|
||||||
|
prepareBundledResourcesBeforeTauri,
|
||||||
runTauriDev,
|
runTauriDev,
|
||||||
spawnTauriCli,
|
spawnTauriCli,
|
||||||
withDevCargoFeatures,
|
withDevCargoFeatures,
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
# resources/plugins 由 build.rs 从 plugins/ 复制生成,属于构建产物。
|
|
||||||
# 它在 dev 监听范围内,重新生成会让 Tauri dev 误判为源码改动而触发
|
|
||||||
# “构建 -> 监听 -> 再构建”的自触发循环。
|
|
||||||
resources/plugins/
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,8 +1,18 @@
|
|||||||
//! 构建与运行共用的平台布局;只允许分发锁定原生包里的明确组件。
|
//! 构建与运行共用的平台布局;只允许分发锁定原生包里的明确组件。
|
||||||
|
//!
|
||||||
|
//! 布局、组件白名单与版本常量来自唯一声明 `build_support/package-layout.json`
|
||||||
|
//! (Rust 侧经 `build_support/package-layout.generated.rs` 取得编译期常量,
|
||||||
|
//! 由 `scripts/check-package-layout.mjs` 生成并在门禁中校验一致)。
|
||||||
|
//! 本模块只读声明,不写任何随包资源。
|
||||||
|
|
||||||
pub const VERSION: &str = "0.155.1";
|
// 共享声明模块:构建脚本、运行期与测试各自只用到其中一部分,未用到的入口不算缺陷。
|
||||||
pub const CLI_VERSION: &str = "codex-cli 0.155.1";
|
#[allow(dead_code)]
|
||||||
pub const SCHEMA: &str = "genarrative-codex-sidecar.v2";
|
#[path = "package_layout.rs"]
|
||||||
|
pub(crate) mod package_layout;
|
||||||
|
|
||||||
|
pub const VERSION: &str = package_layout::CODEX_VERSION;
|
||||||
|
pub const CLI_VERSION: &str = package_layout::CODEX_CLI_VERSION;
|
||||||
|
pub const SCHEMA: &str = package_layout::CODEX_MANIFEST_SCHEMA;
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug)]
|
#[derive(Clone, Copy, Debug)]
|
||||||
pub struct Layout {
|
pub struct Layout {
|
||||||
@@ -12,46 +22,13 @@ pub struct Layout {
|
|||||||
pub files: &'static [&'static str],
|
pub files: &'static [&'static str],
|
||||||
}
|
}
|
||||||
|
|
||||||
const WINDOWS_FILES: &[&str] = &[
|
|
||||||
"bin/codex.exe",
|
|
||||||
"bin/codex-code-mode-host.exe",
|
|
||||||
"codex-path/rg.exe",
|
|
||||||
"codex-resources/codex-command-runner.exe",
|
|
||||||
"codex-resources/codex-windows-sandbox-setup.exe",
|
|
||||||
"codex-package.json",
|
|
||||||
];
|
|
||||||
const MAC_FILES: &[&str] = &[
|
|
||||||
"bin/codex",
|
|
||||||
"bin/codex-code-mode-host",
|
|
||||||
"codex-path/rg",
|
|
||||||
"codex-resources/zsh/bin/zsh",
|
|
||||||
"codex-package.json",
|
|
||||||
];
|
|
||||||
|
|
||||||
pub fn for_target(target: &str) -> Option<Layout> {
|
pub fn for_target(target: &str) -> Option<Layout> {
|
||||||
match target {
|
package_layout::codex_target(target).map(|declared| Layout {
|
||||||
"x86_64-pc-windows-msvc" => Some(Layout {
|
platform: declared.platform,
|
||||||
platform: "win32-x64",
|
directory: declared.directory,
|
||||||
directory: "win-x64",
|
executable: declared.executable,
|
||||||
executable: "bin/codex.exe",
|
files: declared.files,
|
||||||
files: WINDOWS_FILES,
|
})
|
||||||
}),
|
|
||||||
"aarch64-apple-darwin" | "x86_64-apple-darwin" => Some(Layout {
|
|
||||||
platform: if target.starts_with("aarch64") {
|
|
||||||
"darwin-arm64"
|
|
||||||
} else {
|
|
||||||
"darwin-x64"
|
|
||||||
},
|
|
||||||
directory: if target.starts_with("aarch64") {
|
|
||||||
"mac-native/darwin-arm64"
|
|
||||||
} else {
|
|
||||||
"mac-native/darwin-x64"
|
|
||||||
},
|
|
||||||
executable: "bin/codex",
|
|
||||||
files: MAC_FILES,
|
|
||||||
}),
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -80,4 +57,11 @@ mod tests {
|
|||||||
assert!(for_target("aarch64-pc-windows-msvc").is_none());
|
assert!(for_target("aarch64-pc-windows-msvc").is_none());
|
||||||
assert!(for_target("x86_64-unknown-linux-gnu").is_none());
|
assert!(for_target("x86_64-unknown-linux-gnu").is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn constants_come_from_the_shared_declaration() {
|
||||||
|
assert_eq!(VERSION, "0.155.1");
|
||||||
|
assert_eq!(CLI_VERSION, format!("codex-cli {VERSION}"));
|
||||||
|
assert_eq!(SCHEMA, "genarrative-codex-sidecar.v2");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,57 +0,0 @@
|
|||||||
//! 随包阶段的原生包元数据校验,不进入运行时生产模块。
|
|
||||||
|
|
||||||
use super::codex_bundle::{Layout, VERSION};
|
|
||||||
|
|
||||||
pub fn validate_package_metadata(
|
|
||||||
metadata: &serde_json::Value,
|
|
||||||
target: &str,
|
|
||||||
layout: Layout,
|
|
||||||
) -> Result<(), String> {
|
|
||||||
if metadata["layoutVersion"] == 1
|
|
||||||
&& metadata["version"] == VERSION
|
|
||||||
&& metadata["target"] == target
|
|
||||||
&& metadata["entrypoint"] == layout.executable
|
|
||||||
&& metadata["resourcesDir"] == "codex-resources"
|
|
||||||
&& metadata["pathDir"] == "codex-path"
|
|
||||||
{
|
|
||||||
Ok(())
|
|
||||||
} else {
|
|
||||||
Err(format!("Codex 原生包版本、布局或架构不匹配目标 {target}"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::super::codex_bundle::for_target;
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn metadata_rejects_version_architecture_and_layout_drift() {
|
|
||||||
let target = "aarch64-apple-darwin";
|
|
||||||
let layout = for_target(target).unwrap();
|
|
||||||
let valid = serde_json::json!({
|
|
||||||
"layoutVersion": 1,
|
|
||||||
"version": VERSION,
|
|
||||||
"target": target,
|
|
||||||
"entrypoint": "bin/codex",
|
|
||||||
"resourcesDir": "codex-resources",
|
|
||||||
"pathDir": "codex-path",
|
|
||||||
});
|
|
||||||
assert!(validate_package_metadata(&valid, target, layout).is_ok());
|
|
||||||
for (key, value) in [
|
|
||||||
("layoutVersion", serde_json::json!(2)),
|
|
||||||
("version", serde_json::json!("0.0.0")),
|
|
||||||
("target", serde_json::json!("x86_64-apple-darwin")),
|
|
||||||
("entrypoint", serde_json::json!("bin/codex.exe")),
|
|
||||||
("resourcesDir", serde_json::json!("../private")),
|
|
||||||
("pathDir", serde_json::json!(null)),
|
|
||||||
] {
|
|
||||||
let mut invalid = valid.clone();
|
|
||||||
invalid[key] = value;
|
|
||||||
assert!(
|
|
||||||
validate_package_metadata(&invalid, target, layout).is_err(),
|
|
||||||
"{key}"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
use sha2::{Digest, Sha256};
|
use sha2::{Digest, Sha256};
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::Path;
|
||||||
|
|
||||||
pub const BUNDLE_FILES: [&str; 4] = [
|
// 共享随包资源声明:Godot 随包文件清单与构建期校验共用同一份来源。
|
||||||
"bin/win-x64/agc_godot_editor.dll",
|
#[allow(dead_code)]
|
||||||
"bin/win-x64/metadata.json",
|
#[path = "package_layout.rs"]
|
||||||
"vendor/LICENSE.txt",
|
mod package_layout;
|
||||||
"vendor/provenance.json",
|
|
||||||
];
|
pub const BUNDLE_FILES: &[&str] = package_layout::GODOT_BUNDLE_FILES;
|
||||||
|
|
||||||
fn plain_metadata(path: &Path) -> Result<fs::Metadata, String> {
|
fn plain_metadata(path: &Path) -> Result<fs::Metadata, String> {
|
||||||
let metadata = fs::symlink_metadata(path)
|
let metadata = fs::symlink_metadata(path)
|
||||||
@@ -74,39 +74,6 @@ pub fn validate(root: &Path) -> Result<Vec<(&'static str, Vec<u8>)>, String> {
|
|||||||
Ok(files)
|
Ok(files)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn stage(root: &Path, destination: &Path, target: &str, enabled: bool) -> Result<(), String> {
|
|
||||||
if target != "x86_64-pc-windows-msvc" || !enabled {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
for (relative, bytes) in validate(root)? {
|
|
||||||
let path = destination.join(relative);
|
|
||||||
fs::create_dir_all(path.parent().expect("Godot resource parent"))
|
|
||||||
.map_err(|error| format!("创建 Godot 资源目录失败:{error}"))?;
|
|
||||||
fs::write(&path, bytes).map_err(|error| format!("写入 Godot 资源失败:{error}"))?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn source_files(root: &Path) -> Result<Vec<PathBuf>, String> {
|
|
||||||
plain_metadata(root)?;
|
|
||||||
let mut sources = Vec::new();
|
|
||||||
for entry in fs::read_dir(root).map_err(|error| format!("读取 Godot 源码失败:{error}"))?
|
|
||||||
{
|
|
||||||
let entry = entry.map_err(|error| format!("读取 Godot 源码目录项失败:{error}"))?;
|
|
||||||
if matches!(entry.file_name().to_str(), Some("bin" | ".build")) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let metadata = plain_metadata(&entry.path())?;
|
|
||||||
if metadata.is_dir() {
|
|
||||||
sources.extend(source_files(&entry.path())?);
|
|
||||||
} else if metadata.is_file() {
|
|
||||||
sources.push(entry.path());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
sources.sort();
|
|
||||||
Ok(sources)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -134,82 +101,20 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn stage_only_verified_windows_runtime_and_not_build_inputs() {
|
fn validate_rejects_incomplete_bundle() {
|
||||||
let source = tempfile::tempdir().unwrap();
|
let temp = tempfile::tempdir().expect("tempdir");
|
||||||
let destination = tempfile::tempdir().unwrap();
|
let error = validate(temp.path()).expect_err("空目录必须被拒绝");
|
||||||
fixture(source.path());
|
assert!(!error.is_empty(), "失败原因不能为空");
|
||||||
fs::write(source.path().join("bridge.gd"), "source").unwrap();
|
|
||||||
fs::write(source.path().join("bin/win-x64/extra.dll"), "excluded").unwrap();
|
// 只有文件名、内容不合法的「像样」目录也必须被拒绝。
|
||||||
stage(
|
|
||||||
source.path(),
|
|
||||||
destination.path(),
|
|
||||||
"x86_64-pc-windows-msvc",
|
|
||||||
true,
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
for relative in BUNDLE_FILES {
|
for relative in BUNDLE_FILES {
|
||||||
assert_eq!(
|
let file = temp.path().join(relative);
|
||||||
fs::read(source.path().join(relative)).unwrap(),
|
fs::create_dir_all(file.parent().expect("parent")).expect("create dir");
|
||||||
fs::read(destination.path().join(relative)).unwrap()
|
fs::write(&file, b"not a real artifact").expect("write file");
|
||||||
);
|
|
||||||
}
|
}
|
||||||
assert!(!destination.path().join("bridge.gd").exists());
|
assert!(
|
||||||
assert!(!destination.path().join("bin/win-x64/extra.dll").exists());
|
validate(temp.path()).is_err(),
|
||||||
}
|
"内容不合法的随包库必须被拒绝",
|
||||||
|
);
|
||||||
#[test]
|
|
||||||
fn unsupported_or_disabled_targets_need_no_native_artifacts() {
|
|
||||||
let destination = tempfile::tempdir().unwrap();
|
|
||||||
for (target, enabled) in [
|
|
||||||
("aarch64-apple-darwin", true),
|
|
||||||
("x86_64-apple-darwin", true),
|
|
||||||
("x86_64-unknown-linux-gnu", true),
|
|
||||||
("aarch64-pc-windows-msvc", true),
|
|
||||||
("x86_64-pc-windows-msvc", false),
|
|
||||||
] {
|
|
||||||
stage(
|
|
||||||
Path::new("missing-godot-native"),
|
|
||||||
destination.path(),
|
|
||||||
target,
|
|
||||||
enabled,
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(fs::read_dir(destination.path()).unwrap().count(), 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn incomplete_or_tampered_bundle_fails_before_copying() {
|
|
||||||
let source = tempfile::tempdir().unwrap();
|
|
||||||
let destination = tempfile::tempdir().unwrap();
|
|
||||||
fixture(source.path());
|
|
||||||
fs::write(source.path().join(BUNDLE_FILES[0]), b"tampered").unwrap();
|
|
||||||
assert!(stage(
|
|
||||||
source.path(),
|
|
||||||
destination.path(),
|
|
||||||
"x86_64-pc-windows-msvc",
|
|
||||||
true
|
|
||||||
)
|
|
||||||
.unwrap_err()
|
|
||||||
.contains("SHA256"));
|
|
||||||
assert_eq!(fs::read_dir(destination.path()).unwrap().count(), 0);
|
|
||||||
fixture(source.path());
|
|
||||||
fs::remove_file(source.path().join("vendor/LICENSE.txt")).unwrap();
|
|
||||||
assert!(validate(source.path()).is_err());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn source_watch_list_excludes_build_outputs() {
|
|
||||||
let source = tempfile::tempdir().unwrap();
|
|
||||||
fixture(source.path());
|
|
||||||
fs::create_dir(source.path().join(".build")).unwrap();
|
|
||||||
fs::write(source.path().join(".build/bridge.obj"), "generated").unwrap();
|
|
||||||
fs::write(source.path().join("bridge.gd"), "source").unwrap();
|
|
||||||
let sources = source_files(source.path()).unwrap();
|
|
||||||
assert_eq!(sources.len(), 3);
|
|
||||||
assert!(sources.contains(&source.path().join("bridge.gd")));
|
|
||||||
assert!(!sources.iter().any(|path| path
|
|
||||||
.components()
|
|
||||||
.any(|component| component.as_os_str() == "bin" || component.as_os_str() == ".build")));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,163 @@
|
|||||||
|
// @generated by apps/ai-game-creator-shell/scripts/check-package-layout.mjs
|
||||||
|
// 来源:build_support/package-layout.json。不要手工编辑本文件。
|
||||||
|
// 修改随包资源布局请编辑声明文件,然后运行
|
||||||
|
// npm run agc:bundled-resources:sync(在仓库根目录)
|
||||||
|
// 门禁会校验两者一致(npm run agc:typecheck 链内含 check-package-layout.mjs)。
|
||||||
|
|
||||||
|
pub const DECLARATION_SCHEMA: &str = "agc-package-layout.v1";
|
||||||
|
pub const LAYOUT_VERSION: u64 = 2;
|
||||||
|
|
||||||
|
pub const CODEX_VERSION: &str = "0.155.1";
|
||||||
|
pub const CODEX_CLI_VERSION: &str = "codex-cli 0.155.1";
|
||||||
|
pub const CODEX_MANIFEST_SCHEMA: &str = "genarrative-codex-sidecar.v2";
|
||||||
|
|
||||||
|
pub const CLAUDE_AGENT_SDK_VERSION: &str = "0.3.285";
|
||||||
|
|
||||||
|
pub const GODOT_BUNDLE_FILES: &[&str] = &["bin/win-x64/agc_godot_editor.dll", "bin/win-x64/metadata.json", "vendor/LICENSE.txt", "vendor/provenance.json"];
|
||||||
|
|
||||||
|
pub const CODEX: Codex = Codex {
|
||||||
|
package_metadata: PackageMetadata {
|
||||||
|
layout_version: 1,
|
||||||
|
resources_dir: "codex-resources",
|
||||||
|
path_dir: "codex-path",
|
||||||
|
},
|
||||||
|
resource_directory: "resources/codex",
|
||||||
|
manifest_file_name: "manifest.json",
|
||||||
|
package_metadata_file_name: "codex-package.json",
|
||||||
|
notice_file_name: "NOTICE.md",
|
||||||
|
source_roots: &["app", "repo"],
|
||||||
|
source_relative_paths: &["node_modules/@openai/codex-<platform>/vendor/<target>", "node_modules/@openai/codex/node_modules/@openai/codex-<platform>/vendor/<target>"],
|
||||||
|
notice_sources: &[
|
||||||
|
NoticeSource {
|
||||||
|
targets: &["aarch64-apple-darwin", "x86_64-apple-darwin"],
|
||||||
|
source: "resources/codex/【声明】Mac内置Codex组件-2026-09-18.md",
|
||||||
|
preserve: false,
|
||||||
|
},
|
||||||
|
NoticeSource {
|
||||||
|
targets: &["x86_64-pc-windows-msvc"],
|
||||||
|
source: "resources/codex/win-x64/NOTICE.md",
|
||||||
|
preserve: true,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
universal_groups: &[
|
||||||
|
UniversalGroup {
|
||||||
|
name: "mac-native",
|
||||||
|
directory: "mac-native",
|
||||||
|
targets: &["aarch64-apple-darwin", "x86_64-apple-darwin"],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
targets: &[
|
||||||
|
CodexTarget {
|
||||||
|
target: "x86_64-pc-windows-msvc",
|
||||||
|
platform: "win32-x64",
|
||||||
|
directory: "win-x64",
|
||||||
|
executable: "bin/codex.exe",
|
||||||
|
files: &["bin/codex.exe", "bin/codex-code-mode-host.exe", "codex-path/rg.exe", "codex-resources/codex-command-runner.exe", "codex-resources/codex-windows-sandbox-setup.exe", "codex-package.json"],
|
||||||
|
},
|
||||||
|
CodexTarget {
|
||||||
|
target: "aarch64-apple-darwin",
|
||||||
|
platform: "darwin-arm64",
|
||||||
|
directory: "mac-native/darwin-arm64",
|
||||||
|
executable: "bin/codex",
|
||||||
|
files: &["bin/codex", "bin/codex-code-mode-host", "codex-path/rg", "codex-resources/zsh/bin/zsh", "codex-package.json"],
|
||||||
|
},
|
||||||
|
CodexTarget {
|
||||||
|
target: "x86_64-apple-darwin",
|
||||||
|
platform: "darwin-x64",
|
||||||
|
directory: "mac-native/darwin-x64",
|
||||||
|
executable: "bin/codex",
|
||||||
|
files: &["bin/codex", "bin/codex-code-mode-host", "codex-path/rg", "codex-resources/zsh/bin/zsh", "codex-package.json"],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const CLAUDE_AGENT: ClaudeAgent = ClaudeAgent {
|
||||||
|
version: "0.3.285",
|
||||||
|
resource_directory: "resources/claude-agent",
|
||||||
|
entry_relative_path: "agent-sidecar/src/index.mjs",
|
||||||
|
entry_file_name: "index.mjs",
|
||||||
|
node_modules_directory: "node_modules",
|
||||||
|
sdk_package_name: "@anthropic-ai/claude-agent-sdk",
|
||||||
|
sdk_entry_file_name: "sdk.mjs",
|
||||||
|
sdk_package_metadata_file_name: "package.json",
|
||||||
|
targets: &[
|
||||||
|
ClaudeAgentTarget {
|
||||||
|
target: "x86_64-pc-windows-msvc",
|
||||||
|
runtime_package: "claude-agent-sdk-win32-x64",
|
||||||
|
runtime_file_name: "claude.exe",
|
||||||
|
},
|
||||||
|
ClaudeAgentTarget {
|
||||||
|
target: "aarch64-apple-darwin",
|
||||||
|
runtime_package: "claude-agent-sdk-darwin-arm64",
|
||||||
|
runtime_file_name: "claude",
|
||||||
|
},
|
||||||
|
ClaudeAgentTarget {
|
||||||
|
target: "x86_64-apple-darwin",
|
||||||
|
runtime_package: "claude-agent-sdk-darwin-x64",
|
||||||
|
runtime_file_name: "claude",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const PLUGINS: Plugins = Plugins {
|
||||||
|
source_directory: "plugins",
|
||||||
|
destination_directory: "resources/plugins",
|
||||||
|
manifest_file_name: "plugin.json",
|
||||||
|
target_contains_any: &["windows", "apple-darwin"],
|
||||||
|
subdirectories: &[
|
||||||
|
Subdirectory {
|
||||||
|
path: "src",
|
||||||
|
plugin: "",
|
||||||
|
origin: "source",
|
||||||
|
target_contains: &[],
|
||||||
|
targets: &[],
|
||||||
|
features: &[],
|
||||||
|
},
|
||||||
|
Subdirectory {
|
||||||
|
path: "panels",
|
||||||
|
plugin: "",
|
||||||
|
origin: "source",
|
||||||
|
target_contains: &[],
|
||||||
|
targets: &[],
|
||||||
|
features: &[],
|
||||||
|
},
|
||||||
|
Subdirectory {
|
||||||
|
path: "skills",
|
||||||
|
plugin: "",
|
||||||
|
origin: "source",
|
||||||
|
target_contains: &[],
|
||||||
|
targets: &[],
|
||||||
|
features: &[],
|
||||||
|
},
|
||||||
|
Subdirectory {
|
||||||
|
path: "native/payload",
|
||||||
|
plugin: "agc-cocos-editor",
|
||||||
|
origin: "prepared",
|
||||||
|
target_contains: &["windows"],
|
||||||
|
targets: &[],
|
||||||
|
features: &["cocos-editor-injection"],
|
||||||
|
},
|
||||||
|
Subdirectory {
|
||||||
|
path: "dotnet/publish/win-x64",
|
||||||
|
plugin: "agc-unity-editor",
|
||||||
|
origin: "prepared",
|
||||||
|
target_contains: &[],
|
||||||
|
targets: &["x86_64-pc-windows-msvc"],
|
||||||
|
features: &["unity-editor-execute"],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
library_staging: &[
|
||||||
|
LibraryStaging {
|
||||||
|
plugin: "agc-godot-editor",
|
||||||
|
source_subdirectory: "native/gdextension",
|
||||||
|
targets: &["x86_64-pc-windows-msvc"],
|
||||||
|
features: &["godot-editor-execute"],
|
||||||
|
layout: "godot-bundle",
|
||||||
|
files: &["bin/win-x64/agc_godot_editor.dll", "bin/win-x64/metadata.json", "vendor/LICENSE.txt", "vendor/provenance.json"],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
skip_directory_names: &["target", "node_modules"],
|
||||||
|
skip_directory_name_prefixes: &["."],
|
||||||
|
skip_file_name_prefixes: &["."],
|
||||||
|
skip_file_name_fragments: &[".test."],
|
||||||
|
};
|
||||||
@@ -0,0 +1,234 @@
|
|||||||
|
{
|
||||||
|
"schema": "agc-package-layout.v1",
|
||||||
|
"description": "AGC 随包资源布局与复制规则的唯一声明。Rust 侧构建期校验与 Node 侧准备步骤共用本文件,任何一侧都不得再写第二份布局或组件白名单。含 <platform>、<target> 占位符的字段由调用方按目标三元展开。修改布局时同步递增 layoutVersion(准备步骤的缓存 key 组成部分)。",
|
||||||
|
"layoutVersion": 2,
|
||||||
|
"codex": {
|
||||||
|
"version": "0.155.1",
|
||||||
|
"cliVersionPrefix": "codex-cli ",
|
||||||
|
"manifestSchema": "genarrative-codex-sidecar.v2",
|
||||||
|
"packageMetadata": {
|
||||||
|
"layoutVersion": 1,
|
||||||
|
"resourcesDir": "codex-resources",
|
||||||
|
"pathDir": "codex-path"
|
||||||
|
},
|
||||||
|
"resourceDirectory": "resources/codex",
|
||||||
|
"manifestFileName": "manifest.json",
|
||||||
|
"packageMetadataFileName": "codex-package.json",
|
||||||
|
"noticeFileName": "NOTICE.md",
|
||||||
|
"sourceRoots": ["app", "repo"],
|
||||||
|
"sourceRelativePaths": [
|
||||||
|
"node_modules/@openai/codex-<platform>/vendor/<target>",
|
||||||
|
"node_modules/@openai/codex/node_modules/@openai/codex-<platform>/vendor/<target>"
|
||||||
|
],
|
||||||
|
"noticeSources": [
|
||||||
|
{
|
||||||
|
"targets": ["aarch64-apple-darwin", "x86_64-apple-darwin"],
|
||||||
|
"source": "resources/codex/【声明】Mac内置Codex组件-2026-09-18.md",
|
||||||
|
"preserve": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"targets": ["x86_64-pc-windows-msvc"],
|
||||||
|
"source": "resources/codex/win-x64/NOTICE.md",
|
||||||
|
"preserve": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"universalGroups": [
|
||||||
|
{
|
||||||
|
"name": "mac-native",
|
||||||
|
"directory": "mac-native",
|
||||||
|
"targets": ["aarch64-apple-darwin", "x86_64-apple-darwin"]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"target": "x86_64-pc-windows-msvc",
|
||||||
|
"platform": "win32-x64",
|
||||||
|
"directory": "win-x64",
|
||||||
|
"executable": "bin/codex.exe",
|
||||||
|
"files": [
|
||||||
|
"bin/codex.exe",
|
||||||
|
"bin/codex-code-mode-host.exe",
|
||||||
|
"codex-path/rg.exe",
|
||||||
|
"codex-resources/codex-command-runner.exe",
|
||||||
|
"codex-resources/codex-windows-sandbox-setup.exe",
|
||||||
|
"codex-package.json"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"target": "aarch64-apple-darwin",
|
||||||
|
"platform": "darwin-arm64",
|
||||||
|
"directory": "mac-native/darwin-arm64",
|
||||||
|
"executable": "bin/codex",
|
||||||
|
"files": [
|
||||||
|
"bin/codex",
|
||||||
|
"bin/codex-code-mode-host",
|
||||||
|
"codex-path/rg",
|
||||||
|
"codex-resources/zsh/bin/zsh",
|
||||||
|
"codex-package.json"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"target": "x86_64-apple-darwin",
|
||||||
|
"platform": "darwin-x64",
|
||||||
|
"directory": "mac-native/darwin-x64",
|
||||||
|
"executable": "bin/codex",
|
||||||
|
"files": [
|
||||||
|
"bin/codex",
|
||||||
|
"bin/codex-code-mode-host",
|
||||||
|
"codex-path/rg",
|
||||||
|
"codex-resources/zsh/bin/zsh",
|
||||||
|
"codex-package.json"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"claudeAgent": {
|
||||||
|
"version": "0.3.285",
|
||||||
|
"resourceDirectory": "resources/claude-agent",
|
||||||
|
"entryRelativePath": "agent-sidecar/src/index.mjs",
|
||||||
|
"entryFileName": "index.mjs",
|
||||||
|
"nodeModulesDirectory": "node_modules",
|
||||||
|
"sdkPackageName": "@anthropic-ai/claude-agent-sdk",
|
||||||
|
"sdkEntryFileName": "sdk.mjs",
|
||||||
|
"sdkPackageMetadataFileName": "package.json",
|
||||||
|
"sourceRoots": ["app", "repo"],
|
||||||
|
"skipDirectoryNames": ["target", "node_modules"],
|
||||||
|
"skipDirectoryNamePrefixes": ["."],
|
||||||
|
"skipFileNamePrefixes": ["."],
|
||||||
|
"skipFileNameFragments": [".test."],
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"target": "x86_64-pc-windows-msvc",
|
||||||
|
"runtimePackage": "claude-agent-sdk-win32-x64",
|
||||||
|
"runtimeFileName": "claude.exe"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"target": "aarch64-apple-darwin",
|
||||||
|
"runtimePackage": "claude-agent-sdk-darwin-arm64",
|
||||||
|
"runtimeFileName": "claude"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"target": "x86_64-apple-darwin",
|
||||||
|
"runtimePackage": "claude-agent-sdk-darwin-x64",
|
||||||
|
"runtimeFileName": "claude"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"plugins": {
|
||||||
|
"sourceDirectory": "plugins",
|
||||||
|
"destinationDirectory": "resources/plugins",
|
||||||
|
"manifestFileName": "plugin.json",
|
||||||
|
"targetContainsAny": ["windows", "apple-darwin"],
|
||||||
|
"subdirectories": [
|
||||||
|
{
|
||||||
|
"path": "src",
|
||||||
|
"origin": "source"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "panels",
|
||||||
|
"origin": "source"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills",
|
||||||
|
"origin": "source"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "native/payload",
|
||||||
|
"origin": "prepared",
|
||||||
|
"targetContains": ["windows"],
|
||||||
|
"plugin": "agc-cocos-editor",
|
||||||
|
"prepare": "cocos-bridge-build",
|
||||||
|
"features": ["cocos-editor-injection"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "dotnet/publish/win-x64",
|
||||||
|
"origin": "prepared",
|
||||||
|
"prepare": "unity-helper-publish",
|
||||||
|
"targets": ["x86_64-pc-windows-msvc"],
|
||||||
|
"features": ["unity-editor-execute"],
|
||||||
|
"plugin": "agc-unity-editor"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"libraryStaging": [
|
||||||
|
{
|
||||||
|
"plugin": "agc-godot-editor",
|
||||||
|
"sourceSubdirectory": "native/gdextension",
|
||||||
|
"prepare": "godot-extension-build",
|
||||||
|
"targets": ["x86_64-pc-windows-msvc"],
|
||||||
|
"features": ["godot-editor-execute"],
|
||||||
|
"layout": "godot-bundle",
|
||||||
|
"files": [
|
||||||
|
"bin/win-x64/agc_godot_editor.dll",
|
||||||
|
"bin/win-x64/metadata.json",
|
||||||
|
"vendor/LICENSE.txt",
|
||||||
|
"vendor/provenance.json"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"nativePayloads": [
|
||||||
|
{
|
||||||
|
"plugin": "agc-cocos-editor",
|
||||||
|
"prepare": "cocos-bridge-build",
|
||||||
|
"sourceFileName": "cocos_editor_bridge.dll",
|
||||||
|
"destinationSubdirectory": "native/payload",
|
||||||
|
"targets": ["x86_64-pc-windows-msvc"],
|
||||||
|
"features": ["cocos-editor-injection"],
|
||||||
|
"destinationFileName": "cocos-editor-bridge.dll"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"prepareSteps": [
|
||||||
|
{
|
||||||
|
"name": "unity-helper-publish",
|
||||||
|
"kind": "powershell",
|
||||||
|
"workingDirectory": "plugins/agc-unity-editor/dotnet",
|
||||||
|
"scriptFileName": "build.ps1",
|
||||||
|
"fingerprint": {
|
||||||
|
"roots": ["."],
|
||||||
|
"excludeDirectoryNames": ["bin", "obj", "publish", "native-build"],
|
||||||
|
"stampRelativePath": "publish/win-x64/.agc-source.sha256"
|
||||||
|
},
|
||||||
|
"requiredOutputs": [
|
||||||
|
"plugins/agc-unity-editor/dotnet/publish/win-x64/Agc.Unity.Attach.exe",
|
||||||
|
"plugins/agc-unity-editor/dotnet/publish/win-x64/NOTICE",
|
||||||
|
"plugins/agc-unity-editor/dotnet/publish/win-x64/THIRD-PARTY-NOTICES.txt",
|
||||||
|
"plugins/agc-unity-editor/dotnet/publish/win-x64/licenses/DotCraft-Apache-2.0.txt",
|
||||||
|
"plugins/agc-unity-editor/dotnet/publish/win-x64/licenses/Roslyn-MIT.txt",
|
||||||
|
"plugins/agc-unity-editor/dotnet/publish/win-x64/licenses/upstream.json",
|
||||||
|
"plugins/agc-unity-editor/dotnet/publish/win-x64/licenses/dotnet-LICENSE.TXT",
|
||||||
|
"plugins/agc-unity-editor/dotnet/publish/win-x64/licenses/dotnet-THIRD-PARTY-NOTICES.TXT",
|
||||||
|
"plugins/agc-unity-editor/dotnet/publish/win-x64/licenses/microsoft.codeanalysis.common-ThirdPartyNotices.rtf",
|
||||||
|
"plugins/agc-unity-editor/dotnet/publish/win-x64/licenses/microsoft.codeanalysis.csharp-ThirdPartyNotices.rtf"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "godot-extension-build",
|
||||||
|
"kind": "powershell",
|
||||||
|
"workingDirectory": "plugins/agc-godot-editor/native/gdextension",
|
||||||
|
"scriptFileName": "build.ps1",
|
||||||
|
"removeEnvironment": ["PSModulePath"],
|
||||||
|
"requiredOutputs": [
|
||||||
|
"plugins/agc-godot-editor/native/gdextension/bin/win-x64/agc_godot_editor.dll",
|
||||||
|
"plugins/agc-godot-editor/native/gdextension/bin/win-x64/metadata.json",
|
||||||
|
"plugins/agc-godot-editor/native/gdextension/vendor/LICENSE.txt",
|
||||||
|
"plugins/agc-godot-editor/native/gdextension/vendor/provenance.json"
|
||||||
|
],
|
||||||
|
"fingerprint": {
|
||||||
|
"roots": ["."],
|
||||||
|
"excludeDirectoryNames": ["bin", ".build", "native-build"],
|
||||||
|
"stampRelativePath": "bin/win-x64/.agc-source.sha256"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "cocos-bridge-build",
|
||||||
|
"kind": "cargo",
|
||||||
|
"packageDirectory": "plugins/agc-cocos-editor/native/cocos-editor-bridge",
|
||||||
|
"features": ["windows-injection"],
|
||||||
|
"requiredOutputs": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"skipDirectoryNames": ["target", "node_modules"],
|
||||||
|
"skipDirectoryNamePrefixes": ["."],
|
||||||
|
"skipFileNamePrefixes": ["."],
|
||||||
|
"skipFileNameFragments": [".test."]
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1 +0,0 @@
|
|||||||
*.dll
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user