Merge branch 'master' into feat/project-name
Project CI / Repository checks (pull_request) Failing after 52s
Project CI / Frontend tests (pull_request) Successful in 3m3s
Project CI / Backend tests (pull_request) Successful in 8m26s
Project CI / Native shell tests (pull_request) Failing after 20m3s

This commit is contained in:
2026-09-03 14:43:20 +08:00
1461 changed files with 462 additions and 582086 deletions
@@ -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>旧公告 &lt;标题&gt;</h1><p>旧描述 &amp; 需要转义</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 });
}
+1 -6
View File
@@ -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>
);
}
+3 -3
View File
@@ -60,11 +60,11 @@ Linux Docker Engine 若要从宿主机 CLI 连到容器内服务,直接用 `ht
### Jenkins 预览 secrets 镜像边界
Jenkins 分支预览构建固定从宿主 `/data/jenkins/preview-secrets/.env.secrets.local` 读取 secrets。目录由 Jenkins 运行账号所有且权限为 `0700`,文件由同一账号所有且权限为 `0600`;构建入口对缺失、链接、非普通文件、owner 不匹配和过宽权限均失败关闭。不要把真实值写入本 README、仓库示例或 Jenkins 参数。
Jenkins 分支预览构建固定从宿主 `/data/jenkins/preview-secrets/.env.local``/data/jenkins/preview-secrets/.env.secrets.local` 读取运行时配置。目录由 Jenkins 运行账号所有且权限为 `0700`两个文件由同一账号所有且权限为 `0600`;构建入口对缺失、链接、非普通文件、owner 不匹配和过宽权限均失败关闭。两个文件都包含敏感配置,不要把真实值写入本 README、仓库示例或 Jenkins 参数。
文件不复制到源码 checkout 和 Docker build context,而是以 BuildKit `secret` mount 只提供给 `api-runtime` stage。构建会把它安装到 API 运行镜像的 `/srv/genarrative/.env.secrets.local`owner 为 `genarrative`、权限为 `0400`。Web builder、`nginx-runtime`、SpacetimeDB 和其它运行镜像不得获得 mount 或目标文件;构建日志和 artifact 也不得回显或保存文件内容。容器的显式运行环境变量优先于内置文件,可按预览实例覆盖其中的值。
两个文件不复制到源码 checkout 和 Docker build context,而是分别以 BuildKit `secret` mount 只提供给 `api-runtime` stage。构建会把它安装到 API 运行镜像的 `/srv/genarrative/.env.local``/srv/genarrative/.env.secrets.local`owner 为 `genarrative`、权限为 `0400`。Web builder、`nginx-runtime`、SpacetimeDB 和其它运行镜像不得获得这些 mount 或目标文件;构建日志和 artifact 也不得回显或保存文件内容。容器的显式运行环境变量优先于这两个内置文件,可按预览实例覆盖其中的值。
修改宿主固定文件后必须重新构建并替换 API 镜像;重启旧容器不会读取宿主新内容。这个镜像不是可公开分发的无密钥产物:镜像持有者可以提取 `/srv/genarrative/.env.secrets.local`。只允许在当前受信任内网 Docker 主机使用,禁止 push 或 `docker save`、artifact 导出到跨信任边界的 registry、主机或存储。
修改任一宿主固定文件后必须重新构建并替换 API 与 worker 镜像;重启旧容器不会读取宿主新内容。这个镜像不是可公开分发的无密钥产物:镜像持有者可以提取 `/srv/genarrative/.env.local` `/srv/genarrative/.env.secrets.local`。只允许在当前受信任内网 Docker 主机使用,禁止 push 或 `docker save`、artifact 导出到跨信任边界的 registry、主机或存储。
### Gitea CI 预构建 Job 镜像
+8
View File
@@ -24,12 +24,20 @@ RUN mkdir -p /var/lib/genarrative/auth /var/lib/genarrative/tracking-outbox /var
chown -R genarrative:genarrative /srv/genarrative /var/lib/genarrative
ARG GENARRATIVE_PREVIEW_SECRETS_SHA256=
ARG GENARRATIVE_PREVIEW_ENV_LOCAL_SHA256=
RUN --mount=type=secret,id=genarrative_preview_secrets,required=false \
--mount=type=secret,id=genarrative_preview_env_local,required=false \
if [ -n "${GENARRATIVE_PREVIEW_SECRETS_SHA256}" ]; then \
test -f /run/secrets/genarrative_preview_secrets; \
test "$(sha256sum /run/secrets/genarrative_preview_secrets | cut -d ' ' -f 1)" = "${GENARRATIVE_PREVIEW_SECRETS_SHA256}"; \
install -o genarrative -g genarrative -m 0400 \
/run/secrets/genarrative_preview_secrets /srv/genarrative/.env.secrets.local; \
fi; \
if [ -n "${GENARRATIVE_PREVIEW_ENV_LOCAL_SHA256}" ]; then \
test -f /run/secrets/genarrative_preview_env_local; \
test "$(sha256sum /run/secrets/genarrative_preview_env_local | cut -d ' ' -f 1)" = "${GENARRATIVE_PREVIEW_ENV_LOCAL_SHA256}"; \
install -o genarrative -g genarrative -m 0400 \
/run/secrets/genarrative_preview_env_local /srv/genarrative/.env.local; \
fi
USER genarrative
@@ -23,6 +23,16 @@
- 验证方式:运行评论弹层恢复竞态回归、完整 `appSurface.test.ts`,并执行类型、编码和 diff 检查。
- 关联文档:`docs/technical/【技术方案】立项策划AgentFast 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/**` 归档及 `packages/shared/src/contracts/**` 中已无仓库内消费者的旧玩法公共契约;`legacy_schema/**`、生成表 bindings、现役 shared contracts 和持久化 schema 不在本次删除范围。
## 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 伪装成资产成功响应。
@@ -4926,8 +4926,8 @@
- 现象:构建时使用 BuildKit secret mount,日志和普通 build context 都没有出现明文,于是误以为最终镜像也能不可提取地保存 secrets,随后将镜像 push 或导出给不同信任域。
- 原因:BuildKit secret mount 只避免秘密作为 `ARG` / `COPY` 进入构建上下文和中间指令;一旦 Dockerfile 把 mount 的内容安装到最终 rootfs,任何能读取、保存或运行该镜像的主体都可以提取它。
- 处理:预览固定 secrets 只从 Jenkins 宿主受控路径读取,严格校验目录 `0700`、文件 `0600`、owner、普通文件与非链接边界;只将安装到 `api-runtime:/srv/genarrative/.env.secrets.local` 并设为 `0400`,明确排除 Nginx、Web、artifact 和其它镜像。镜像禁止推送或导出到跨信任边界。
- 更新与验证:源文件变更不会改动已存镜像,必须重建并替换容器;不能用重启代替。验收同时扫描 transcript/context/artifact 零泄漏,检查只有 API 最终 rootfs 存在目标文件,并验证容器显式运行 env 优先覆盖内置值。
- 处理:预览固定 `.env.local` secrets 只从 Jenkins 宿主受控路径读取,严格校验目录 `0700`、文件 `0600`、owner、普通文件与非链接边界;只将它们安装到 `api-runtime:/srv/genarrative/.env.local``/srv/genarrative/.env.secrets.local` 并设为 `0400`,明确排除 Nginx、Web、artifact 和其它镜像。镜像禁止推送或导出到跨信任边界。
- 更新与验证:任一源文件变更不会改动已存镜像,必须重建并替换 API 与 worker 容器;不能用重启代替。验收同时扫描 transcript/context/artifact 零泄漏,检查只有 API 与 worker 最终 rootfs 存在目标文件,并验证容器显式运行 env 优先覆盖内置值。
## SpacetimeDB ping 健康不代表完整模块能在内存上限内实例化(2026-08-22)
@@ -51,9 +51,9 @@ SpacetimeDB 2.7 CLI 发布到受控 Compose 网络地址时固定使用 `--yes=r
## 预览 secrets 内置
Jenkins 节点上的预览 secrets 只允许来自受控的 Jenkins 凭据目录(目录与文件权限、owner、普通文件和非链接约束由流水线检查)。该文件不进 Git、Docker build context、构建日志或 artifact;构建时只通过 BuildKit `secret` mount 临时提供给 `api-runtime` stage,运行镜像权限固定为 `0400``nginx-runtime`、Web 静态产物、SpacetimeDB 镜像及其它镜像不得包含该文件
Jenkins 节点上的预览 `.env.local` secrets 只允许来自受控的 Jenkins 凭据目录(目录与文件权限、owner、普通文件和非链接约束由流水线检查)。固定宿主副本不进 Git、Docker build context、构建日志或 artifact;构建时分别通过两个 BuildKit `secret` mount 临时提供给 `api-runtime` stage并安装为 `/srv/genarrative/.env.local``/srv/genarrative/.env.secrets.local`运行镜像权限固定为 `0400``nginx-runtime`、Web 静态产物、SpacetimeDB 镜像及其它镜像不得包含这些文件;仓库工作区 `.env.local` 不得替代固定宿主副本
宿主固定目录应由 Jenkins 运行账号所有且权限为 `0700`,源文件权限为 `0600`;缺失、不是普通文件、owner 不匹配或权限过宽时,预览构建必须失败关闭。源文件变更后必须重新构建并替换预览镜像,只重启容器不会刷新已内置的内容。容器启动时显式注入的运行环境变量优先级高于镜像内的 `.env.secrets.local`,用于按实例覆盖非通用值。
宿主固定目录应由 Jenkins 运行账号所有且权限为 `0700`两个源文件权限`0600`;缺失、不是普通文件、owner 不匹配或权限过宽时,预览构建必须失败关闭。任一源文件变更后必须重新构建并替换 API 与 worker 预览镜像,只重启容器不会刷新已内置的内容。容器启动时显式注入的运行环境变量优先级高于镜像内的 `.env.local` `.env.secrets.local`,用于按实例覆盖非通用值。
这种方案只隐藏构建传输过程,不能让内置后的 secrets 对镜像持有者保密:能读取、保存或运行 `api-runtime` 镜像的人可以提取该文件。因此该镜像只能留在当前受信任内网 Docker 主机,禁止 push 到公共或跨信任边界的 registry,也禁止通过 `docker save`/构建 artifact 导出传播。需要跨边界分发时必须改用不含 secrets 的镜像与运行时密钥注入。
@@ -118,7 +118,7 @@ Jenkins 在构建完成、归档 artifact 和更新 REST 状态之间可能短
- Jenkins service account 只授予 `shared/Genarrative-Preview-Deployer``Job/Read``Job/Build` 和读取构建产物所需权限,不授 `Overall/Administer``Job/Configure``Job/Delete`
- 后端固定 Jenkins origin、Job 路径和参数白名单;客户端不能传 URL、Job 名、Compose project、容器名、宿主端口或 Jenkins 凭据。
- Git 查询固定使用本机 Gitea SSH 地址和服务端只读凭据;客户端不能传 remote、SSH 参数或凭据。Git 缓存只写入预览控制服务的受控状态目录,搜索接口需要控制台会话且结果有数量上限。
- 预览 secrets 只从固定宿主路径读取,构建前校验 owner、类型和权限;不允许分支、Jenkins 参数或控制面请求改写 secrets 路径、BuildKit secret ID 或镜像内目标路径。
- 预览 `.env.local` secrets 只从固定宿主路径读取,构建前校验目录和文件的 owner、类型和权限;不允许分支、Jenkins 参数或控制面请求改写这些路径、BuildKit secret ID 或镜像内目标路径。
- Jenkins POST 支持动态 CrumbAPI Token 即使免 Crumb,也不能把 Token 放进 URL 或日志。
- API 默认只接受同源请求,写请求校验 Origin;内网本身不作为认证。
- 同一 deployment 的发布和卸载串行执行;重复请求必须幂等或明确返回冲突。
@@ -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
View File
@@ -15,6 +15,7 @@ pipeline {
GIT_REMOTE_CREDENTIAL_ID = 'genarrative-local-gitea-ssh'
GENARRATIVE_PREVIEW_STATE_ROOT = '/data/jenkins/preview-deployments'
GENARRATIVE_PREVIEW_SECRETS_FILE = '/data/jenkins/preview-secrets/.env.secrets.local'
GENARRATIVE_PREVIEW_ENV_LOCAL_FILE = '/data/jenkins/preview-secrets/.env.local'
GENARRATIVE_PREVIEW_WEB_HOST = '192.168.35.82'
}
@@ -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);
});
});
-312
View File
@@ -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;
}
-199
View File
@@ -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';

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