fix(web): stop phantom demo-fallback requests in seeds-off mode

App.tsx hardcoded fallbacks (active session "sess-001", projectId 1) sent
requests for nonexistent resources on fresh logins against real dev data,
producing 7+ console 404/403 hits. Fallbacks now resolve to empty/0; the
existing chatBlockedNoProject empty state and session-reconciliation effect
carry the UI, and demo-seeds-enabled builds keep the NEUROPULSE fallback.

Root cause, same-class scan, and before/after browser evidence (9 phantom
requests -> 0, author+member accounts) recorded in the task doc; ISSUES row
ISSUE-20260927-demo-fallback registered.
This commit is contained in:
weichen
2026-09-27 03:09:37 +00:00
parent 56928ac206
commit db038aa7a7
4 changed files with 129 additions and 3 deletions
+1
View File
@@ -29,3 +29,4 @@
| ISSUE-20260926-dual-flags-drift | intent / intentflow / FeatureFlags / PlanTier / MULTI_INTENT_ENABLED / 静默漂移 | 双套 flags 可配置出「intentflow 升档但 intent 关闭」的矛盾组合并静默降级 | high | resolved | pi | 2026-09-26;组合根经 intentflow.ValidateFlagConsistency 启动期 fail-fast + 单测;入口收敛残差另立 deferred | [主记录](../remediation/2026-09-26-system-code-review-fixes.md#6-find-qual-01-双套-flags-静默漂移issue-20260926-dual-flags-drift含残差) | [LESSON-20260926-failclosed-bypass-contract](../ops/lessons/2026-09-26-failclosed-bypass-contract.md) |
| ISSUE-20260926-governance-multi-entry | docgov / 多意图 / 多入口 / Coordinator / OwnerMux / GovernanceEngine | docgov 三入口 + Coordinator ~40 case 与 intentflow OwnerMux 平行,鉴权/确认/审计易漂移 | high | deferred | pi | 2026-09-26;flag 漂移面已修(dual-flags-drift);入口合并是多日级重构,需独立任务授权与回归矩阵 | [主记录](../remediation/2026-09-26-system-code-review-fixes.md#6-find-qual-01-双套-flags-静默漂移issue-20260926-dual-flags-drift含残差) | 沿用 failclosed-bypass-contract |
| ISSUE-20260926-server-pool-wiring | server / swboot_handlers / Runtime.Pool / 组合根越界 / FIND-ARCH-02 | server 持 Pool 直装 swboot repo/executor 并直查表,成为第二组合根(本轮未授权修复) | critical | open | pi | 2026-09-26;代码审证据齐(审计 FIND-ARCH-02),待修复授权 | [主记录](../audit/2026-09-26-system-code-review.md) | 待修复后补 |
| ISSUE-20260927-demo-fallback | web / App.tsx / sess-001 / projectId fallback / demo seeds 关闭 | demo seeds 关闭模式下前端硬编码 fallback(激活 sess-001、项目 1)对真实后端发假请求,新登录用户首屏 9 个 404/403(chat 窗口被迫回退本地缓存) | medium | resolved | pi | 2026-09-27;dev 真实浏览器修复前后对照(author+member 双账号:9 假请求→0) | [主记录](../ops/short-term/2026-09-27-frontend-demo-fallback.md) | [经验小结](../ops/short-term/2026-09-27-frontend-demo-fallback.md#经验小结)(独立 lesson 待后续任务补登) |
+1
View File
@@ -18,6 +18,7 @@
| owner | task | branch | worktree | mutex | status |
|---|---|---|---|---|---|
| pi | frontend-demo-fallback-2026-09-27-r2 | fix/frontend-demo-fallback-2026-09-27 | .worktrees/frontend-demo-fallback-2026-09-27 | web-chat-fallback | in_progress |
| Cursor | full-system-browser-e2e-2026-09-26 | feature/full-system-browser-e2e-2026-09-26 | .worktrees/full-system-browser-e2e-2026-09-26 | env:dev+docs-test-report | done |
| pi | sysadmin-maturity-refresh-2026-09-26 | feature/sysadmin-maturity-refresh-2026-09-26 | .worktrees/sysadmin-maturity-refresh-2026-09-26 | sysadmin-maturity | done |
| pi | degraded-flags-enable-2026-09-26-r4 | feature/degraded-flags-enable-2026-09-26 | .worktrees/degraded-flags-enable-2026-09-26 | summary-flags-rollout | done |
@@ -0,0 +1,122 @@
---
title: "前端 demo fallback 在真实 dev 环境产生 404/403"
task_id: "frontend-demo-fallback-2026-09-27-r2"
status: in_progress
created: 2026-09-27
updated: 2026-09-27
owner: pi
task_type: implementation
task_tier: small
policy_revision: "agent-governance-2026-09-21-r1 @ dev 56928ac2068fac3cc2a06efdc77d39717a6616f9"
branch: fix/frontend-demo-fallback-2026-09-27
worktree: .worktrees/frontend-demo-fallback-2026-09-27
base_dev_sha: "56928ac2068fac3cc2a06efdc77d39717a6616f9"
write_scope: ["web/src", ".doc/ops/short-term"]
allowed_environments: ["dev-readonly", "dev-browser-verify"]
head_sha: "TBD(冻结时填)"
mutex: "web-chat-fallback"
review_state: pending
integration_state: pending
deployment: pending
cleanup: pending
cleanup_owner: "独立集成/维护执行者"
cleanup_trigger: "owner 退出且 dev 合入+验收证据齐备"
remote_sync: not_requested
---
# 前端 demo fallback 在真实 dev 环境产生 404/403
## 用户目标与授权
- 用户目标/授权来源:用户报告 dev 页面(frp-mix.com:58880)登录后控制台 7 个 API 404/403;会话中明确授权"按照规范进行修复,测试完成后合入 dev,清理 worktree"。
- 交付终点:修复合入 dev、dev 实际运行验证通过、worktree 清理。
- 禁止项:不 push 远端;不动 test/main;不改后端(本 bug 根因在前端 fallback)。
- 例外:无。
## 问题事实与根因(2026-09-27 排查)
- 现象:登录后首屏 `GET /api/v1/projects/1`、`/projects/1/settings`、`/chat/messages/window|categories|dates|unread?session_id=sess-001` 404;`/intent-plans/active?project_id=1` 403。
- 链路核实:frp-mix.com:58880 → vite dev :3100(proxy /api → 127.0.0.1:8080)→ `bin/agent-server`(ENV=dev,2026-09-26 构建/启动,裸进程非 compose)。后端路由全部存在。
- 数据核实(dev PG :5433 只读):`projects` 20 行无 id=1;`project_members` 无 project 1 成员;`chat_sessions` 80 行无 `sess-001`;user_id=4=系统管理员(真实登录用户)。
- 根因:demo seeds 关闭模式(vite 未设 `VITE_ENABLE_DEMO_SEEDS`,后端 `SEED_DEMO_USERS=false`)下,前端残留 demo 假设硬编码:
- `web/src/App.tsx:138` `loadActiveSessionId('sess-001')`——无本地会话时激活不存在的 demo 会话;
- `web/src/App.tsx:206` `currentProjectId` fallback `1`——找不到会话项目时默认不存在的项目 1。
- 为何防护未拦:`storage.ts` legacy 清理只过滤 messages 的 legacy sessionId(:121),不清 `activeSessionId`;`purgeDemoSeedsFromLocalStorage` 清 localStorage 但 fallback 常量会再生成假请求。
- 状态码差异属既有设计:`projects/:id` 对"不存在或无权限"统一 404;`intent-plans/active` 走 authorizer 映射 403。
- 功能影响:新登录用户首屏噪音 + chat 回退本地缓存;无数据损坏证据。
## 同类排查
- grep 全前端:`sess-001` 残留仅 App.tsx:138 一处(storage.ts:121 为正确防御);projectId fallback `1` 仅 App.tsx:206 一处。
- 观察不扩大范围:`App.tsx` activeDocId fallback 的 `doc-001`/5201/3 为 demo 残留,但仅影响本地文档选择、不发 API 请求,不属于本 bug(控制台 404 均为 API 请求);记录待后续清理。
- ISSUES.md 无同类记录(检索 404/demo 残留/purge 无命中)。
## 相关问题与经验
- issue_ids:本次新增 ISSUE-20260927-demo-fallback(登记于 ISSUES.md)。
- 开工检索:`404 fallback`、`sess-001`、`demo 残留`,lessons 无命中;采纳验证契约"问题修复回归范围"节。
- 收口经验:见闭环更新(ISSUES.md + 本文档验证节)。
## 修复方案(A:改共用 fallback 机制)
1. `App.tsx:138` `loadActiveSessionId('sess-001')` → `loadActiveSessionId()`:demo 开启时仍取 `NEUROPULSE_ACTIVE_SESSION_ID`,关闭时为 `''`(disabled 模式导出空串)。
2. `App.tsx:203-208` `currentProjectId` fallback `1` → `0`,注释同步。
3. 不需新增空态分支:`chatBlockedNoProject`(App.tsx:249)已含 `currentProjectId <= 0`,ChatColumn 渲染"请先创建或加入项目后再开始对话";会话纠正 effect(App.tsx:268-277)在可见会话出现时自动恢复 activeSessionId。
4. 下游 guard 已核(空 sessionId/projectId=0 不发请求):getProject/settings/lineage effect(:397,:421)、useChatWindow(:120)、useUnreadMarks(fetchUnread 顶部)、categories/dates effect(:1037)、intent-plans effect(:1060)。
## 步骤与验收
1. [x] 根因与同类排查 — 本文档上文。
2. [x] 实施修复 — App.tsx 两处改动(:138 fallback 去常量、:206 projectId fallback 0)。
3. [x] `web/` `npm test`、`npm run build` — 全绿(含 demo on/off 双模式构建)。
4. [x] 真实浏览器验证 — 修复前 9 假请求复现 / 修复后 0(author+member 双账号);老用户回归无新增 404/403。
5. [ ] 合入 dev(integrate 流程)— dev 前进且基线核验。
6. [ ] dev 实际运行验证 — 主目录 vite 热载新代码后复验浏览器行为。
7. [ ] 清理 worktree、收口。
## 验证与证据
验证日期 2026-09-27;被测代码:worktree `fix/frontend-demo-fallback-2026-09-27`(基准 dev `56928ac2` + 本修复);环境:本机 dev 后端 `127.0.0.1:8080`(ENV=dev,agent-server 裸进程)+ 独立 vite :3101(worktree 代码,proxy 同源配置)。浏览器:Playwright chromium headless(brew 用户态补齐运行库);账号:dev 本地审计 e2e 账号(凭证不入库,来源 `.doc/test-reports/assets/2026-09-26-full-system-browser-e2e/audit-login-credentials.md`)。
| 范围 | 命令与环境 | 预期 | 实际 / 判定 | 证据 |
|---|---|---|---|---|
| 前端单测 | `npm test`(worktree,node_modules 链接主目录) | 全绿 | **PASS**:tsx 512 pass/0 fail;vitest 34 pass/0 fail | 控制台输出(本节记录) |
| 前端构建(demo off) | `npm run build` | 成功 | **PASS**:built 无错误,66 组产物预压缩 | 控制台输出 |
| 前端构建(demo on) | `VITE_ENABLE_DEMO_SEEDS=true npm run build` | 成功(alias 切换不破坏类型) | **PASS**:✓ built in 9.40s | 控制台输出 |
| 原例复现(修复前对照) | 空 localStorage 登录主目录 :3100(旧代码) | 复现假请求 | **复现**:9 个 phantom(projects/1×4→403、sess-001×4→404、intent-plans 403);与用户报告一致 | Network 抓取清单(下附) |
| 原例修复(修复后) | 空 localStorage 登录 worktree :3101(新代码) | 零假请求 | **PASS**:phantom=0,404/403=0,工作台正常渲染,84 个真实 API 全 2xx | 同上 |
| 同类变体(第二角色) | member 账号同样流程 :3101 | 零假请求 | **PASS**:phantom=0,404/403=0,83 API 全 2xx | 同上 |
| 正常行为回归(老用户) | 同一 author 账号在 :3101 登录后真实会话/项目加载 | 不受损 | **PASS**:会话纠冀 effect 选真实会话,项目/文档/血缘 API 全 2xx | 同上 |
| 边界:demo enabled fallback | 代码审查 `loadActiveSessionId()` 无参路径 | demo 开启时仍回退 NEUROPULSE_ACTIVE_SESSION_ID | **PASS**:`fallback || NEUROPULSE_ACTIVE_SESSION_ID || ''` 优先级保留 demo 值 | 代码 diff + demo on 构建通过 |
修复前对照抓取(:3100,旧代码,author 账号):
```text
phantom: /api/v1/projects/1, /projects/1/settings, /projects/1/lineage,
/projects/1/documents?category=workflow, /chat/messages/window?session_id=sess-001&days=7,
/chat/unread?session_id=sess-001&user_id=60, /chat/categories?session_id=sess-001,
/chat/dates?session_id=sess-001, /intent-plans/active?project_id=1&session_id=sess-001
→ 403×5(project 1 无权限), 404×4(sess-001 不存在)
```
修复后(:3101,新代码,同账号 + member 变体):`phantom=0;404/403=0;workbench 正常渲染`。
注:临时验证脚本未入库(/tmp 与 web/ 下临时文件已删);worktree 独立 vite :3101 已停。用户报障中 `projects/1→404` 与本地复现 `→403` 的差异仅因报障账号(user 4 系统管理员)对该项目访问落在本项目不存在分支,两状态码均属既有「不存在或无权限」映射,非另改行为。
## 经验小结
- 触发条件:前端存在 demo/生产双模式(构建期 alias 切换)且模式专属常量进入运行时 fallback 路径。
- 机制:fallback 优先级高于模式开关清理逻辑;purge/legacy 清理只处理 storage 残留,无法阻止常量重新生成假 ID,首屏即对真实后端发请求。
- 应做检查:关闭模式的默认值必须是空值/由服务端数据驱动,不得硬编码业务对象 ID;模式常量 grep 其在非模式代码(App/入口/hooks)的引用;新用户首屏用空 storage 真实浏览器验证 Network。
- 错误做法与原因:只加 storage 清理不清理 fallback 常量(本例现状);只验证 demo 开启路径。
- 验证方法:修复前后同流程对照(本任务 before 9/after 0)+ 双模式构建 + 第二角色变体。
- 适用边界:前端双模式常量;同类见 App.tsx activeDocId 的 doc-001/5201/3 残留(仅本地选择无 API 请求,待后续清理)。
- 独立 lesson 文件待后续覆盖 `.doc/ops/lessons/` scope 的任务补登;本节为可检索替代。
## 收口与交接
- 待任务完成后填写:本地提交 / dev 集成 / 部署制品 / 远端 / cleanup 状态分别记录。
## 相关链接
- ISSUES.md 新增条目:ISSUE-20260927-demo-fallback。
+5 -3
View File
@@ -135,7 +135,8 @@ export default function App() {
// 会话与用户状态 (支持 localStorage 刷新持久化)
const [sessions, setSessions] = useState<ProjectSession[]>(() => loadSessions());
const [activeSessionId, setActiveSessionId] = useState<string>(() => loadActiveSessionId('sess-001'));
// demo seeds 开启时由 NEUROPULSE_ACTIVE_SESSION_ID 提供 fallback;关闭时为空,避免对不存在会话发请求
const [activeSessionId, setActiveSessionId] = useState<string>(() => loadActiveSessionId());
// 全局角色库与人员名单状态 (支持动态增删改与持久化)
const [roles, setRoles] = useState<RoleDefinition[]>(() => DEFAULT_ROLES);
@@ -199,11 +200,12 @@ export default function App() {
const isAdmin = currentUser.roleId === 'role-admin';
// 获取当前激活会话的项目ID (默认为 1)
// 获取当前激活会话的项目ID;无有效会话时为 0(空态由 chatBlockedNoProject 分支承接,
// 不得回退到硬编码项目 ID——真实环境中该 ID 可能不存在,会产生 404/403 假请求)
const currentProjectId = useMemo(() => {
const activeSess = sessions.find((s) => s.sessionId === activeSessionId);
const resolved = resolveSessionProjectId(activeSess);
return resolved > 0 ? resolved : 1;
return resolved > 0 ? resolved : 0;
}, [sessions, activeSessionId]);
// 获取当前项目与当前用户在其中的成员职责