新增旧玩法表阶段一清空迁移能力
增加受 migration operator 保护的固定 63 张旧表清空 procedure 生成 spacetime-client procedure、输入和结果绑定 更新后端数据契约与两阶段退役决策记录 清理未挂载的旧入口、分享路由、快照持久化与旧状态死链 清理未挂载的后台旧入口、api-server 旧玩法模块与旧 prompt 清理未挂载的 SpacetimeDB 旧模块根文件与前端类型孤儿
This commit is contained in:
@@ -1,328 +0,0 @@
|
||||
/* @vitest-environment jsdom */
|
||||
|
||||
import {
|
||||
fireEvent,
|
||||
render,
|
||||
screen,
|
||||
waitFor,
|
||||
within,
|
||||
} from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { beforeEach, expect, test, vi } from 'vitest';
|
||||
|
||||
import {
|
||||
getAdminCreationEntryConfig,
|
||||
upsertAdminCreationEntryBanners,
|
||||
upsertAdminCreationEntryConfig,
|
||||
upsertAdminPublicWorkInteractions,
|
||||
} from '../api/adminApiClient';
|
||||
import type {
|
||||
AdminCreationEntryConfigResponse,
|
||||
UnifiedCreationSpecPayload,
|
||||
} from '../api/adminApiTypes';
|
||||
import { AdminCreationEntrySwitchPage } from './AdminCreationEntrySwitchPage';
|
||||
|
||||
vi.mock('../api/adminApiClient', () => ({
|
||||
formatAdminApiError: vi.fn((error: unknown) =>
|
||||
error instanceof Error ? error.message : '请求失败',
|
||||
),
|
||||
getAdminCreationEntryConfig: vi.fn(),
|
||||
isAdminApiError: vi.fn(() => false),
|
||||
upsertAdminCreationEntryBanners: vi.fn(),
|
||||
upsertAdminCreationEntryConfig: vi.fn(),
|
||||
upsertAdminPublicWorkInteractions: vi.fn(),
|
||||
}));
|
||||
|
||||
const puzzleSpec: UnifiedCreationSpecPayload = {
|
||||
playId: 'puzzle',
|
||||
title: '拼图',
|
||||
mudPointCost: 10,
|
||||
workspaceStage: 'puzzle-agent-workspace',
|
||||
generationStage: 'puzzle-generating',
|
||||
resultStage: 'puzzle-result',
|
||||
fields: [
|
||||
{
|
||||
id: 'pictureDescription',
|
||||
kind: 'text',
|
||||
label: '画面描述',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const configResponse: AdminCreationEntryConfigResponse = {
|
||||
eventBanners: [
|
||||
{
|
||||
title: '创作公告',
|
||||
description: '',
|
||||
coverImageSrc: '',
|
||||
prizePoolMudPoints: 0,
|
||||
startsAtText: '',
|
||||
endsAtText: '',
|
||||
renderMode: 'html',
|
||||
htmlCode: '<section>后台公告</section>',
|
||||
},
|
||||
],
|
||||
publicWorkInteractions: [
|
||||
{
|
||||
sourceType: 'puzzle',
|
||||
likeEnabled: true,
|
||||
remixEnabled: true,
|
||||
likeDisabledMessage: '拼图点赞暂不可用。',
|
||||
remixDisabledMessage: '拼图作品改造暂不可用。',
|
||||
},
|
||||
],
|
||||
entries: [
|
||||
{
|
||||
id: 'puzzle',
|
||||
title: '拼图',
|
||||
subtitle: '拼图关卡创作',
|
||||
badge: '可创建',
|
||||
imageSrc: '/creation-type-references/puzzle.webp',
|
||||
visible: true,
|
||||
open: true,
|
||||
sortOrder: 30,
|
||||
categoryId: 'recommended',
|
||||
categoryLabel: '热门推荐',
|
||||
categorySortOrder: 20,
|
||||
updatedAtMicros: 1,
|
||||
unifiedCreationSpec: puzzleSpec,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.mocked(getAdminCreationEntryConfig).mockResolvedValue(configResponse);
|
||||
vi.mocked(upsertAdminCreationEntryBanners).mockResolvedValue(configResponse);
|
||||
vi.mocked(upsertAdminCreationEntryConfig).mockResolvedValue(configResponse);
|
||||
vi.mocked(upsertAdminPublicWorkInteractions).mockResolvedValue(
|
||||
configResponse,
|
||||
);
|
||||
});
|
||||
|
||||
test('创作入口后台展示并保存统一创作契约', async () => {
|
||||
const user = userEvent.setup();
|
||||
const { container } = render(
|
||||
<AdminCreationEntrySwitchPage
|
||||
token="admin-token"
|
||||
onUnauthorized={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
await screen.findByText('pictureDescription');
|
||||
expect(
|
||||
container.querySelector('.admin-subsection .admin-info-list'),
|
||||
).not.toBeNull();
|
||||
expect(
|
||||
container.querySelector('.admin-subsection .admin-info-list')?.textContent,
|
||||
).toContain('拼图');
|
||||
expect(container.querySelector('.admin-panel .admin-panel')).toBeNull();
|
||||
expect(container.querySelector('.admin-muted')).toBeNull();
|
||||
expect(screen.queryByLabelText('契约 JSON')).toBeNull();
|
||||
expect(screen.queryByText('puzzle-generating')).toBeNull();
|
||||
|
||||
await user.click(screen.getByRole('button', { name: '修改契约' }));
|
||||
const dialog = screen.getByRole('dialog', { name: '统一创作契约' });
|
||||
expect(within(dialog).queryByLabelText('玩法 ID')).toBeNull();
|
||||
expect(within(dialog).queryByLabelText('工作台阶段')).toBeNull();
|
||||
expect(within(dialog).queryByLabelText('生成阶段')).toBeNull();
|
||||
expect(within(dialog).queryByLabelText('结果阶段')).toBeNull();
|
||||
fireEvent.change(within(dialog).getByLabelText('泥点消耗'), {
|
||||
target: { value: '12' },
|
||||
});
|
||||
await user.click(within(dialog).getByRole('button', { name: '应用修改' }));
|
||||
|
||||
expect(screen.queryByRole('dialog', { name: '统一创作契约' })).toBeNull();
|
||||
expect(screen.getByText('12泥点数')).toBeTruthy();
|
||||
|
||||
await user.click(screen.getByRole('button', { name: '保存入库' }));
|
||||
await user.click(screen.getByRole('button', { name: '确认' }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(upsertAdminCreationEntryConfig).toHaveBeenCalledWith(
|
||||
'admin-token',
|
||||
expect.objectContaining({
|
||||
id: 'puzzle',
|
||||
unifiedCreationSpec: {
|
||||
...puzzleSpec,
|
||||
mudPointCost: 12,
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('创作入口后台拒绝 playId 不一致的统一创作契约', async () => {
|
||||
const user = userEvent.setup();
|
||||
vi.mocked(getAdminCreationEntryConfig).mockResolvedValueOnce({
|
||||
...configResponse,
|
||||
entries: [
|
||||
{
|
||||
...configResponse.entries[0]!,
|
||||
unifiedCreationSpec: {
|
||||
...puzzleSpec,
|
||||
playId: 'match3d',
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
render(
|
||||
<AdminCreationEntrySwitchPage
|
||||
token="admin-token"
|
||||
onUnauthorized={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
await screen.findByText('pictureDescription');
|
||||
await user.click(screen.getByRole('button', { name: '保存入库' }));
|
||||
|
||||
expect(
|
||||
await screen.findByText('统一创作契约 playId 必须与入口 ID 一致'),
|
||||
).toBeTruthy();
|
||||
expect(upsertAdminCreationEntryConfig).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('创作入口后台用表单保存公告配置', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(
|
||||
<AdminCreationEntrySwitchPage
|
||||
mode="announcements"
|
||||
token="admin-token"
|
||||
onUnauthorized={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(
|
||||
await screen.findAllByRole('heading', { name: '创作入口公告' }),
|
||||
).toHaveLength(2);
|
||||
expect(screen.queryByLabelText('公告代码 JSON')).toBeNull();
|
||||
fireEvent.change(await screen.findByLabelText('公告 1 标题'), {
|
||||
target: { value: '周末创作赛' },
|
||||
});
|
||||
fireEvent.change(screen.getByLabelText('公告 1 HTML'), {
|
||||
target: { value: '<section>新的入口公告</section>' },
|
||||
});
|
||||
await user.click(screen.getByRole('button', { name: '新增公告' }));
|
||||
fireEvent.change(screen.getByLabelText('公告 2 标题'), {
|
||||
target: { value: '第二条公告' },
|
||||
});
|
||||
fireEvent.change(screen.getByLabelText('公告 2 HTML'), {
|
||||
target: { value: '<section>轮播第二条</section>' },
|
||||
});
|
||||
await user.click(screen.getByRole('button', { name: '保存公告' }));
|
||||
await user.click(screen.getByRole('button', { name: '确认' }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(upsertAdminCreationEntryBanners).toHaveBeenCalled();
|
||||
});
|
||||
const [, payload] = vi.mocked(upsertAdminCreationEntryBanners).mock.calls[0]!;
|
||||
expect(JSON.parse(payload.eventBannersJson)).toEqual([
|
||||
{
|
||||
title: '周末创作赛',
|
||||
htmlCode: '<section>新的入口公告</section>',
|
||||
},
|
||||
{
|
||||
title: '第二条公告',
|
||||
htmlCode: '<section>轮播第二条</section>',
|
||||
},
|
||||
]);
|
||||
expect(JSON.parse(payload.eventBannersJson)[0]).not.toHaveProperty(
|
||||
'description',
|
||||
);
|
||||
expect(JSON.parse(payload.eventBannersJson)[0]).not.toHaveProperty(
|
||||
'coverImageSrc',
|
||||
);
|
||||
});
|
||||
|
||||
test('创作入口后台用表单保存作品互动配置', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(
|
||||
<AdminCreationEntrySwitchPage
|
||||
token="admin-token"
|
||||
onUnauthorized={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
await screen.findByText('作品互动');
|
||||
const likeToggle = screen.getAllByRole('checkbox')[0]!;
|
||||
await user.click(likeToggle);
|
||||
fireEvent.change(screen.getByLabelText('拼图 / puzzle 点赞关闭提示'), {
|
||||
target: { value: '拼图点赞维护中。' },
|
||||
});
|
||||
await user.click(screen.getByRole('button', { name: '保存作品互动' }));
|
||||
await user.click(screen.getByRole('button', { name: '确认' }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(upsertAdminPublicWorkInteractions).toHaveBeenCalledWith(
|
||||
'admin-token',
|
||||
{
|
||||
publicWorkInteractions: [
|
||||
{
|
||||
sourceType: 'puzzle',
|
||||
likeEnabled: false,
|
||||
remixEnabled: true,
|
||||
likeDisabledMessage: '拼图点赞维护中。',
|
||||
remixDisabledMessage: '拼图作品改造暂不可用。',
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('创作入口后台把旧结构化公告回显成 HTML 表单', async () => {
|
||||
vi.mocked(getAdminCreationEntryConfig).mockResolvedValueOnce({
|
||||
...configResponse,
|
||||
eventBanners: [
|
||||
{
|
||||
title: '旧公告 <标题>',
|
||||
description: '旧描述 & 需要转义',
|
||||
coverImageSrc: '/legacy.png',
|
||||
prizePoolMudPoints: 120,
|
||||
startsAtText: '2026-06-01',
|
||||
endsAtText: '2026-06-30',
|
||||
renderMode: 'structured',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
render(
|
||||
<AdminCreationEntrySwitchPage
|
||||
mode="announcements"
|
||||
token="admin-token"
|
||||
onUnauthorized={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(await screen.findByLabelText('公告 1 标题')).toHaveProperty(
|
||||
'value',
|
||||
'旧公告 <标题>',
|
||||
);
|
||||
expect(screen.getByLabelText('公告 1 HTML')).toHaveProperty(
|
||||
'value',
|
||||
'<section><h1>旧公告 <标题></h1><p>旧描述 & 需要转义</p></section>',
|
||||
);
|
||||
});
|
||||
|
||||
test('创作入口后台拒绝空公告表单', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(
|
||||
<AdminCreationEntrySwitchPage
|
||||
mode="announcements"
|
||||
token="admin-token"
|
||||
onUnauthorized={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.change(await screen.findByLabelText('公告 1 标题'), {
|
||||
target: { value: '' },
|
||||
});
|
||||
fireEvent.change(screen.getByLabelText('公告 1 HTML'), {
|
||||
target: { value: '' },
|
||||
});
|
||||
await user.click(screen.getByRole('button', { name: '保存公告' }));
|
||||
|
||||
expect(await screen.findByText('公告 1 标题和 HTML 都不能为空')).toBeTruthy();
|
||||
expect(upsertAdminCreationEntryBanners).not.toHaveBeenCalled();
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,283 +0,0 @@
|
||||
import { Eye, EyeOff, RefreshCcw } from 'lucide-react';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import {
|
||||
listAdminWorkVisibility,
|
||||
updateAdminWorkVisibility,
|
||||
} from '../api/adminApiClient';
|
||||
import type { AdminWorkVisibilityEntryPayload } from '../api/adminApiTypes';
|
||||
import { AdminUserReferenceButton } from '../components/AdminUserReferenceButton';
|
||||
import { useAdminWriteConfirm } from '../components/useAdminWriteConfirm';
|
||||
import { handlePageError } from './pageUtils';
|
||||
|
||||
interface AdminWorkVisibilityPageProps {
|
||||
token: string;
|
||||
onUnauthorized: (message?: string) => void;
|
||||
}
|
||||
|
||||
const sourceLabels: Record<string, string> = {
|
||||
puzzle: '拼图',
|
||||
'puzzle-clear': '拼消消',
|
||||
'custom-world': '自定义世界',
|
||||
'jump-hop': '跳一跳',
|
||||
'wooden-fish': '敲木鱼',
|
||||
match3d: '抓大鹅',
|
||||
'square-hole': '方洞挑战',
|
||||
'visual-novel': '视觉小说',
|
||||
'big-fish': '大鱼吃小鱼',
|
||||
'bark-battle': '汪汪声浪',
|
||||
};
|
||||
|
||||
export function AdminWorkVisibilityPage({
|
||||
token,
|
||||
onUnauthorized,
|
||||
}: AdminWorkVisibilityPageProps) {
|
||||
const [entries, setEntries] = useState<AdminWorkVisibilityEntryPayload[]>([]);
|
||||
const [keyword, setKeyword] = useState('');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [savingKey, setSavingKey] = useState('');
|
||||
const [errorMessage, setErrorMessage] = useState('');
|
||||
const { confirmWrite, confirmDialog } = useAdminWriteConfirm();
|
||||
|
||||
useEffect(() => {
|
||||
void refreshEntries();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [token]);
|
||||
|
||||
const filteredEntries = useMemo(() => {
|
||||
const normalizedKeyword = keyword.trim().toLowerCase();
|
||||
if (!normalizedKeyword) {
|
||||
return entries;
|
||||
}
|
||||
return entries.filter((entry) =>
|
||||
[
|
||||
entry.sourceType,
|
||||
sourceLabels[entry.sourceType] ?? '',
|
||||
entry.title,
|
||||
entry.subtitle,
|
||||
entry.authorDisplayName,
|
||||
entry.publicWorkCode,
|
||||
entry.profileId,
|
||||
entry.workId,
|
||||
]
|
||||
.join(' ')
|
||||
.toLowerCase()
|
||||
.includes(normalizedKeyword),
|
||||
);
|
||||
}, [entries, keyword]);
|
||||
|
||||
async function refreshEntries() {
|
||||
setIsLoading(true);
|
||||
setErrorMessage('');
|
||||
try {
|
||||
const response = await listAdminWorkVisibility(token);
|
||||
setEntries(sortEntries(response.entries));
|
||||
} catch (error: unknown) {
|
||||
handlePageError(error, onUnauthorized, setErrorMessage);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleToggle(entry: AdminWorkVisibilityEntryPayload) {
|
||||
const nextVisible = !entry.visible;
|
||||
const target =
|
||||
entry.title.trim() || entry.publicWorkCode || entry.profileId;
|
||||
const confirmed = await confirmWrite({
|
||||
action: nextVisible ? '显示作品' : '隐藏作品',
|
||||
target,
|
||||
});
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
const rowKey = buildEntryKey(entry);
|
||||
setSavingKey(rowKey);
|
||||
setErrorMessage('');
|
||||
try {
|
||||
const response = await updateAdminWorkVisibility(token, {
|
||||
sourceType: entry.sourceType,
|
||||
profileId: entry.profileId,
|
||||
visible: nextVisible,
|
||||
});
|
||||
upsertEntry(response.entry);
|
||||
} catch (error: unknown) {
|
||||
handlePageError(error, onUnauthorized, setErrorMessage);
|
||||
} finally {
|
||||
setSavingKey('');
|
||||
}
|
||||
}
|
||||
|
||||
function upsertEntry(next: AdminWorkVisibilityEntryPayload) {
|
||||
setEntries((current) =>
|
||||
sortEntries([
|
||||
...current.filter(
|
||||
(entry) => buildEntryKey(entry) !== buildEntryKey(next),
|
||||
),
|
||||
next,
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="admin-page admin-page-wide">
|
||||
<div className="admin-page-heading">
|
||||
<div>
|
||||
<h2>作品可见性</h2>
|
||||
</div>
|
||||
<button
|
||||
className="admin-secondary-button"
|
||||
disabled={isLoading}
|
||||
type="button"
|
||||
onClick={refreshEntries}
|
||||
>
|
||||
<RefreshCcw size={17} aria-hidden="true" />
|
||||
<span>{isLoading ? '刷新中' : '刷新'}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<section className="admin-panel">
|
||||
<label className="admin-field">
|
||||
<span>搜索</span>
|
||||
<input
|
||||
placeholder="标题 / 作者 / 公开码 / profileId"
|
||||
value={keyword}
|
||||
onChange={(event) => setKeyword(event.target.value)}
|
||||
/>
|
||||
</label>
|
||||
|
||||
{errorMessage ? (
|
||||
<div className="admin-alert" role="status">
|
||||
{errorMessage}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="admin-table-wrap">
|
||||
<table className="admin-table admin-table-wide">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>玩法</th>
|
||||
<th>作品</th>
|
||||
<th>作者</th>
|
||||
<th>公开码</th>
|
||||
<th>更新时间</th>
|
||||
<th>状态</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{filteredEntries.map((entry) => {
|
||||
const rowKey = buildEntryKey(entry);
|
||||
const isSaving = savingKey === rowKey;
|
||||
return (
|
||||
<tr key={rowKey}>
|
||||
<td>
|
||||
<span className="admin-tag">
|
||||
{sourceLabels[entry.sourceType] ?? entry.sourceType}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<strong>{entry.title || entry.profileId}</strong>
|
||||
<small>{entry.subtitle || entry.profileId}</small>
|
||||
</td>
|
||||
<td>
|
||||
<div className="admin-inline-identity">
|
||||
<div>
|
||||
{entry.authorDisplayName || '玩家'}
|
||||
<small>{entry.ownerUserId}</small>
|
||||
</div>
|
||||
<AdminUserReferenceButton
|
||||
token={token}
|
||||
userId={entry.ownerUserId}
|
||||
onUnauthorized={onUnauthorized}
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<span className="admin-table-cell-ellipsis">
|
||||
{entry.publicWorkCode}
|
||||
</span>
|
||||
<small>{entry.profileId}</small>
|
||||
</td>
|
||||
<td>{formatMicros(entry.updatedAtMicros)}</td>
|
||||
<td>
|
||||
<span
|
||||
className={
|
||||
entry.visible
|
||||
? 'admin-status admin-status-ok'
|
||||
: 'admin-status admin-status-error'
|
||||
}
|
||||
>
|
||||
{entry.visible ? '显示' : '隐藏'}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<button
|
||||
className={
|
||||
entry.visible
|
||||
? 'admin-danger-button'
|
||||
: 'admin-secondary-button'
|
||||
}
|
||||
disabled={isSaving}
|
||||
type="button"
|
||||
onClick={() => handleToggle(entry)}
|
||||
>
|
||||
{entry.visible ? (
|
||||
<EyeOff size={16} aria-hidden="true" />
|
||||
) : (
|
||||
<Eye size={16} aria-hidden="true" />
|
||||
)}
|
||||
<span>
|
||||
{isSaving
|
||||
? '处理中'
|
||||
: entry.visible
|
||||
? '隐藏'
|
||||
: '显示'}
|
||||
</span>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{!isLoading && filteredEntries.length === 0 ? (
|
||||
<div className="admin-empty-state">暂无作品</div>
|
||||
) : null}
|
||||
</section>
|
||||
|
||||
{confirmDialog}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function sortEntries(entries: AdminWorkVisibilityEntryPayload[]) {
|
||||
return [...entries].sort((left, right) => {
|
||||
const timeCompare = right.updatedAtMicros - left.updatedAtMicros;
|
||||
if (timeCompare !== 0) {
|
||||
return timeCompare;
|
||||
}
|
||||
const sourceCompare = left.sourceType.localeCompare(right.sourceType);
|
||||
if (sourceCompare !== 0) {
|
||||
return sourceCompare;
|
||||
}
|
||||
return left.profileId.localeCompare(right.profileId);
|
||||
});
|
||||
}
|
||||
|
||||
function buildEntryKey(entry: AdminWorkVisibilityEntryPayload) {
|
||||
return `${entry.sourceType}:${entry.profileId}`;
|
||||
}
|
||||
|
||||
function formatMicros(value: number) {
|
||||
if (!Number.isFinite(value)) {
|
||||
return '-';
|
||||
}
|
||||
const date = new Date(Math.floor(value / 1000));
|
||||
if (!Number.isFinite(date.getTime())) {
|
||||
return '-';
|
||||
}
|
||||
return date.toLocaleString('zh-CN', { hour12: false });
|
||||
}
|
||||
@@ -15,10 +15,5 @@
|
||||
"noEmit": true,
|
||||
"types": ["vite/client"]
|
||||
},
|
||||
"include": ["src", "vite.config.ts"],
|
||||
"exclude": [
|
||||
"src/pages/AdminCreationEntrySwitchPage.tsx",
|
||||
"src/pages/AdminCreationEntrySwitchPage.test.tsx",
|
||||
"src/pages/AdminWorkVisibilityPage.tsx"
|
||||
]
|
||||
"include": ["src", "vite.config.ts"]
|
||||
}
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
export type {
|
||||
CanvasLayer,
|
||||
CanvasViewport,
|
||||
ImageCanvasAssetPort,
|
||||
ImageCanvasCompletionPort,
|
||||
ImageCanvasGenerationPort,
|
||||
ImageCanvasProjectPort,
|
||||
} from '@genarrative/image-canvas-core';
|
||||
export {
|
||||
CanvasViewport as SharedCanvasViewport,
|
||||
CanvasWorld as SharedCanvasWorld,
|
||||
LayerRenderer as SharedLayerRenderer,
|
||||
Minimap as SharedMinimap,
|
||||
SelectionOverlay as SharedSelectionOverlay,
|
||||
ZoomControls as SharedZoomControls,
|
||||
} from '@genarrative/image-canvas-react';
|
||||
@@ -1,9 +0,0 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
/**
|
||||
* UI 布局在父级坐标系中的轴对齐矩形。
|
||||
*
|
||||
* `min` 是矩形的最小角,`size` 是沿两个坐标轴的尺寸。这里不规定 Y 轴方向,
|
||||
* 因而既能用于 Y 轴向上的游戏坐标,也能用于 Y 轴向下的画布坐标。
|
||||
*/
|
||||
export type UIRect = { min: [number, number], size: [number, number], };
|
||||
@@ -1,27 +0,0 @@
|
||||
import type { UiEditorPrerequisiteIssue } from '../../../features/ui-editor/requisites';
|
||||
|
||||
export function PrerequisiteIssues({
|
||||
issues,
|
||||
}: {
|
||||
issues: UiEditorPrerequisiteIssue[];
|
||||
}) {
|
||||
if (issues.length === 0) {
|
||||
return (
|
||||
<div className="rounded-xl border border-emerald-200 bg-emerald-50 p-3 text-xs text-emerald-800">
|
||||
当前 State 已满足该工具的前置数据要求。
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="rounded-xl border border-amber-200 bg-amber-50 p-3 text-xs text-amber-900">
|
||||
<strong>还不能执行此操作</strong>
|
||||
<ul className="mt-2 space-y-1 pl-4">
|
||||
{issues.map((issue, index) => (
|
||||
<li key={`${issue.code}-${issue.resourceId ?? 'none'}-${index}`}>
|
||||
{issue.message}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -23,6 +23,16 @@
|
||||
- 验证方式:运行评论弹层恢复竞态回归、完整 `appSurface.test.ts`,并执行类型、编码和 diff 检查。
|
||||
- 关联文档:`docs/technical/【技术方案】立项策划Agent(Fast GDD)-2026-08-10.md`、`apps/ai-game-creator-shell/src/features/project-workspace/GddApprovalCard.tsx`。
|
||||
|
||||
## 2026-09-02 旧玩法表采用两阶段退役清理
|
||||
|
||||
- 背景:旧创作模板的业务代码已退出现役编译链,但 SpacetimeDB 中的历史表仍需先完成数据清理;直接删除表定义会扩大 schema 迁移和客户端兼容风险。
|
||||
- 决策:阶段一只在 `spacetime-module/src/migration.rs` 增加受 `database_migration_operator` 保护的 `clear_retired_database_tables` procedure。procedure 使用固定的 63 张旧玩法表清单,不接受动态表名;`dry_run=true` 只返回逐表行数统计,`dry_run=false` 在同一事务内逐表清空,任一失败整体回滚。阶段一不删除表定义、不修改 `legacy_schema/**`、migration 导入导出白名单或生成 bindings。
|
||||
- 阶段边界:清理清单包含旧 gameplay、`custom_world`、Puzzle / Puzzle Clear、Bark Battle、Match3D、Jump Hop、Wooden Fish、Square Hole、Visual Novel 和 Big Fish 表;`runtime_setting`、`runtime_snapshot`、`user_browse_history`、`creation_entry_config` 等现役表明确排除。阶段二只有在备份、客户端兼容性和运行态确认完成后,才评估从 module 定义与 migration 白名单移除空表,并按 schema / bindings 流程发布;固定清单旁保留 TODO。
|
||||
- 影响范围:SpacetimeDB migration procedure、`spacetime-client` 生成 bindings、后端数据契约和本决策记录;禁止新增 SQL `DROP TABLE`、`--delete-data=always` 或直接写系统表的实现。
|
||||
- 验证方式:固定清单测试确认数量为 63 且不含现役表;本地数据库以已授权 operator 执行 dry-run,确认 63 张表均返回 0 行且未写入;apply 的单事务回滚由 procedure 实现,实际 apply 仅在另行授权的维护窗口执行。另运行 bindings 生成、SpacetimeDB schema / runtime 检查、编码和 diff 门禁。
|
||||
- 关联文档:`docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md`、`server-rs/crates/spacetime-module/src/migration.rs`。
|
||||
- 补充落地:阶段一同时删除此前仅因退役而保留的旧玩法业务实现、未挂载 API handler/router/worker、旧客户端 facade/mapper、旧领域 crate 和 `retired/legacy-creation-templates/**` 归档;`legacy_schema/**`、生成表 bindings 和当前前端仍引用的公共契约不在本次删除范围。
|
||||
|
||||
## 2026-08-31 DirectProject 客户端扩展按独立 Skill/MCP 导入
|
||||
|
||||
- 背景:DirectProject 需要使用用户在 AGC 客户端导入的市面原生 Skill、MCP 和 Plugin 内容,但第三方内容不应直接安装到运行时 Codex,也不应要求用户转换为 AGC 自定义格式。
|
||||
@@ -6558,7 +6568,7 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在
|
||||
- 并发补验:确定性 Provider 只在 Runtime 明确返回 revision blocker、专业 verification-only repair、成功验证 observation,或项目锁 / repository context drift 这两类可恢复 observation 时重放终态;每个 logical run 最多 16 次。只读职责不得借补验调用未授权命令,验证失败或缺少 `ok` observation 不能交付,Provider completion 计数始终 exactly-once。
|
||||
- 画布审计:资源 manifest 可以保留生成 prompt 作为本地来源元数据,但公开 `asset.register / asset.update` 审计记录必须移除 `source.prompt`,只保留 canvas/resource/task/model 等身份字段,避免完整生成正文进入公开 Agent DB 表面。
|
||||
- 当前测试事实:已有回归覆盖固定画布合同不允许被模型改写、已登记 spritesheet 禁止先删除、只有静态 repair 可原位替换、替换期间原文件 fingerprint 漂移时拒绝覆盖,以及 `design-foundation` 对 `game/index.html` 的 write / patchset / delete 和预览工具均被 Runtime 策略阻断。2026-07-27 的独立 75 分钟上限外部真实 E2E 已按上一节单轮证据完整 **PASS**;后续合同变化仍须新起独立轮次,不能复用这次结果替代未来验收。
|
||||
- 最终落地:本次退役范围覆盖整个旧创作模板体系,包括 RPG / 自定义世界、拼图、拼消消、大鱼吃小鱼、敲木鱼、方洞挑战、视觉小说、汪汪声浪、寓教于乐、Creative Agent、Match3D、跳一跳和儿童动作 Demo。全部相关历史表继续作为数据壳参与 `spacetime-module` 编译,`migration.rs` 白名单与历史数据不变;旧 reducer/procedure/view、API 路由/handler/worker、前端页面/工作台/运行态、共享业务 DTO 和纯业务 crate 从编译链与依赖图移除,但旧源码和素材保留在仓库中用于历史追溯。
|
||||
- 最终落地:本次退役范围覆盖整个旧创作模板体系,包括 RPG / 自定义世界、拼图、拼消消、大鱼吃小鱼、敲木鱼、方洞挑战、视觉小说、汪汪声浪、寓教于乐、Creative Agent、Match3D、跳一跳和儿童动作 Demo。全部相关历史表继续作为数据壳参与 `spacetime-module` 编译,`migration.rs` 白名单与历史数据不变;旧 reducer/procedure/view、API 路由/handler/worker、前端页面/工作台/运行态、共享业务 DTO、纯业务 crate、未挂载旧实现和归档源码均删除,现役公共能力与历史表 schema 保留。
|
||||
- 兼容读取:只保留历史审计、迁移和资产归属核对所需的最小读取定义;旧 `worldType`、公开作品号、URL、详情页和专属运行态均不再形成用户可访问入口。
|
||||
- 方案文档:`docs/technical/【架构下线】旧创作模板业务退役方案-2026-07-17.md`。
|
||||
|
||||
@@ -6573,7 +6583,7 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在
|
||||
- Rust 产物边界:`module-runtime` 继续承载账号、钱包、公共设置、追踪和 feature gate,但 `CreationEntry*`、旧公开作品、存档、浏览历史与游玩统计 DTO / command / mapper / 规则必须退出实际 rlib;只保留历史表需要的 `RuntimeBrowseHistoryThemeMode`、完整保序的钱包流水来源枚举等持久化 ABI。`check:server-rs-ddd` 必须执行 `check:module-runtime-artifact`,同时验证旧符号和字面量为零、必要 ABI 仍存在,不能以源码存在 `#[cfg(any())]` 或路由未挂载代替产物证明。
|
||||
- 外围编译边界:`platform-auth` 不再编译 runtime guest token,`platform-wechat` 不再编译旧玩法生成结果订阅消息,小程序不再注册订阅授权页;旧公开作品资产授权 view 退出 SpacetimeDB module,匿名素材读取只保留现役 editor showcase 派生授权。
|
||||
- 历史队列边界:现役 external generation worker 只领取 `source_module = editor-canvas` 的任务,历史旧玩法 pending / running 行保持原状态,不得被新 worker 领取后改写为失败。
|
||||
- Agent crate 边界:`platform-agent` 的执行器、工具注册表、回调和拼图 Phase 1 输入均属于已退役 Creative Agent 业务,不得因现役编辑器 Agent 共用一个模型名常量而留在 workspace 或 `api-server` 依赖图。该常量收口到 `platform-llm`,`platform-agent` 与仅由它引入的 `langchainrust` 退出在运 Cargo resolve graph,源码目录继续仅作历史追溯。
|
||||
- Agent crate 边界:`platform-agent` 的执行器、工具注册表、回调和拼图 Phase 1 输入均属于已退役 Creative Agent 业务,不得因现役编辑器 Agent 共用一个模型名常量而留在 workspace 或 `api-server` 依赖图。该常量收口到 `platform-llm`,`platform-agent` 与仅由它引入的 `langchainrust` 退出在运 Cargo resolve graph,相关源码已删除。
|
||||
- AI 游戏创作兼容边界:独立 AGC Tauri 壳仍复用 `platform-agent::game_creation` 的任务图与隔离协作数据模型。`platform-agent` 继续排除在 `server-rs` workspace 之外,但其独立 manifest 默认只编译 `game_creation` / `error`,旧执行器、工具注册表、回调、拼图 Phase 1 与 `langchainrust` 统一受关闭的 `legacy-creative-agent` feature 隔离;AGC lock 不得重新引入这些退役依赖。
|
||||
- 防回流补充:顶层 `creationEntryConfigService`、`creationUrlState`、`customWorld*`、`runtimeGuestAuth`、`runtimeRequest`、`input-devices`、`useCombatFlow`、`useStoryOptions`、`useMocapInput` 和微信生成订阅 facade 同样属于退役前端模块;Vite dev 对旧 `/api/creation*` 与 `/api/public-works*` 前缀直接返回 404,不能回落 SPA HTML。
|
||||
- Vite 全量边界补充:`src/games/**`、`src/data/**`、`src/prompts/**`、旧顶层 App / Playground、旧路由和 `services/ai.ts` 必须由 pre-transform 门禁直接拒绝;所有同源 `/generated-*` 裸读在 dev 与生产统一为空 `404`,历史对象只经现役签名读取接口兼容,不允许 SPA fallback 伪装成资产成功响应。
|
||||
|
||||
@@ -70,7 +70,7 @@ src/
|
||||
|
||||
根级原生壳检查同时锁定 HostBridge 模块分类:`dispatch` / `protocol` 是微信、移动、桌面三端共同模块;`appearance`、`badge`、`capabilities`、`clipboard`、`file-payloads`、`files`、`navigation`、`network`、`notifications`、`runtime`、`share` 是 Expo / Tauri 原生 App 壳共同模块;移动端专属 `bridge`、`haptics`、`scanner`,桌面端专属 `mod`、`title`,微信端专属 `payment`、`shareGrid`、`webView`。后续新增或拆分桥接模块不能只改文件清单,必须先说明它属于三端共同、原生 App 共同还是某端专属。
|
||||
|
||||
2026-07-18 调整:旧玩法生成结果订阅授权已退役,微信 capability profile 不再声明 `navigation.openNativePage`,`subscribeMessage` host-bridge / shell / page 历史源码移入 `retired/legacy-creation-templates/frontend/miniprogram/`,不再进入小程序包和 `check:native-shells` 现役文件清单。
|
||||
2026-07-18 调整:旧玩法生成结果订阅授权已退役,微信 capability profile 不再声明 `navigation.openNativePage`,`subscribeMessage` host-bridge / shell / page 已从仓库删除,不再进入小程序包和 `check:native-shells` 现役文件清单。
|
||||
|
||||
生产替身词扫描只覆盖上述壳源码、分发配置、共享 HostBridge 契约和已接入真实宿主能力的 H5 调用链;Expo export、Tauri `target/`、Cargo / Metro 缓存和 release 构建产物不进入扫描范围,避免本地或 CI 生成文件污染源码门禁。
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,229 +0,0 @@
|
||||
import { describe, expect, test } from 'vitest';
|
||||
|
||||
import {
|
||||
BARK_BATTLE_ASSET_SLOTS,
|
||||
BARK_BATTLE_DIFFICULTY_PRESETS,
|
||||
type BarkBattleDraftConfig,
|
||||
type BarkBattleDraftConfigUpdateRequest,
|
||||
type BarkBattleFinishResponse,
|
||||
type BarkBattleGeneratedImageAsset,
|
||||
type BarkBattleImageAssetGenerateRequest,
|
||||
type BarkBattlePersonalBestSummary,
|
||||
type BarkBattleWorkStats,
|
||||
} from './barkBattle';
|
||||
|
||||
describe('Bark Battle shared contracts', () => {
|
||||
test('default draft config fixture uses normal difficulty and v1 description fields', () => {
|
||||
const draft: BarkBattleDraftConfig = {
|
||||
draftId: 'draft-bark-1',
|
||||
workId: 'work-bark-1',
|
||||
configVersion: 2,
|
||||
rulesetVersion: 'bark-battle-ruleset-v1',
|
||||
title: '汪汪声浪挑战',
|
||||
description: '轻配置草稿',
|
||||
themeDescription: '傍晚城市公园里的声浪擂台',
|
||||
playerImageDescription: '戴红围巾的柯基主角',
|
||||
opponentImageDescription: '蓝色运动头带的哈士奇对手',
|
||||
onomatopoeia: ['轰汪!', '嗷呜!', '咚咚!'],
|
||||
playerCharacterImageSrc: '/generated-bark-battle/player/image.png',
|
||||
opponentCharacterImageSrc: 'https://example.test/opponent.png',
|
||||
uiBackgroundImageSrc: '/generated-bark-battle/ui/background.png',
|
||||
difficultyPreset: 'normal',
|
||||
updatedAt: '2026-05-13T03:00:00.000Z',
|
||||
};
|
||||
|
||||
expect(BARK_BATTLE_DIFFICULTY_PRESETS).toEqual(['easy', 'normal', 'hard']);
|
||||
expect(draft.difficultyPreset).toBe('normal');
|
||||
expect(Object.keys(draft)).toEqual([
|
||||
'draftId',
|
||||
'workId',
|
||||
'configVersion',
|
||||
'rulesetVersion',
|
||||
'title',
|
||||
'description',
|
||||
'themeDescription',
|
||||
'playerImageDescription',
|
||||
'opponentImageDescription',
|
||||
'onomatopoeia',
|
||||
'playerCharacterImageSrc',
|
||||
'opponentCharacterImageSrc',
|
||||
'uiBackgroundImageSrc',
|
||||
'difficultyPreset',
|
||||
'updatedAt',
|
||||
]);
|
||||
expect(draft.playerCharacterImageSrc).toContain('/generated-bark-battle/');
|
||||
expect('barkSoundSrc' in draft).toBe(false);
|
||||
expect('leaderboardEnabled' in draft).toBe(false);
|
||||
});
|
||||
|
||||
test('draft config update contract persists generated image slots only', () => {
|
||||
const update: BarkBattleDraftConfigUpdateRequest = {
|
||||
draftId: 'draft-bark-1',
|
||||
workId: 'BB-12345678',
|
||||
configVersion: 2,
|
||||
rulesetVersion: 'bark-battle-ruleset-v1',
|
||||
title: '汪汪声浪挑战',
|
||||
description: '轻配置草稿',
|
||||
themeDescription: '傍晚城市公园里的声浪擂台',
|
||||
playerImageDescription: '戴红围巾的柯基主角',
|
||||
opponentImageDescription: '蓝色运动头带的哈士奇对手',
|
||||
onomatopoeia: ['轰!', '燃起来!', '破阵!'],
|
||||
playerCharacterImageSrc: '/generated-bark-battle/player/image.png',
|
||||
opponentCharacterImageSrc: '/generated-bark-battle/opponent/image.png',
|
||||
uiBackgroundImageSrc: '/generated-bark-battle/ui/background.png',
|
||||
difficultyPreset: 'normal',
|
||||
};
|
||||
|
||||
expect(Object.keys(update)).toEqual([
|
||||
'draftId',
|
||||
'workId',
|
||||
'configVersion',
|
||||
'rulesetVersion',
|
||||
'title',
|
||||
'description',
|
||||
'themeDescription',
|
||||
'playerImageDescription',
|
||||
'opponentImageDescription',
|
||||
'onomatopoeia',
|
||||
'playerCharacterImageSrc',
|
||||
'opponentCharacterImageSrc',
|
||||
'uiBackgroundImageSrc',
|
||||
'difficultyPreset',
|
||||
]);
|
||||
expect('barkSoundSrc' in update).toBe(false);
|
||||
expect('leaderboardEnabled' in update).toBe(false);
|
||||
});
|
||||
|
||||
test('image generation contract uses dedicated Bark Battle slots and backend prompt result', () => {
|
||||
expect(BARK_BATTLE_ASSET_SLOTS).toEqual([
|
||||
'player-character',
|
||||
'opponent-character',
|
||||
'ui-background',
|
||||
]);
|
||||
|
||||
const request: BarkBattleImageAssetGenerateRequest = {
|
||||
slot: 'opponent-character',
|
||||
draftId: 'bark-battle-draft-1',
|
||||
config: {
|
||||
title: '汪汪冠军杯',
|
||||
description: '',
|
||||
themeDescription: '霓虹公园擂台',
|
||||
playerImageDescription: '红围巾柴犬',
|
||||
opponentImageDescription: '蓝头带哈士奇',
|
||||
onomatopoeia: ['轰汪!', '炸场!', '冲啊!'],
|
||||
difficultyPreset: 'normal',
|
||||
},
|
||||
};
|
||||
const response: BarkBattleGeneratedImageAsset = {
|
||||
imageSrc: '/generated-bark-battle-assets/draft/opponent/image.webp',
|
||||
assetId: 'asset-1',
|
||||
sourceType: 'generated',
|
||||
model: 'gpt-image-2',
|
||||
size: '1024*1024',
|
||||
taskId: 'task-1',
|
||||
prompt: '后端拼装后的对手形象 prompt',
|
||||
};
|
||||
|
||||
expect(JSON.parse(JSON.stringify(request))).toMatchObject({
|
||||
slot: 'opponent-character',
|
||||
config: {
|
||||
opponentImageDescription: '蓝头带哈士奇',
|
||||
onomatopoeia: ['轰汪!', '炸场!', '冲啊!'],
|
||||
},
|
||||
});
|
||||
expect(JSON.parse(JSON.stringify(response))).toMatchObject({
|
||||
imageSrc: '/generated-bark-battle-assets/draft/opponent/image.webp',
|
||||
prompt: '后端拼装后的对手形象 prompt',
|
||||
});
|
||||
});
|
||||
|
||||
test('finish accepted player_win fixture exposes backend adjudication result', () => {
|
||||
const response: BarkBattleFinishResponse = {
|
||||
status: 'accepted',
|
||||
runId: 'run-bark-1',
|
||||
workId: 'work-bark-1',
|
||||
configVersion: 3,
|
||||
rulesetVersion: 'bark-battle-ruleset-v1',
|
||||
difficultyPreset: 'hard',
|
||||
serverResult: 'player_win',
|
||||
scoreSummary: {
|
||||
finalEnergy: 87,
|
||||
triggerCount: 42,
|
||||
maxVolume: 0.96,
|
||||
averageVolume: 0.61,
|
||||
comboMax: 9,
|
||||
durationMs: 30000,
|
||||
},
|
||||
leaderboardScore: 870429630,
|
||||
antiCheatFlags: [],
|
||||
updatedAt: '2026-05-13T03:00:30.000Z',
|
||||
};
|
||||
|
||||
expect(response.status).toBe('accepted');
|
||||
expect(response.serverResult).toBe('player_win');
|
||||
expect(response.scoreSummary.finalEnergy).toBe(87);
|
||||
expect(response.antiCheatFlags).toEqual([]);
|
||||
});
|
||||
|
||||
test('work stats fixture tracks starts, finishes, result counts, flags and energy summary', () => {
|
||||
const stats: BarkBattleWorkStats = {
|
||||
workId: 'work-bark-1',
|
||||
configVersion: 3,
|
||||
rulesetVersion: 'bark-battle-ruleset-v1',
|
||||
difficultyPreset: 'normal',
|
||||
playStartCount: 18,
|
||||
finishCount: 15,
|
||||
winCount: 8,
|
||||
drawCount: 2,
|
||||
lossCount: 5,
|
||||
flaggedCount: 1,
|
||||
leaderboardEntryCount: 7,
|
||||
bestLeaderboardScore: 930389410,
|
||||
bestFinalEnergy: 93,
|
||||
averageFinalEnergy: 41.25,
|
||||
updatedAt: '2026-05-13T04:00:00.000Z',
|
||||
};
|
||||
|
||||
expect(stats.playStartCount).toBe(18);
|
||||
expect(stats.finishCount).toBe(15);
|
||||
expect(stats.winCount + stats.drawCount + stats.lossCount).toBe(15);
|
||||
expect(stats.flaggedCount).toBe(1);
|
||||
expect(stats.bestFinalEnergy).toBeGreaterThan(stats.averageFinalEnergy);
|
||||
});
|
||||
|
||||
test('optional score fields may be omitted instead of serialized as null', () => {
|
||||
const finishWithoutLeaderboard: BarkBattleFinishResponse = {
|
||||
status: 'accepted',
|
||||
runId: 'run-bark-no-rank',
|
||||
workId: 'work-bark-1',
|
||||
configVersion: 3,
|
||||
rulesetVersion: 'bark-battle-ruleset-v1',
|
||||
difficultyPreset: 'normal',
|
||||
serverResult: 'draw',
|
||||
scoreSummary: {
|
||||
finalEnergy: 50,
|
||||
triggerCount: 12,
|
||||
maxVolume: 0.7,
|
||||
averageVolume: 0.5,
|
||||
comboMax: 3,
|
||||
durationMs: 30000,
|
||||
},
|
||||
antiCheatFlags: [],
|
||||
updatedAt: '2026-05-13T03:00:30.000Z',
|
||||
};
|
||||
const personalBestWithoutWin: BarkBattlePersonalBestSummary = {
|
||||
workId: 'work-bark-1',
|
||||
rulesetVersion: 'bark-battle-ruleset-v1',
|
||||
difficultyPreset: 'normal',
|
||||
winCount: 0,
|
||||
drawCount: 1,
|
||||
lossCount: 2,
|
||||
finishCount: 3,
|
||||
updatedAt: '2026-05-13T04:00:00.000Z',
|
||||
};
|
||||
|
||||
expect('leaderboardScore' in finishWithoutLeaderboard).toBe(false);
|
||||
expect('bestLeaderboardScore' in personalBestWithoutWin).toBe(false);
|
||||
expect('bestFinalEnergy' in personalBestWithoutWin).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -1,312 +0,0 @@
|
||||
export const BARK_BATTLE_DIFFICULTY_PRESETS = [
|
||||
'easy',
|
||||
'normal',
|
||||
'hard',
|
||||
] as const;
|
||||
|
||||
export type BarkBattleDifficultyPreset =
|
||||
(typeof BARK_BATTLE_DIFFICULTY_PRESETS)[number];
|
||||
|
||||
export type BarkBattleServerResult = 'player_win' | 'opponent_win' | 'draw';
|
||||
|
||||
export type BarkBattleFinishStatus =
|
||||
| 'accepted'
|
||||
| 'accepted_with_flags'
|
||||
| 'rejected';
|
||||
|
||||
export type BarkBattlePlayTypeId = 'bark-battle';
|
||||
|
||||
export const BARK_BATTLE_ASSET_SLOTS = [
|
||||
'player-character',
|
||||
'opponent-character',
|
||||
'ui-background',
|
||||
] as const;
|
||||
|
||||
export type BarkBattleAssetSlot = (typeof BARK_BATTLE_ASSET_SLOTS)[number];
|
||||
|
||||
export interface BarkBattleReplacementConfig {
|
||||
playerCharacterImageSrc?: string;
|
||||
opponentCharacterImageSrc?: string;
|
||||
uiBackgroundImageSrc?: string;
|
||||
}
|
||||
|
||||
export type BarkBattleOnomatopoeia = string[];
|
||||
|
||||
export interface BarkBattleConfigEditorPayload
|
||||
extends BarkBattleReplacementConfig {
|
||||
title: string;
|
||||
description?: string;
|
||||
themeDescription: string;
|
||||
playerImageDescription: string;
|
||||
opponentImageDescription: string;
|
||||
onomatopoeia?: BarkBattleOnomatopoeia;
|
||||
difficultyPreset: BarkBattleDifficultyPreset;
|
||||
}
|
||||
|
||||
export interface BarkBattleDraftCreateRequest
|
||||
extends BarkBattleConfigEditorPayload {}
|
||||
|
||||
export interface BarkBattleDraftConfigUpdateRequest
|
||||
extends BarkBattleConfigEditorPayload {
|
||||
draftId: string;
|
||||
workId?: string | null;
|
||||
configVersion?: number;
|
||||
rulesetVersion?: string;
|
||||
}
|
||||
|
||||
export interface BarkBattleWorkPublishRequest {
|
||||
draftId: string;
|
||||
workId: string;
|
||||
publishedSnapshot?: BarkBattleConfigEditorPayload;
|
||||
}
|
||||
|
||||
export interface BarkBattleImageAssetGenerateRequest {
|
||||
slot: BarkBattleAssetSlot;
|
||||
draftId?: string | null;
|
||||
billingPurpose?: 'initial_draft_generation' | null;
|
||||
config: BarkBattleConfigEditorPayload;
|
||||
}
|
||||
|
||||
export interface BarkBattleGeneratedImageAsset {
|
||||
imageSrc: string;
|
||||
assetId: string;
|
||||
sourceType?: 'generated' | string;
|
||||
model: string;
|
||||
size: string;
|
||||
taskId: string;
|
||||
prompt: string;
|
||||
actualPrompt?: string;
|
||||
}
|
||||
|
||||
export interface BarkBattleDraftConfig extends BarkBattleConfigEditorPayload {
|
||||
draftId: string;
|
||||
workId?: string;
|
||||
configVersion?: number;
|
||||
rulesetVersion?: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface BarkBattlePublishedConfig {
|
||||
workId: string;
|
||||
draftId?: string | null;
|
||||
configVersion: number;
|
||||
rulesetVersion: string;
|
||||
playTypeId: BarkBattlePlayTypeId;
|
||||
title: string;
|
||||
description?: string;
|
||||
themeDescription: string;
|
||||
playerImageDescription: string;
|
||||
opponentImageDescription: string;
|
||||
onomatopoeia?: BarkBattleOnomatopoeia;
|
||||
playerCharacterImageSrc?: string;
|
||||
opponentCharacterImageSrc?: string;
|
||||
uiBackgroundImageSrc?: string;
|
||||
difficultyPreset: BarkBattleDifficultyPreset;
|
||||
updatedAt: string;
|
||||
publishedAt: string;
|
||||
}
|
||||
|
||||
export type BarkBattleWorkStatus = 'draft' | 'published';
|
||||
|
||||
export type BarkBattleGenerationStatus =
|
||||
| 'pending_assets'
|
||||
| 'ready'
|
||||
| 'partial_failed'
|
||||
| string;
|
||||
|
||||
export interface BarkBattleWorkSummary {
|
||||
workId: string;
|
||||
draftId?: string | null;
|
||||
ownerUserId: string;
|
||||
authorDisplayName: string;
|
||||
title: string;
|
||||
summary: string;
|
||||
themeDescription: string;
|
||||
playerImageDescription: string;
|
||||
opponentImageDescription: string;
|
||||
onomatopoeia?: BarkBattleOnomatopoeia;
|
||||
playerCharacterImageSrc?: string | null;
|
||||
opponentCharacterImageSrc?: string | null;
|
||||
uiBackgroundImageSrc?: string | null;
|
||||
difficultyPreset: BarkBattleDifficultyPreset;
|
||||
status: BarkBattleWorkStatus;
|
||||
generationStatus?: BarkBattleGenerationStatus | null;
|
||||
publishReady: boolean;
|
||||
playCount: number;
|
||||
finishCount?: number;
|
||||
winCount?: number;
|
||||
drawCount?: number;
|
||||
lossCount?: number;
|
||||
recentPlayCount7d?: number;
|
||||
updatedAt: string;
|
||||
publishedAt?: string | null;
|
||||
}
|
||||
|
||||
export interface BarkBattleWorksResponse {
|
||||
items: BarkBattleWorkSummary[];
|
||||
}
|
||||
|
||||
export interface BarkBattleWorkDetailResponse {
|
||||
item: BarkBattleWorkSummary;
|
||||
}
|
||||
|
||||
export interface BarkBattleRuntimeConfig {
|
||||
workId: string;
|
||||
configVersion: number;
|
||||
rulesetVersion: string;
|
||||
playTypeId: BarkBattlePlayTypeId;
|
||||
durationMs: number;
|
||||
energyMin: number;
|
||||
energyMax: number;
|
||||
drawThreshold: number;
|
||||
minBarkGapMs: number;
|
||||
difficultyPreset: BarkBattleDifficultyPreset;
|
||||
themeDescription: string;
|
||||
playerImageDescription: string;
|
||||
opponentImageDescription: string;
|
||||
onomatopoeia?: BarkBattleOnomatopoeia;
|
||||
playerCharacterImageSrc?: string;
|
||||
opponentCharacterImageSrc?: string;
|
||||
uiBackgroundImageSrc?: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface BarkBattleRunStartRequest {
|
||||
workId: string;
|
||||
configVersion?: number;
|
||||
sourceRoute?: string;
|
||||
clientRuntimeVersion?: string;
|
||||
}
|
||||
|
||||
export interface BarkBattleRunStartResponse {
|
||||
runId: string;
|
||||
runToken: string;
|
||||
workId: string;
|
||||
configVersion: number;
|
||||
rulesetVersion: string;
|
||||
difficultyPreset: BarkBattleDifficultyPreset;
|
||||
runtimeConfig: BarkBattleRuntimeConfig;
|
||||
serverStartedAt: string;
|
||||
expiresAt: string;
|
||||
}
|
||||
|
||||
export interface BarkBattleDerivedMetrics {
|
||||
triggerCount: number;
|
||||
maxVolume: number;
|
||||
averageVolume: number;
|
||||
finalEnergy: number;
|
||||
comboMax: number;
|
||||
}
|
||||
|
||||
export interface BarkBattleRunFinishRequest {
|
||||
runId: string;
|
||||
runToken: string;
|
||||
workId: string;
|
||||
configVersion: number;
|
||||
rulesetVersion: string;
|
||||
difficultyPreset: BarkBattleDifficultyPreset;
|
||||
clientStartedAt: string;
|
||||
clientFinishedAt: string;
|
||||
durationMs: number;
|
||||
derivedMetrics: BarkBattleDerivedMetrics;
|
||||
clientResult?: BarkBattleServerResult;
|
||||
sampleDigest?: string;
|
||||
clientRuntimeVersion?: string;
|
||||
}
|
||||
|
||||
export interface BarkBattleScoreSummary extends BarkBattleDerivedMetrics {
|
||||
durationMs: number;
|
||||
}
|
||||
|
||||
export interface BarkBattleFinishResponse {
|
||||
status: BarkBattleFinishStatus;
|
||||
runId: string;
|
||||
workId: string;
|
||||
configVersion: number;
|
||||
rulesetVersion: string;
|
||||
difficultyPreset: BarkBattleDifficultyPreset;
|
||||
serverResult: BarkBattleServerResult;
|
||||
scoreSummary: BarkBattleScoreSummary;
|
||||
leaderboardScore?: number;
|
||||
antiCheatFlags: string[];
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface BarkBattleLeaderboardEntry {
|
||||
rank: number;
|
||||
runId: string;
|
||||
workId: string;
|
||||
configVersion: number;
|
||||
rulesetVersion: string;
|
||||
difficultyPreset: BarkBattleDifficultyPreset;
|
||||
displayName: string;
|
||||
serverResult: BarkBattleServerResult;
|
||||
scoreSummary: BarkBattleScoreSummary;
|
||||
leaderboardScore: number;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface BarkBattleLeaderboardResponse {
|
||||
workId: string;
|
||||
configVersion?: number;
|
||||
rulesetVersion: string;
|
||||
difficultyPreset: BarkBattleDifficultyPreset;
|
||||
entries: BarkBattleLeaderboardEntry[];
|
||||
viewerBest?: BarkBattleLeaderboardEntry | null;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface BarkBattlePersonalHistoryItem {
|
||||
runId: string;
|
||||
workId: string;
|
||||
configVersion: number;
|
||||
rulesetVersion: string;
|
||||
difficultyPreset: BarkBattleDifficultyPreset;
|
||||
serverResult: BarkBattleServerResult;
|
||||
scoreSummary: BarkBattleScoreSummary;
|
||||
leaderboardScore?: number;
|
||||
antiCheatFlags: string[];
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface BarkBattlePersonalBestSummary {
|
||||
workId: string;
|
||||
configVersion?: number;
|
||||
rulesetVersion: string;
|
||||
difficultyPreset: BarkBattleDifficultyPreset;
|
||||
bestLeaderboardScore?: number;
|
||||
bestFinalEnergy?: number;
|
||||
bestTriggerCount?: number;
|
||||
bestMaxVolume?: number;
|
||||
winCount: number;
|
||||
drawCount: number;
|
||||
lossCount: number;
|
||||
finishCount: number;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface BarkBattlePersonalHistoryResponse {
|
||||
workId?: string;
|
||||
difficultyPreset?: BarkBattleDifficultyPreset;
|
||||
items: BarkBattlePersonalHistoryItem[];
|
||||
bestSummary?: BarkBattlePersonalBestSummary | null;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface BarkBattleWorkStats {
|
||||
workId: string;
|
||||
configVersion?: number;
|
||||
rulesetVersion: string;
|
||||
difficultyPreset: BarkBattleDifficultyPreset;
|
||||
playStartCount: number;
|
||||
finishCount: number;
|
||||
winCount: number;
|
||||
drawCount: number;
|
||||
lossCount: number;
|
||||
flaggedCount: number;
|
||||
leaderboardEntryCount: number;
|
||||
bestLeaderboardScore?: number;
|
||||
bestFinalEnergy?: number;
|
||||
averageFinalEnergy?: number;
|
||||
updatedAt: string;
|
||||
}
|
||||
@@ -1,199 +0,0 @@
|
||||
/**
|
||||
* 大鱼吃小鱼玩法域前端共享契约。
|
||||
* 字段与 server-rs/shared-contracts/src/big_fish.rs 保持 camelCase 对齐。
|
||||
*/
|
||||
export type CreateBigFishSessionRequest = {
|
||||
seedText?: string;
|
||||
};
|
||||
|
||||
export type SendBigFishMessageRequest = {
|
||||
clientMessageId: string;
|
||||
text: string;
|
||||
quickFillRequested?: boolean;
|
||||
};
|
||||
|
||||
export type BigFishActionId =
|
||||
| 'big_fish_compile_draft'
|
||||
| 'big_fish_generate_level_main_image'
|
||||
| 'big_fish_generate_level_motion'
|
||||
| 'big_fish_generate_stage_background'
|
||||
| 'big_fish_publish_game';
|
||||
|
||||
export type ExecuteBigFishActionRequest = {
|
||||
action: BigFishActionId;
|
||||
level?: number;
|
||||
motionKey?: 'idle_float' | 'move_swim' | string;
|
||||
};
|
||||
|
||||
export type RecordBigFishPlayRequest = {
|
||||
elapsedMs?: number;
|
||||
};
|
||||
|
||||
export type SubmitBigFishInputRequest = {
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
|
||||
export type BigFishAnchorStatus =
|
||||
| 'confirmed'
|
||||
| 'inferred'
|
||||
| 'missing'
|
||||
| 'locked'
|
||||
| string;
|
||||
|
||||
export type BigFishAnchorItemResponse = {
|
||||
key: string;
|
||||
label: string;
|
||||
value: string;
|
||||
status: BigFishAnchorStatus;
|
||||
};
|
||||
|
||||
export type BigFishAnchorPackResponse = {
|
||||
gameplayPromise: BigFishAnchorItemResponse;
|
||||
ecologyVisualTheme: BigFishAnchorItemResponse;
|
||||
growthLadder: BigFishAnchorItemResponse;
|
||||
riskTempo: BigFishAnchorItemResponse;
|
||||
};
|
||||
|
||||
export type BigFishLevelBlueprintResponse = {
|
||||
level: number;
|
||||
name: string;
|
||||
oneLineFantasy: string;
|
||||
textDescription: string;
|
||||
silhouetteDirection: string;
|
||||
sizeRatio: number;
|
||||
visualDescription: string;
|
||||
visualPromptSeed: string;
|
||||
idleMotionDescription: string;
|
||||
moveMotionDescription: string;
|
||||
motionPromptSeed: string;
|
||||
mergeSourceLevel?: number | null;
|
||||
preyWindow: number[];
|
||||
threatWindow: number[];
|
||||
isFinalLevel: boolean;
|
||||
};
|
||||
|
||||
export type BigFishBackgroundBlueprintResponse = {
|
||||
theme: string;
|
||||
colorMood: string;
|
||||
foregroundHints: string;
|
||||
midgroundComposition: string;
|
||||
backgroundDepth: string;
|
||||
safePlayAreaHint: string;
|
||||
spawnEdgeHint: string;
|
||||
backgroundPromptSeed: string;
|
||||
};
|
||||
|
||||
export type BigFishRuntimeParamsResponse = {
|
||||
levelCount: number;
|
||||
mergeCountPerUpgrade: number;
|
||||
spawnTargetCount: number;
|
||||
leaderMoveSpeed: number;
|
||||
followerCatchUpSpeed: number;
|
||||
offscreenCullSeconds: number;
|
||||
preySpawnDeltaLevels: number[];
|
||||
threatSpawnDeltaLevels: number[];
|
||||
winLevel: number;
|
||||
};
|
||||
|
||||
export type BigFishGameDraftResponse = {
|
||||
title: string;
|
||||
subtitle: string;
|
||||
coreFun: string;
|
||||
ecologyTheme: string;
|
||||
levels: BigFishLevelBlueprintResponse[];
|
||||
background: BigFishBackgroundBlueprintResponse;
|
||||
runtimeParams: BigFishRuntimeParamsResponse;
|
||||
};
|
||||
|
||||
export type BigFishAgentMessageResponse = {
|
||||
id: string;
|
||||
role: 'user' | 'assistant' | string;
|
||||
kind: 'chat' | 'system' | 'warning' | string;
|
||||
text: string;
|
||||
createdAt: string;
|
||||
};
|
||||
|
||||
export type BigFishAssetKind =
|
||||
| 'level_main_image'
|
||||
| 'level_motion'
|
||||
| 'stage_background'
|
||||
| string;
|
||||
|
||||
export type BigFishAssetStatus = 'empty' | 'ready' | 'generating' | string;
|
||||
|
||||
export type BigFishAssetSlotResponse = {
|
||||
slotId: string;
|
||||
assetKind: BigFishAssetKind;
|
||||
level?: number | null;
|
||||
motionKey?: string | null;
|
||||
status: BigFishAssetStatus;
|
||||
assetUrl?: string | null;
|
||||
promptSnapshot: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
export type BigFishAssetCoverageResponse = {
|
||||
levelMainImageReadyCount: number;
|
||||
levelMotionReadyCount: number;
|
||||
backgroundReady: boolean;
|
||||
requiredLevelCount: number;
|
||||
publishReady: boolean;
|
||||
blockers: string[];
|
||||
};
|
||||
|
||||
export type BigFishSessionSnapshotResponse = {
|
||||
sessionId: string;
|
||||
currentTurn: number;
|
||||
progressPercent: number;
|
||||
stage: string;
|
||||
anchorPack: BigFishAnchorPackResponse;
|
||||
draft?: BigFishGameDraftResponse | null;
|
||||
assetSlots: BigFishAssetSlotResponse[];
|
||||
assetCoverage: BigFishAssetCoverageResponse;
|
||||
messages: BigFishAgentMessageResponse[];
|
||||
lastAssistantReply?: string | null;
|
||||
publishReady: boolean;
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
export type BigFishSessionResponse = {
|
||||
session: BigFishSessionSnapshotResponse;
|
||||
};
|
||||
|
||||
export type BigFishActionResponse = {
|
||||
session: BigFishSessionSnapshotResponse;
|
||||
};
|
||||
|
||||
export type BigFishVector2Response = {
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
|
||||
export type BigFishRuntimeEntityResponse = {
|
||||
entityId: string;
|
||||
level: number;
|
||||
position: BigFishVector2Response;
|
||||
radius: number;
|
||||
offscreenSeconds: number;
|
||||
};
|
||||
|
||||
export type BigFishRuntimeSnapshotResponse = {
|
||||
runId: string;
|
||||
sessionId: string;
|
||||
status: 'running' | 'won' | 'failed' | string;
|
||||
tick: number;
|
||||
playerLevel: number;
|
||||
winLevel: number;
|
||||
leaderEntityId?: string | null;
|
||||
ownedEntities: BigFishRuntimeEntityResponse[];
|
||||
wildEntities: BigFishRuntimeEntityResponse[];
|
||||
cameraCenter: BigFishVector2Response;
|
||||
lastInput: BigFishVector2Response;
|
||||
eventLog: string[];
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
export type BigFishRunResponse = {
|
||||
run: BigFishRuntimeSnapshotResponse;
|
||||
};
|
||||
@@ -1,28 +0,0 @@
|
||||
export type BigFishWorkStatus = 'draft' | 'published';
|
||||
|
||||
export interface BigFishWorkSummary {
|
||||
workId: string;
|
||||
sourceSessionId: string;
|
||||
ownerUserId: string;
|
||||
authorDisplayName: string;
|
||||
title: string;
|
||||
subtitle: string;
|
||||
summary: string;
|
||||
coverImageSrc: string | null;
|
||||
status: BigFishWorkStatus;
|
||||
updatedAt: string;
|
||||
publishedAt?: string | null;
|
||||
publishReady: boolean;
|
||||
levelCount: number;
|
||||
levelMainImageReadyCount: number;
|
||||
levelMotionReadyCount: number;
|
||||
backgroundReady: boolean;
|
||||
playCount?: number;
|
||||
remixCount?: number;
|
||||
likeCount?: number;
|
||||
recentPlayCount7d?: number;
|
||||
}
|
||||
|
||||
export interface BigFishWorksResponse {
|
||||
items: BigFishWorkSummary[];
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
export interface ParseCreationAgentDocumentInputRequest {
|
||||
fileName: string;
|
||||
contentType?: string | null;
|
||||
contentBase64: string;
|
||||
}
|
||||
|
||||
export interface CreationAgentDocumentInputPayload {
|
||||
fileName: string;
|
||||
contentType?: string | null;
|
||||
sizeBytes: number;
|
||||
text: string;
|
||||
sourceAssetId?: string | null;
|
||||
}
|
||||
|
||||
export interface ParseCreationAgentDocumentInputResponse {
|
||||
document: CreationAgentDocumentInputPayload;
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
export type CreationAudioGenerationKind = 'background_music' | 'sound_effect';
|
||||
|
||||
export interface CreationAudioAsset {
|
||||
taskId: string;
|
||||
provider: string;
|
||||
assetObjectId?: string | null;
|
||||
assetKind?: string | null;
|
||||
audioSrc: string;
|
||||
prompt?: string | null;
|
||||
title?: string | null;
|
||||
updatedAt?: string | null;
|
||||
}
|
||||
|
||||
export interface CreateBackgroundMusicRequest {
|
||||
prompt: string;
|
||||
title: string;
|
||||
tags?: string | null;
|
||||
model?: string | null;
|
||||
}
|
||||
|
||||
export interface CreateSoundEffectRequest {
|
||||
prompt: string;
|
||||
duration?: number | null;
|
||||
seed?: number | null;
|
||||
}
|
||||
|
||||
export interface AudioGenerationTaskResponse {
|
||||
kind: CreationAudioGenerationKind;
|
||||
taskId: string;
|
||||
provider: string;
|
||||
status: string;
|
||||
}
|
||||
|
||||
export interface PublishGeneratedAudioAssetRequest {
|
||||
entityKind: string;
|
||||
entityId: string;
|
||||
slot: string;
|
||||
assetKind: string;
|
||||
profileId?: string | null;
|
||||
storagePrefix?:
|
||||
| 'puzzle_assets'
|
||||
| 'match3d_assets'
|
||||
| 'wooden_fish_assets'
|
||||
| 'custom_world_scenes'
|
||||
| null;
|
||||
}
|
||||
|
||||
export interface GeneratedAudioAssetResponse {
|
||||
kind: CreationAudioGenerationKind;
|
||||
taskId: string;
|
||||
provider: string;
|
||||
status: string;
|
||||
assetObjectId?: string | null;
|
||||
assetKind?: string | null;
|
||||
audioSrc?: string | null;
|
||||
}
|
||||
@@ -1,244 +0,0 @@
|
||||
import type { PuzzleResultDraft } from './puzzleAgentDraft';
|
||||
import type { PuzzleAgentSessionSnapshot } from './puzzleAgentSession';
|
||||
import type {
|
||||
PuzzleCreativeTemplateProtocol,
|
||||
PuzzleCreativeTemplateSelection,
|
||||
PuzzleDraftFieldPatch,
|
||||
PuzzleImageGenerationPlan,
|
||||
PuzzleTemplateCostRange,
|
||||
} from './puzzleCreativeTemplate';
|
||||
|
||||
export type CreativeAgentStage =
|
||||
| 'idle'
|
||||
| 'perceiving'
|
||||
| 'thinking'
|
||||
| 'remembering'
|
||||
| 'selecting_puzzle_template'
|
||||
| 'waiting_template_confirmation'
|
||||
| 'planning_puzzle_levels'
|
||||
| 'acting'
|
||||
| 'reflecting'
|
||||
| 'collaborating'
|
||||
| 'target_ready'
|
||||
| 'waiting_user'
|
||||
| 'failed';
|
||||
|
||||
export type CreativeAgentEntryContext =
|
||||
| 'creation_home'
|
||||
| 'puzzle_workspace'
|
||||
| 'gallery_remix'
|
||||
| 'draft_restore';
|
||||
|
||||
export type CreativeAgentMessageRole = 'user' | 'assistant' | 'system';
|
||||
|
||||
export type CreativeAgentMessageKind =
|
||||
| 'chat'
|
||||
| 'stage'
|
||||
| 'action_result'
|
||||
| 'warning';
|
||||
|
||||
export type CreativeAgentInputPart =
|
||||
| {
|
||||
type: 'input_text';
|
||||
text: string;
|
||||
}
|
||||
| {
|
||||
type: 'input_image';
|
||||
imageUrl: string;
|
||||
assetId?: string | null;
|
||||
thumbnailUrl?: string | null;
|
||||
};
|
||||
|
||||
export interface CreativeImageInput {
|
||||
assetId: string;
|
||||
readUrl: string;
|
||||
thumbnailUrl?: string | null;
|
||||
width?: number | null;
|
||||
height?: number | null;
|
||||
}
|
||||
|
||||
export interface CreativeImageSummary {
|
||||
assetId: string | null;
|
||||
readUrl: string | null;
|
||||
thumbnailUrl: string | null;
|
||||
width: number | null;
|
||||
height: number | null;
|
||||
summary: string | null;
|
||||
}
|
||||
|
||||
export type CreativeUnsupportedPlayType =
|
||||
| 'rpg'
|
||||
| 'match3d'
|
||||
| 'big_fish'
|
||||
| 'square_hole';
|
||||
|
||||
export interface CreativeUnsupportedCapability {
|
||||
playType: CreativeUnsupportedPlayType;
|
||||
title: string;
|
||||
status: 'unsupported';
|
||||
reason: string;
|
||||
}
|
||||
|
||||
export interface CreativeInputSummary {
|
||||
text: string | null;
|
||||
entryContext: CreativeAgentEntryContext;
|
||||
images: CreativeImageSummary[];
|
||||
materialSummary: string | null;
|
||||
unsupportedCapabilities: CreativeUnsupportedCapability[];
|
||||
}
|
||||
|
||||
export interface CreativeAgentMessage {
|
||||
id: string;
|
||||
role: CreativeAgentMessageRole;
|
||||
kind: CreativeAgentMessageKind;
|
||||
text: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface CreativeTargetSessionBinding {
|
||||
playType: 'puzzle';
|
||||
targetSessionId: string;
|
||||
targetStage: 'puzzle-agent-workspace' | 'puzzle-result' | 'puzzle-runtime';
|
||||
resultProfileId: string | null;
|
||||
}
|
||||
|
||||
export interface CreativeAgentSessionSnapshot {
|
||||
sessionId: string;
|
||||
stage: CreativeAgentStage;
|
||||
inputSummary: CreativeInputSummary;
|
||||
messages: CreativeAgentMessage[];
|
||||
puzzleTemplateCatalog: PuzzleCreativeTemplateProtocol[];
|
||||
puzzleTemplateSelection: PuzzleCreativeTemplateSelection | null;
|
||||
puzzleImageGenerationPlan: PuzzleImageGenerationPlan | null;
|
||||
targetBinding: CreativeTargetSessionBinding | null;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface CreateCreativeAgentSessionRequest {
|
||||
text?: string | null;
|
||||
images?: CreativeImageInput[];
|
||||
entryContext?: CreativeAgentEntryContext;
|
||||
}
|
||||
|
||||
export interface CreativeAgentSessionResponse {
|
||||
session: CreativeAgentSessionSnapshot;
|
||||
}
|
||||
|
||||
export interface StreamCreativeAgentMessageRequest {
|
||||
clientMessageId: string;
|
||||
content: CreativeAgentInputPart[];
|
||||
}
|
||||
|
||||
export interface ConfirmCreativePuzzleTemplateRequest {
|
||||
selection: PuzzleCreativeTemplateSelection;
|
||||
}
|
||||
|
||||
export interface CreativeDraftEditStreamRequest {
|
||||
clientMessageId: string;
|
||||
instruction: string;
|
||||
targetPuzzleSessionId: string;
|
||||
currentDraft: PuzzleResultDraft;
|
||||
}
|
||||
|
||||
export interface CreativeDraftEditResult {
|
||||
editInstructions: PuzzleDraftFieldPatch[];
|
||||
session: CreativeAgentSessionSnapshot;
|
||||
puzzleSession: PuzzleAgentSessionSnapshot;
|
||||
}
|
||||
|
||||
export interface CreativeAgentStageEvent {
|
||||
sessionId: string;
|
||||
stage: CreativeAgentStage;
|
||||
}
|
||||
|
||||
export interface CreativeAgentMessageDeltaEvent {
|
||||
sessionId: string;
|
||||
messageId: string;
|
||||
role: CreativeAgentMessageRole;
|
||||
kind: CreativeAgentMessageKind;
|
||||
textDelta: string;
|
||||
}
|
||||
|
||||
export interface CreativeAgentThoughtSummaryDeltaEvent {
|
||||
sessionId: string;
|
||||
thoughtId: string;
|
||||
textDelta: string;
|
||||
}
|
||||
|
||||
export interface CreativeAgentTemplateCatalogEvent {
|
||||
sessionId: string;
|
||||
templates: PuzzleCreativeTemplateProtocol[];
|
||||
}
|
||||
|
||||
export interface CreativeAgentTemplateSelectionEvent {
|
||||
sessionId: string;
|
||||
selection: PuzzleCreativeTemplateSelection;
|
||||
}
|
||||
|
||||
export interface CreativeAgentCostRangeEvent {
|
||||
sessionId: string;
|
||||
costRange: PuzzleTemplateCostRange;
|
||||
}
|
||||
|
||||
export interface CreativeAgentLevelPlanEvent {
|
||||
sessionId: string;
|
||||
plan: PuzzleImageGenerationPlan;
|
||||
}
|
||||
|
||||
export interface CreativeAgentToolEvent {
|
||||
sessionId: string;
|
||||
toolCallId: string;
|
||||
toolName: string;
|
||||
summary: string | null;
|
||||
}
|
||||
|
||||
export interface CreativeAgentReflectionEvent {
|
||||
sessionId: string;
|
||||
pass: boolean;
|
||||
summary: string;
|
||||
warnings: string[];
|
||||
}
|
||||
|
||||
export interface CreativeAgentTargetSessionEvent {
|
||||
sessionId: string;
|
||||
binding: CreativeTargetSessionBinding;
|
||||
}
|
||||
|
||||
export interface CreativeAgentErrorEvent {
|
||||
sessionId: string | null;
|
||||
code: string;
|
||||
message: string;
|
||||
recoverable: boolean;
|
||||
}
|
||||
|
||||
export interface CreativeAgentDoneEvent {
|
||||
sessionId: string;
|
||||
}
|
||||
|
||||
export type CreativeAgentSseEvent =
|
||||
| { event: 'stage'; data: CreativeAgentStageEvent }
|
||||
| { event: 'agent_message_delta'; data: CreativeAgentMessageDeltaEvent }
|
||||
| {
|
||||
event: 'thought_summary_delta';
|
||||
data: CreativeAgentThoughtSummaryDeltaEvent;
|
||||
}
|
||||
| {
|
||||
event: 'puzzle_template_catalog';
|
||||
data: CreativeAgentTemplateCatalogEvent;
|
||||
}
|
||||
| {
|
||||
event: 'puzzle_template_selection';
|
||||
data: CreativeAgentTemplateSelectionEvent;
|
||||
}
|
||||
| { event: 'puzzle_cost_range'; data: CreativeAgentCostRangeEvent }
|
||||
| { event: 'puzzle_level_plan'; data: CreativeAgentLevelPlanEvent }
|
||||
| { event: 'tool_started'; data: CreativeAgentToolEvent }
|
||||
| { event: 'tool_completed'; data: CreativeAgentToolEvent }
|
||||
| { event: 'reflection'; data: CreativeAgentReflectionEvent }
|
||||
| { event: 'target_session'; data: CreativeAgentTargetSessionEvent }
|
||||
| {
|
||||
event: 'session';
|
||||
data: { session: CreativeAgentSessionSnapshot };
|
||||
}
|
||||
| { event: 'error'; data: CreativeAgentErrorEvent }
|
||||
| { event: 'done'; data: CreativeAgentDoneEvent };
|
||||
@@ -1,12 +0,0 @@
|
||||
/**
|
||||
* 兼容出口:
|
||||
* 当前仓库仍有大量旧 customWorld 命名导入,这个文件继续作为过渡层保留。
|
||||
* 工作包 H 完成后,真实类型定义已经迁移到 rpg* 契约文件中;这里仅聚合旧命名分文件。
|
||||
*/
|
||||
|
||||
export type * from './customWorldAgentActions';
|
||||
export type * from './customWorldAgentAnchors';
|
||||
export type * from './customWorldAgentDraft';
|
||||
export type * from './customWorldAgentSession';
|
||||
export type * from './customWorldResultPreview';
|
||||
export type * from './customWorldWorkSummary';
|
||||
@@ -1,14 +0,0 @@
|
||||
/**
|
||||
* 旧 custom world 动作契约兼容出口。
|
||||
* 后续若逐步迁移旧代码,建议直接改用 rpgAgentActions.ts。
|
||||
*/
|
||||
|
||||
export type {
|
||||
RpgAgentActionRequest as CustomWorldAgentActionRequest,
|
||||
RpgAgentActionResponse as CustomWorldAgentActionResponse,
|
||||
RpgAgentOperationRecord as CustomWorldAgentOperationRecord,
|
||||
RpgAgentOperationStatus as CustomWorldAgentOperationStatus,
|
||||
RpgAgentOperationType as CustomWorldAgentOperationType,
|
||||
RpgAgentSuggestedAction as CustomWorldSuggestedAction,
|
||||
RpgAgentSupportedAction as CustomWorldSupportedAction,
|
||||
} from './rpgAgentActions';
|
||||
@@ -1,9 +0,0 @@
|
||||
/**
|
||||
* 旧 custom world 八锚点兼容出口。
|
||||
* 这里只保留旧命名到 RPG 创作域新契约的映射,便于旧导入渐进迁移。
|
||||
*/
|
||||
|
||||
export type {
|
||||
RpgCreationAnchorText as AnchorTextValue,
|
||||
RpgCreationAnchorContent as EightAnchorContent,
|
||||
} from './rpgAgentAnchors';
|
||||
@@ -1,29 +0,0 @@
|
||||
/**
|
||||
* 旧 custom world 草稿契约兼容出口。
|
||||
* 工作包 H 完成后,真实定义已经迁到 rpgAgentDraft.ts,这里只负责旧命名映射。
|
||||
*/
|
||||
|
||||
export type {
|
||||
RpgAgentAssetCoverageSummary as CustomWorldAssetCoverageSummary,
|
||||
RpgAgentAssetPriorityTier as CustomWorldAssetPriorityTier,
|
||||
RpgAgentDraftCardDetail as CustomWorldDraftCardDetail,
|
||||
RpgAgentDraftCardDetailSection as CustomWorldDraftCardDetailSection,
|
||||
RpgAgentDraftCardKind as CustomWorldDraftCardKind,
|
||||
RpgAgentDraftCardStatus as CustomWorldDraftCardStatus,
|
||||
RpgAgentDraftCardSummary as CustomWorldDraftCardSummary,
|
||||
RpgAgentFoundationDraftCamp as CustomWorldFoundationDraftCamp,
|
||||
RpgAgentFoundationDraftChapter as CustomWorldFoundationDraftChapter,
|
||||
RpgAgentFoundationDraftCharacter as CustomWorldFoundationDraftCharacter,
|
||||
RpgAgentFoundationDraftFaction as CustomWorldFoundationDraftFaction,
|
||||
RpgAgentFoundationDraftLandmark as CustomWorldFoundationDraftLandmark,
|
||||
RpgAgentFoundationDraftProfile as CustomWorldFoundationDraftProfile,
|
||||
RpgAgentFoundationDraftResult as CustomWorldFoundationDraftResult,
|
||||
RpgAgentFoundationDraftSceneAct as CustomWorldFoundationDraftSceneAct,
|
||||
RpgAgentFoundationDraftSceneChapter as CustomWorldFoundationDraftSceneChapter,
|
||||
RpgAgentFoundationDraftThread as CustomWorldFoundationDraftThread,
|
||||
RpgAgentRoleAssetStatus as CustomWorldRoleAssetStatus,
|
||||
RpgAgentRoleAssetSummary as CustomWorldRoleAssetSummary,
|
||||
RpgAgentSceneActAdvanceRule as CustomWorldSceneActAdvanceRule,
|
||||
RpgAgentSceneActStage as CustomWorldSceneActStage,
|
||||
RpgAgentSceneAssetSummary as CustomWorldSceneAssetSummary,
|
||||
} from './rpgAgentDraft';
|
||||
@@ -1,20 +0,0 @@
|
||||
/**
|
||||
* 旧 custom world 会话契约兼容出口。
|
||||
* 这一层只做命名映射,不再承担 session 真相源结构定义。
|
||||
*/
|
||||
|
||||
export type {
|
||||
CreateRpgAgentSessionRequest as CreateCustomWorldAgentSessionRequest,
|
||||
CreateRpgAgentSessionResponse as CreateCustomWorldAgentSessionResponse,
|
||||
RpgCreationIntentReadiness as CreatorIntentReadiness,
|
||||
RpgAgentMessage as CustomWorldAgentMessage,
|
||||
RpgAgentMessageKind as CustomWorldAgentMessageKind,
|
||||
RpgAgentMessageRole as CustomWorldAgentMessageRole,
|
||||
RpgAgentQualityFinding as CustomWorldAgentQualityFinding,
|
||||
RpgAgentSessionSnapshot as CustomWorldAgentSessionSnapshot,
|
||||
RpgAgentStage as CustomWorldAgentStage,
|
||||
RpgAgentPendingClarification as CustomWorldPendingClarification,
|
||||
GetRpgAgentCardDetailResponse as GetCustomWorldAgentCardDetailResponse,
|
||||
SendRpgAgentMessageRequest as SendCustomWorldAgentMessageRequest,
|
||||
SendRpgAgentMessageResponse as SendCustomWorldAgentMessageResponse,
|
||||
} from './rpgAgentSession';
|
||||
@@ -1,12 +0,0 @@
|
||||
/**
|
||||
* 旧 custom world 结果页预览兼容出口。
|
||||
* 额外单独拆一个 preview 兼容文件,避免预览别名继续堆回 customWorldAgent.ts 聚合层。
|
||||
*/
|
||||
|
||||
export type {
|
||||
RpgCreationPreview as CustomWorldResultPreview,
|
||||
RpgCreationPreviewBlocker as CustomWorldResultPreviewBlocker,
|
||||
RpgCreationPreviewEnvelope as CustomWorldResultPreviewEnvelope,
|
||||
RpgCreationPreviewFinding as CustomWorldResultPreviewFinding,
|
||||
RpgCreationPreviewSource as CustomWorldResultPreviewSource,
|
||||
} from './rpgCreationPreview';
|
||||
@@ -1,11 +0,0 @@
|
||||
/**
|
||||
* 旧 custom world works 读模型兼容出口。
|
||||
* 用于把旧作品列表命名平滑映射到新的 RPG 创作域 works 契约。
|
||||
*/
|
||||
|
||||
export type {
|
||||
RpgCreationWorkSource as CustomWorldWorkSource,
|
||||
RpgCreationWorkStatus as CustomWorldWorkStatus,
|
||||
RpgCreationWorkSummary as CustomWorldWorkSummary,
|
||||
ListRpgCreationWorksResponse as ListCustomWorldWorksResponse,
|
||||
} from './rpgCreationWorkSummary';
|
||||
@@ -1,82 +0,0 @@
|
||||
export const BABY_LOVE_DRAWING_TEMPLATE_ID = 'baby-love-drawing';
|
||||
export const BABY_LOVE_DRAWING_TEMPLATE_NAME = '宝贝爱画';
|
||||
export const BABY_LOVE_DRAWING_EDUTAINMENT_TAG = '寓教于乐';
|
||||
|
||||
export type BabyLoveDrawingTemplateId = typeof BABY_LOVE_DRAWING_TEMPLATE_ID;
|
||||
|
||||
export type BabyLoveDrawingTool = 'brush' | 'eraser';
|
||||
|
||||
export type BabyLoveDrawingSaveMode = 'original-only' | 'original-and-magic';
|
||||
|
||||
export type BabyLoveDrawingGenerationProvider =
|
||||
| 'vector-engine-gpt-image-2'
|
||||
| 'local-demo';
|
||||
|
||||
export type BabyLoveDrawingPoint = {
|
||||
x: number;
|
||||
y: number;
|
||||
t: number;
|
||||
};
|
||||
|
||||
export type BabyLoveDrawingStroke = {
|
||||
strokeId: string;
|
||||
tool: BabyLoveDrawingTool;
|
||||
color: string;
|
||||
points: BabyLoveDrawingPoint[];
|
||||
};
|
||||
|
||||
export type BabyLoveDrawingRecord = {
|
||||
drawingId: string;
|
||||
templateId: BabyLoveDrawingTemplateId;
|
||||
templateName: typeof BABY_LOVE_DRAWING_TEMPLATE_NAME;
|
||||
originalImageSrc: string;
|
||||
magicImageSrc: string | null;
|
||||
strokeTrace: BabyLoveDrawingStroke[];
|
||||
saveMode: BabyLoveDrawingSaveMode;
|
||||
themeTags: string[];
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
export type CreateBabyLoveDrawingMagicRequest = {
|
||||
originalImageSrc: string;
|
||||
strokeTrace: BabyLoveDrawingStroke[];
|
||||
};
|
||||
|
||||
export type CreateBabyLoveDrawingMagicResponse = {
|
||||
magicImageSrc: string;
|
||||
generationProvider: BabyLoveDrawingGenerationProvider;
|
||||
prompt: string;
|
||||
};
|
||||
|
||||
export type SaveBabyLoveDrawingRequest = {
|
||||
originalImageSrc: string;
|
||||
magicImageSrc?: string | null;
|
||||
strokeTrace: BabyLoveDrawingStroke[];
|
||||
};
|
||||
|
||||
export type SaveBabyLoveDrawingResponse = {
|
||||
record: BabyLoveDrawingRecord;
|
||||
};
|
||||
|
||||
export const BABY_LOVE_DRAWING_RAINBOW_COLORS = [
|
||||
{ id: 'red', label: '红', value: '#ef4444' },
|
||||
{ id: 'orange', label: '橙', value: '#f97316' },
|
||||
{ id: 'yellow', label: '黄', value: '#facc15' },
|
||||
{ id: 'green', label: '绿', value: '#22c55e' },
|
||||
{ id: 'cyan', label: '青', value: '#06b6d4' },
|
||||
{ id: 'blue', label: '蓝', value: '#3b82f6' },
|
||||
{ id: 'purple', label: '紫', value: '#a855f7' },
|
||||
] as const;
|
||||
|
||||
export type BabyLoveDrawingRainbowColorId =
|
||||
(typeof BABY_LOVE_DRAWING_RAINBOW_COLORS)[number]['id'];
|
||||
|
||||
export function normalizeBabyLoveDrawingTags(tags: string[]) {
|
||||
return [
|
||||
...new Set([
|
||||
BABY_LOVE_DRAWING_EDUTAINMENT_TAG,
|
||||
...tags.map((tag) => tag.trim()).filter(Boolean),
|
||||
]),
|
||||
];
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user