diff --git a/.doc/audit/ISSUES.md b/.doc/audit/ISSUES.md index ab30954f..897d31f9 100644 --- a/.doc/audit/ISSUES.md +++ b/.doc/audit/ISSUES.md @@ -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 待后续任务补登) | diff --git a/.doc/ops/CURRENT.md b/.doc/ops/CURRENT.md index 3cb2474a..b3376a0f 100644 --- a/.doc/ops/CURRENT.md +++ b/.doc/ops/CURRENT.md @@ -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 | diff --git a/.doc/ops/short-term/2026-09-27-frontend-demo-fallback.md b/.doc/ops/short-term/2026-09-27-frontend-demo-fallback.md new file mode 100644 index 00000000..7b0a90dc --- /dev/null +++ b/.doc/ops/short-term/2026-09-27-frontend-demo-fallback.md @@ -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。 diff --git a/web/src/App.tsx b/web/src/App.tsx index e1b5a317..94dbae78 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -135,7 +135,8 @@ export default function App() { // 会话与用户状态 (支持 localStorage 刷新持久化) const [sessions, setSessions] = useState(() => loadSessions()); - const [activeSessionId, setActiveSessionId] = useState(() => loadActiveSessionId('sess-001')); + // demo seeds 开启时由 NEUROPULSE_ACTIVE_SESSION_ID 提供 fallback;关闭时为空,避免对不存在会话发请求 + const [activeSessionId, setActiveSessionId] = useState(() => loadActiveSessionId()); // 全局角色库与人员名单状态 (支持动态增删改与持久化) const [roles, setRoles] = useState(() => 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]); // 获取当前项目与当前用户在其中的成员职责