Merge remote-tracking branch 'origin/master' into hermes/hermes-19e77eb0

This commit is contained in:
2026-05-08 12:15:51 +08:00
78 changed files with 4073 additions and 420 deletions
+16
View File
@@ -16,6 +16,14 @@
---
## 2026-05-07 server-rs Cargo 依赖集中到 workspace
- 背景:`server-rs` 多 crate 已稳定成 DDD workspace,成员 `Cargo.toml` 中重复散写第三方版本和本地 path 依赖,升级 SpacetimeDB SDK、`serde``reqwest``tokio` 等依赖时容易漂移。
- 决策:`server-rs/Cargo.toml``[workspace.dependencies]` 统一维护第三方依赖版本和 workspace 内部 crate path;成员 crate 默认使用 `{ workspace = true }`,只保留自身 feature、optional 或 target-specific 差异;不再新增 `sha1`OSS 与阿里云 OpenAPI 签名统一走 `sha2::Sha256` 对应的 V4/V3 口径。
- 影响范围:`server-rs/Cargo.toml`、所有 `server-rs/crates/*/Cargo.toml``platform-oss``platform-auth`、后续新增 Rust crate 或新增 Rust 依赖的开发流程。
- 验证方式:修改 Cargo 配置后先执行 `cargo metadata --manifest-path server-rs\Cargo.toml --format-version 1 --no-deps`,再按影响范围执行 `cargo check`、DDD 边界检查和编码检查。
- 关联文档:`docs/technical/RUST_WORKSPACE_DEPENDENCY_CONSOLIDATION_2026-05-07.md`
## 2026-05-06 Maincloud 历史残留引用禁止再使用
- 背景:项目已经全面移除 Maincloud 运行口径,但历史脚本、测试名和文档仍可能让后续开发误用 `api-server:maincloud``GENARRATIVE_SPACETIME_MAINCLOUD_*`
@@ -24,6 +32,14 @@
- 验证方式:新增或修改后端相关文档时,检查不得要求 `api-server:maincloud``GENARRATIVE_SPACETIME_MAINCLOUD_*`;触碰历史残留时同步删除或改名。
- 关联文档:`docs/technical/MAINCLOUD_REFERENCE_REMOVAL_POLICY_2026-05-06.md``docs/technical/SPACETIMEDB_CLOUD_CONFIG_REMOVAL_2026-05-02.md`
## 2026-05-05 新手引导首版复用拼图本地运行时
- 背景:首次打开产品的新用户需要先体验输入想法、生成拼图、通关、登录保存、回到首页的闭环,但首版不应引入新的持久化表或独立玩法运行时。
- 决策:未登录首次访问由前端 localStorage 标记触发;生成入口走公开 BFF `POST /api/runtime/puzzle/onboarding/generate` 生成 1 关临时拼图;登录后保存走鉴权 BFF `POST /api/runtime/puzzle/onboarding/save`,由服务端创建当前用户拼图 agent session 并更新其草稿作品 profile;游玩阶段复用现有本地拼图运行时。
- 影响范围:平台入口首屏、新手引导 PRD、拼图 BFF、拼图作品契约与前端 puzzle runtime。
- 验证方式:未登录首次访问应展示新手引导;生成后只进入 1 关本地拼图;通关后登录保存应在当前用户拼图作品架出现草稿作品;不应产生 SpacetimeDB schema 变更。
- 关联文档:`docs/prd/FIRST_LAUNCH_PUZZLE_ONBOARDING_PRD_2026-05-05.md`
## 2026-05-04 在仓库 `.hermes/` 中建立团队共享记忆
- 背景:团队有 3 名开发人员,均在各自本地安装 Hermes,并需要独立拉取仓库、修改代码、本地测试;团队希望形成共享的长期项目记忆。
+8
View File
@@ -69,6 +69,14 @@
- 验证:请求返回 JSON,相关页面不再出现 HTML parse 错误。
- 关联:`docs/technical/PROFILE_MAIN_ROUTE_VITE_PROXY_FIX_2026-05-02.md`
## 拼图 APIMart 图片生成密钥不能复用 DashScope / ARK key
- 现象:拼图新手引导或拼图创作点击生成后返回 `APIMart 图片生成密钥未配置`
- 原因:拼图 `gpt-image-2` / `nanobanana2` 图片生成已按技术方案统一走 APIMart;后端只读取 `APIMART_BASE_URL``APIMART_API_KEY``APIMART_IMAGE_REQUEST_TIMEOUT_MS`,不会用 `DASHSCOPE_API_KEY``LLM_API_KEY``ARK_API_KEY` 兜底。
- 处理:在本机私密配置 `.env.secrets.local` 或进程环境中配置真实 `APIMART_API_KEY`,不要提交到 Git;填入后必须重启 `api-server` / `npm run dev`,运行中的进程不会自动加载新 env。
- 验证:不打印密钥内容,只检查 `APIMART_API_KEY` 非空;重启后触发拼图生成不再返回本地配置缺失的 503。
- 关联:`docs/technical/PUZZLE_APIMART_IMAGE_MODEL_ROUTING_2026-05-01.md``.codex/skills/gpt-image-2-apimart/SKILL.md`
## Rust 冷编译导致 api-server 健康检查误超时
- 现象:`npm run dev:rust` 在 Windows 冷编译/链接阶段误判 `/healthz` 等待超时并杀掉 `cargo run`
@@ -81,6 +81,10 @@ DDD 分层边界以 `docs/technical/SERVER_RS_DDD_FULL_REFACTOR_2026-04-28.md`
注意:`server-rs` 的默认 `cargo build` 只构建 `crates/api-server`SpacetimeDB 模块产物继续走 `spacetime build` / 发布链路。
Cargo 依赖口径:第三方依赖版本和 workspace 内部 crate path 统一维护在 `server-rs/Cargo.toml``[workspace.dependencies]`,成员 crate 默认继承 workspace 依赖,只保留自身 `features``optional` 或 target-specific 差异。
Rust 加密摘要依赖口径:新代码不再引入 `sha1`OSS V4 签名、阿里云 OpenAPI V3 签名和 refresh session token 摘要统一使用 `sha2::Sha256`
## SpacetimeDB 表域总览
`docs/technical/SPACETIMEDB_TABLE_CATALOG.md` 为持续维护入口。当前表域包括:
@@ -135,4 +139,5 @@ DDD 分层边界以 `docs/technical/SERVER_RS_DDD_FULL_REFACTOR_2026-04-28.md`
- 契约与路由矩阵:`docs/technical/SERVER_RS_DDD_G1_CONTRACT_AND_ROUTE_MATRIX_2026-04-29.md`
- SpacetimeDB 表结构变更约束:`docs/technical/SPACETIMEDB_SCHEMA_CHANGE_CONSTRAINTS.md`
- SpacetimeDB 表目录:`docs/technical/SPACETIMEDB_TABLE_CATALOG.md`
- Rust workspace 依赖集中配置:`docs/technical/RUST_WORKSPACE_DEPENDENCY_CONSOLIDATION_2026-05-07.md`
- 生产部署计划:`docs/technical/PRODUCTION_DEPLOYMENT_PLAN_2026-05-02.md`
+36
View File
@@ -6,6 +6,8 @@ import type {
AdminLoginResponse,
AdminMeResponse,
AdminOverviewResponse,
AdminTrackingEventListQuery,
AdminTrackingEventListResponse,
AdminUpsertProfileInviteCodeRequest,
AdminUpsertProfileRedeemCodeRequest,
AdminUpsertProfileTaskConfigRequest,
@@ -135,6 +137,16 @@ export function debugAdminHttp(token: string, payload: AdminDebugHttpRequest) {
});
}
export function listAdminTrackingEvents(
token: string,
query: AdminTrackingEventListQuery = {},
) {
return request<AdminTrackingEventListResponse>(
`/admin/api/tracking/events${buildQueryString(query)}`,
{token},
);
}
export function listProfileRedeemCodes(token: string) {
return request<ProfileRedeemCodeAdminListResponse>(
'/admin/api/profile/redeem-codes',
@@ -232,6 +244,30 @@ function buildRequestUrl(path: string) {
return `${ADMIN_API_BASE_URL}${normalizedPath}`;
}
function buildQueryString(query: AdminTrackingEventListQuery) {
const params = new URLSearchParams();
appendQueryParam(params, 'eventKey', query.eventKey);
appendQueryParam(params, 'userId', query.userId);
appendQueryParam(params, 'scopeKind', query.scopeKind);
appendQueryParam(params, 'scopeId', query.scopeId);
if (typeof query.limit === 'number' && Number.isFinite(query.limit)) {
params.set('limit', String(query.limit));
}
const queryString = params.toString();
return queryString ? `?${queryString}` : '';
}
function appendQueryParam(
params: URLSearchParams,
key: string,
value: string | null | undefined,
) {
const trimmed = value?.trim();
if (trimmed) {
params.set(key, trimmed);
}
}
function parseJsonResponse(responseText: string): unknown {
if (!responseText.trim()) {
return null;
+27
View File
@@ -109,6 +109,14 @@ export type ProfileRedeemCodeMode = 'public' | 'unique' | 'private';
export type ProfileTaskCycle = 'daily';
export type TrackingScopeKind = 'site' | 'work' | 'module' | 'user';
export interface AdminTrackingEventListQuery {
eventKey?: string;
userId?: string;
scopeKind?: TrackingScopeKind | '';
scopeId?: string;
limit?: number;
}
export interface AdminUpsertProfileRedeemCodeRequest {
code: string;
mode: ProfileRedeemCodeMode;
@@ -199,3 +207,22 @@ export interface ProfileTaskConfigAdminResponse {
export interface ProfileTaskConfigAdminListResponse {
entries: ProfileTaskConfigAdminResponse[];
}
export interface AdminTrackingEventEntryPayload {
eventId: string;
eventKey: string;
eventTitle: string;
scopeKind: TrackingScopeKind | string;
scopeId: string;
dayKey: number;
userId?: string | null;
ownerUserId?: string | null;
profileId?: string | null;
moduleKey?: string | null;
metadataJson: string;
occurredAt: string;
}
export interface AdminTrackingEventListResponse {
entries: AdminTrackingEventEntryPayload[];
}
+7
View File
@@ -23,6 +23,7 @@ import {AdminLoginPage} from '../pages/AdminLoginPage';
import {AdminOverviewPage} from '../pages/AdminOverviewPage';
import {AdminRedeemCodePage} from '../pages/AdminRedeemCodePage';
import {AdminTaskConfigPage} from '../pages/AdminTaskConfigPage';
import {AdminTrackingEventsPage} from '../pages/AdminTrackingEventsPage';
import {AdminShell} from './AdminShell';
import type {AdminRouteId} from './adminRoutes';
import {resolveAdminRoute, routeHash} from './adminRoutes';
@@ -162,6 +163,12 @@ export function AdminApp() {
{routeId === 'debug' ? (
<AdminDebugHttpPage token={token} onUnauthorized={handleUnauthorized} />
) : null}
{routeId === 'tracking' ? (
<AdminTrackingEventsPage
token={token}
onUnauthorized={handleUnauthorized}
/>
) : null}
{routeId === 'redeem' ? (
<AdminRedeemCodePage
result={redeemResult}
+2
View File
@@ -4,6 +4,7 @@ import {
LogOut,
ShieldCheck,
ListChecks,
Table2,
TicketCheck,
TicketPercent,
} from 'lucide-react';
@@ -24,6 +25,7 @@ interface AdminShellProps {
const routeIcons = {
overview: LayoutDashboard,
debug: Bug,
tracking: Table2,
redeem: TicketPercent,
invite: TicketCheck,
tasks: ListChecks,
+2 -1
View File
@@ -1,4 +1,4 @@
export type AdminRouteId = 'overview' | 'debug' | 'redeem' | 'invite' | 'tasks';
export type AdminRouteId = 'overview' | 'debug' | 'tracking' | 'redeem' | 'invite' | 'tasks';
export interface AdminRouteDefinition {
id: AdminRouteId;
@@ -9,6 +9,7 @@ export interface AdminRouteDefinition {
export const adminRoutes: AdminRouteDefinition[] = [
{id: 'overview', label: '总览', hash: '#overview'},
{id: 'debug', label: 'API 调试', hash: '#debug'},
{id: 'tracking', label: '埋点数据', hash: '#tracking'},
{id: 'redeem', label: '兑换码', hash: '#redeem'},
{id: 'invite', label: '邀请码', hash: '#invite'},
{id: 'tasks', label: '任务配置', hash: '#tasks'},
@@ -0,0 +1,438 @@
import {Download, Eye, RefreshCcw, Search, X} from 'lucide-react';
import {FormEvent, useEffect, useMemo, useState} from 'react';
import {listAdminTrackingEvents} from '../api/adminApiClient';
import type {
AdminTrackingEventEntryPayload,
TrackingScopeKind,
} from '../api/adminApiTypes';
import {
filterAdminTrackingEventDefinitions,
findAdminTrackingEventDefinition,
} from '../config/trackingEventDefinitions';
import {handlePageError} from './pageUtils';
interface AdminTrackingEventsPageProps {
token: string;
onUnauthorized: (message?: string) => void;
}
const scopeKindOptions: Array<{value: TrackingScopeKind | ''; label: string}> = [
{value: '', label: '全部'},
{value: 'site', label: 'site'},
{value: 'work', label: 'work'},
{value: 'module', label: 'module'},
{value: 'user', label: 'user'},
];
const exportColumns: Array<{
key: keyof AdminTrackingEventEntryPayload;
label: string;
}> = [
{key: 'eventId', label: '事件 ID'},
{key: 'eventKey', label: 'Event Key'},
{key: 'eventTitle', label: '事件名称'},
{key: 'scopeKind', label: 'Scope Kind'},
{key: 'scopeId', label: 'Scope ID'},
{key: 'dayKey', label: 'Day Key'},
{key: 'userId', label: 'User ID'},
{key: 'ownerUserId', label: 'Owner User ID'},
{key: 'profileId', label: 'Profile ID'},
{key: 'moduleKey', label: 'Module Key'},
{key: 'metadataJson', label: 'Metadata JSON'},
{key: 'occurredAt', label: '发生时间'},
];
export function AdminTrackingEventsPage({
token,
onUnauthorized,
}: AdminTrackingEventsPageProps) {
const [entries, setEntries] = useState<AdminTrackingEventEntryPayload[]>([]);
const [eventKey, setEventKey] = useState('');
const [userId, setUserId] = useState('');
const [scopeKind, setScopeKind] = useState<TrackingScopeKind | ''>('');
const [scopeId, setScopeId] = useState('');
const [limit, setLimit] = useState('200');
const [errorMessage, setErrorMessage] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [detailEntry, setDetailEntry] =
useState<AdminTrackingEventEntryPayload | null>(null);
useEffect(() => {
void refreshTrackingEvents();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [token]);
const filteredEventDefinitions = useMemo(
() => filterAdminTrackingEventDefinitions(eventKey),
[eventKey],
);
async function refreshTrackingEvents() {
setIsLoading(true);
setErrorMessage('');
try {
const response = await listAdminTrackingEvents(token, {
eventKey,
userId,
scopeKind,
scopeId,
limit: parseLimit(limit),
});
setEntries(response.entries);
} catch (error: unknown) {
handlePageError(error, onUnauthorized, setErrorMessage);
} finally {
setIsLoading(false);
}
}
function handleSearch(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
void refreshTrackingEvents();
}
function handleExport() {
if (!entries.length) {
setErrorMessage('当前没有可导出的埋点数据');
return;
}
exportTrackingEventsAsExcel(entries);
}
return (
<section className="admin-page admin-page-wide">
<div className="admin-page-heading">
<div>
<h2></h2>
<p></p>
</div>
<div className="admin-action-row">
<button
className="admin-secondary-button"
disabled={isLoading}
type="button"
onClick={refreshTrackingEvents}
>
<RefreshCcw size={17} aria-hidden="true" />
<span>{isLoading ? '刷新中' : '刷新'}</span>
</button>
<button
className="admin-primary-button"
disabled={!entries.length}
type="button"
onClick={handleExport}
>
<Download size={17} aria-hidden="true" />
<span> Excel</span>
</button>
</div>
</div>
<form className="admin-panel admin-form" onSubmit={handleSearch}>
<div className="admin-filter-grid">
<label className="admin-field">
<span>Event Key</span>
<input
list="admin-tracking-event-keys"
placeholder="全部"
value={eventKey}
onChange={(event) => setEventKey(event.target.value)}
/>
<datalist id="admin-tracking-event-keys">
{filteredEventDefinitions.map((definition) => (
<option key={definition.key} value={definition.key}>
{definition.title}
</option>
))}
</datalist>
</label>
<label className="admin-field">
<span>User ID</span>
<input
placeholder="全部"
value={userId}
onChange={(event) => setUserId(event.target.value)}
/>
</label>
<label className="admin-field">
<span>Scope Kind</span>
<select
value={scopeKind}
onChange={(event) =>
setScopeKind(event.target.value as TrackingScopeKind | '')
}
>
{scopeKindOptions.map((option) => (
<option key={option.value || 'all'} value={option.value}>
{option.label}
</option>
))}
</select>
</label>
<label className="admin-field">
<span>Scope ID</span>
<input
placeholder="全部"
value={scopeId}
onChange={(event) => setScopeId(event.target.value)}
/>
</label>
<label className="admin-field">
<span></span>
<input
inputMode="numeric"
value={limit}
onChange={(event) => setLimit(event.target.value)}
/>
</label>
<button className="admin-secondary-button" disabled={isLoading} type="submit">
<Search size={17} aria-hidden="true" />
<span>{isLoading ? '查询中' : '查询'}</span>
</button>
</div>
</form>
{errorMessage ? (
<div className="admin-alert" role="status">
{errorMessage}
</div>
) : null}
<div className="admin-panel">
<div className="admin-panel-heading">
<h3></h3>
<span>{entries.length} </span>
</div>
<div className="admin-table-wrap">
<table className="admin-table admin-table-wide">
<thead>
<tr>
<th></th>
<th>Scope</th>
<th></th>
<th></th>
<th>Metadata</th>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
{entries.length ? (
entries.map((entry) => (
<tr key={entry.eventId}>
<td>
<strong>{resolveEventTitle(entry)}</strong>
<small>{entry.eventKey}</small>
<small>{entry.eventId}</small>
</td>
<td>
<span className="admin-status admin-status-pending">
{entry.scopeKind}
</span>
<small>{entry.scopeId || '-'}</small>
<small>dayKey: {entry.dayKey}</small>
</td>
<td>
{entry.userId || '-'}
<small>owner: {entry.ownerUserId || '-'}</small>
</td>
<td>
{entry.profileId || '-'}
<small>module: {entry.moduleKey || '-'}</small>
</td>
<td>
<pre className="admin-json-preview">
{formatMetadataJson(entry.metadataJson)}
</pre>
</td>
<td>{formatOccurredAt(entry.occurredAt)}</td>
<td>
<button
className="admin-secondary-button"
type="button"
onClick={() => setDetailEntry(entry)}
>
<Eye size={16} aria-hidden="true" />
<span></span>
</button>
</td>
</tr>
))
) : (
<tr>
<td colSpan={7}>{isLoading ? '正在加载' : '暂无数据'}</td>
</tr>
)}
</tbody>
</table>
</div>
</div>
{detailEntry ? (
<TrackingEventDetailPanel
entry={detailEntry}
onClose={() => setDetailEntry(null)}
/>
) : null}
</section>
);
}
function TrackingEventDetailPanel({
entry,
onClose,
}: {
entry: AdminTrackingEventEntryPayload;
onClose: () => void;
}) {
return (
<div className="admin-confirm-backdrop" role="presentation">
<section
aria-label="埋点详情"
className="admin-detail-panel"
role="dialog"
>
<div className="admin-panel-heading">
<h3>{resolveEventTitle(entry)}</h3>
<button
aria-label="关闭详情"
className="admin-ghost-button"
type="button"
onClick={onClose}
>
<X size={17} aria-hidden="true" />
</button>
</div>
<dl className="admin-info-list admin-detail-list">
{exportColumns.map((column) => (
<div key={column.key}>
<dt>{column.label}</dt>
<dd>
{column.key === 'metadataJson' ? (
<pre className="admin-code-block">
{formatMetadataJson(entry.metadataJson)}
</pre>
) : (
formatExportCell(entry[column.key], column.key) || '-'
)}
</dd>
</div>
))}
</dl>
</section>
</div>
);
}
function parseLimit(value: string) {
const parsed = Number.parseInt(value.trim(), 10);
if (!Number.isFinite(parsed)) {
return 200;
}
return Math.min(Math.max(parsed, 1), 1000);
}
function resolveEventTitle(entry: AdminTrackingEventEntryPayload) {
return (
findAdminTrackingEventDefinition(entry.eventKey)?.title ||
entry.eventTitle ||
entry.eventKey
);
}
function formatMetadataJson(value: string) {
const trimmed = value.trim();
if (!trimmed) {
return '-';
}
try {
return JSON.stringify(JSON.parse(trimmed), null, 2);
} catch {
return trimmed;
}
}
function exportTrackingEventsAsExcel(entries: AdminTrackingEventEntryPayload[]) {
const tableRows = [
exportColumns.map((column) => `<th>${escapeHtml(column.label)}</th>`).join(''),
...entries.map((entry) =>
exportColumns
.map(
(column) =>
`<td style="mso-number-format:'\\@';">${escapeHtml(formatExportCell(entry[column.key], column.key))}</td>`,
)
.join(''),
),
];
const html = `\uFEFF<html><head><meta charset="UTF-8" /></head><body><table>${tableRows
.map((row) => `<tr>${row}</tr>`)
.join('')}</table></body></html>`;
const blob = new Blob([html], {type: 'application/vnd.ms-excel;charset=utf-8'});
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = `tracking-events-${buildTimestamp()}.xls`;
document.body.appendChild(link);
link.click();
link.remove();
URL.revokeObjectURL(url);
}
function formatExportCell(value: unknown, key?: keyof AdminTrackingEventEntryPayload) {
if (value === null || typeof value === 'undefined') {
return '';
}
if (key === 'occurredAt') {
return formatOccurredAt(String(value));
}
return String(value);
}
function formatOccurredAt(value: string) {
const trimmed = value.trim();
if (!trimmed) {
return '-';
}
if (/^\d+$/.test(trimmed)) {
const micros = Number(trimmed);
if (Number.isSafeInteger(micros)) {
return formatDateTime(new Date(Math.floor(micros / 1000)));
}
}
const parsed = new Date(trimmed);
if (!Number.isNaN(parsed.getTime())) {
return formatDateTime(parsed);
}
return trimmed;
}
function formatDateTime(date: Date) {
const pad = (value: number, size = 2) => String(value).padStart(size, '0');
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(
date.getHours(),
)}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
}
function escapeHtml(value: string) {
return value
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
function buildTimestamp() {
const now = new Date();
const pad = (value: number) => String(value).padStart(2, '0');
return [
now.getFullYear(),
pad(now.getMonth() + 1),
pad(now.getDate()),
'-',
pad(now.getHours()),
pad(now.getMinutes()),
pad(now.getSeconds()),
].join('');
}
+55 -1
View File
@@ -216,6 +216,10 @@ button:disabled {
margin: 0 auto;
}
.admin-page-wide {
max-width: 1480px;
}
.admin-page-heading,
.admin-panel-heading,
.admin-subsection-heading {
@@ -291,6 +295,20 @@ button:disabled {
align-items: end;
}
.admin-filter-grid {
display: grid;
grid-template-columns: repeat(5, minmax(120px, 1fr)) auto;
gap: 12px;
align-items: end;
}
.admin-action-row {
display: flex;
flex-wrap: wrap;
justify-content: flex-end;
gap: 10px;
}
.admin-field {
display: grid;
min-width: 0;
@@ -478,6 +496,23 @@ button:disabled {
padding: 18px;
}
.admin-detail-panel {
display: grid;
width: min(100%, 760px);
max-height: min(90dvh, 760px);
gap: 16px;
overflow: auto;
border: 1px solid #d8e2e8;
border-radius: 10px;
background: #ffffff;
box-shadow: 0 22px 60px rgba(23, 33, 43, 0.24);
padding: 18px;
}
.admin-detail-list .admin-code-block {
max-height: 280px;
}
.admin-confirm-warning {
border: 1px solid #efc894;
border-radius: 8px;
@@ -602,6 +637,24 @@ button:disabled {
min-width: 360px;
}
.admin-table-wide {
min-width: 1180px;
}
.admin-json-preview {
max-width: 360px;
max-height: 160px;
margin: 0;
overflow: auto;
color: #2f4550;
font-family:
"SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
font-size: 12px;
line-height: 1.45;
white-space: pre-wrap;
overflow-wrap: anywhere;
}
.admin-status {
display: inline-flex;
max-width: 460px;
@@ -757,7 +810,8 @@ button:disabled {
.admin-overview-grid,
.admin-two-column,
.admin-two-column-wide,
.admin-form-row {
.admin-form-row,
.admin-filter-grid {
grid-template-columns: 1fr;
}
@@ -0,0 +1,110 @@
# 百梦线下展会易拉宝设计记录 2026-05-07
## 1. 目标
为百梦线下展会制作一张纵向易拉宝广告展板,用于在展位现场快速传达:
1. 产品名称:百梦。
2. 产品愿景:百梦AI团队致力于打造AI互动内容UGC平台。
3. 产品slogan:每个人都可以在10分钟内轻松创作出一款精品互动作品。
4. 产品特点:低门槛创作、高完成度作品、玩过后可改造并发布。
5. 关键技术:Harness Engineering、多Agent调度、AI创作工具、AI原生游戏框架。
6. 产品心智:想玩但找不到、玩到不满意、平台外体验不满意时,都可以来百梦做成自己满意的。
## 2. 视觉方向
本次延续 `BAIMENG_LOGO_GPT_IMAGE_2_CONCEPTS_2026-05-05.md` 中最新收敛的气泡共创方向:
- 参考 logo`output/imagegen/baimeng-logo-bubble-04-07-refine-batch13/baimeng-bubble-04-07-refine-01-04-flat-rainbow-band-core.png`
- 主视觉语义:轻盈气泡、很多创意、UGC共创、作品改造与分享。
- 色彩:暖白、珊瑚粉、薰衣草紫、天蓝、薄荷青、少量金色光感。
- 展会阅读策略:远看先读产品名与slogan,近看再读产品心智与关键技术。
## 3. 文案层级
最终展板文案压缩为四层。
第一层:品牌识别
```text
百梦
AI互动内容UGC平台
把想玩的世界,亲手做出来
```
第二层:远读slogan
```text
每个人都可以在10分钟内
轻松创作出一款
精品互动作品
```
第三层:产品特点
```text
10分钟成品级创作
玩过就能改造发布
创意到作品闭环
```
并拆成三张近读卡:
```text
创作:从一句灵感开始,AI帮助完成剧本、角色、场景、系统与视觉草稿。
游玩:作品不是静态文本,而是可进入、可推进、可演出的互动体验。
改造:玩到不满意的作品,可以快速改成自己喜欢的版本并再次发布。
```
第四层:产品心智与关键技术
```text
当用户找不到想玩的游戏 -> 来百梦做给自己玩
当用户玩到了不好玩的游戏 -> 快速改成自己喜欢玩的
当用户在平台外玩到了不满意的游戏 -> 来百梦做成自己满意的
什么游戏最好玩? -> 来百梦玩自己做的游戏最好玩
```
关键技术压缩为:
```text
基于 Harness Engineering 理论,将专家知识融入 AI 创作工具与 AI 原生游戏框架。
AI创作工具:
通过多Agent调度算法,把策划、美术SOP与专家知识融入模板框架,提升剧本类、数值类、系统类、角色设计、场景设计、CG设计等垂类任务的完成率和效率。
AI原生游戏框架:
通过系统化约束模型输入输出,把实时剧本创作和游戏设计专家知识内嵌在规则中,提升实时剧情、数值对齐、画面对齐、任务与物品生成质量。
```
## 4. 生成方式
主视觉底图使用仓库内 APIMart OpenAI 兼容 `gpt-image-2` 工作流生成:
```text
model: gpt-image-2
size: 1536x3840
reference image: 百梦气泡共创logo方向图
output: output/imagegen/baimeng-expo-rollup/baimeng-rollup-background-gpt-image-2.png
```
因为图片模型直接生成中文长文案存在错字风险,最终稿采用“gpt-image-2 底图 + 本地精确中文排版”的方式生成:
```text
tmp/imagegen/generate-baimeng-rollup-background.mjs
tmp/imagegen/render-baimeng-rollup.py
```
最终输出:
```text
output/imagegen/baimeng-expo-rollup/baimeng-rollup-final-cn.png
output/imagegen/baimeng-expo-rollup/baimeng-rollup-final-cn-preview.png
```
## 5. 后续改版建议
1. 若印刷厂提供具体尺寸和出血要求,优先在 `render-baimeng-rollup.py` 中调整画布比例、边距和安全区。
2. 若需要放二维码,应放在底部独立留白区,不遮挡产品心智和关键技术段。
3. 若展会现场观众偏投资人或B端合作方,可以把“产品心智”段压缩,放大“关键技术”与平台愿景。
4. 若观众偏玩家或普通创作者,可以把“关键技术”段压缩,放大“10分钟创作、玩过就改、发布分享”的闭环。
@@ -207,6 +207,26 @@
- `01-ring-three-bubbles`:识别直接,但工具 icon 感略强。
- `04-breath-origin`:梦感更强,但现实吹泡泡行为弱。
## 12. 吹泡泡主标再收敛版补充
在“方向正确,再来一些”的基础上,本轮继续压缩泡泡棒方向,目标是减少玩具感,让它更像成熟产品主标。
本轮生成 prompt
`tmp/imagegen/baimeng_logo_bubble_refine_batch12_prompts.jsonl`
本轮输出目录:
`output/imagegen/baimeng-logo-bubble-refine-batch12/`
当前最值得继续推进的是:
- `02-simple-action-mark`:泡泡棒行为明确,结构干净,比上一批更接地气,也不太幼稚。
- `06-one-plus-two-bubbles`:更抽象、更高级,但现实吹泡泡行为感弱一些。
- `08-final-simple-bubbles`:适合 App icon,但聊天气泡联想较强,需弱化社交聊天框尾巴。
不建议推进 `03/04/07`,它们出现了不稳定文字或方案式字标;`01` 容易被误认为放大镜。
批量生成 prompt 已放在:
`tmp/imagegen/baimeng_logo_gpt_image_2_prompts.jsonl`
@@ -233,3 +253,35 @@ python "C:\Users\wuxiangwanzi\.codex\skills\.system\imagegen\scripts\image_gen.p
APIMART_BASE_URL=https://api.apimart.ai/v1
APIMART_API_KEY=...
```
## 13. 04/07 气泡方向单独优化补充
在用户反馈“更喜欢 04 和 07”后,本轮不再横向发散其它生活物件,而是只围绕两条已被接受的视觉方向做收敛:
- 继承 `04` 的中心大泡泡:保留多条虹彩色带组成的饱满主视觉,但压低小泡泡的高光、阴影和玻璃拟物感。
- 继承 `07` 的色彩和轻轻吹泡泡行为:减少元素数量,确保整体居中,避免一串气泡散向右上角。
- 明确排除 `03/05/08` 中容易出现的聊天软件联想,不使用聊天尾巴、对话框轮廓、碎小装饰点和星形元素。
本轮生成 prompt
`tmp/imagegen/baimeng_logo_bubble_04_07_refine_batch13_prompts.jsonl`
本轮输出目录:
`output/imagegen/baimeng-logo-bubble-04-07-refine-batch13/`
联系表:
`output/imagegen/baimeng-logo-bubble-04-07-refine-batch13/baimeng-bubble-04-07-refine-batch13-contact-sheet.png`
当前最值得继续推进的是:
- `01-04-flat-rainbow-band-core`:保留了 04 的彩虹色带饱满度,同时只留下一个小辅助泡泡,聊天气泡联想弱,适合作为“梦 / 很多 / 共创”的品牌主标继续精修。
- `02-04-single-full-bubble-ring`:最克制、最像可注册主符号,完全去掉碎元素;缺点是“吹泡泡行为”和 UGC 共创感比其它方案弱,可作为极简品牌基线。
- `08-07-rainbow-breath-symbol`:较好融合了 04 的虹彩大环与 07 的吹泡泡动作,亲和度高;后续需要继续压缩右上两个泡泡的体量,避免重心偏右。
不建议优先推进:
- `03-04-overlap-two-bubbles-brand`:结构清楚,但小环容易变成独立图标,整体略硬。
- `04-04-rainbow-bubble-wand-minimal`:泡泡棒语义明确,但下方手柄过于具象,容易像工具图标。
- `05/06/07`:色彩和亲和力可参考,但泡泡簇仍比 01/02/08 更接近装饰插画,产品主标凝聚力不足。
+1
View File
@@ -15,6 +15,7 @@
- [PLATFORM_CATEGORY_AND_CREATE_TAB_DESIGN_2026-04-24.md](./PLATFORM_CATEGORY_AND_CREATE_TAB_DESIGN_2026-04-24.md):平台入口新增分类 Tab、登录态导航裁剪与创作 Tab 视觉强化设计。
- [PLATFORM_BIG_FISH_ENTRY_HIDE_2026-04-28.md](./PLATFORM_BIG_FISH_ENTRY_HIDE_2026-04-28.md):平台入口暂时隐藏大鱼吃小鱼创作卡片,但保留现有玩法链路。
- [UNIFIED_MODAL_WINDOW_DESIGN_2026-04-25.md](./UNIFIED_MODAL_WINDOW_DESIGN_2026-04-25.md):统一平台风与 RPG 像素风模态窗口外壳、交互边界和迁移顺序。
- [BAIMENG_EXPO_ROLLUP_BANNER_DESIGN_2026-05-07.md](./BAIMENG_EXPO_ROLLUP_BANNER_DESIGN_2026-05-07.md):百梦线下展会易拉宝广告展板的文案层级、视觉方向与 gpt-image-2 生成记录。
- [AI_NATIVE_RUNTIME_ITEM_SYSTEM_REDESIGN_2026-04-02.md](./AI_NATIVE_RUNTIME_ITEM_SYSTEM_REDESIGN_2026-04-02.md):运行时物品生成系统重设计。
- [LEVEL_PROGRESS_AND_CHAPTER_NPC_AUTO_SCALING_DESIGN_2026-04-20.md](./LEVEL_PROGRESS_AND_CHAPTER_NPC_AUTO_SCALING_DESIGN_2026-04-20.md):等级成长、章节经验节奏与 NPC 自动定级设计。
- [RPG_NARRATIVE_PLANNING_FULL_PIPELINE_WORKFLOW_2026-04-12.md](./RPG_NARRATIVE_PLANNING_FULL_PIPELINE_WORKFLOW_2026-04-12.md):专业剧情策划构建 RPG 游戏全剧情的工作流程与交付模板。
@@ -0,0 +1,61 @@
# 新手引导流程 PRD
## 1. 目标
引导未登录且首次访问产品的用户,快速体验从输入想法、AI 生成拼图游戏、完成拼图、注册或登录保留作品,到进入产品首页的创作闭环。
## 2. 触发条件
1. 用户处于未登录状态。
2. 用户首次访问产品。
## 3. 流程
1. 用户首次打开产品。
2. 页面浮现文字:`待定待定待定`
3. 文字下方展示文字输入框。
4. 输入框提示文字:`把你的梦讲给我听吧`
5. 用户输入内容后,点击生成按钮。
6. 系统拉起拼图游戏创作机制。
7. 系统根据用户输入内容生成一个仅包含 1 关的拼图游戏。
8. 生成完成后,页面浮现文字:`待定待定待定`
9. 用户进入当前生成的拼图游戏。
10. 用户完成该拼图游戏的第 1 关。
11. 页面浮现文字:`只差一步,就可以永久保留你的梦`
12. 文字下方展示注册账号/登录模块。
13. 用户完成注册或登录。
14. 系统进入产品首页。
## 4. 文案
| 位置 | 文案 |
| --- | --- |
| 首次启动浮现文案 | `待定待定待定` |
| 输入框提示文字 | `把你的梦讲给我听吧` |
| 生成完成浮现文案 | `待定待定待定` |
| 完成拼图后的注册/登录引导文案 | `只差一步,就可以永久保留你的梦` |
## 5. 范围边界
1. 不增加跳过入口。
2. 不定义额外功能说明文案。
3. 不扩展拼图为多关。
4. 不调整注册/登录后的去向,当前进入产品首页。
5. 不新增未确认的 UI 动画、样式、奖励、埋点或保存策略。
## 6. 验收标准
1. 未登录首次访问产品时,进入新手引导首屏。
2. 首屏展示确认文案、输入框和生成按钮。
3. 用户输入内容并点击生成后,系统生成 1 关拼图。
4. 生成完成后,用户可以进入该拼图并完成第 1 关。
5. 第 1 关完成后,页面展示注册/登录引导文案和登录模块。
6. 用户完成注册或登录后,进入产品首页。
## 7. 落地接口与状态
1. 首次访问判定由前端本地状态承载,未登录用户首次访问平台首页时展示;标记键为 `genarrative.puzzle-onboarding.first-visit.v1`
2. 临时生成入口为 `POST /api/runtime/puzzle/onboarding/generate`,不要求登录,只返回本次新手引导使用的 1 关拼图作品摘要与关卡数据。
3. 登录后保存入口为 `POST /api/runtime/puzzle/onboarding/save`,要求登录;服务端为当前用户创建拼图 agent session,并把临时 1 关拼图保存为当前用户作品草稿。
4. 新手引导游玩阶段复用现有本地拼图运行时,不新增 SpacetimeDB 表、reducer 或运行时真相。
5. 保存完成后清空新手引导临时态,刷新拼图作品架,并回到产品首页。
@@ -0,0 +1,170 @@
# 后台埋点数据明细与 Excel 导出方案
> **For Hermes:** Use subagent-driven-development skill to implement this plan task-by-task.
**Goal:** 在百梦后台新增“埋点数据”页,展示每条埋点原始事件的详细字段,并支持导出为 Excel 可直接打开的表格文件。
**Architecture:** 后端继续由 `api-server` 作为后台 BFF,经 SpacetimeDB HTTP SQL 只读查询 `tracking_event`,不改变表结构和 reducer。前端在 `apps/admin-web` 中新增独立路由与页面,页面渲染后端返回的原始明细,并在浏览器侧导出 Excel 兼容的 `.xls` HTML 表格,避免新增依赖。
**Tech Stack:** Rust Axum、SpacetimeDB HTTP SQL、shared-contracts、React 19、TypeScript、Vite。
---
## 范围
本次只做后台只读能力:
- 展示 `tracking_event` 原始事件明细。
- 每条埋点展示:事件 ID、Event Key、事件名称、Scope、Scope ID、Day Key、用户 ID、作品拥有者、Profile ID、模块、metadata、发生时间。
- 支持按 Event Key、用户 ID、Scope Kind、Scope ID 筛选。
- 支持导出当前筛选结果为 Excel 可打开文件。
不做:
- 不新增或修改 SpacetimeDB 表结构。
- 不在后台写入或删除埋点。
- 不把埋点聚合口径下沉到前端计算。
## 后端契约
新增接口:
```text
GET /admin/api/tracking/events?eventKey=&userId=&scopeKind=&scopeId=&limit=
```
鉴权:复用后台 `require_admin_auth`
返回:
```json
{
"entries": [
{
"eventId": "daily-login:user:xxx:123",
"eventKey": "daily_login",
"eventTitle": "每日登录",
"scopeKind": "user",
"scopeId": "xxx",
"dayKey": 20580,
"userId": "xxx",
"ownerUserId": null,
"profileId": null,
"moduleKey": "profile",
"metadataJson": "{}",
"occurredAt": "2026-05-07T00:00:00Z"
}
]
}
```
后端实现要点:
1. DTO 放在 `shared-contracts/src/admin.rs`,避免 Rust 与前端口径分叉。
2. Handler 放在 `api-server/src/admin.rs`,使用当前已有 SpacetimeDB HTTP SQL helper 思路。
3. SQL 只读 `tracking_event`,固定白名单列;由于 SpacetimeDB 2.2 HTTP SQL 不支持 `ORDER BY`,后端取回默认 200 / 最大 1000 条后在 API 层按 `occurred_at` 倒序排序。
4. 查询条件只通过字符串转义函数拼接,禁止直接拼接未转义用户输入。
5. `eventTitle` 由后端根据已知事件 key 映射,未知事件返回 `eventKey`
## 前端页面
新增路由:`#tracking`,导航标题为“埋点数据”。
页面能力:
1. 顶部筛选区:Event Key、用户 ID、Scope Kind、Scope ID、刷新、导出 Excel。
2. 列表区:移动端可横向滚动,桌面端表格展示。
3. 详情区:每行有“详情”按钮,弹出独立面板展示完整字段与格式化后的 metadata JSON。
4. 导出:导出当前页面已加载结果,文件名形如 `tracking-events-2026-05-07.xls`
导出实现:
- 使用 HTML table + Excel MIME`application/vnd.ms-excel;charset=utf-8`
- 文件扩展名使用 `.xls`Excel/WPS 可直接打开。
- 所有单元格做 HTML 转义。
- metadata 保留原始 JSON 文本,便于运营继续筛选。
## 验收命令
```bash
npm run check:encoding
npm run admin-web:typecheck
cargo test -p shared-contracts -p api-server admin_tracking -- --nocapture
```
如后端接口改动较大,再补充:
```bash
npm run api-server
curl http://127.0.0.1:<port>/healthz
```
## 实施任务
### Task 1: 补充 shared-contracts 后台埋点 DTO
**Files:**
- Modify: `server-rs/crates/shared-contracts/src/admin.rs`
**Steps:**
1. 新增 `AdminTrackingEventListQuery`
2. 新增 `AdminTrackingEventEntryPayload`
3. 新增 `AdminTrackingEventListResponse`
4. 为 DTO 添加中文注释。
### Task 2: 增加后端后台埋点查询接口
**Files:**
- Modify: `server-rs/crates/api-server/src/admin.rs`
- Modify: `server-rs/crates/api-server/src/app.rs`
**Steps:**
1.`admin.rs` 新增 query 解析与 SQL 构造。
2. 复用 SpacetimeDB HTTP SQL 调用风格读取 rows。
3. 新增 `admin_list_tracking_events` handler。
4.`app.rs` 挂载 `/admin/api/tracking/events`
5. 添加单元测试覆盖 SQL 字符串转义、limit clamp、SQL 响应解析。
### Task 3: 增加前端 API 类型与客户端方法
**Files:**
- Modify: `apps/admin-web/src/api/adminApiTypes.ts`
- Modify: `apps/admin-web/src/api/adminApiClient.ts`
**Steps:**
1. 新增埋点 entry/list/query 类型。
2. 新增 `listAdminTrackingEvents(token, query)`
3. 使用 `URLSearchParams` 拼接非空查询字段。
### Task 4: 新增后台埋点数据页面
**Files:**
- Create: `apps/admin-web/src/pages/AdminTrackingEventsPage.tsx`
- Modify: `apps/admin-web/src/styles/admin.css`
**Steps:**
1. 实现筛选、刷新、错误状态。
2. 实现明细表格。
3. 实现独立详情面板。
4. 实现 Excel `.xls` 导出。
5. 保持 UI 简洁,不添加说明类大段文案。
### Task 5: 接入后台路由与导航
**Files:**
- Modify: `apps/admin-web/src/app/adminRoutes.ts`
- Modify: `apps/admin-web/src/app/AdminShell.tsx`
- Modify: `apps/admin-web/src/app/AdminApp.tsx`
**Steps:**
1. 增加 `tracking` 路由。
2. 导航增加图标。
3. `AdminApp` 渲染新页面。
### Task 6: 验证并提交
**Steps:**
1. 运行 `npm run check:encoding`
2. 运行 `npm run admin-web:typecheck`
3. 运行后端相关 cargo test。
4. 修复问题后提交并推送当前分支。
@@ -0,0 +1,69 @@
# 短信验证码阿里云时间戳格式修复(2026-05-07)
## 背景
使用阿里云短信验证码真实 provider 发送验证码时,接口返回:
```text
短信验证码发送失败:Specified time stamp or date value is not well formatted.
```
该错误来自阿里云 OpenAPI 网关对签名请求头 `x-acs-date` 的格式校验。
## 根因
`server-rs/crates/platform-auth/src/lib.rs` 中阿里云 ACS3 签名逻辑会构造 `x-acs-date` 请求头。
原实现使用 `time::format_description::well_known::Rfc3339`,当 `OffsetDateTime::now_utc()` 带纳秒时会生成形如:
```text
2026-05-07T14:23:59.364767Z
```
阿里云 ACS3 签名要求 `x-acs-date` 使用不带小数秒的 UTC ISO 8601 格式:
```text
yyyy-MM-dd'T'HH:mm:ss'Z'
```
即:
```text
2026-05-07T14:23:59Z
```
带小数秒的时间戳会被阿里云网关判定为格式非法,从而返回 `Specified time stamp or date value is not well formatted.`
## 修复方案
`current_aliyun_timestamp()` 改为手动输出不带小数秒的 UTC ISO 8601 格式:
```text
yyyy-MM-dd'T'HH:mm:ss'Z'
```
并新增单元测试,确保:
- 长度等于 `2026-05-07T12:34:56Z`
- 固定位置包含 `-``T``:``Z`
- 不包含小数点;
- 除固定分隔符外均为数字。
## 影响范围
- 仅影响阿里云短信验证码 provider 的请求签名头 `x-acs-date`
- 不改动短信模板、签名、验证码业务参数。
- 不改动 mock 短信 provider。
- 不涉及前端接口契约变化。
## 验收
执行:
```bash
cd server-rs
cargo test -p platform-auth aliyun -- --nocapture
cargo fmt -p platform-auth --check
```
预期:相关测试通过,格式检查通过。
@@ -649,6 +649,8 @@ src/components/match3d-runtime/
1. 名称:`抓大鹅`
2. 子标题:`经典消除玩法`
3. `src/config/newWorkEntryConfig.ts``match3d` 必须保持 `visible: true``open: true`,平台首屏卡带和创作类型弹层都从该配置派生,不允许只保留路由能力却隐藏创作入口。
4. 入口点击后进入 `match3d-agent-workspace`,对应前端路径为 `/creation/match3d/agent`,并通过 `/api/creation/match3d/sessions` 创建正式 Agent 会话;如果公开广场读取失败,只降级广场列表,不能阻断或隐藏抓大鹅创作入口。
## 11.4 运行态 UI
@@ -195,3 +195,58 @@ cannon-es
4. 托盘仍使用共享 `WebGLRenderer`,继续按当前 `visualKey` 和尺寸关系生成同款模型;不得新增每格独立 renderer。
5. 托盘缩放不能继续只按本局最大模型统一压缩所有物体;小尺寸模型需要保留最低可读显示尺寸,但仍不能改动场内真实尺寸、碰撞尺寸和后端权威尺寸。
6. 备选栏单格高度可大于宽度,优先保证局内 3D 预览的识别面积;不得为了适配旧正方形格子把模型再次压小。
## 16. 中心场地隐藏纵深与动态上顶
2026-05-05 针对中心场地高数量局面穿模严重、消除后中下层物体长期陷在深处的问题,追加隐藏纵深与动态上顶表现修正。
编码口径:
1. 该纵深只存在于 3D 物理表现层,不修改锅体图案、锅壁模型、托盘表现、后端快照、点击权威判定、消除和胜负规则。
2. 物体生成高度不再使用固定极小层级步长,而是按本局总物体数计算一个隐藏初始纵深;物体总量越大,初始逻辑纵深越深,用来减少大量放大后模型被挤进同一高度区间导致的穿模。
3. 当前剩余场内物体数会动态缩短可用纵深;随着玩家持续消除,下层物体的目标高度逐步上移,表现为中下层物体陆续向上顶到表面层。
4. 动态上顶只通过向上托举力和目标高度调整完成,不增加中心引力,不修改水平约束半径,不改变碰撞体尺寸倍率。
5. 表面层高度保持稳定,避免越消除越显得物体掉进深处或视觉尺寸异常变小。
## 17. 高数量局面物理稳定与动态锅容量
2026-05-06 继续按方案 C 和方案 D 优化 `clearCount=100` 等高数量局面的稳定性。
方案 C 编码口径:
1. 只调整 3D 表现层的物理稳定参数,包括求解迭代次数、接触摩擦、接触弹性、线性阻尼、角阻尼、睡眠阈值和速度上限。
2. 物体数量越大,物理世界越偏向高摩擦、低弹性和更强阻尼,减少大量物体同时生成后的持续弹跳、穿插和边界挤压。
3. 速度保护只限制极端水平速度和垂直速度,不改物体位置生成规则、点击判定、备选栏、消除和胜负规则。
方案 D 编码口径:
1. 隐藏锅容量的纵深按本局总物体数,也就是用户配置的消除次数乘 `3` 后动态计算;消除次数越大,锅内容量纵深越深。
2. 动态纵深只影响 3D 物理层的生成高度、目标层高度和消除后的上顶回补;锅底、锅壁、锅沿和 DOM 场地外观不随纵深变化。
3. 高数量局面需要降低单层容量,让更多物体分散到纵向层级中,避免 `300` 个物体被压进少量高度层。
4. 随着消除进度推进,当前可用纵深继续按剩余物体数收缩,确保下层物体逐步向表面回补,保持中心场地表层稳定可见。
5. 本节不改变中心引力默认值,不改水平活动半径,不改碰撞体与视觉模型的尺寸一致性规则。
## 18. 原型入场节奏与创建限流
2026-05-07 根据原型视频补充创建过程优化。原型不是在同一帧把全部物体摆进容器,而是先短暂空场,再用连续小批量把物体投放到容器中,批与批之间留出自然沉降时间,最后再进入可操作局面。
编码口径:
1. 该优化只作用于前端 3D 表现层的物理 body 创建节奏,不改变后端快照、消除目标数量、点击权威判定、备选栏、三消和胜负规则。
2. `totalItemCount < 30` 时保留较快创建节奏;`30 <= totalItemCount <= 50` 进入中速波次投放,降低每波数量并增加波次沉降窗口,避免最后一层物体压进尚未稳定的表层堆叠。
3. `totalItemCount > 50` 后进入更强限流投放,单帧创建数量下降,避免同一帧把过多碰撞体塞入物理世界。
4. 随着总物体数增加,投放初始等待、层级间隔和同层错峰间隔都要逐步变长,模拟原型中“持续落入、短暂沉降、继续补入”的节奏。
5. `clearCount=100` 对应 `300` 个物体时,投放节奏应接近连续数秒完成,而不是在一秒左右完成全量创建。
6. 该节不允许通过缩小碰撞体、扩大锅半径、开启中心引力或修改模型尺寸来掩盖穿模;如果后续仍需调整,只继续围绕创建节拍和物理沉降窗口处理。
## 19. 生成高度避让已有堆叠
2026-05-07 继续按方案 2 优化 `30` 件左右局面最后一层或最后一波物体仍会穿进已有堆叠的问题。
编码口径:
1. 该优化只作用于前端 3D 表现层的新物体创建高度,不改变后端快照、物品数量、模型尺寸、碰撞体尺寸、锅半径、点击判定、备选栏、三消和胜负规则。
2. 新物体进入物理世界前,先根据当前同一水平区域附近已有物体的碰撞体顶部高度,计算一个不低于原计划高度的生成高度。
3. 只有水平外接半径发生重叠的已有物体会影响本次生成高度;远处物体不能把新物体整体抬高,避免破坏原有随机洒落和分层节奏。
4. 该避让只解决“直接创建在已有模型内部”的初始穿插,后续沉降、翻滚、堆叠仍交给 cannon-es 物理模拟。
5. 本节不允许额外引入中心引力、扩大锅容量或修改模型生成规则;若后续仍需优化,只继续围绕生成高度、入场节拍和沉降窗口做局部迭代。
@@ -104,9 +104,11 @@
1. 发送验证码调用 `SendSmsVerifyCode`
2. 校验验证码调用 `CheckSmsVerifyCode`
3. 使用阿里云 RPC 签名口径:
- `SignatureMethod=HMAC-SHA1`
- `SignatureVersion=1.0`
3. 使用阿里云 OpenAPI V3 请求头签名口径:
- `Authorization: ACS3-HMAC-SHA256 ...`
- `x-acs-action`
- `x-acs-version`
- `x-acs-content-sha256`
4. 当前仍只支持中国大陆手机号。
## 7. 状态与快照
+1
View File
@@ -4,6 +4,7 @@
## 文档列表
- [RUST_WORKSPACE_DEPENDENCY_CONSOLIDATION_2026-05-07.md](./RUST_WORKSPACE_DEPENDENCY_CONSOLIDATION_2026-05-07.md):记录 `server-rs` Cargo 依赖集中配置口径,第三方版本和 workspace 内部 crate path 统一维护在根 `server-rs/Cargo.toml`,成员 crate 只保留 feature/optional 差异。
- [API_SERVER_EXTERNAL_SERVICE_ENV_CONFIG_2026-05-07.md](./API_SERVER_EXTERNAL_SERVICE_ENV_CONFIG_2026-05-07.md):冻结 api-server 外部服务配置边界,公共服务 URL 可保留代码默认值,非公共模型名和私有网关 URL 统一通过环境变量注入。
- [PROFILE_TASK_AND_TRACKING_SYSTEM_2026-05-03.md](./PROFILE_TASK_AND_TRACKING_SYSTEM_2026-05-03.md):冻结个人任务与埋点系统首版方案,明确 `tracking_event``tracking_daily_stat``profile_task_config`、任务进度、领奖记录和光点钱包流水的边界。
- [SQUARE_HOLE_IMAGE_SLOT_AND_RUNTIME_INTERACTION_FIX_2026-05-06.md](./SQUARE_HOLE_IMAGE_SLOT_AND_RUNTIME_INTERACTION_FIX_2026-05-06.md):记录方洞挑战结果页图片槽位局部生成、洞口图历史素材、运行态拖拽与点击投放交互的修正口径。
@@ -0,0 +1,55 @@
# Rust workspace 依赖集中配置记录
日期:`2026-05-07`
## 1. 背景
`server-rs` workspace 已经包含 `api-server``spacetime-module``spacetime-client`、多个 `module-*` 领域 crate、`platform-*` 适配 crate 和共享 crate。随着 DDD 收口推进,成员 `Cargo.toml` 中重复散写了第三方 crate 版本和本地 path 依赖,后续升级 `serde``reqwest``tokio``time`、SpacetimeDB SDK 或内部 crate 路径时容易出现漂移。
本次只做 Cargo 配置收敛,不改变业务代码、表结构、reducer/procedure 签名、HTTP contract 或前端绑定。
## 2. 配置规则
1. 共享第三方依赖版本统一维护在 `server-rs/Cargo.toml``[workspace.dependencies]`
2. workspace 内部 crate 的 `path` 也统一维护在根 `server-rs/Cargo.toml`
3. 成员 crate 默认使用 `{ workspace = true }` 继承依赖。
4. 成员 crate 只保留自身需要表达的差异,例如 `features``optional = true` 或 target-specific dependency。
5. 需要关闭 default features 的依赖,应优先在 workspace 根依赖中声明;成员 crate 不再重复覆盖同一项。
6. `module-assets` 这类有默认服务端 feature 的领域 crate,在 workspace 根内按 `default-features = false` 维护;需要服务端 OSS/HTTP 能力的 adapter crate 显式启用 `features = ["server-service"]`
## 3. 本次收敛范围
已上提到 workspace 根的依赖包括:
1. 本地路径依赖:`module-*``platform-*``shared-*``spacetime-client`
2. 常用第三方依赖:`serde``serde_json``serde_urlencoded``reqwest``tokio``time``tracing``base64``hmac``sha2``uuid``url` 等。
3. SpacetimeDB 相关依赖:`spacetimedb``spacetimedb-sdk``spacetimedb-lib`
`spacetimedb-lib` 在 workspace 根统一关闭 default features`spacetime-module` 只继承并补充 `features = ["serde"]`。这样避免成员 crate 尝试覆盖 workspace default-feature 设定导致 manifest 解析失败。
阿里云 OSS 相关签名不再依赖不推荐的 `sha1` crate,统一使用 `sha2::Sha256`
1. 浏览器直传 ticket 使用 OSS V4 表单签名字段:`x-oss-signature-version=OSS4-HMAC-SHA256``x-oss-credential``x-oss-date``x-oss-signature`
2. 服务端 OSS 读写请求和测试辅助签名统一使用 `OSS4-HMAC-SHA256` Authorization。
3. 阿里云短信 OpenAPI 请求统一使用 `ACS3-HMAC-SHA256` 请求头签名,不再在表单中传旧 `SignatureMethod=HMAC-SHA1` / `SignatureVersion=1.0`
## 4. 不在本次范围
1. 不新增或删除 crate。
2. 不修改 `server-rs` workspace `members` / `default-members` 语义。
3. 不修改 SpacetimeDB 表、reducer、procedure、migration 白名单或生成绑定。
4. 不改变 `module-*` 的 DDD 依赖方向。
## 5. 验收口径
配置改动后至少执行:
```powershell
cargo metadata --manifest-path server-rs\Cargo.toml --format-version 1 --no-deps
cargo check -p api-server --manifest-path server-rs\Cargo.toml
cargo check -p spacetime-module --manifest-path server-rs\Cargo.toml
npm.cmd run check:server-rs-ddd
npm.cmd run check:encoding -- docs/technical/RUST_WORKSPACE_DEPENDENCY_CONSOLIDATION_2026-05-07.md docs/technical/README.md server-rs/README.md .hermes/shared-memory/decision-log.md .hermes/shared-memory/project-overview.md
```
若仅改 Cargo 依赖配置且未触碰 API smoke 相关代码,不强制启动 `npm run api-server`;若后续改动同时涉及 API 路由、SpacetimeDB facade 或运行时行为,仍按 `AGENTS.md` 和 DDD 文档执行后端 smoke。
@@ -1,6 +1,6 @@
pipeline {
agent {
label 'built-in && windows'
label 'windows'
}
options {
@@ -16,7 +16,7 @@ pipeline {
CARGO_INCREMENTAL = '0'
RUSTC_WRAPPER = 'sccache'
SCCACHE_DIR = '${env.USERPROFILE}\\.cache\\sccache-stdb-module'
SCCACHE_CACHE_SIZE = '30G'
SCCACHE_CACHE_SIZE = '30G'o
}
parameters {
@@ -0,0 +1,16 @@
import type { PuzzleDraftLevel } from './puzzleAgentDraft';
import type { PuzzleWorkSummary } from './puzzleWorkSummary';
export interface PuzzleOnboardingGenerateRequest {
promptText: string;
}
export interface PuzzleOnboardingGenerateResponse {
item: PuzzleWorkSummary;
level: PuzzleDraftLevel;
}
export interface PuzzleOnboardingSaveRequest {
promptText: string;
item: PuzzleWorkSummary;
}
+1
View File
@@ -10,6 +10,7 @@ export * from './contracts/match3dWorks';
export * from './contracts/puzzleAgentActions';
export * from './contracts/puzzleAgentDraft';
export * from './contracts/puzzleAgentSession';
export * from './contracts/puzzleOnboarding';
export * from './contracts/puzzleResultPreview';
export * from './contracts/puzzleRuntimeSession';
export * from './contracts/puzzleWorkSummary';
+3
View File
@@ -1,3 +1,6 @@
[build]
rustc-wrapper = "sccache"
[target.x86_64-unknown-linux-gnu]
linker = "clang"
rustflags = ["-C", "link-arg=-fuse-ld=lld"]

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