Merge remote-tracking branch 'origin/master' into fix/wrong-report
Project CI / AI game creator shell Rust crates (pull_request) Successful in 1m59s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Failing after 2m9s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 2m23s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Failing after 3m6s
Project CI / Frontend tests (pull_request) Successful in 3m31s
Project CI / Repository checks (pull_request) Successful in 3m34s
Project CI / AI game creator shell web tests (pull_request) Successful in 1m57s
Project CI / Backend tests (pull_request) Successful in 6m48s
Project CI / Native shell tests (pull_request) Successful in 7m29s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 1m59s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Failing after 2m9s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 2m23s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Failing after 3m6s
Project CI / Frontend tests (pull_request) Successful in 3m31s
Project CI / Repository checks (pull_request) Successful in 3m34s
Project CI / AI game creator shell web tests (pull_request) Successful in 1m57s
Project CI / Backend tests (pull_request) Successful in 6m48s
Project CI / Native shell tests (pull_request) Successful in 7m29s
# Conflicts: # docs/project-memory/shared-memory/pitfalls.md
This commit is contained in:
@@ -2,6 +2,7 @@ use super::*;
|
||||
use axum::extract::DefaultBodyLimit;
|
||||
use axum::extract::State as AxumState;
|
||||
use axum::http::{HeaderMap, StatusCode};
|
||||
use axum::response::IntoResponse;
|
||||
use axum::routing::post;
|
||||
use axum::Json;
|
||||
use axum::Router;
|
||||
@@ -2090,7 +2091,7 @@ async fn handle_external_mcp_http_request(
|
||||
AxumState(state): AxumState<ExternalMcpHttpState>,
|
||||
headers: HeaderMap,
|
||||
Json(request): Json<Value>,
|
||||
) -> Result<Json<Value>, StatusCode> {
|
||||
) -> Result<axum::response::Response, StatusCode> {
|
||||
if !external_mcp_authorized(&headers, &state.token) {
|
||||
return Err(StatusCode::UNAUTHORIZED);
|
||||
}
|
||||
@@ -2107,6 +2108,13 @@ async fn handle_external_mcp_http_request(
|
||||
.in_flight
|
||||
.try_acquire()
|
||||
.map_err(|_| StatusCode::TOO_MANY_REQUESTS)?;
|
||||
// JSON-RPC 通知(没有 `id`)没有响应体:MCP 客户端在 `initialize` 之后一定会发
|
||||
// `notifications/initialized`,按错误回 400 会被判成握手失败,整个服务器就被标记
|
||||
// 成 `failed`、一个工具都不广告(真实案例:2026-10-02 的 cc DirectProject 因此
|
||||
// 拿到 0 个工具)。规范要求的应答是 202 Accepted(空体)。
|
||||
if request.get("id").is_none() {
|
||||
return Ok(StatusCode::ACCEPTED.into_response());
|
||||
}
|
||||
let response = EXTERNAL_MCP_BRIDGE_URL
|
||||
.scope(
|
||||
state.bridge_url.clone(),
|
||||
@@ -2114,7 +2122,7 @@ async fn handle_external_mcp_http_request(
|
||||
)
|
||||
.await
|
||||
.ok_or(StatusCode::BAD_REQUEST)?;
|
||||
Ok(Json(response))
|
||||
Ok(Json(response).into_response())
|
||||
}
|
||||
|
||||
pub(crate) async fn start_external_mcp_loopback(
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
# 决策记录
|
||||
## 2026-10-01 Web、后台与 AGC 一键联调
|
||||
|
||||
- 背景:Web、管理后台和 AGC 同时开发时,分别启动入口容易产生两套 API/worker/SpacetimeDB,以及重复后台 Vite。
|
||||
- 决策:新增 `npm run dev:all`,保持 `npm run dev` 现有主站完整栈语义不变;一键入口固定使用 AGC 的 database/data dir,先启动根完整栈,待五个服务就绪后由 AGC 复用该后端,再启动 AGC Vite 与 Tauri,并关闭 AGC 自带后台。
|
||||
- 影响范围:根开发脚本、AGC 开发启动编排、本地开发运维文档;不改变 API、schema、生产部署和独立 `npm run agc` 行为。
|
||||
- 验证方式:参数/状态单测、开发栈健康端点 smoke、`.app/dev-stack.json` 身份复用检查、进程树收束检查。
|
||||
|
||||
## 2026-10-01 AGC 命令错误结构化与错误报告口径
|
||||
|
||||
|
||||
@@ -12,18 +12,22 @@
|
||||
- **Rust 侧不得把结构化错误降级成字符串**:`refresh_session_inner` 的非权威失败直接返回 `Err(ClientAuthError)`,视图不带 `errorMessage`;一旦折成 `String`,前端就只能拿文案判断,变体信息永久丢失。
|
||||
- **关联**:`apps/ai-game-creator-shell/src-tauri/src/auth_session.rs`、`apps/ai-game-creator-shell/src/services/{clientAuth.ts,errorReporting.ts,platformSession.ts}`、`apps/ai-game-creator-shell/src/app/AuthenticatedClient.tsx`。
|
||||
|
||||
## 2026-10-01 Windows `dev:all` 启动 npm 子进程报 `spawn EINVAL`
|
||||
|
||||
- **现象**:`npm run dev:all` 能完成 AGC Vite 端口预留,但在启动根开发栈前输出 `spawn EINVAL`。
|
||||
- **原因**:Windows 的 `npm.cmd` 是批处理入口;Node `child_process.spawn('npm.cmd', args, { shell: false })` 会直接返回 `EINVAL`,还未执行根 `npm run dev`。
|
||||
- **处理**:`scripts/dev-all.mjs` 在 Windows 使用 `shell: true`、`windowsHide: true` 启动 npm 子进程;POSIX 仍使用独立进程组,退出时按进程组收束。
|
||||
- **验证**:Windows 实测根开发栈已启动并完成端口漂移(Web `3001`、API `8084`、worker `8085`、SpacetimeDB `3104`、后台 `3105`),之后 AGC 因当前工作区缺少 `@anthropic-ai/claude-agent-sdk` 退出;dev:all 已收束根栈进程。
|
||||
## 2026-10-02 AGC 页面在自绘标题栏外壳里自己算 `100vh`:底部被裁而且没得滚
|
||||
|
||||
- **现象**:帮助页(使用指南 / 联系客服 / 更新日志)在矮窗口里底部卡片看不到,把窗口拉高才出现;外壳 `.launcher-main { overflow: hidden }` 之下没有任何可滚动祖先,页面既滚不动也裁得干净。首页在通知横幅出现时用 `h-[calc(100vh-32px)]`,同样把窗口高度当成了舞台高度。
|
||||
- **原因**:AGC 桌面外壳是自绘标题栏(`--window-chrome-height`;窗口 100vh=800 时舞台只有 750),页面根节点写 `100vh` / `100dvh` / `calc(100vh - Npx)` 就比真实舞台高一整个标题栏,差额被外壳裁掉;横幅是 `.launcher-main` 里的真实行,再写 `-32px` 等于重复扣一次。帮助页还没有内层滚动容器,连「内容超高就在内部滚动」这条兜底也不存在。
|
||||
- **处理(现行口径)**:页面高度只由外壳分配——`apps/ai-game-creator-shell/src/styles.css` 里 `.launcher-main:has(<页面钩子>)` 是纵向 flex 列(`height: 100dvh`,窗口外壳命中 `height: 100%` 时贴合真实舞台),`.launcher-main > <页面根节点>` 统一 `flex: 1 1 auto; height: auto; min-height: 0`,帮助页这类没有内层滚动容器的再加 `overflow-y: auto`。页面根节点一律不再写 `100vh` / `100dvh` / `calc(100vh - Npx)`;有横幅就靠 flex 自动少一份,不要手算偏移。
|
||||
- **验证**:真机判据是 Vite + Chromium 量页面根节点是否正好等于 `.window-chrome__content` 的高度(1440x800 / 1440x560 / 390x844 / 390x560,带与不带横幅),帮助页应可滚动到底。
|
||||
|
||||
## 2026-10-02 固定试玩误判祖先的指针穿透样式
|
||||
|
||||
- `pointer-events:none` 不会强制禁用整棵子树;后代显式 `auto` 可以恢复命中。控件探针只检查目标的计算样式,继承未覆盖的 `none` 仍拒绝;可见性、遮挡、disabled 与 inert 保留各自检查。
|
||||
- 修复和回归必须经过生产输入入口及可信事件驱动的状态变化,不能用程序化点击证明真实可玩。双视口 generic 回归与真实触摸验收需要区分,详见 AGC 实施计划“固定试玩控件的指针命中边界”。
|
||||
|
||||
## 2026-10-01 Rust 分片编译失败只剩汇总错误
|
||||
|
||||
- **原因**:`--message-format=json` 把编译诊断写到 stdout;只读取 `compiler-artifact` 的运行器会丢弃 `compiler-message`,CI 只能看到「due to 1 previous error」。
|
||||
|
||||
@@ -23,7 +23,7 @@ Genarrative / 陶泥儿当前主站聚焦图片画布创作、编辑器项目与
|
||||
|
||||
## 本地开发端口真相
|
||||
|
||||
`scripts/dev.mjs` 的默认值是 Web `3000`、API `8082`、BgFilter worker `8083`、SpacetimeDB `3101`、后台 Web `3102`。Linux 用户端口段会把这五个服务映射到 `start` 至 `start+4`,AGC Vite 使用 `start+5`;显式端口或端口段配置可覆盖默认值。
|
||||
`scripts/dev.mjs` 的默认值是 Web `3000`、API `8082`、BgFilter worker `8083`、SpacetimeDB `3101`、后台 Web `3102`。Linux 用户端口段会把这五个服务映射到 `start` 至 `start+4`,AGC Vite 使用 `start+5`;显式端口或端口段配置可覆盖默认值。`npm run dev:all` 复用 AGC 的数据库和 data dir 启动一份共享后端,并在根 dev 栈就绪后启动 AGC Vite 与 Tauri;管理后台只由根 dev 栈启动。
|
||||
|
||||
端口的运行时权威始终是当前工作区的 `.app/dev-stack.json` 与启动日志(该文件可能在未启动时不存在),不得从文档默认值推断当前监听端口。前端代理、API URL、SpacetimeDB 地址和 AGC Vite 地址都必须读取同一份运行时状态。
|
||||
|
||||
|
||||
@@ -94,6 +94,15 @@ npm run dev
|
||||
- 后台 Vite。
|
||||
|
||||
`npm run dev` 和单模块 `npm run dev:web`、`npm run dev:api-server`、`npm run dev:bgfilter-worker`、`npm run dev:spacetime`、`npm run dev:admin-web` 启动后都会更新根目录 `.app/dev-stack.json`。该文件记录本次命令、数据库、更新时间,以及 `spacetime`、`api-server`、`bgfilter-worker`、`web`、`admin-web` 的 `pid`、监听 host / port、可访问 URL、启动状态和当前命令;稳定版状态还记录顶层 `repoRoot + instanceId`,每个服务记录 `repoRoot + instanceId + dataDir`,与端口组成复用身份。`.app/` 是本地运行态目录,不提交 Git;端口漂移、服务重启或子进程退出后以该文件里的实际状态为准。缺少身份字段或身份不匹配的旧状态不得被 AGC 静默复用。
|
||||
一键启动 Web、后台与 AGC:
|
||||
|
||||
```bash
|
||||
npm run dev:all
|
||||
```
|
||||
|
||||
`npm run dev:all` 保持 `npm run dev` 的主站完整栈语义不变,使用 AGC 当前配套后端的数据库 `genarrative-game-creator-dev` 与 data dir `server-rs/.spacetimedb/ai-game-creator/data` 启动一份 SpacetimeDB、BgFilter worker、api-server、主站 Vite 和后台 Vite;待这五个服务就绪后,再启动 AGC Vite 与 Tauri。AGC 通过匹配的 `.app/dev-stack.json` 复用这份后端,且设置 `AGC_DEV_ADMIN_WEB=0`,因此一键入口只保留一份管理后台。该入口不接受覆盖 database 或 SpacetimeDB data dir 的参数;需要独立数据库时分别使用 `npm run dev` / `npm run agc`。
|
||||
|
||||
一键入口由自身负责收束根 dev 栈与 AGC 客户端的进程树。AGC Vite marker 可访问后,终端会打印包含 SpacetimeDB、BgFilter worker、api-server、主站、后台、AGC Vite 和 Tauri 窗口的实际地址表;任一子进程异常退出都应停止另一侧并返回非零退出码。端口漂移和运行态地址仍以启动日志及 `.app/dev-stack.json` 为准。
|
||||
|
||||
通过 `nohup` 在仓库根目录启动 dev 栈且未显式重定向 stdout / stderr 时,默认 `nohup.out` 会持续收集 SpacetimeDB、api-server、bgfilter-worker、主站 Vite 和后台 Vite 的整套 dev 栈输出;该文件已被主站 Vite watcher 和 Git 忽略,避免日志追加触发页面刷新循环,重启主站 Vite 后生效。若把输出显式重定向到其它仓库内文件(例如 `> dev.out`),该自定义文件不会自动获得同样的 watcher 保护,应改为写到 Vite root 之外,或同步配置精确的忽略规则。
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
"scripts": {
|
||||
"prepare": "husky",
|
||||
"dev": "node scripts/dev.mjs",
|
||||
"dev:all": "node scripts/dev-all.mjs",
|
||||
"dev:spacetime": "node scripts/dev.mjs spacetime",
|
||||
"dev:api-server": "node scripts/dev.mjs api-server",
|
||||
"dev:bgfilter-worker": "node scripts/dev.mjs bgfilter-worker",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,252 @@
|
||||
import { EventEmitter } from 'node:events';
|
||||
import { createServer } from 'node:http';
|
||||
import type { AddressInfo } from 'node:net';
|
||||
import { resolve } from 'node:path';
|
||||
|
||||
import { describe, expect, test } from 'vitest';
|
||||
|
||||
import {
|
||||
agcDatabase,
|
||||
agcDataDir,
|
||||
buildDevAllBaseEnvironment,
|
||||
buildDevAllEnvironment,
|
||||
buildManagedSpawnOptions,
|
||||
buildRootDevArgs,
|
||||
buildStartupSummary,
|
||||
isHttpReady,
|
||||
isSharedRootStackReady,
|
||||
renderStartupTable,
|
||||
runDevAll,
|
||||
validateDevAllArgs,
|
||||
} from './dev-all.mjs';
|
||||
type FakeChild = EventEmitter & {
|
||||
pid: number;
|
||||
exitCode: number | null;
|
||||
signalCode: string | null;
|
||||
};
|
||||
|
||||
function makeReadyState(root = resolve('fixture-root')) {
|
||||
const instanceId = 'dev-fixture';
|
||||
const service = (status = 'running', url = 'http://127.0.0.1:1') => ({
|
||||
repoRoot: root,
|
||||
instanceId,
|
||||
status,
|
||||
url,
|
||||
});
|
||||
|
||||
return {
|
||||
command: 'all',
|
||||
instanceId,
|
||||
repoRoot: root,
|
||||
database: agcDatabase,
|
||||
spacetimeDataDir: agcDataDir,
|
||||
services: {
|
||||
spacetime: service('reused', 'http://127.0.0.1:3101'),
|
||||
'api-server': service('running', 'http://127.0.0.1:8082'),
|
||||
'bgfilter-worker': service('running', 'http://127.0.0.1:8083'),
|
||||
web: service('running', 'http://127.0.0.1:3000'),
|
||||
'admin-web': service('running', 'http://127.0.0.1:3102/admin/'),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe('dev:all 参数和共享开发栈合同', () => {
|
||||
test('根 dev 固定使用 AGC 配套数据库,并保留调用方选项', () => {
|
||||
expect(buildRootDevArgs(['--watch', '--api-port', '9082'])).toEqual([
|
||||
'run',
|
||||
'dev',
|
||||
'--',
|
||||
'--database',
|
||||
agcDatabase,
|
||||
'--spacetime-data-dir',
|
||||
'server-rs/.spacetimedb/ai-game-creator/data',
|
||||
'--preserve-database',
|
||||
'--no-interactive',
|
||||
'--watch',
|
||||
'--api-port',
|
||||
'9082',
|
||||
]);
|
||||
});
|
||||
|
||||
test('拒绝会破坏单后端拓扑的数据库和 data dir 覆盖', () => {
|
||||
expect(() => validateDevAllArgs(['--database', 'other'])).toThrow(
|
||||
'不能覆盖 --database 或 --spacetime-data-dir',
|
||||
);
|
||||
expect(() => validateDevAllArgs(['--database=other'])).toThrow(
|
||||
'不能覆盖 --database 或 --spacetime-data-dir',
|
||||
);
|
||||
expect(() => validateDevAllArgs(['--spacetime-data-dir', 'other'])).toThrow(
|
||||
'不能覆盖 --database 或 --spacetime-data-dir',
|
||||
);
|
||||
expect(() => validateDevAllArgs(['backend'])).toThrow('只接受 dev 选项');
|
||||
});
|
||||
|
||||
test('预留 AGC Vite 端口并关闭 AGC 自带后台', () => {
|
||||
const baseEnv = { AGC_DEV_ADMIN_WEB: '1' };
|
||||
const env = buildDevAllEnvironment(baseEnv, 3080);
|
||||
|
||||
expect(env.GENARRATIVE_AGC_VITE_PORT).toBe('3080');
|
||||
expect(env.AGC_DEV_ADMIN_WEB).toBe('0');
|
||||
expect(baseEnv.AGC_DEV_ADMIN_WEB).toBe('1');
|
||||
});
|
||||
|
||||
test('CLI port-range 覆盖旧环境并同步给 AGC', () => {
|
||||
const env = buildDevAllBaseEnvironment(['--port-range', '10100-10199'], {
|
||||
GENARRATIVE_DEV_PORT_RANGE: '10000-10099',
|
||||
});
|
||||
|
||||
expect(env.GENARRATIVE_DEV_PORT_RANGE).toBe('10100-10199');
|
||||
});
|
||||
|
||||
test.each([
|
||||
[200, true],
|
||||
[204, true],
|
||||
[299, true],
|
||||
[400, false],
|
||||
[401, false],
|
||||
[403, false],
|
||||
[404, false],
|
||||
[500, false],
|
||||
[503, false],
|
||||
])('HTTP %i 的前端就绪结果为 %s', async (status, expected) => {
|
||||
const server = createServer((_request, response) => {
|
||||
response.writeHead(status).end();
|
||||
});
|
||||
await new Promise<void>((resolveListen) => {
|
||||
server.listen(0, '127.0.0.1', resolveListen);
|
||||
});
|
||||
|
||||
try {
|
||||
const { port } = server.address() as AddressInfo;
|
||||
await expect(isHttpReady(`http://127.0.0.1:${port}`)).resolves.toBe(
|
||||
expected,
|
||||
);
|
||||
} finally {
|
||||
server.closeAllConnections();
|
||||
await new Promise<void>((resolveClose, rejectClose) => {
|
||||
server.close((error) => {
|
||||
if (error) rejectClose(error);
|
||||
else resolveClose();
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test('Windows 下通过 shell 启动 npm.cmd', () => {
|
||||
const options = buildManagedSpawnOptions(process.env);
|
||||
|
||||
expect(options.shell).toBe(process.platform === 'win32');
|
||||
expect(options.windowsHide).toBe(process.platform === 'win32');
|
||||
expect(options.detached).toBe(process.platform !== 'win32');
|
||||
});
|
||||
|
||||
test('按最终端口构造完整启动摘要表', () => {
|
||||
const rows = buildStartupSummary(makeReadyState(), {
|
||||
url: 'http://127.0.0.1:3080/',
|
||||
});
|
||||
|
||||
expect(rows).toHaveLength(7);
|
||||
expect(rows).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
项目: 'SpacetimeDB',
|
||||
状态: '已复用',
|
||||
地址: 'http://127.0.0.1:3101',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
项目: 'Admin 后台',
|
||||
地址: 'http://127.0.0.1:3102/admin/',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
项目: 'AGC Vite',
|
||||
地址: 'http://127.0.0.1:3080/',
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
test('渲染摘要表时不输出字符串引号', () => {
|
||||
const rows = buildStartupSummary(makeReadyState(), {
|
||||
url: 'http://127.0.0.1:3080/',
|
||||
});
|
||||
const table = renderStartupTable(rows);
|
||||
|
||||
expect(table).not.toContain("'");
|
||||
expect(table).toContain('| Web 主站');
|
||||
expect(table).toContain('http://127.0.0.1:3080/');
|
||||
});
|
||||
|
||||
test('仅接受同一工作树、实例、数据库和 data dir 的完整栈', () => {
|
||||
const root = resolve('fixture-root');
|
||||
const ready = makeReadyState(root);
|
||||
|
||||
expect(
|
||||
isSharedRootStackReady(ready, {
|
||||
expectedRepoRoot: root,
|
||||
expectedDatabase: agcDatabase,
|
||||
expectedDataDir: agcDataDir,
|
||||
}),
|
||||
).toBe(true);
|
||||
|
||||
const wrongInstance = structuredClone(ready);
|
||||
wrongInstance.services.web.instanceId = 'other-instance';
|
||||
expect(
|
||||
isSharedRootStackReady(wrongInstance, {
|
||||
expectedRepoRoot: root,
|
||||
expectedDatabase: agcDatabase,
|
||||
expectedDataDir: agcDataDir,
|
||||
}),
|
||||
).toBe(false);
|
||||
|
||||
const incomplete = structuredClone(ready);
|
||||
incomplete.services['admin-web'].status = 'idle';
|
||||
expect(
|
||||
isSharedRootStackReady(incomplete, {
|
||||
expectedRepoRoot: root,
|
||||
expectedDatabase: agcDatabase,
|
||||
expectedDataDir: agcDataDir,
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
test('任一子进程退出时收束另一侧进程树', async () => {
|
||||
const children: FakeChild[] = [];
|
||||
const stopCalls: FakeChild[] = [];
|
||||
let nextPid = 100;
|
||||
const run = runDevAll([], {
|
||||
resolveEndpoint: async () => ({
|
||||
port: 3080,
|
||||
url: 'http://127.0.0.1:3080/',
|
||||
}),
|
||||
spawnProcess: () => {
|
||||
const child = new EventEmitter() as FakeChild;
|
||||
child.pid = nextPid;
|
||||
child.exitCode = null;
|
||||
child.signalCode = null;
|
||||
nextPid += 1;
|
||||
children.push(child);
|
||||
if (children.length === 2) {
|
||||
queueMicrotask(() => {
|
||||
child.exitCode = 1;
|
||||
child.emit('exit', 1, null);
|
||||
});
|
||||
}
|
||||
return child;
|
||||
},
|
||||
waitForRoot: async () => makeReadyState(),
|
||||
readState: () => null,
|
||||
waitForAgc: async () => true,
|
||||
stopProcessTree: async (child) => {
|
||||
stopCalls.push(child);
|
||||
if (child?.exitCode == null) {
|
||||
child.exitCode = 0;
|
||||
child.emit('exit', 0, null);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
await expect(run).resolves.toBe(1);
|
||||
expect(children).toHaveLength(2);
|
||||
expect(stopCalls).toEqual([children[1], children[0]]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user