Merge remote-tracking branch 'origin/master' into hermes/hermes-19e77eb0
This commit is contained in:
@@ -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,并需要独立拉取仓库、修改代码、本地测试;团队希望形成共享的长期项目记忆。
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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[];
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
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('');
|
||||
}
|
||||
@@ -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 更接近装饰插画,产品主标凝聚力不足。
|
||||
|
||||
@@ -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. 状态与快照
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user