Merge remote-tracking branch 'origin/master' into codex/wooden-fish-template

This commit is contained in:
2026-05-21 23:34:35 +08:00
76 changed files with 4259 additions and 662 deletions
+16
View File
@@ -0,0 +1,16 @@
# CodeGraph data files
# These are local to each machine and should not be committed
# Database
*.db
*.db-wal
*.db-shm
# Cache
cache/
# Logs
*.log
# Hook markers
.dirty
+143
View File
@@ -0,0 +1,143 @@
{
"version": 1,
"include": [
"**/*.ts",
"**/*.tsx",
"**/*.js",
"**/*.jsx",
"**/*.py",
"**/*.go",
"**/*.rs",
"**/*.java",
"**/*.c",
"**/*.h",
"**/*.cpp",
"**/*.hpp",
"**/*.cc",
"**/*.cxx",
"**/*.cs",
"**/*.php",
"**/*.rb",
"**/*.swift",
"**/*.kt",
"**/*.kts",
"**/*.dart",
"**/*.svelte",
"**/*.vue",
"**/*.liquid",
"**/*.pas",
"**/*.dpr",
"**/*.dpk",
"**/*.lpr",
"**/*.dfm",
"**/*.fmx",
"**/*.scala",
"**/*.sc"
],
"exclude": [
"**/.git/**",
"**/node_modules/**",
"**/vendor/**",
"**/Pods/**",
"**/dist/**",
"**/build/**",
"**/out/**",
"**/bin/**",
"**/obj/**",
"**/target/**",
"**/*.min.js",
"**/*.bundle.js",
"**/.next/**",
"**/.nuxt/**",
"**/.svelte-kit/**",
"**/.output/**",
"**/.turbo/**",
"**/.cache/**",
"**/.parcel-cache/**",
"**/.vite/**",
"**/.astro/**",
"**/.docusaurus/**",
"**/.gatsby/**",
"**/.webpack/**",
"**/.nx/**",
"**/.yarn/cache/**",
"**/.pnpm-store/**",
"**/storybook-static/**",
"**/.expo/**",
"**/web-build/**",
"**/ios/Pods/**",
"**/ios/build/**",
"**/android/build/**",
"**/android/.gradle/**",
"**/__pycache__/**",
"**/.venv/**",
"**/venv/**",
"**/site-packages/**",
"**/dist-packages/**",
"**/.pytest_cache/**",
"**/.mypy_cache/**",
"**/.ruff_cache/**",
"**/.tox/**",
"**/.nox/**",
"**/*.egg-info/**",
"**/.eggs/**",
"**/go/pkg/mod/**",
"**/target/debug/**",
"**/target/release/**",
"**/.gradle/**",
"**/.m2/**",
"**/generated-sources/**",
"**/.kotlin/**",
"**/.dart_tool/**",
"**/.vs/**",
"**/.nuget/**",
"**/artifacts/**",
"**/publish/**",
"**/cmake-build-*/**",
"**/CMakeFiles/**",
"**/bazel-*/**",
"**/vcpkg_installed/**",
"**/.conan/**",
"**/Debug/**",
"**/Release/**",
"**/x64/**",
"**/.pio/**",
"**/release/**",
"**/*.app/**",
"**/*.asar",
"**/DerivedData/**",
"**/.build/**",
"**/.swiftpm/**",
"**/xcuserdata/**",
"**/Carthage/Build/**",
"**/SourcePackages/**",
"**/__history/**",
"**/__recovery/**",
"**/*.dcu",
"**/.composer/**",
"**/storage/framework/**",
"**/bootstrap/cache/**",
"**/.bundle/**",
"**/tmp/cache/**",
"**/public/assets/**",
"**/public/packs/**",
"**/.yardoc/**",
"**/coverage/**",
"**/htmlcov/**",
"**/.nyc_output/**",
"**/test-results/**",
"**/.coverage/**",
"**/.idea/**",
"**/logs/**",
"**/tmp/**",
"**/temp/**",
"**/_build/**",
"**/docs/_build/**",
"**/site/**"
],
"languages": [],
"frameworks": [],
"maxFileSize": 1048576,
"extractDocstrings": true,
"trackCallSites": true
}
+23
View File
@@ -0,0 +1,23 @@
# Genarrative 项目级 Codex 配置。
# 这里仅保存可进入仓库的 hook 配置与脚本;个人 token、MCP server、模型路由仍放在个人 ~/.codex/config.toml。
[features]
hooks = true
# Codex 准备执行 git commit 前检查 TypeScript / admin-web / api-server 编译错误。
# 脚本也可手动运行:
# node .codex/hooks/pre-submit-compile-check.mjs
[[hooks.PreToolUse]]
matcher = "Bash|shell_command|functions.shell_command"
command = "node .codex/hooks/pre-submit-compile-check.mjs"
timeout = 180
statusMessage = "提交前检查编译错误"
# Codex 每次工具修改文件后执行:同步 CodeGraph 索引。
# 脚本也可手动运行:
# node .codex/hooks/post-edit-codegraph-sync.mjs
[[hooks.PostToolUse]]
matcher = "Bash|Edit|MultiEdit|Write|apply_patch|shell_command|functions.shell_command|functions.apply_patch"
command = "node .codex/hooks/post-edit-codegraph-sync.mjs"
timeout = 60
statusMessage = "更新 CodeGraph 索引"
+51
View File
@@ -0,0 +1,51 @@
#!/usr/bin/env node
import { spawnSync } from 'node:child_process';
import { existsSync, mkdirSync } from 'node:fs';
import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
const scriptDir = dirname(fileURLToPath(import.meta.url));
const repoRoot = resolve(scriptDir, '..', '..');
const logDir = resolve(repoRoot, '.codex', 'logs');
const hasCodegraphConfig = existsSync(resolve(repoRoot, '.codegraph', 'config.json'));
const npmCommand = process.platform === 'win32' ? 'cmd' : 'npm';
if (!hasCodegraphConfig) {
console.log('[codex-hook] 未发现 .codegraph/config.json,跳过 CodeGraph 同步。');
process.exit(0);
}
const result = spawnSync(npmCommand, process.platform === 'win32' ? ['/d', '/s', '/c', 'npm run codegraph:sync'] : ['run', 'codegraph:sync'], {
cwd: repoRoot,
shell: false,
encoding: 'utf8',
env: {
...process.env,
NO_COLOR: process.env.NO_COLOR ?? '1',
},
});
mkdirSync(logDir, { recursive: true });
if (result.stdout) {
process.stdout.write(result.stdout);
}
if (result.stderr) {
process.stderr.write(result.stderr);
}
if (result.error) {
console.error(`[codex-hook] CodeGraph 同步启动失败:${result.error.message}`);
process.exit(1);
}
if (result.signal) {
console.error(`[codex-hook] CodeGraph 同步被信号终止:${result.signal}`);
process.exit(1);
}
if ((result.status ?? 0) !== 0) {
console.error('[codex-hook] CodeGraph 同步失败,请手动运行 npm run codegraph:sync 查看详情。');
process.exit(result.status ?? 1);
}
console.log('[codex-hook] CodeGraph 已同步。');
+122
View File
@@ -0,0 +1,122 @@
#!/usr/bin/env node
import { spawnSync } from 'node:child_process';
import { readFileSync } from 'node:fs';
import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
const scriptDir = dirname(fileURLToPath(import.meta.url));
const repoRoot = resolve(scriptDir, '..', '..');
const npmCommand = process.platform === 'win32' ? 'cmd' : 'npm';
const hookInput = readHookInput();
if (hookInput && !isGitCommitCommand(extractShellCommand(hookInput))) {
process.exit(0);
}
const validationSteps = [
{
label: 'TypeScript typecheck',
command: npmCommand,
args: process.platform === 'win32' ? ['/d', '/s', '/c', 'npm run typecheck'] : ['run', 'typecheck'],
},
{
label: 'Admin web typecheck',
command: npmCommand,
args: process.platform === 'win32' ? ['/d', '/s', '/c', 'npm run admin-web:typecheck'] : ['run', 'admin-web:typecheck'],
},
{
label: 'Rust api-server compile check',
command: 'cargo',
args: ['check', '-p', 'api-server', '--manifest-path', 'server-rs/Cargo.toml'],
},
];
for (const step of validationSteps) {
const result = runStep(step);
if (!result.ok) {
const reason = `[codex-hook] 提交前编译检查失败:${step.label}。请修复编译错误后再提交。`;
console.error(reason);
if (hookInput) {
console.log(JSON.stringify({ decision: 'block', reason }));
process.exit(0);
}
process.exit(result.status ?? 1);
}
}
console.error('[codex-hook] 提交前编译检查通过。');
function runStep(step) {
console.error(`[codex-hook] ${step.label}`);
const result = spawnSync(step.command, step.args, {
cwd: repoRoot,
shell: false,
encoding: 'utf8',
env: {
...process.env,
NO_COLOR: process.env.NO_COLOR ?? '1',
},
});
if (result.stdout) {
process.stderr.write(result.stdout);
}
if (result.stderr) {
process.stderr.write(result.stderr);
}
if (result.error) {
console.error(`[codex-hook] ${step.label} 启动失败:${result.error.message}`);
return { ok: false, status: 1 };
}
if (result.signal) {
console.error(`[codex-hook] ${step.label} 被信号终止:${result.signal}`);
return { ok: false, status: 1 };
}
return {
ok: (result.status ?? 0) === 0,
status: result.status ?? 1,
};
}
function readHookInput() {
try {
const rawInput = readFileSync(0, 'utf8').trim();
if (!rawInput) {
return null;
}
return JSON.parse(rawInput);
} catch {
return null;
}
}
function extractShellCommand(input) {
const candidates = [
input?.tool_input?.command,
input?.toolInput?.command,
input?.tool_args?.command,
input?.toolArgs?.command,
input?.arguments?.command,
input?.params?.command,
input?.command,
];
const command = candidates.find(value => typeof value === 'string' && value.trim().length > 0);
if (command) {
return command;
}
const shellCommand = input?.tool_input?.cmd ?? input?.toolInput?.cmd ?? input?.arguments?.cmd;
if (Array.isArray(shellCommand)) {
return shellCommand.join(' ');
}
return '';
}
function isGitCommitCommand(command) {
return /(^|[;&|]\s*)git(?:\.exe)?\b[\s\S]{0,200}\bcommit\b/iu.test(command);
}
+36 -1
View File
@@ -16,6 +16,32 @@
---
## 2026-05-21 外部 API 失败必须 OTLP 上报并落库
- 背景:图片生成等外部供应商调用失败时,仅返回 502/504 或普通日志无法支持后续按 provider、阶段和重试属性聚合排障。
- 决策:外部 API 调用未成功时,`api-server` 必须同时发送 OTLP 失败观测并写入 `tracking_event`。当前通用 VectorEngine `gpt-image-2-all` 图片生成 / 编辑适配器记录 `external_api_call_failure``scope_kind = module``scope_id = provider``module_key = external-api`metadata 包含 endpoint、operation、failureStage、statusCode、statusClass、timeout、retryable、errorMessage、latencyMs、promptChars、referenceImageCount、imageModel 和 rawExcerpt。
- 落库方式:优先复用 tracking outbox 异步批量写入;outbox 不可写或因保护阈值拒绝时回退同步直写 SpacetimeDB。不新增 SpacetimeDB 表,不让 reducer 做外部 I/O。
- 影响范围:`server-rs/crates/api-server/src/external_api_audit.rs``server-rs/crates/api-server/src/openai_image_generation.rs``server-rs/crates/api-server/src/telemetry.rs`、tracking outbox、后端架构文档和开发运维文档。
- 验证方式:执行 `cargo test -p api-server external_api_audit --manifest-path server-rs/Cargo.toml -- --nocapture``cargo test -p api-server openai_image_generation --manifest-path server-rs/Cargo.toml -- --nocapture``cargo check -p api-server --manifest-path server-rs/Cargo.toml``npm run check:encoding`
- 关联文档:`docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md``docs/【开发运维】本地开发验证与生产运维-2026-05-15.md`
## 2026-05-21 拼图参考图主链改为 OSS assetObjectId 与只读签名 URL
- 背景:release 上拼图图生图生成草稿时,旧链路把上传图转成 Data URL/base64 放进创作 action JSON body,容易先触发 Nginx `413 Request Entity Too Large`,也让外部模型调用前的 HTTP body 过大。
- 决策:浏览器参考图先通过资产直传票据上传 OSS,并确认 `asset_object`;拼图 action 主链只提交 `referenceImageAssetObjectId(s)``api-server` 按当前登录用户校验 asset owner、bucket、kind、图片 MIME 和大小后签发 OSS 只读 URL,传给 VectorEngine 的 generation fallback 使用;需要 edits multipart 时由后端用该签名 URL 拉取字节,不再让前端把图片塞进 JSON body。
- 兼容边界:旧 `referenceImageSrc(s)` Data URL 与历史 `/generated-*` 路径仅保留给旧草稿、旧入口和迁移期请求;调大 Nginx `client_max_body_size` 只作为兼容兜底,不是长期创作主链。
- 影响范围:拼图创作前端、`packages/shared` / `shared-contracts` action DTO、`api-server` 拼图 VectorEngine 编排、资产确认和 `spacetime-client` 资产读取 facade。
- 验证方式:前端 payload 中 AI 重绘优先出现 `referenceImageAssetObjectId(s)``referenceImageSrc(s)` 不再携带 Data URL;后端 `puzzle_vector_engine_generation_prefers_signed_reference_url``puzzle_reference_image_sources_prefer_asset_object_ids``puzzle_asset_object_reference_requires_matching_owner` 通过。
- 关联文档:`docs/【玩法创作】平台入口与玩法链路-2026-05-15.md``docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md``docs/【开发运维】本地开发验证与生产运维-2026-05-15.md`
## 2026-05-21 Nginx 通用 API 入口放行创作参考图请求体
- 背景:release 上拼图结果页重绘动作携带参考图 Data URL 时,Nginx access log 出现 `413``request_time=0.000``upstream_status=-`,说明请求被反代层默认 1 MiB 上限拦截,未进入 `api-server`
- 决策:发布、开发服和容器 Nginx 模板的通用 `location ~ ^/api(?:/|$)` 统一设置 `client_max_body_size 64m`。该值只作为反代放行和旧 Data URL 请求兼容兜底,具体业务请求体和图片字节上限继续由 `api-server` 路由 `DefaultBodyLimit`、OSS asset 确认和业务校验控制,不能替代接口级限制;拼图参考图长期主链见同日 `OSS assetObjectId` 决策。
- 影响范围:`deploy/nginx/genarrative.conf``deploy/nginx/genarrative-dev-http.conf``deploy/container/nginx.conf`、Nginx README、生产运维文档和 release 排障口径。
- 验证方式:目标机 `nginx -T 2>/dev/null | grep client_max_body_size` 应看到 `client_max_body_size 64m;`;大于 1 MiB 的参考图请求不再在 Nginx 层直接 413access log 应出现有效 `upstream_status`
- 关联文档:`deploy/nginx/README.md``docs/【开发运维】本地开发验证与生产运维-2026-05-15.md`
## 2026-05-18 Rust 手写模块入口统一不用 mod.rs
@@ -39,8 +65,9 @@
- 背景:`server-rs/crates/api-server/src/puzzle.rs` 已膨胀为数千行大文件,混合 Axum handler、草稿编译、图片生成、VectorEngine / OSS 持久化、DTO mapper、标签生成和测试;继续在单文件内迭代会降低定位和评审效率。
- 决策:原超大 `puzzle.rs` 改为同名入口 `server-rs/crates/api-server/src/puzzle.rs``server-rs/crates/api-server/src/puzzle/` 子模块目录。`puzzle.rs` 只保留聚合入口和 handler re-export`handlers.rs` 放 HTTP handler`draft.rs` 放表单草稿 / 编译 / snapshot helper`generation.rs` 放图片与 UI 背景生成编排;`vector_engine.rs` 放 VectorEngine、下载、OSS、asset object / binding 和错误归一;`mappers.rs` / `tags.rs` 保留映射和标签 / 错误 helper;`tests.rs` 承接原 puzzle 单测。
- 2026-05-21 追加决策:拼图 HTTP/BFF handler 不再直接提取完整 `AppState`,统一通过 `PuzzleApiState` 暴露拼图能力需要的 SpacetimeDB facade、gallery cache、OSS、作者查询、LLM 和少量配置快照。`modules/puzzle.rs` 仍接收全局 `AppState` 以挂接鉴权和回到全局路由树,但内部路由先 `.with_state(PuzzleApiState::from_ref(&state))`handler 使用 `State<PuzzleApiState>`。确需复用计费、外部失败审计等仍要求 `AppState` 的横切 helper 时,先经 `PuzzleApiState::root_state()` 显式过渡,后续再继续收窄。
- 边界:本次只改变 `api-server` 内部文件组织,不改变 `/api/runtime/puzzle/*` 路由、DTO、error envelope、SpacetimeDB schema、公开 gallery cache 语义或计费语义。领域规则后续仍应逐步沉到 `module-puzzle`SpacetimeDB 表、reducer、procedure 和 row shape 仍留在 `spacetime-module`
- 影响范围:`server-rs/crates/api-server/src/puzzle/``server-rs/crates/api-server/src/modules/puzzle.rs` 的 handler 引用、后端架构文档。
- 影响范围:`server-rs/crates/api-server/src/state.rs``server-rs/crates/api-server/src/puzzle/``server-rs/crates/api-server/src/modules/puzzle.rs` 的 handler 引用、后端架构文档。
- 验证方式:执行 `cargo check -p api-server --manifest-path server-rs\Cargo.toml`;后续若改动 puzzle API 行为,再按对应路由补充定向测试和 `npm run dev:api-server` `/healthz` smoke。
- 关联文档:`docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md`
@@ -716,3 +743,11 @@
- 影响范围:平台入口、推荐流、公开详情、试玩启动、跳一跳运行态、`api-server` / SpacetimeDB 公开投影和 shared contracts。
- 验证方式:从平台推荐或公开详情进入跳一跳作品时,路由 source type 为 `jump-hop`、public code 为 `JH-*`,运行态启动消费后端返回的完整 profile / run 数据;后端 smoke 统一使用 `npm run dev:api-server` 启动并检查 `/healthz`
- 关联文档:`docs/prd/【玩法创作】跳一跳俯视角玩法模板PRD-2026-05-19.md``docs/【玩法创作】平台入口与玩法链路-2026-05-15.md``docs/【开发运维】本地开发验证与生产运维-2026-05-15.md`
# 2026-05-20 陶泥儿主视觉配色回收为暖白/陶土橙
- 背景:用户要求只替换产品各界面的 UI 颜色,不改布局,并以两张陶泥儿主视觉图作为配色依据。
- 决策:平台亮色主题的主色回收到暖白 / 米杏底、陶土橙主按钮、深棕正文与浅杏边框;后台管理也同步切换到同一暖橙体系。主题变量优先通过 `src/index.css``--platform-*` token 统一控制,零散组件只做必要的局部替换。
- 影响范围:主站平台壳层、常用表单 / 按钮 / 卡片 / 背景、后台管理 UI、业务进度条和小游戏结果条的通用强调色。
- 验证方式:优先检查 `src/index.css``apps/admin-web/src/styles/admin.css` 是否还存在旧粉色主色;再用编码检查和可执行的本地 typecheck / build 验证。
- 关联文档:`docs/【项目基线】当前产品与工程约束-2026-05-15.md`
@@ -112,6 +112,24 @@ SpacetimeDB bindings 生成:
npm run spacetime:generate
```
CodeGraph 本地语义索引:
```bash
npm run codegraph:init
npm run codegraph:status
npm run codegraph:sync
npm run codegraph:index
```
`.codegraph/config.json` 可随仓库共享;`.codegraph/codegraph.db`、缓存和日志为本机生成物,不提交。
Codex 项目级 hook 保存在 `.codex/config.toml``.codex/hooks/`
- `PreToolUse` hook`node .codex/hooks/pre-submit-compile-check.mjs`Codex 准备执行 `git commit` 前检查 `npm run typecheck``npm run admin-web:typecheck``cargo check -p api-server --manifest-path server-rs/Cargo.toml`
- `PostToolUse` hook`node .codex/hooks/post-edit-codegraph-sync.mjs`,工具修改文件后执行 `npm run codegraph:sync`
个人 token、模型路由、MCP server 仍属于个人环境;需要时由成员本机执行 `codegraph install` 或查看 `codegraph install --print-config codex`,不要提交个人全局配置。
## 常用检查命令
- 后端通用用户行为埋点统一通过 `record_tracking_event_and_return` procedure、`SpacetimeRuntimeClient::record_tracking_event(...)` 与 api-server `tracking` 中间件写入 `tracking_event` / `tracking_daily_stat`;后台、RPG、大鱼吃小鱼、Visual Novel、Story、Combat 默认排除;作品级游玩埋点统一使用 `work_play_start`,详细事件清单见 `docs/technical/BACKEND_TRACKING_EVENT_COVERAGE_2026-05-09.md`
+25 -1
View File
@@ -46,6 +46,30 @@
- 验证:普通 route 请求在 SpacetimeDB 不可用时仍能返回,恢复后 sealed 文件会继续被清理。
- 关联:`server-rs/crates/api-server/src/tracking_outbox.rs``docs/【开发运维】本地开发验证与生产运维-2026-05-15.md`
## release tracking outbox 权限错误先查 env 缺失
- 现象:release 机器 `journalctl -u genarrative-api.service` 每秒刷 `tracking outbox 定时封存 active 文件失败 error=Permission denied (os error 13)``tracking outbox 批量写入 SpacetimeDB 失败`
- 原因:旧 `/etc/genarrative/api-server.env` 没有 `GENARRATIVE_TRACKING_OUTBOX_DIR` 时,api-server 会回退到本地开发默认相对路径 `server-rs/.data/tracking-outbox`;systemd 工作目录是只读发布目录 `/opt/genarrative/releases/<version>``genarrative` 用户不能在其中创建 `server-rs`
- 处理:补齐 `GENARRATIVE_TRACKING_OUTBOX_DIR=/var/lib/genarrative/tracking-outbox` 及 batch/flush/max 配置,创建并授权 `/var/lib/genarrative/tracking-outbox``genarrative:genarrative`,再重启 `genarrative-api.service`。Server-Provision 与 API-Deploy 会保留旧 env 但自动补缺这些运行态路径。
- 验证:`tr '\0' '\n' < /proc/$(systemctl show genarrative-api.service -p MainPID --value)/environ | grep GENARRATIVE_TRACKING_OUTBOX_DIR` 应指向 `/var/lib/genarrative/tracking-outbox`;重启后当前 PID 不再出现 `Permission denied (os error 13)`
- 关联:`scripts/deploy/production-api-deploy.sh``scripts/jenkins-server-provision.sh``docs/【开发运维】本地开发验证与生产运维-2026-05-15.md`
## 外部 API 失败没法追溯先查 external_api_call_failure
- 现象:VectorEngine 图片生成 / 编辑接口对前端只表现为 `502` / `504` 或“上游服务请求失败”,但难以区分是请求发送失败、上游 429/5xx、响应解析失败、未返回图片,还是下载图片失败。
- 原因:外部 API 失败如果只靠普通日志,不一定能和 OTLP 指标、trace 与 SpacetimeDB 历史查询稳定关联;重启后也容易丢失上下文。
- 处理:先查 OTLP 指标 `genarrative.external_api.failures{provider,failure_stage,status_class,retryable}`,再查 `tracking_event``event_key = 'external_api_call_failure'``metadata_json`。当前通用 VectorEngine `gpt-image-2-all` 适配器会记录 provider、endpoint、operation、failureStage、statusCode、statusClass、timeout、retryable、errorMessage、latencyMs、promptChars、referenceImageCount、imageModel 和 rawExcerpt。
- 验证:`SELECT event_id, scope_id AS provider, metadata_json, occurred_at FROM tracking_event WHERE event_key = 'external_api_call_failure' ORDER BY occurred_at DESC LIMIT 50;`;如果查不到同时看 tracking outbox 目录权限和 sealed 文件是否堆积。
- 关联:`server-rs/crates/api-server/src/external_api_audit.rs``server-rs/crates/api-server/src/openai_image_generation.rs``docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md``docs/【开发运维】本地开发验证与生产运维-2026-05-15.md`
## release 创作接口 413 先查是否还在提交 Data URL
- 现象:release 上 `POST /api/runtime/puzzle/agent/sessions/{session_id}/actions` 携带参考图 Data URL 时返回 `413 Request Entity Too Large`access log 显示 `request_time=0.000``upstream_status=-`
- 原因:Nginx 默认 `client_max_body_size` 只有 1 MiB,请求在反代层被拒绝,根本没有到达 `api-server`;即使模板放宽到 `64m`,把图片 base64 放进创作 JSON body 仍会放大请求体并把上限问题推给下一层。
- 处理:长期修复不是继续调大 Nginx,而是让浏览器先走 `/api/assets/direct-upload-tickets` 直传 OSS,再 `/api/assets/objects/confirm` 确认 `asset_object`,拼图 action 只提交 `referenceImageAssetObjectId(s)`;后端校验 owner / bucket / kind / MIME / size 后签只读 URL 给 VectorEngine。Nginx `client_max_body_size 64m` 只保留为旧客户端和兼容输入兜底,发布后仍需 `nginx -t && nginx -s reload`
- 验证:前端 action payload 不应再出现大段 `data:image/...;base64``nginx -T 2>/dev/null | grep client_max_body_size` 可确认反代兜底;再次提交参考图时 access log 应有正常 `upstream_status`,后端测试 `puzzle_reference_image_sources_prefer_asset_object_ids` / `puzzle_asset_object_reference_requires_matching_owner` 应通过。
- 关联:`src/services/puzzle-works/puzzleAssetClient.ts``server-rs/crates/api-server/src/puzzle/vector_engine.rs``deploy/nginx/genarrative.conf``deploy/nginx/genarrative-dev-http.conf``deploy/container/nginx.conf``docs/【玩法创作】平台入口与玩法链路-2026-05-15.md``docs/【开发运维】本地开发验证与生产运维-2026-05-15.md`
## 汪汪声浪入口不要再回到独立配置阶段
- 现象:汪汪声浪入口如果继续切换到独立配置阶段,会和拼图、抓大鹅的创作页内嵌结构不一致,用户会感觉入口跳页。
@@ -860,7 +884,7 @@
- 现象:抓大鹅结果页看似有容器生成入口,但真实生成出的局内容器不像 `pot-fused-reference.png`,或进入试玩后仍被默认圆形锅壳、金色边框和径向底色覆盖/裁切。
- 原因:`/v1/images/generations``image` 数组更适合弱参考文生图,难以稳定锁定大尺寸轻俯视容器构图;即使生成了容器图,如果运行态继续保留默认 `rounded-full` 锅壳和 `overflow-hidden`,生成图也会被默认视觉覆盖或裁掉。
- 处理:抓大鹅 `1:1` 容器 UI 图必须用 VectorEngine `POST /v1/images/edits` multipart `public/match3d-background-references/pot-fused-reference.png` 作为 `image` part 上传;共享 GPT-image-2 HTTP client 承载 multipart 时强制 HTTP/1.1。`Match3DRuntimeShell` 在容器图换签并成功加载后,把棋盘外壳切为透明和 `overflow-visible`,只在容器缺失或加载失败时使用默认圆形容器。
- 处理:抓大鹅 `1:1` 容器 UI 图必须用 VectorEngine `POST /v1/images/edits` multipart参考 `public/match3d-background-references/pot-fused-reference.png` 的透明容器图作为 `image` part该参考图属于后端生图协议输入,需通过 `include_bytes!` 编译进 `api-server`,不能在运行时按当前工作目录读取 `public/`共享 GPT-image-2 HTTP client 承载 multipart 时强制 HTTP/1.1。`Match3DRuntimeShell` 在容器图换签并成功加载后,把棋盘外壳切为透明和 `overflow-visible`,只在容器缺失或加载失败时使用默认圆形容器。
- 验证:执行 `cargo test -p api-server vector_engine --manifest-path server-rs/Cargo.toml``cargo test -p api-server match3d_background --manifest-path server-rs/Cargo.toml``npm run test -- src/components/match3d-runtime/Match3DRuntimeShell.test.tsx src/components/match3d-result/Match3DResultView.test.tsx`;真实联调看容器生成请求是否命中 `/v1/images/edits`,局内 `match3d-container-image` 是否渲染且 `match3d-board` 不再含默认 `rounded-full`
- 关联:`server-rs/crates/api-server/src/openai_image_generation.rs``server-rs/crates/api-server/src/match3d.rs``src/components/match3d-runtime/Match3DRuntimeShell.tsx``docs/technical/MATCH3D_DRAFT_ASSET_GENERATION_PIPELINE_2026-05-10.md`
File diff suppressed because it is too large Load Diff
+2
View File
@@ -170,6 +170,8 @@ http {
location ~ ^/api(?:/|$) {
default_type application/json;
# 中文注释:创作接口会携带参考图 Data URLNginx 只放行到 api-server;真实大小限制仍由路由 DefaultBodyLimit 和业务字节校验负责。
client_max_body_size 64m;
limit_conn genarrative_api_conn 64;
limit_req zone=genarrative_api_rps burst=64 nodelay;
+6
View File
@@ -2,6 +2,12 @@
本配置片段由 `scripts/jenkins-server-provision.sh` 在安装 Nginx 站点配置时展开。
## 请求体大小
- 生产、开发服和容器模板都在通用 `location ~ ^/api(?:/|$)` 内设置 `client_max_body_size 64m`
- 该值只用于让携带参考图 Data URL 的创作接口抵达 `api-server`;不要把它当作业务上传上限。Rust 路由仍通过 `DefaultBodyLimit` 和解码后字节校验限制具体接口,例如拼图参考图路由只放宽到 12 MiB 请求体,图片字节继续按业务规则拒绝。
- 若线上看到 `413 Request Entity Too Large`,并且 access log 里 `request_time=0.000 upstream_status=-`,通常是 Nginx 没有加载该模板或未 reload;先执行 `nginx -T | grep client_max_body_size``nginx -t` 再检查 `api-server`
## gzip
- `deploy/nginx/genarrative.conf``deploy/nginx/genarrative-dev-http.conf` 默认开启 gzip。
+2
View File
@@ -190,6 +190,8 @@ server {
# 临时兼容主站仍在使用的 /api/* HTTP facade;前端完成 SpacetimeDB SDK 迁移后删除。
location ~ ^/api(?:/|$) {
default_type application/json;
# 中文注释:创作接口会携带参考图 Data URLNginx 只放行到 api-server;真实大小限制仍由路由 DefaultBodyLimit 和业务字节校验负责。
client_max_body_size 64m;
limit_conn genarrative_api_conn 64;
limit_req zone=genarrative_api_rps burst=64 nodelay;
+2
View File
@@ -210,6 +210,8 @@ server {
# 临时兼容主站仍在使用的 /api/* HTTP facade;前端完成 SpacetimeDB SDK 迁移后删除。
location ~ ^/api(?:/|$) {
default_type application/json;
# 中文注释:创作接口会携带参考图 Data URLNginx 只放行到 api-server;真实大小限制仍由路由 DefaultBodyLimit 和业务字节校验负责。
client_max_body_size 64m;
limit_conn genarrative_api_conn 64;
limit_req zone=genarrative_api_rps burst=64 nodelay;
@@ -78,13 +78,16 @@ npm run check:server-rs-ddd
1. 每个能力 Module 只暴露 `router(state) -> Router<AppState>`,由 `app.rs` 统一 `.merge(...)`
2. `app.rs` 只保留全局 middleware、TraceLayer、request context、tracking middleware、入口开关和少量顶层 glue。
3. 路由迁移和业务重构分阶段处理;先移动路由装配,再拆 handler 内部实现
4. 大 handler 拆分时优先按 `router.rs``handlers.rs``application.rs``assets.rs``mapper.rs``errors.rs` 分层。`handlers.rs` 只做 Axum extract、鉴权和 request/response,业务规则继续下沉到 `module-*`
5. 手写 Rust 模块入口统一使用同名 `.rs` 文件,例如 `puzzle.rs` + `puzzle/*.rs``match3d.rs` + `match3d/*.rs`;不要再新增 `mod.rs` 入口。生成的 SpacetimeDB Rust bindings 也由生成脚本同步为 `module_bindings.rs` + `module_bindings/*.rs` 布局
3. 能力 Module 可在路由内部用 `FromRef<AppState>` 派生自己的 Feature State,例如 `PuzzleApiState`。全局 `AppState` 仍作为进程组合根、鉴权层和全局中间件状态,但业务 handler 优先只提取对应 Feature State,不直接暴露完整 `AppState`
4. Feature State 只暴露该能力实际需要的 facade / adapter / 配置快照;若必须复用仍要求 `AppState` 的横切 helper(例如计费、外部失败审计或通用 tracking),应通过 Feature State 的窄方法或显式 `root_state()` 过渡,并在后续继续收窄
5. 路由迁移和业务重构分阶段处理;先移动路由装配,再拆 handler 内部实现,再收窄 handler 可见状态
6. 大 handler 拆分时优先按 `router.rs``handlers.rs``application.rs``assets.rs``mapper.rs``errors.rs` 分层。`handlers.rs` 只做 Axum extract、鉴权和 request/response,业务规则继续下沉到 `module-*`
7. 手写 Rust 模块入口统一使用同名 `.rs` 文件,例如 `puzzle.rs` + `puzzle/*.rs``match3d.rs` + `match3d/*.rs`;不要再新增 `mod.rs` 入口。生成的 SpacetimeDB Rust bindings 也由生成脚本同步为 `module_bindings.rs` + `module_bindings/*.rs` 布局。
拼图 `api-server` 内部拆分:
- `server-rs/crates/api-server/src/modules/puzzle.rs` 只负责路由装配、鉴权层和参考图 body limit;对外继续引用同一批 handler 名称。
- `server-rs/crates/api-server/src/state.rs` 中的 `PuzzleApiState` 是拼图 HTTP/BFF 的 Feature State,集中暴露 `SpacetimeClient``PuzzleGalleryCache`、OSS client、作者查询所需认证服务、拼图 LLM client 和少量 VectorEngine / Agent 配置快照。拼图 handler 只提取 `State<PuzzleApiState>`,不得重新改回 `State<AppState>`
- `server-rs/crates/api-server/src/puzzle.rs` 只作为聚合入口,保留共享 import / 常量、内部模块声明和 handler re-export,不继续承载大段实现。
- `server-rs/crates/api-server/src/puzzle/handlers.rs` 承接 Axum handler,负责 extract、鉴权上下文、调用 SpacetimeDB facade / 编排 helper,并返回 HTTP/SSE 响应。
- `server-rs/crates/api-server/src/puzzle/draft.rs` 承接表单草稿保存、草稿编译、首关命名、UI 背景 prompt、降级 snapshot 和初始资产就绪校验。
@@ -115,7 +118,8 @@ npm run check:server-rs-ddd
3. Adapter 输出应保留 legacy public path、object key、asset object id、MIME、extension、task id 和实际 prompt。
4. Adapter 不负责扣费、退款或钱包读取;计费仍由调用方显式包裹。
5. Puzzle、Match3D、音频、GLB、视频等复杂媒体可以复用 OSS + asset object + binding 的底层持久化能力,但玩法专属处理规则留在各自编排层,不塞进公共接口。
6. 系列素材图集使用 `server-rs/crates/api-server/src/generated_asset_sheets.rs`:调用方必须传入 `grid_size` 作为 `n*n``n`,可选传入物品名称 prompt 模板和特殊设定 prompt;模块负责 sheet prompt 组装、按 `n*n` 切片、透明化、PNG 输出、OSS private upload 请求构造和 sheet / item / special prompt 元数据持久化。玩法只负责规划 slot、调用具体生图 provider、计费、失败回写,以及把通用切片结果映射回自己的 DTO / 草稿 / runtime 字段
6. 拼图图生图参考图主链不得再把大图 Data URL 塞进创作 JSON body;前端先直传 OSS 并提交 `referenceImageAssetObjectId(s)``api-server` 校验 `asset_object` 的 bucket、kind、图片 MIME、大小和 owner 后签发只读 URL 给 VectorEngine 读取,Data URL / `/generated-*` 仅作为旧请求兼容
7. 系列素材图集使用 `server-rs/crates/api-server/src/generated_asset_sheets.rs`:调用方必须传入 `grid_size` 作为 `n*n``n`,可选传入物品名称 prompt 模板和特殊设定 prompt;模块负责 sheet prompt 组装、按 `n*n` 切片、透明化、PNG 输出、OSS private upload 请求构造和 sheet / item / special prompt 元数据持久化。玩法只负责规划 slot、调用具体生图 provider、计费、失败回写,以及把通用切片结果映射回自己的 DTO / 草稿 / runtime 字段。
## SpacetimeDB schema 变更规则
@@ -155,10 +159,11 @@ npm run check:server-rs-ddd
- 图片生成:VectorEngine / APIMart / DashScope,密钥只在后端环境变量中。
- Match3D 物品 sheetVectorEngine Gemini `gemini-3-pro-image-preview` 原生 `generateContent`;图集 prompt、切图、透明化和切片持久化走 `generated_asset_sheets` 通用模块,Match3D 只补题材 / 风格 / 五视角设定和字段映射。
- Match3D 封面和 9:16 纯背景:VectorEngine `/v1/images/generations`
- Match3D 1:1 容器 UIVectorEngine `/v1/images/edits` multipart 参考图。
- Match3D 1:1 容器 UIVectorEngine `/v1/images/edits` multipart 参考图。该容器参考图是后端生图协议输入,必须通过 `include_bytes!``api-server` 编译进二进制,避免 API 单独发布或运行目录缺少 `public/` 时生成失败。
- Hyper3D / Rodin:只保留后端安全代理和旧数据兼容;新 Match3D 草稿和批量新增不再生成 GLB。
- 音频:视觉小说专用音频路由保留;拼图和抓大鹅生成入口暂时关闭,通用 `/api/creation/audio/*` 对相关目标返回 `410 Gone`;敲木鱼 `hit_sound` 目标例外开放,复用 VectorEngine Vidu 音效生成、OSS 私有对象、`asset_object` 和 entity binding 链路,目标字段固定为 `entityKind='wooden_fish_work'``slot='hit_sound'``assetKind='wooden_fish_hit_sound'``storagePrefix='wooden_fish_assets'`
- OSS:私有 generated legacy path 进入浏览器前必须通过 `/api/assets/read-url` 换签;不要裸请求 `/generated-*`
- 外部 API 失败审计:外部供应商调用未成功时,`api-server` 必须发送 OTLP 失败事件并写入 `tracking_event`。当前通用 VectorEngine `gpt-image-2-all` 图片生成 / 编辑适配器在 `request_send``response_body``upstream_status``response_parse``missing_image``image_download` 阶段失败时记录 `external_api_call_failure``scope_kind = module``scope_id = provider``module_key = external-api`metadata 固定包含 provider、endpoint、operation、failureStage、statusCode、statusClass、timeout、retryable、errorMessage、latencyMs、promptChars、referenceImageCount 和 imageModel。入库优先复用 tracking outboxoutbox 不可写或保护阈值拒绝时回退同步写 SpacetimeDB;不得新增前端兜底或在 SpacetimeDB reducer 内做外部 I/O。
## SpacetimeDB 表目录
@@ -707,6 +712,7 @@ npm run check:server-rs-ddd
- Rust 结构体:`TrackingEvent`
- 源码:`server-rs/crates/spacetime-module/src/runtime/profile.rs`
- 写入:关键业务埋点同步调用单条 procedure;普通 HTTP route tracking 由 `api-server` 本机 outbox 批量调用 `record_tracking_events_and_return`。outbox 到达批量阈值时先封存 active 文件并切新 active,后台 worker 异步 flush sealed 文件,HTTP 请求线程不等待 SpacetimeDB。`FLUSH_INTERVAL_MS` 只负责兜底封存长时间未满批的 active 文件,`MAX_BYTES` 只做磁盘保护阈值。`event_id` 必须稳定且全局唯一,批量重试时用唯一索引做幂等跳过。
- 外部 API 失败:`event_key = external_api_call_failure` 使用同一张表落库;它是供应商失败审计事实,不新增 SpacetimeDB 表,查询时按 `module_key = 'external-api'``scope_kind = module AND scope_id = '<provider>'` 过滤。
### `treasure_record`
@@ -98,6 +98,33 @@ SpacetimeDB bindings
npm run spacetime:generate
```
## CodeGraph 本地代码索引
项目已安装 `@colbymchenry/codegraph` 作为开发期依赖,用于在本地生成语义代码索引,辅助 AI / IDE 做符号搜索、调用关系和影响范围分析。索引目录为 `.codegraph/`,其中 `config.json` 可提交,数据库、缓存和日志由 `.codegraph/.gitignore` 保持本机私有。
首次拉取或需要重建索引时:
```bash
npm install
npm run codegraph:init
```
日常使用:
```bash
npm run codegraph:status
npm run codegraph:sync
npm run codegraph:index
```
Codex 项目级 hook 已放在 `.codex/config.toml``.codex/hooks/`
- `PreToolUse` hook 会在 Codex 准备执行 `git commit` 前运行 `node .codex/hooks/pre-submit-compile-check.mjs`,依次执行 `npm run typecheck``npm run admin-web:typecheck``cargo check -p api-server --manifest-path server-rs/Cargo.toml`,发现编译错误会阻止本次提交。
- `PostToolUse` hook 会在 Codex 工具修改文件后运行 `node .codex/hooks/post-edit-codegraph-sync.mjs`,执行 `npm run codegraph:sync` 刷新本地语义索引。
- 如果某个 Codex 客户端版本尚未自动加载项目级 hook,可先手动运行 `node .codex/hooks/pre-submit-compile-check.mjs``node .codex/hooks/post-edit-codegraph-sync.mjs`;个人模型、token、MCP server 仍放在个人 `~/.codex/config.toml`,不要提交。
若要把 CodeGraph 接到 Codex CLI / Cursor / Claude Code 等 MCP 客户端,按本机 agent 配置执行 `codegraph install` 或参考 `codegraph install --print-config codex` 输出;不要把个人全局 agent 配置、token 或本机绝对路径提交到仓库。
## 后端改动验收
后端代码修改后,按变更范围选择:
@@ -157,7 +184,7 @@ Windows Stdb module 构建流水线运行在 Jenkins `windows` 节点上。该
生产环境变量模板:`deploy/env/api-server.env.example`。真实密钥只放服务器,不提交 Git,不写入文档示例。
`Genarrative-Server-Provision` 会安装基础构建依赖、systemd 模板和 Nginx 站点模板。Ubuntu / apt 目标机会额外安装 `libnginx-mod-http-brotli-filter``libnginx-mod-http-brotli-static`,随后由 `scripts/jenkins-server-provision.sh` 通过临时 `nginx -t` 配置探测 Brotli 指令是否可用;该临时配置必须先 `include /etc/nginx/modules-enabled/*.conf`,因为 apt 安装的 Brotli 是动态模块,不会出现在普通 `nginx -V` 编译参数里。探测成功才在渲染后的 `deploy/nginx/genarrative.conf` / `genarrative-dev-http.conf` 中启用 Brotli,避免未安装模块的机器直接写入无效配置。
`Genarrative-Server-Provision` 会安装基础构建依赖、systemd 模板和 Nginx 站点模板。Ubuntu / apt 目标机会额外安装 `libnginx-mod-http-brotli-filter``libnginx-mod-http-brotli-static`,随后由 `scripts/jenkins-server-provision.sh` 通过临时 `nginx -t` 配置探测 Brotli 指令是否可用;该临时配置必须先 `include /etc/nginx/modules-enabled/*.conf`,因为 apt 安装的 Brotli 是动态模块,不会出现在普通 `nginx -V` 编译参数里。探测成功才在渲染后的 `deploy/nginx/genarrative.conf` / `genarrative-dev-http.conf` 中启用 Brotli,避免未安装模块的机器直接写入无效配置。Provision 写入 Genarrative Nginx 站点时会把 `/etc/nginx/sites-enabled/default*` 移到 `/etc/nginx/sites-disabled/`,避免 Debian / Certbot 默认站点继续占用 `genarrative.world` / `www.genarrative.world` 并在 `nginx -T` 中出现 `conflicting server name ... ignored`。如果 `nginx -t` 失败,脚本会恢复写入前的 Genarrative 配置和被移动的默认站点。
50 HTTP req/s 首版压测优化口径:
@@ -168,7 +195,7 @@ Windows Stdb module 构建流水线运行在 Jenkins `windows` 节点上。该
- Windows 下载阶段如果出现 `curl: (18)` 或响应体截断,流水线会保留同名 `.download` 临时文件并用 `curl -C -` 断点续传;只有完整返回但 SHA256 digest 仍不匹配时才删除临时文件后重新下载。目标 Linux 节点仍只接收 `stash/unstash` 带过去的本地下载件,不回退外网下载。
- Windows 下载阶段如果走代理,在 `Genarrative-Server-Provision` 参数 `PROVISION_DOWNLOAD_PROXY` 填写 Windows Jenkins 节点可访问的 HTTP 代理,例如 `http://127.0.0.1:7890`;不要填写目标 release 机器视角的 `127.0.0.1`,除非代理确实运行在该 Windows 节点本机。Linux 目标机阶段会强制要求使用本地下载件,缺少文件直接失败,不再回退到外网下载。
- `otelcol-contrib.service` 作为可选系统服务加入 provision,默认监听 `127.0.0.1:4317/4318` 并使用 `deploy/otelcol/genarrative-debug.yaml`。api-server 是否发送 OTLP 仍由 `GENARRATIVE_OTEL_ENABLED` 控制,服务 unit 见 `deploy/systemd/otelcol-contrib.service`
- Nginx `/api/``/admin/api/` 通过 `genarrative_api` upstream 代理到 `127.0.0.1:8082`upstream keepalive 为 64`limit_conn` 负责连接 / 并发保护,`limit_req` 负责入口 RPS 快拒绝。当前模板把公开 gallery list 单独放到 `genarrative_gallery_rps`,默认 `rate=5000r/s``burst=4096``limit_conn=320`;公开详情和普通 API 放到 `genarrative_api_rps`,后台 API 放到 `genarrative_admin_rps`。压测时看 `/var/log/nginx/genarrative.access.log` 中的 `request_time``upstream_connect_time``upstream_header_time``upstream_response_time``upstream_status``request_id`
- Nginx `/api/``/admin/api/` 通过 `genarrative_api` upstream 代理到 `127.0.0.1:8082`upstream keepalive 为 64`limit_conn` 负责连接 / 并发保护,`limit_req` 负责入口 RPS 快拒绝。当前模板把公开 gallery list 单独放到 `genarrative_gallery_rps`,默认 `rate=5000r/s``burst=4096``limit_conn=320`;公开详情和普通 API 放到 `genarrative_api_rps`,后台 API 放到 `genarrative_admin_rps`通用 `/api` location 设置 `client_max_body_size 64m` 只是反代兜底,防止旧客户端或兼容请求在到达 `api-server` 前被默认 1 MiB 上限拦截;长期主链不得依赖大 JSON body 承载图片,拼图参考图应先直传 OSS,只向创作接口提交 `referenceImageAssetObjectId(s)`,由后端签只读 URL 给外部模型读取。真实业务上限仍由 Rust 路由 `DefaultBodyLimit`、资产确认时 OSS HEAD 和解码后字节校验控制。若线上出现 `413 Request Entity Too Large` 且 access log 中 `request_time=0.000``upstream_status=-`,说明请求在 Nginx 层被拦截,先用 `nginx -T | grep client_max_body_size` 检查 release 模板是否已渲染并 reload,同时检查前端是否仍在提交 Data URL 而不是 `assetObjectId``limit_conn_status 429``limit_req_status 429` 必须在 HTTP 与 HTTPS server 中同时生效;若线上压测看到 `limiting connections by zone "genarrative_api_conn"` 却返回 503,优先检查 `nginx -T` 里 HTTPS server 是否缺少这些状态码,以及 `/api/runtime/puzzle/gallery` 是否误落到通用 `location ~ ^/api``limit_conn=64`压测时看 `/var/log/nginx/genarrative.access.log` 中的 `request_time``upstream_connect_time``upstream_header_time``upstream_response_time``upstream_status``request_id`
- 作品列表 K6 脚本一次 iteration 默认请求两个公开接口,因此约 50 HTTP req/s 的目标命令使用 `SCENARIO=spike START_RPS=5 PEAK_RPS=25 HOLD=60s END_RPS=5 DETAIL_RATIO=0 npm run loadtest:k6:works`
- 作品列表短期继续由 `api-server` / BFF 订阅 SpacetimeDB 公开 read model 后读本地 cache,不让浏览器前端直接订阅完整列表;未来如新增 `public_work_gallery_entry` 等专用公开作品列表 read model,前端只可订阅稳定、低基数、公开的专用投影,禁止订阅 `puzzle_work_profile``custom_world_profile` 等玩法源表后自行 join、聚合或判断权限。前端直订阅落地前必须先补齐权限、字段契约、排序 / 分页、埋点和 BFF 回退策略。
- 50 HTTP req/s 验收目标为 `http_req_failed < 1%``p95 < 2s``dropped_iterations = 0`,同时压测窗口内 Nginx 无新增 502。2026-05-19 容器 2C / 2G 连续 10 轮不重启 SpacetimeDB 压测:`PEAK_RPS=2500` 等价约 5000 HTTP req/s,平均实际吞吐约 `4219 HTTP req/s`10 轮总计 `1,897,357` 个 200、`212,542` 个 429、`0` 个 5xx200 请求平均 `p95=123ms``p99=234ms`;该档会把 SpacetimeDB 容器内存从约 `366MiB` 推到约 `885MiB / 896MiB`,因此当前不要继续抬公开 gallery 入口并发,应优先处理 SpacetimeDB 侧连接 / 订阅 / tracking 写入后的内存高水位。
@@ -197,6 +224,7 @@ OpenTelemetry 现阶段默认开启 OTLP traces / metrics / logs,但本地日
- debug exporter / Rider 转发都会同时接收 traces、metrics 和 logs。
- api-server 会随 metrics 发送进程级指标:`process.memory.usage``process.memory.virtual``process.cpu.time``genarrative.process.cpu.usage_percent``process.thread.count``genarrative.process.memory.private`Windows 额外发送 `process.windows.handle.count`Linux 额外发送 `process.unix.file_descriptor.count`。这些指标只描述当前进程,不携带请求、用户或作品 label。
- HTTP 运行态补充发送 `genarrative.http.server.response_bodies.in_flight``genarrative.http.server.request_permits.available`,后者带低基数 `pool=default|gallery|detail|admin` label,用于区分业务 handler / 背压 permit 是否仍被占用;拼图广场热点缓存补充发送 `genarrative.puzzle_gallery.cache.*` 指标,记录 fresh hit、stale hit、未命中、后台刷新开始 / 失败、重建耗时和预序列化 data JSON 字节数。
- 外部 API 失败统一发送 OTLP 并落库。当前 VectorEngine `gpt-image-2-all` 图片生成 / 编辑失败会输出 `外部 API 调用失败` trace/log,并记录指标 `genarrative.external_api.failures{provider,failure_stage,status_class,retryable}`;同时写入 `tracking_event``event_key = external_api_call_failure``module_key = external-api``scope_kind = module``scope_id = provider`。排障时先按 provider / failureStage 聚合,再结合 request 日志和上游响应 excerpt 判断是限流、超时、解析失败还是未返回图片。
- SpacetimeDB 观测分为两类:procedure / reducer 调用继续用 `genarrative.spacetime.procedure.*`,订阅本地 cache 读使用 `genarrative.spacetime.read.*``read=list_puzzle_gallery` 表示拼图广场当前从 `puzzle_gallery_card_view` 本地 cache 读取,不再每个 HTTP 请求调用 `list_puzzle_gallery` procedure。
- 本地 Windows 直连压测的内存高水位要结合 K6 VU / 连接数解释。250 RPS 下过高 `PREALLOCATED_VUS` 可能让 300 个本地 Established 连接把 `api-server` private memory 瞬时推到 GB 级,且 `/healthz` 小响应也能复现;若压测结束后回落、`response_bodies.in_flight` 和背压 permit 未显示业务积压,应优先按连接 / 发送链路高水位处理,而不是判断为 SpacetimeDB 或 JSON 缓存泄漏。
- Rider 的 Logs 面板只展示 log event 自身字段,不会自动展开父 span 的全部 attributes;请求完成日志会直接带 `request_id``http.request.method``http.route``url.scheme``url.path``http.response.status_code``status_class``latency_ms``slow_request`,完整链路继续到 Traces 面板按 trace/span 查看。
@@ -252,6 +280,16 @@ cargo test -p platform-auth --manifest-path server-rs/Cargo.toml aliyun_send_sms
个人任务首版 scope 仅支持 `user`。后台、RPG、大鱼吃小鱼、Visual Novel、Story、Combat 等特定链路按 tracking 中间件排除规则处理;作品游玩统一使用 `work_play_start`
外部 API 失败审计复用 `tracking_event`,不新增表。失败事件优先写入本机 tracking outbox,再由后台 worker 批量落库;如果 outbox 因权限、磁盘或保护阈值不可写,会回退同步直写 SpacetimeDB。`metadata_json` 包含 endpoint、operation、failureStage、statusCode、statusClass、timeout、retryable、errorMessage、latencyMs、promptChars、referenceImageCount、imageModel 和 rawExcerpt。常用查询:
```sql
SELECT event_id, scope_id AS provider, metadata_json, occurred_at
FROM tracking_event
WHERE event_key = 'external_api_call_failure'
ORDER BY occurred_at DESC
LIMIT 50;
```
tracking outbox 默认配置:
```env
@@ -264,6 +302,17 @@ GENARRATIVE_TRACKING_OUTBOX_MAX_BYTES=268435456
outbox 采用 NDJSON 文件保存原始事件。达到 `BATCH_SIZE` 时会立刻把当前 active 文件原子封存为 sealed 文件,并马上切到新的 active 继续写入;后台 worker 异步 flush sealed 文件,HTTP 请求线程不等待 SpacetimeDB。`FLUSH_INTERVAL_MS` 只负责兜底封存长时间未满批的 active 文件。SpacetimeDB 批量 procedure 返回成功后删除 sealed 文件,失败则保留文件并重试。`MAX_BYTES` 是磁盘保护阈值,不是 flush 阈值;超过后低价值 route tracking 可以被丢弃并记录日志 / 指标,关键同步事件不进入该丢弃路径。sealed 文件若出现无法解析的坏行,会重命名为 `corrupt-*` 隔离并记录 `genarrative.tracking_outbox.files.corrupt` 指标,避免一个坏文件阻塞后续批量入库。该机制提供至少一次投递语义,依赖 `tracking_event.event_id` 幂等跳过重复事件。
release 机器如果日志每秒刷 `tracking outbox ... Permission denied (os error 13)`,先检查 `/etc/genarrative/api-server.env` 是否缺少 `GENARRATIVE_TRACKING_OUTBOX_DIR`。缺少时 `api-server` 会回退到本地开发默认相对路径 `server-rs/.data/tracking-outbox`,而 systemd 的工作目录是只读发布目录 `/opt/genarrative/releases/<version>``genarrative` 用户无法在其中创建 `server-rs`。修复顺序:
```bash
install -d -o genarrative -g genarrative -m 0750 /var/lib/genarrative/tracking-outbox
grep -n '^GENARRATIVE_TRACKING_OUTBOX' /etc/genarrative/api-server.env
systemctl restart genarrative-api.service
journalctl -u genarrative-api.service --since '30 seconds ago' --no-pager | grep -E 'tracking outbox|Permission denied|os error 13'
```
`Genarrative-Server-Provision``Genarrative-Api-Deploy` 会在保留旧 `/etc/genarrative/api-server.env` 的前提下补齐缺失的 tracking outbox 与 auth-store 运行态路径,并确保 `/var/lib/genarrative/tracking-outbox``/var/lib/genarrative/auth` 归属 `genarrative:genarrative`
常用检查思路:
```sql
@@ -46,7 +46,7 @@
- 图像输入复用 `CreativeImageInputPanel`
- 结果页每关画面编辑和素材配置里的 UI 背景生成也复用 `CreativeImageInputPanel`;三处只共享受控 UI 模块,不共享数据源、状态、action 或存储位置:入口页继续写 `formDraft` 与草稿编译 payload,关卡画面写 `levels[].pictureReference/pictureDescription` 并触发 `generate_puzzle_images`UI 背景写 `levels[0].uiBackgroundPrompt/uiBackgroundImage*` 并触发 `generate_puzzle_ui_background`。通用图片面板的展示图和 AI 重绘参考图能力必须分开控制:结果页正式关卡图只作为预览图,不因存在 `displayImageSrc` 自动暴露 AI 重绘开关;只有本地上传、历史选择或已保存 `pictureReference` 可作为重绘参考图时,才显示 AI 重绘开关并把状态带入 `generate_puzzle_images`
- 支持画面描述生图、多参考图生图、上传或历史生成主图后 AI 重绘、上传或历史生成主图后不重绘;关闭 AI 重绘时,前端可提交本地上传 Data URL 历史 `/generated-*` 图片路径,后端统一解析为首关正式图后再持久化
- 支持画面描述生图、多参考图生图、上传或历史生成主图后 AI 重绘、上传或历史生成主图后不重绘;主链要求浏览器先经 `/api/assets/direct-upload-tickets` 直传 OSS 并确认 `asset_object`,创作 action 只提交 `referenceImageAssetObjectId(s)`,由后端换取 OSS 只读签名 URL 给 VectorEngine 读取;本地上传 Data URL 历史 `/generated-*` 图片路径仅保留为旧草稿、旧入口或未迁移客户端的兼容输入
- 草稿生成会先持久化 `generationStatus=generating` 的作品摘要,生成完成并回写关卡图、UI 背景后再变为 `ready`;首关关卡图和 UI 背景在命名稳定后并行启动,当前不自动生成背景音乐。
- 作品架拼图草稿的“生成中”遮罩只表示初始草稿还没有可查看结果;只要作品摘要、首关封面或任一关卡候选图已经可用,后续 UI 背景重生成和追加关卡生图都必须作为结果页局部生成态处理,不能阻止打开草稿结果页。
- 拼图草稿编译是长耗时 action,前端 action 请求默认等待 `1_000_000ms` 且不自动重试,生成页预计完成时间按 `5` 分钟展示;生成页恢复时必须沿用作品摘要 `updatedAt` 作为原始 `startedAtMs`,失败/完成态用 `finishedAtMs` 冻结耗时,不能在锁屏或返回草稿页后重新从 0 计时。
@@ -136,7 +136,7 @@
6. 切图前先在整张 sheet 上做绿幕 / 近白底透明化和边缘去污染,再按格子导出独立 PNG;每个视角图再以扩大的 PNG 边界带为种子,把连通的浅绿 / 近白抗锯齿边直接改为透明,并对贴透明背景的弱绿 / 暗绿轮廓像素做去绿污染处理,最后按剩余可见主体二次收紧;不要先裁剪单格再各自去绿。
7. `generatedItemAssets[].imageViews[]` 是新素材主字段,`imageSrc/imageObjectKey` 只兼容首张视角。
8. 文本生成物品名称时必须同时生成 `itemSize`,只允许 `大``中``小`。该字段随 `generatedItemAssets[].itemSize` 持久化并下发;历史缺失字段的素材按 `大` 兼容,模型缺失或非法值按物品名本地推断。
9. 局内 `9:16` 纯背景图和 `1:1` 中心容器 UI 图分开生成。纯背景不得包含锅、盘、托盘、HUD、按钮、文字或物品,且入库前必须合成为全画幅不透明图片,不允许出现透明区域;容器图走 `/v1/images/edits` 参考透明容器图。
9. 局内 `9:16` 纯背景图和 `1:1` 中心容器 UI 图分开生成。纯背景不得包含锅、盘、托盘、HUD、按钮、文字或物品,且入库前必须合成为全画幅不透明图片,不允许出现透明区域;容器图走 `/v1/images/edits` 参考透明容器图。该容器参考图由后端内嵌到 `api-server`,不要依赖运行时当前目录下的 `public/` 文件。
10. 当前抓大鹅音频生成关闭:入口无 `生成音效`,草稿不生成背景音乐或点击音效,结果页不展示背景音乐 Tab 或点击音效生成入口。历史 `backgroundMusic` / `clickSound` 字段继续兼容传递。
11. UI 背景和容器资产的持久化真相仍在 `generatedItemAssets[].backgroundAsset`Agent session、work summary/detail、结果页和运行态入口都必须把该字段提升为 `backgroundImageSrc/backgroundImageObjectKey/generatedBackgroundAsset` 读取。草稿编译后的 `draftJson` 自身也必须携带 `generatedItemAssets` 快照;HTTP facade 不能只依赖 work detail 回读补齐 UI 资产,外部回读为空时也不得清空草稿内已有的背景 / 容器图。平台壳层从作品架、广场、生成完成回调、结果页保存 / 发布 / 试玩回调进入 Match3D profile 时也要先归一化并提升,避免首次试玩、手动试玩、推荐流或公开详情运行态退回默认背景 / 默认容器。
@@ -94,6 +94,7 @@ server-rs + Axum + SpacetimeDB
8. 图像输入通用 UI 统一走 `src/components/common/CreativeImageInputPanel.tsx`。外层页面持有业务状态,组件只承担上传卡、预览、参考图缩略图、AI 重绘开关、错误展示和提交按钮。
9. 发现页 `分类` 子频道的筛选必须打开独立 dialog / drawer / modal,至少支持玩法类型过滤与排序切换;筛选结果为空时显示空状态,不把筛选内容展开在当前列表下方。
10. “我的”页泥点、游戏时长、玩过三张统计卡只展示各自标签和值,内容居中且不换行,不在统计区底部展示“更新于”时间。
11. 平台亮色 UI 配色以陶泥儿主视觉为准:暖白 / 米杏底、陶土橙主按钮、深棕正文与浅杏边框;新增界面优先复用 `src/index.css``--platform-*` 主题变量和 `apps/admin-web/src/styles/admin.css` 的同系色值,不再引入粉红、蓝绿等独立主色方案。
## 文案与编码
+1058 -3
View File
File diff suppressed because it is too large Load Diff
+6 -1
View File
@@ -57,7 +57,11 @@
"check:data": "node scripts/run-tsx.cjs scripts/validate-content.ts",
"check:overrides": "node scripts/run-tsx.cjs scripts/validate-overrides.ts",
"check:smoke": "node scripts/run-tsx.cjs scripts/smoke-content.ts",
"check:content": "npm run check:data && npm run check:overrides && npm run check:smoke"
"check:content": "npm run check:data && npm run check:overrides && npm run check:smoke",
"codegraph:init": "codegraph init -i .",
"codegraph:index": "codegraph index .",
"codegraph:sync": "codegraph sync .",
"codegraph:status": "codegraph status ."
},
"dependencies": {
"@tailwindcss/vite": "^4.1.14",
@@ -73,6 +77,7 @@
"vite": "^6.2.0"
},
"devDependencies": {
"@colbymchenry/codegraph": "^0.8.0",
"@testing-library/react": "^16.3.2",
"@testing-library/user-event": "^14.6.1",
"@types/node": "^22.14.0",
@@ -52,6 +52,8 @@ export type PuzzleAgentActionRequest =
pictureDescription?: string;
referenceImageSrc?: string | null;
referenceImageSrcs?: string[];
referenceImageAssetObjectId?: string | null;
referenceImageAssetObjectIds?: string[];
imageModel?: string | null;
aiRedraw?: boolean;
}
@@ -63,6 +65,8 @@ export type PuzzleAgentActionRequest =
pictureDescription?: string;
referenceImageSrc?: string | null;
referenceImageSrcs?: string[];
referenceImageAssetObjectId?: string | null;
referenceImageAssetObjectIds?: string[];
imageModel?: string | null;
aiRedraw?: boolean;
candidateCount?: number;
@@ -73,6 +77,8 @@ export type PuzzleAgentActionRequest =
promptText?: string | null;
referenceImageSrc?: string | null;
referenceImageSrcs?: string[];
referenceImageAssetObjectId?: string | null;
referenceImageAssetObjectIds?: string[];
imageModel?: string | null;
aiRedraw?: boolean;
candidateCount?: number;
@@ -51,6 +51,8 @@ export interface CreatePuzzleAgentSessionRequest {
pictureDescription?: string;
referenceImageSrc?: string | null;
referenceImageSrcs?: string[];
referenceImageAssetObjectId?: string | null;
referenceImageAssetObjectIds?: string[];
imageModel?: string | null;
aiRedraw?: boolean;
}
+111
View File
@@ -120,6 +120,115 @@ PY
fi
}
read_env_value() {
local file_path="$1"
local key="$2"
if [[ ! -f "${file_path}" ]]; then
return 0
fi
local python_script='
import sys
from pathlib import Path
path = Path(sys.argv[1])
key = sys.argv[2]
if not path.exists():
raise SystemExit(0)
for raw_line in path.read_text(encoding="utf-8").splitlines():
line = raw_line.strip()
if not line or line.startswith("#") or "=" not in line:
continue
current_key, value = line.split("=", 1)
if current_key == key:
value = value.strip()
if len(value) >= 2 and value[0] == value[-1] and value[0] in ("\"", "'\''"):
value = value[1:-1]
print(value)
raise SystemExit(0)
'
if [[ -r "${file_path}" ]]; then
python3 -c "${python_script}" "${file_path}" "${key}"
else
if ! sudo -n true >/dev/null 2>&1; then
echo "[production-api-deploy] 当前用户无权读取 ${file_path},且 sudo -n 不可用;无法检查运行态环境变量。" >&2
exit 1
fi
sudo -n python3 -c "${python_script}" "${file_path}" "${key}"
fi
}
ensure_env_value() {
local file_path="$1"
local key="$2"
local default_value="$3"
local current_value
current_value="$(read_env_value "${file_path}" "${key}")"
if [[ -n "${current_value}" ]]; then
return
fi
echo "[production-api-deploy] 补齐 api-server 环境变量: ${key} -> ${file_path}"
write_env_value "${file_path}" "${key}" "${default_value}"
}
run_privileged() {
if [[ "$(id -u)" -eq 0 ]]; then
"$@"
return
fi
if ! sudo -n true >/dev/null 2>&1; then
echo "[production-api-deploy] 当前用户不是 root,且 sudo -n 不可用;无法执行: $*" >&2
exit 1
fi
sudo -n "$@"
}
ensure_runtime_dir() {
local path="$1"
local mode="$2"
if [[ -z "${path}" ]]; then
return
fi
if [[ "${path}" != /* ]]; then
echo "[production-api-deploy] 运行态目录必须使用绝对路径,避免写入只读发布目录: ${path}" >&2
exit 1
fi
echo "[production-api-deploy] 确保运行态目录可写: ${path}"
run_privileged install -d -o genarrative -g genarrative -m "${mode}" "${path}"
}
ensure_runtime_env_and_dirs() {
local api_env_file="$1"
local tracking_enabled tracking_outbox_dir auth_store_path auth_store_dir
# 旧生产环境文件会被 server-provision 保留,不一定包含新增的运行态写入路径。
# 发布前只补缺省值,不覆盖线上已经定制过的目录或开关。
ensure_env_value "${api_env_file}" "GENARRATIVE_TRACKING_OUTBOX_ENABLED" "true"
ensure_env_value "${api_env_file}" "GENARRATIVE_TRACKING_OUTBOX_DIR" "/var/lib/genarrative/tracking-outbox"
ensure_env_value "${api_env_file}" "GENARRATIVE_TRACKING_OUTBOX_BATCH_SIZE" "500"
ensure_env_value "${api_env_file}" "GENARRATIVE_TRACKING_OUTBOX_FLUSH_INTERVAL_MS" "1000"
ensure_env_value "${api_env_file}" "GENARRATIVE_TRACKING_OUTBOX_MAX_BYTES" "268435456"
ensure_env_value "${api_env_file}" "GENARRATIVE_AUTH_STORE_PATH" "/var/lib/genarrative/auth/auth-store.json"
tracking_enabled="$(read_env_value "${api_env_file}" "GENARRATIVE_TRACKING_OUTBOX_ENABLED")"
tracking_outbox_dir="$(read_env_value "${api_env_file}" "GENARRATIVE_TRACKING_OUTBOX_DIR")"
if [[ "$(printf "%s" "${tracking_enabled}" | tr '[:upper:]' '[:lower:]')" != "false" ]]; then
ensure_runtime_dir "${tracking_outbox_dir}" "0750"
fi
auth_store_path="$(read_env_value "${api_env_file}" "GENARRATIVE_AUTH_STORE_PATH")"
if [[ -n "${auth_store_path}" ]]; then
auth_store_dir="$(dirname "${auth_store_path}")"
ensure_runtime_dir "${auth_store_dir}" "0750"
fi
}
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
SOURCE_DIR=""
VERSION=""
@@ -243,6 +352,8 @@ if [[ -n "${SPACETIME_SERVER_URL}" ]]; then
write_env_value "${API_ENV_FILE}" "GENARRATIVE_SPACETIME_SERVER_URL" "${SPACETIME_SERVER_URL}"
fi
ensure_runtime_env_and_dirs "${API_ENV_FILE}"
mkdir -p "$(dirname "${CURRENT_LINK}")"
ln -sfn "${RELEASE_DIR}" "${CURRENT_LINK}"
+88 -3
View File
@@ -292,6 +292,42 @@ write_env_value() {
chown root:root "${file}"
}
ensure_env_value() {
local file="$1"
local key="$2"
local default_value="$3"
local current_value
current_value="$(read_env_value "${file}" "${key}")"
if [[ -n "${current_value}" ]]; then
return
fi
echo "[server-provision] 补齐 api-server 环境变量: ${key} -> ${file}"
if [[ "${DRY_RUN}" != "true" ]]; then
write_env_value "${file}" "${key}" "${default_value}"
fi
}
ensure_api_runtime_env_defaults() {
if [[ "${DRY_RUN}" == "true" ]]; then
echo "+ ensure api-server runtime env defaults in ${API_ENV_FILE}"
return
fi
if [[ ! -f "${API_ENV_FILE}" ]]; then
echo "[server-provision] 环境文件不存在,无法补齐 api-server 运行态目录变量: ${API_ENV_FILE}" >&2
exit 1
fi
# 已存在的生产 env 会被保留,不会整文件覆盖;这里仅补后续版本新增的运行态写入路径。
ensure_env_value "${API_ENV_FILE}" "GENARRATIVE_TRACKING_OUTBOX_ENABLED" "true"
ensure_env_value "${API_ENV_FILE}" "GENARRATIVE_TRACKING_OUTBOX_DIR" "/var/lib/genarrative/tracking-outbox"
ensure_env_value "${API_ENV_FILE}" "GENARRATIVE_TRACKING_OUTBOX_BATCH_SIZE" "500"
ensure_env_value "${API_ENV_FILE}" "GENARRATIVE_TRACKING_OUTBOX_FLUSH_INTERVAL_MS" "1000"
ensure_env_value "${API_ENV_FILE}" "GENARRATIVE_TRACKING_OUTBOX_MAX_BYTES" "268435456"
ensure_env_value "${API_ENV_FILE}" "GENARRATIVE_AUTH_STORE_PATH" "/var/lib/genarrative/auth/auth-store.json"
}
parse_json_string_field() {
local json="$1"
local key="$2"
@@ -437,11 +473,55 @@ validate_nginx_tls() {
fi
}
disable_nginx_default_sites_enabled() {
local moves_file="$1"
local sites_enabled="/etc/nginx/sites-enabled"
local sites_disabled="/etc/nginx/sites-disabled"
local stamp source target base
local candidates=("${sites_enabled}/default" "${sites_enabled}/default."*)
stamp="$(date +%Y%m%d%H%M%S)"
for source in "${candidates[@]}"; do
if [[ ! -e "${source}" && ! -L "${source}" ]]; then
continue
fi
base="$(basename "${source}")"
target="${sites_disabled}/${base}.disabled-${stamp}"
echo "[server-provision] 禁用 Debian 默认 Nginx 站点,避免与 Genarrative server_name 冲突: ${source} -> ${target}"
mkdir -p "${sites_disabled}"
mv "${source}" "${target}"
printf "%s\t%s\n" "${target}" "${source}" >>"${moves_file}"
done
}
restore_nginx_default_sites_enabled() {
local moves_file="$1"
local target source
if [[ ! -f "${moves_file}" ]]; then
return
fi
while IFS=$'\t' read -r target source || [[ -n "${target:-}" ]]; do
if [[ -z "${target:-}" || -z "${source:-}" ]]; then
continue
fi
if [[ -e "${target}" || -L "${target}" ]]; then
mkdir -p "$(dirname "${source}")"
if [[ ! -e "${source}" && ! -L "${source}" ]]; then
echo "[server-provision] 恢复 Debian 默认 Nginx 站点: ${target} -> ${source}"
mv "${target}" "${source}"
fi
fi
done <"${moves_file}"
}
install_nginx_config_with_rollback() {
local config_target="/etc/nginx/conf.d/genarrative.conf"
local snippet_target="/etc/nginx/snippets/genarrative-maintenance.conf"
local config_source
local rendered_config rendered_snippet config_backup snippet_backup
local rendered_config rendered_snippet config_backup snippet_backup disabled_sites
local had_config="false"
local had_snippet="false"
@@ -459,6 +539,7 @@ install_nginx_config_with_rollback() {
echo "+ install -m 0644 deploy/nginx/snippets/genarrative-maintenance.conf ${snippet_target}"
if [[ "${DRY_RUN}" == "true" ]]; then
echo "+ disable /etc/nginx/sites-enabled/default* if present"
echo "+ nginx -t"
echo "+ nginx -s reload"
return
@@ -468,6 +549,7 @@ install_nginx_config_with_rollback() {
rendered_snippet="$(mktemp)"
config_backup="$(mktemp)"
snippet_backup="$(mktemp)"
disabled_sites="$(mktemp)"
if [[ "${NGINX_CONFIG_MODE}" == "production-https" ]]; then
validate_nginx_tls
render_nginx_https_config >"${rendered_config}"
@@ -487,6 +569,7 @@ install_nginx_config_with_rollback() {
install -m 0644 "${rendered_config}" "${config_target}"
install -m 0644 "${rendered_snippet}" "${snippet_target}"
disable_nginx_default_sites_enabled "${disabled_sites}"
if ! nginx -t; then
echo "[server-provision] nginx -t 失败,恢复写入前的 Nginx 配置。" >&2
@@ -500,13 +583,14 @@ install_nginx_config_with_rollback() {
else
rm -f "${snippet_target}"
fi
rm -f "${rendered_config}" "${rendered_snippet}" "${config_backup}" "${snippet_backup}"
restore_nginx_default_sites_enabled "${disabled_sites}"
rm -f "${rendered_config}" "${rendered_snippet}" "${config_backup}" "${snippet_backup}" "${disabled_sites}"
exit 1
fi
echo "+ nginx -s reload"
nginx -s reload
rm -f "${rendered_config}" "${rendered_snippet}" "${config_backup}" "${snippet_backup}"
rm -f "${rendered_config}" "${rendered_snippet}" "${config_backup}" "${snippet_backup}" "${disabled_sites}"
}
cleanup_placeholder_nginx_config() {
@@ -625,6 +709,7 @@ if [[ ! -f "${API_ENV_FILE}" ]]; then
else
echo "[server-provision] 已存在环境文件,保留不覆盖: ${API_ENV_FILE}"
fi
ensure_api_runtime_env_defaults
if [[ "${ENABLE_OTELCOL:-true}" == "true" ]]; then
sync_otelcol_install
+17 -4
View File
@@ -394,9 +394,13 @@ pub async fn confirm_asset_object(
let result = state
.spacetime_client()
.confirm_asset_object(
build_confirm_asset_object_upsert_input(oss_client, payload)
.await
.map_err(map_confirm_asset_object_prepare_error)?,
build_confirm_asset_object_upsert_input(
oss_client,
payload,
authenticated.claims().user_id(),
)
.await
.map_err(map_confirm_asset_object_prepare_error)?,
)
.await
.map_err(map_confirm_asset_object_error)?;
@@ -592,6 +596,7 @@ fn supported_asset_history_kind_message() -> String {
async fn build_confirm_asset_object_upsert_input(
oss_client: &platform_oss::OssClient,
payload: ConfirmAssetObjectRequest,
authenticated_owner_user_id: &str,
) -> Result<module_assets::AssetObjectUpsertInput, ConfirmAssetObjectPrepareError> {
let configured_bucket = oss_client.config_bucket().to_string();
let resolved_bucket = payload
@@ -629,6 +634,14 @@ async fn build_confirm_asset_object_upsert_input(
{
return Err(ConfirmAssetObjectPrepareError::ContentLengthMismatch);
}
let owner_user_id = normalize_optional_value(payload.owner_user_id).or_else(|| {
let owner = authenticated_owner_user_id.trim();
if owner.is_empty() {
None
} else {
Some(owner.to_string())
}
});
let now_micros = current_utc_micros();
build_asset_object_upsert_input(
@@ -645,7 +658,7 @@ async fn build_confirm_asset_object_upsert_input(
normalize_optional_value(payload.content_hash),
payload.asset_kind,
payload.source_job_id,
payload.owner_user_id,
owner_user_id,
payload.profile_id,
payload.entity_id,
now_micros,
@@ -1049,6 +1049,7 @@ mod tests {
base_url: "https://vector.example".to_string(),
api_key: "secret".to_string(),
request_timeout_ms: 180_000,
external_api_audit_state: None,
});
assert_eq!(

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