Skip to content

feat(map): confirm browser locations in reports - #179

Merged
cg8-5712 merged 11 commits into
mainfrom
feat/location-confirmation-169-170
Aug 4, 2026
Merged

cg8-5712 merged 11 commits into
mainfrom
feat/location-confirmation-169-170

Conversation

@cg8-5712

@cg8-5712 cg8-5712 commented Aug 2, 2026 •

Copy link
Copy Markdown
Collaborator

Implements explicit browser geolocation and AMap JSAPI confirmation for supplemental places, supplemental clues, and volunteer field observations. Users can click or drag the map marker and only the final confirmed values update the existing forms. Manual entry and the existing review, visibility, and API contracts are preserved when permission or map loading fails. The browser key is separate from the server Web Service key and is injected only during frontend builds. Verification: the new picker tests, lint, TypeScript checking, and production build pass; the full frontend suite has one unrelated pre-existing FamilyIntakeForm test failure. Closes #169. Closes #170.

Summary by CodeRabbit

  • 新功能

    • 新增位置确认功能,支持浏览器定位、地图选点、标记拖动、地址反查及地点搜索。
    • 确认位置后可自动回填地址、坐标和定位精度,也可清除位置。
    • 定位或地图服务不可用时,仍可手动填写地点。
    • 线索、地点及现场观察表单支持位置确认。
    • AI 草稿生成和初始审核支持流式反馈,并显示处理状态。
  • 文档

    • 补充地图服务配置、安全代理及本地开发说明。

Copilot AI review requested due to automatic review settings August 2, 2026 16:34

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@coderabbitai

coderabbitai Bot commented Aug 2, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 38763af8-48d5-4ea5-96ab-d46873e7617f

📥 Commits

Reviewing files that changed from the base of the PR and between 8d40190 and 445aa20.

⛔ Files ignored due to path filters (1)
  • frontend/yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (1)
  • frontend/package.json
🚧 Files skipped from review as they are similar to previous changes (1)
  • frontend/package.json

📝 Walkthrough

Walkthrough

新增 LocationConfirmationPicker,支持浏览器定位、高德地图选点、标记拖动、地址解析、确认和清除。组件接入案件及志愿者表单,并配置 AMap 密钥注入和预览安全代理。Intake AI 草稿生成和初始审核改用 SSE,并强化画像来源校验。

Changes

位置确认功能

Layer / File(s) Summary
地图配置与预览安全代理
.github/WORKFLOWS.md, .github/workflows/*, frontend/.env.example, frontend/README.md, frontend/package.json, deploy/preview/*
新增 AMap JSAPI 配置、构建密钥注入和预览 Nginx 安全代理。部署脚本校验安全码格式。
位置选择组件与测试
frontend/src/components/LocationConfirmationPicker.*
新增定位、地图选点、标记拖动、逆地理编码、地点搜索、确认、清除和错误处理。测试覆盖异步过期结果、失败重试、坐标转换失败、地址解析失败及权限拒绝。
工作区表单接入与测试支持
frontend/src/pages/CaseWorkspacePage.tsx, frontend/src/pages/VolunteerWorkspacePage.tsx, frontend/src/pages/FamilyIntakeForm.test.tsx
在案件、补充地点和现场观察表单中接入位置确认,并回填地址、坐标和位置精度字段。补充 intake 测试 mock 和异步等待。

Intake AI 处理

Layer / File(s) Summary
画像抽取契约与来源校验
src/application/services/intake_session_service.rs
画像抽取支持兼容候选格式,并要求固定字段来源、可验证摘录和严格 schema。校验失败时回退规则生成。
Intake SSE 后端流
src/api/routes/intake_sessions.rs
草稿生成和初始审核接口改为 SSE。接口发送 started、completed 或 error 事件,并发送 keep-alive。
SSE 客户端与表单接入
frontend/src/api/*, frontend/src/pages/FamilyIntakeForm.tsx
新增 apiSseRequest,解析 SSE 事件并处理不完整流、错误和无效 JSON。两个 intake API 改用 SSE,表单增加生成中状态和错误处理。
AI 网关诊断、启动配置与集成测试
src/integrations/ai_gateway.rs, src/main.rs, tests/api/intake_session_confirm_post.rs, tests/support/mod.rs, Cargo.toml
记录 AI 请求、响应和 SSE 分块的 diagnostic 日志。服务启动时加载本地环境文件。集成测试改用 SSE 完成事件辅助函数。

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Browser as 浏览器
  participant Picker as LocationConfirmationPicker
  participant AMap as 高德地图 API
  participant Form as 工作区表单
  Browser->>Picker: 用户主动请求当前位置
  Picker->>Browser: 获取浏览器坐标
  Picker->>AMap: 转换坐标并查询地址
  AMap-->>Picker: 返回地址和坐标
  Picker->>Form: 用户确认后回填位置
Loading
sequenceDiagram
  participant Form as FamilyIntakeForm
  participant Client as apiSseRequest
  participant Route as intake_sessions
  participant Gateway as AI Gateway
  Form->>Client: 请求草稿或初始审核
  Client->>Route: 发送 SSE 请求
  Route->>Gateway: 执行 AI 服务调用
  Gateway-->>Route: 返回 AI 结果
  Route-->>Client: 发送 completed 或 error 事件
  Client-->>Form: 返回解析后的结果
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning PR还包含AI intake SSE、AI schema与日志、本地环境加载及相关测试变更,与#169和#170无关。 将AI intake、配置加载、依赖解析和相关测试变更拆分到独立PR,仅保留地图定位及其必要配置变更。
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 标题准确概括了本次新增浏览器位置确认地图功能的主要变更。
Linked Issues check ✅ Passed 实现覆盖两个问题的定位、地图确认、拖拽或点击改点、手动回退、清除及最终值回填要求。
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/location-confirmation-169-170

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Aug 2, 2026

Copy link
Copy Markdown

Preview ready

The preview for this build is available at https://pr-179-a8f39993ce82.angui.cg8.site.

Temporary demo accounts (all use the password 76ac34bcbc780e4569d277d49f3a145ee189cffed59647e3):

Role Email
Family family@demo.invalid
Commander commander@demo.invalid
Volunteer volunteer@demo.invalid
Learner learner@demo.invalid
Administrator admin@demo.invalid

These accounts and all preview data belong only to this deployment. The next preview deployment resets the SQLite database and invalidates these credentials.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
.github/WORKFLOWS.md (1)

216-229: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

将 AMAP_JS_API_KEY 的说明移出"Optional preview secret"分组。

AMAP_JS_API_KEY 不只用于预览环境。ci.yml 在每次前端检查构建时注入它。release.yml 在生产发布构建时也注入它。当前文档把这一条紧跟在"Optional preview secret"标题下的 AMAP_WEBSERVICE_KEY 条目之后,容易让配置者误以为它只影响预览部署,从而遗漏为生产发布配置该密钥。

为该条目新增独立的小标题(例如"前端构建密钥"),或明确说明它适用于 CI 检查、预览部署和生产发布这三种构建场景。

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/WORKFLOWS.md around lines 216 - 229, 将 .github/WORKFLOWS.md 中的
AMAP_JS_API_KEY 条目移出“Optional preview secret”分组,并为其添加独立的小标题或明确标注其适用于 CI
前端检查构建、预览部署构建和生产发布构建;保留其仅注入前端构建、可公开暴露且不得替代 AMAP_WEBSERVICE_KEY 的说明。
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@frontend/.env.example`:
- Line 2: 在 frontend/.env.example 中调整环境变量顺序,将 VITE_AMAP_JS_API_KEY 放到
VITE_API_BASE_URL 之前,保持变量内容不变。

In `@frontend/src/components/LocationConfirmationPicker.tsx`:
- Around line 154-184: Update resolveAddress so that successful address parsing
clears the existing message state before updating the candidate location; keep
the failure path’s existing setMessage behavior unchanged and perform the reset
only after a non-empty formatted address is obtained.
- Around line 95-134: Update loadAmap so every script-loading failure clears
amapPromise and removes the failed script element identified by amapScriptId
before rejecting. Ensure all existing-script and newly-created-script error
paths perform this cleanup, allowing the next loadAmap call to create a fresh
script and retry.

In `@frontend/src/pages/CaseWorkspacePage.tsx`:
- Around line 1383-1392: 为线索表单和地点表单中渲染的两个 LocationConfirmationPicker 添加
key={detail.id},分别定位到 CaseDetailView
中的两处选择器渲染位置。确保切换案件时选择器重新挂载并重置未确认的候选位置及其内部状态,同时保留现有确认和清除回调行为。

In `@frontend/src/pages/VolunteerWorkspacePage.tsx`:
- Around line 744-763: Update the ClueDraft type and its initialization to
include locationPrecision as "exact" | "approximate" | "". In
LocationConfirmationPicker’s onConfirm, set the selected address and
locationPrecision to "exact"; in onClear, reset both location and precision.
Update the clue submission logic to use clueDraft.locationPrecision, falling
back to "approximate" only for non-empty manually entered locations and null
otherwise.

---

Outside diff comments:
In @.github/WORKFLOWS.md:
- Around line 216-229: 将 .github/WORKFLOWS.md 中的 AMAP_JS_API_KEY 条目移出“Optional
preview secret”分组,并为其添加独立的小标题或明确标注其适用于 CI
前端检查构建、预览部署构建和生产发布构建;保留其仅注入前端构建、可公开暴露且不得替代 AMAP_WEBSERVICE_KEY 的说明。
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 4323301c-3d44-4471-a672-46c41a2f5749

📥 Commits

Reviewing files that changed from the base of the PR and between 98d7835 and a8f3999.

📒 Files selected for processing (9)
  • .github/WORKFLOWS.md
  • .github/workflows/ci.yml
  • .github/workflows/release.yml
  • frontend/.env.example
  • frontend/README.md
  • frontend/src/components/LocationConfirmationPicker.test.tsx
  • frontend/src/components/LocationConfirmationPicker.tsx
  • frontend/src/pages/CaseWorkspacePage.tsx
  • frontend/src/pages/VolunteerWorkspacePage.tsx

Comment thread frontend/.env.example
@@ -1 +1,2 @@
VITE_API_BASE_URL=/api
VITE_AMAP_JS_API_KEY=

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

按字母顺序排列环境变量键,消除 dotenv-linter 警告。

静态分析工具报告 VITE_AMAP_JS_API_KEY 应排在 VITE_API_BASE_URL 之前。调整两行的顺序即可消除该警告。

🔧 调整变量顺序
+VITE_AMAP_JS_API_KEY=
 VITE_API_BASE_URL=/api
-VITE_AMAP_JS_API_KEY=
🧰 Tools
🪛 dotenv-linter (4.0.0)

[warning] 2-2: [UnorderedKey] The VITE_AMAP_JS_API_KEY key should go before the VITE_API_BASE_URL key

(UnorderedKey)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/.env.example` at line 2, 在 frontend/.env.example 中调整环境变量顺序,将
VITE_AMAP_JS_API_KEY 放到 VITE_API_BASE_URL 之前,保持变量内容不变。

Source: Linters/SAST tools

Comment thread frontend/src/components/LocationConfirmationPicker.tsx Outdated
Comment thread frontend/src/components/LocationConfirmationPicker.tsx
Comment on lines +1383 to +1392
<LocationConfirmationPicker
onConfirm={(location) => {
setClueLocation(location.address);
setClueLocationPrecision("exact");
}}
onClear={() => {
setClueLocation("");
setClueLocationPrecision("");
}}
/>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

为位置选择器绑定 detail.id,避免跨案件残留未确认位置。

CaseDetailView 在两处渲染时都没有 key={detail.id}(约第 316 行和第 463 行)。切换所选案件,或在指挥端路由间切换案件,都不会重新挂载 CaseDetailView,只会更新它的 props。clueLocation、place 以及嵌套的 LocationConfirmationPicker 内部状态(candidate、map.current、marker.current)都不会因此重置。

如果用户在案件 A 打开地图选点,得到一个候选位置后没有确认也没有清除,然后切换到案件 B,地图仍会显示案件 A 的候选坐标。此时如果用户点击"确认此位置",案件 A 的坐标和地址会被写入案件 B 的线索或地点表单字段。在走失人员搜救场景中,这会把错误的位置提交给错误的案件。

为线索表单和地点表单中的 LocationConfirmationPicker 添加 key={detail.id}。案件切换时会重新挂载选择器,清空未确认的候选位置,不影响 CaseDetailView 中其他字段的现有行为。

🔧 按案件重置选择器状态
                 <LocationConfirmationPicker
+                  key={detail.id}
                   onConfirm={(location) => {
                     setClueLocation(location.address);
                     setClueLocationPrecision("exact");
                   }}
                   onClear={() => {
                     setClueLocation("");
                     setClueLocationPrecision("");
                   }}
                 />
                 <LocationConfirmationPicker
+                  key={detail.id}
                   onConfirm={(location) =>
                     setPlace({
                       ...place,
                       address: location.address,
                       longitude: location.longitude,
                       latitude: location.latitude,
                     })
                   }
                   onClear={() =>
                     setPlace({
                       ...place,
                       address: "",
                       longitude: null,
                       latitude: null,
                     })
                   }
                 />

As per path instructions for frontend/**: "Focus on frontend/backend API contract compatibility, authentication state, validation, data exposure, and user-visible failure paths."

Also applies to: 1587-1604

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/pages/CaseWorkspacePage.tsx` around lines 1383 - 1392,
为线索表单和地点表单中渲染的两个 LocationConfirmationPicker 添加 key={detail.id},分别定位到
CaseDetailView 中的两处选择器渲染位置。确保切换案件时选择器重新挂载并重置未确认的候选位置及其内部状态,同时保留现有确认和清除回调行为。

Source: Path instructions

Comment thread frontend/src/pages/VolunteerWorkspacePage.tsx

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (2)
frontend/src/components/LocationConfirmationPicker.test.tsx (1)

236-239: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

提取 navigator.geolocation 的 stub 辅助函数。

五个测试都重复了同一段 Object.defineProperty(navigator, "geolocation", ...)。请提取一个辅助函数,并在 afterEach 中恢复原始的 navigator.geolocation,避免污染同一文件中的其他测试。

♻️ 建议改法
+function stubGeolocation(getCurrentPosition: unknown) {
+  Object.defineProperty(navigator, "geolocation", {
+    configurable: true,
+    value: { getCurrentPosition },
+  });
+}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/components/LocationConfirmationPicker.test.tsx` around lines 236
- 239, 在 LocationConfirmationPicker 测试文件中提取统一 stub navigator.geolocation
的辅助函数,替换五个测试中的重复 Object.defineProperty 配置,并在 afterEach 中保存后恢复原始
navigator.geolocation,确保测试间不共享被修改的全局状态。
frontend/src/components/LocationConfirmationPicker.tsx (1)

193-204: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

把对 candidateRef 的写入移出 state updater。

setCandidate 的 updater 必须是纯函数。第 202 行在 updater 内部写 candidateRef.current。React 在 StrictMode 下会重复调用 updater,副作用会被执行多次。当前赋值是幂等的,所以现在没有可见缺陷。但这是反模式,后续修改容易引入真实 bug。

建议在调用 setCandidate 之前,先基于 candidateRef.current 计算并写入新值,再用同一个值更新 state。

♻️ 建议改法
       setIsGeocoding(false);
       setMessage("");
+      const currentRef = candidateRef.current;
+      if (
+        currentRef &&
+        currentRef.longitude === longitude &&
+        currentRef.latitude === latitude
+      ) {
+        candidateRef.current = { ...currentRef, address };
+      }
       setCandidate((current) => {
         if (
           !current ||
           current.longitude !== longitude ||
           current.latitude !== latitude
         ) {
           return current;
         }
-        const next = { ...current, address };
-        candidateRef.current = next;
-        return next;
+        return { ...current, address };
       });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/components/LocationConfirmationPicker.tsx` around lines 193 -
204, 将 LocationConfirmationPicker 中 setCandidate 的 updater 保持为纯函数,移除其中对
candidateRef.current 的写入;在调用 setCandidate 前基于 candidateRef.current 计算 next,先同步写入
candidateRef.current,再使用同一个 next 值更新 state,同时保留现有坐标校验和无变化时的行为。
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@deploy/preview/nginx.conf`:
- Around line 35-38: 更新 nginx 配置中围绕 $arg_jscode 的校验:通过检查 $args 判断请求是否显式包含 jscode
参数,并在该参数值为空时返回 400,同时保留缺少 jscode 参数时的正常转发行为,避免 rewrite 生成重复的 jscode 参数。

In `@frontend/src/components/LocationConfirmationPicker.test.tsx`:
- Around line 94-104: 修复测试间共享状态:在
frontend/src/components/LocationConfirmationPicker.test.tsx 的 94-104 行将 amap
初始化改为每次 beforeEach 都调用 createAmapMock,并删除 145-148 行和 166-168 行对 convertFrom
的手动保存与恢复;同时在 106-142 行的测试 setup 中清除 LocationConfirmationPicker.tsx 的模块级
amapPromise 缓存,使用 vi.resetModules() 配合动态导入,或导出仅供测试使用的缓存重置函数。

In `@frontend/src/components/LocationConfirmationPicker.tsx`:
- Around line 306-351: 修复 requestLocation
在选点面板已打开时重复定位导致地图状态失效的问题:获取新坐标后复用已有地图实例并更新标记、地图视野及地址解析;若无法复用,则先销毁旧实例再触发重新初始化,确保
isMapReady、candidate.address 和确认状态最终恢复。补充测试覆盖面板打开后再次点击“获取当前位置”,并断言“确认此位置”最终可用。

---

Nitpick comments:
In `@frontend/src/components/LocationConfirmationPicker.test.tsx`:
- Around line 236-239: 在 LocationConfirmationPicker 测试文件中提取统一 stub
navigator.geolocation 的辅助函数,替换五个测试中的重复 Object.defineProperty 配置,并在 afterEach
中保存后恢复原始 navigator.geolocation,确保测试间不共享被修改的全局状态。

In `@frontend/src/components/LocationConfirmationPicker.tsx`:
- Around line 193-204: 将 LocationConfirmationPicker 中 setCandidate 的 updater
保持为纯函数,移除其中对 candidateRef.current 的写入;在调用 setCandidate 前基于 candidateRef.current
计算 next,先同步写入 candidateRef.current,再使用同一个 next 值更新 state,同时保留现有坐标校验和无变化时的行为。
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 84c26f86-a89b-4f6c-9bed-64f43ac844ed

📥 Commits

Reviewing files that changed from the base of the PR and between a8f3999 and 7759a81.

⛔ Files ignored due to path filters (1)
  • frontend/yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (15)
  • .github/WORKFLOWS.md
  • .github/workflows/pr-preview.yml
  • .github/workflows/preview.yml
  • .github/workflows/release.yml
  • deploy/preview/Dockerfile.frontend
  • deploy/preview/compose.local.yml
  • deploy/preview/deploy-local.sh
  • deploy/preview/nginx.conf
  • frontend/.env.example
  • frontend/README.md
  • frontend/package.json
  • frontend/src/components/LocationConfirmationPicker.test.tsx
  • frontend/src/components/LocationConfirmationPicker.tsx
  • frontend/src/pages/CaseWorkspacePage.tsx
  • frontend/src/pages/VolunteerWorkspacePage.tsx
🚧 Files skipped from review as they are similar to previous changes (3)
  • .github/workflows/release.yml
  • frontend/src/pages/CaseWorkspacePage.tsx
  • frontend/src/pages/VolunteerWorkspacePage.tsx

Comment thread deploy/preview/nginx.conf
Comment on lines +35 to +38
if ($arg_jscode != "") {
return 400;
}
rewrite ^/_AMapService/(.*)$ /$1?$amap_forward_args break;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

According to Nginx documentation, does the $arg_namevariable have the same empty value when a query parameter is absent and when it is present with an empty value, such as?jscode=?

💡 Result:

In the Nginx configuration language, the $arg_name variable does not distinguish between an absent query parameter and a present-but-empty query parameter (e.g., ?name=); both render as an empty string [1]. Internally, however, Nginx maintains a distinction between these states [1]. While the standard Nginx configuration language cannot differentiate between them, it is possible to observe this difference using modules that provide access to the internal variable states, such as the Nginx Lua module [2][1]. In Lua, a missing argument evaluates to nil, whereas an empty string argument evaluates to an empty string "" [2][1]. Because Nginx's built-in $arg_ variables are computed on-the-fly by scanning the query string each time they are referenced, they do not have a fixed value container [3][4]. When a parameter is absent, the variable internally holds a "not found" state, which the Nginx configuration processor converts to an empty string for output or comparison purposes [2][1][3]. Consequently, simple configuration directives like if ($arg_name = "") will evaluate as true for both absent and empty parameters [1].

Citations:


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- target configuration ---'
cat -n deploy/preview/nginx.conf | sed -n '1,90p'
printf '%s\n' '--- related jscode and amap_forward_args references ---'
rg -n -C 3 'jscode|amap_forward_args|AMapService' deploy
printf '%s\n' '--- repository files relevant to nginx configuration ---'
git ls-files deploy

Repository: AG-Angui/Angui

Length of output: 3644


🌐 Web query:

site:nginx.org/en/docs/http/ngx_http_core_module.html $arg_name empty query argument absent

💡 Result:

There is no standard technical definition or specific programming concept associated with the phrase "arg_name empty query argument absent." The provided search results did not return any authoritative documentation or industry standard definitions for this specific sequence of terms. In general software engineering and web development contexts, these terms refer to distinct states when handling request parameters: 1. Argument Absent: This occurs when a specific query parameter (often referred to as an argument) is entirely missing from the request URI (e.g., example.com/page does not include?arg_name=...). 2. Empty Argument: This occurs when the parameter is present in the URI but has no assigned value (e.g., example.com/page?arg_name=). Developers typically handle these cases by implementing validation logic to check if a parameter exists and if its value is null, undefined, or an empty string before processing the request.

Citations:


🌐 Web query:

site:nginx.org/en/docs/http/ngx_http_rewrite_module.html rewrite replacement question mark original arguments

💡 Result:

In the context of Nginx rewrite directives, placing a question mark at the end of a replacement string is used to prevent the original request arguments from being automatically appended to the rewritten URI [1]. By default, if a replacement string includes new request arguments, Nginx automatically appends the original request arguments to the end of the new URI [1]. Adding a question mark at the end of the replacement string signifies the end of the argument string, effectively instructing Nginx to discard or ignore the original request arguments [1]. For example, in the directive: rewrite ^/users/(.*)$ /show?user=$1? last; The question mark after $1 prevents the original request arguments from being appended to the /show?user=$1 URI [1].

Citations:


🏁 Script executed:

#!/bin/bash
set -e
python3 - <<'PY'
from urllib.parse import parse_qsl

secret = "SECRET"

def nginx_arg(args, name):
    # Model the documented nginx behavior relevant to this condition:
    # an absent argument and an argument with an empty value both compare
    # as an empty string through $arg_name.
    for key, value in parse_qsl(args, keep_blank_values=True):
        if key == name:
            return value
    return ""

def forwarded_args(args):
    return f"jscode={secret}" if args == "" else f"{args}&jscode={secret}"

for args in ("", "foo=1", "jscode=", "foo=1&jscode=", "jscode=client&foo=1"):
    rejected = nginx_arg(args, "jscode") != ""
    rewritten = forwarded_args(args)
    print(f"{args!r}: rejected={rejected}, rewritten_args={rewritten!r}")
PY

Repository: AG-Angui/Angui

Length of output: 498


拒绝空的客户端 jscode 参数。

$arg_jscode 无法区分缺少参数和 ?jscode=。因此,空参数会通过检查,并生成两个 jscode 参数。请检查 $args 中是否存在 jscode,并在参数值为空时返回 400。

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@deploy/preview/nginx.conf` around lines 35 - 38, 更新 nginx 配置中围绕 $arg_jscode
的校验:通过检查 $args 判断请求是否显式包含 jscode 参数,并在该参数值为空时返回 400,同时保留缺少 jscode 参数时的正常转发行为,避免
rewrite 生成重复的 jscode 参数。

Comment on lines +94 to +104
beforeEach(() => {
amap ??= createAmapMock();
amapLoad.mockReset();
amapLoad.mockResolvedValue(amap.api as never);
vi.stubEnv("VITE_AMAP_JS_API_KEY", "test-key");
vi.stubEnv("VITE_AMAP_JS_API_SERVICE_HOST", "/_AMapService");
});

afterEach(() => {
vi.unstubAllEnvs();
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

测试之间存在共享状态,必须一起修复。 共同根因是状态的生命周期超出了单个测试:amap mock 对象在整个 describe 中只创建一次,LocationConfirmationPicker.tsx 中的模块级 amapPromise 也在测试之间保留。只修其中一处会让测试套件失败。

  • frontend/src/components/LocationConfirmationPicker.test.tsx#L94-L104:把 amap ??= createAmapMock() 改为 amap = createAmapMock(),并删除第 145-148 行、第 166-168 行对 convertFrom 的手动保存与恢复。
  • frontend/src/components/LocationConfirmationPicker.test.tsx#L106-L142:同时清除模块级 amapPromise 缓存。在 beforeEach 中调用 vi.resetModules() 并动态导入组件,或从组件模块导出一个仅测试使用的缓存重置函数。
📍 Affects 1 file
  • frontend/src/components/LocationConfirmationPicker.test.tsx#L94-L104 (this comment)
  • frontend/src/components/LocationConfirmationPicker.test.tsx#L106-L142
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/components/LocationConfirmationPicker.test.tsx` around lines 94
- 104, 修复测试间共享状态:在 frontend/src/components/LocationConfirmationPicker.test.tsx 的
94-104 行将 amap 初始化改为每次 beforeEach 都调用 createAmapMock,并删除 145-148 行和 166-168 行对
convertFrom 的手动保存与恢复;同时在 106-142 行的测试 setup 中清除 LocationConfirmationPicker.tsx
的模块级 amapPromise 缓存,使用 vi.resetModules() 配合动态导入,或导出仅供测试使用的缓存重置函数。

Comment thread frontend/src/components/LocationConfirmationPicker.tsx
@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown

Preview ready

The preview for this build is available at https://pr-179-7759a81bec51.angui.cg8.site.

Temporary demo accounts (all use the password bececb3dc5252c669020fe646875dfacdc6a413b19e12de2):

Role Email
Family family@demo.invalid
Commander commander@demo.invalid
Volunteer volunteer@demo.invalid
Learner learner@demo.invalid
Administrator admin@demo.invalid

These accounts and all preview data belong only to this deployment. The next preview deployment resets the SQLite database and invalidates these credentials.

@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown

Preview ready

The preview for this build is available at https://pr-179-af36dc558280.angui.cg8.site.

Temporary demo accounts (all use the password 1950b66cb36e1ca043337dd225a93dc5d9c7d4a20f08c8fc):

Role Email
Family family@demo.invalid
Commander commander@demo.invalid
Volunteer volunteer@demo.invalid
Learner learner@demo.invalid
Administrator admin@demo.invalid

These accounts and all preview data belong only to this deployment. The next preview deployment resets the SQLite database and invalidates these credentials.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
deploy/preview/nginx.conf (1)

34-38: 🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

限制公开 AMap 代理的路径和调用速率。

该路由会将任意 /_AMapService/<path> 重写到对应的上游路径,并追加服务端 jscode。调用者可以通过公开的浏览器端 key 访问未列入业务需求的 restapi.amap.com 接口,并消耗 AMap 配额。

请仅放行 LocationConfirmationPicker 所需的请求路径,并配置 limit_req 或等效限流。

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@deploy/preview/nginx.conf` around lines 34 - 38, 限制 nginx 配置中 location ^~
/_AMapService/ 的代理范围,仅允许 LocationConfirmationPicker 所需的明确请求路径,拒绝其他 AMap
上游路径;同时为该路由配置现有 nginx 限流机制(limit_req 或等效方案),保留 GET-only 访问控制并确保请求仍能正常转发。

Source: Path instructions

🧹 Nitpick comments (2)
deploy/preview/nginx.conf (1)

22-23: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

不要在整个 /api/ 路由关闭请求和响应缓冲。

proxy_buffering off 和 proxy_request_buffering off 当前作用于所有 API。慢客户端会直接占用上游连接。SSE 通常只需要关闭响应缓冲。请将这些设置限制到实际的 SSE 路由,或确认所有 /api/ 端点都需要流式传输。

As per path instructions:该检查覆盖 deploy/** 的容器可用性和路由回归。

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@deploy/preview/nginx.conf` around lines 22 - 23, 将 proxy_buffering 和
proxy_request_buffering 的配置从整个 /api/ 路由范围移除,并仅应用到实际需要流式传输的 SSE 路由;保留普通 API
的请求和响应缓冲,除非配置中明确确认所有端点都需要流式传输。

Source: Path instructions

frontend/src/api/client.test.ts (1)

2-2: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

建议补充 error 事件的测试用例。

当前测试覆盖了 completed 事件和不完整流(stream_incomplete)两种情况,但没有覆盖 event: error 分支(对应 frontend/src/api/client.ts Line 188-197)。这是本次改动新增的、逻辑相对复杂的分支(状态码固定为 500、消息直接来自负载),建议补充一个测试用例,验证 apiSseRequest 在收到 error 事件时的行为符合预期。

✅ 示例测试
+  it("rejects with the error event message when the stream reports a failure", async () => {
+    vi.stubGlobal(
+      "fetch",
+      vi.fn().mockResolvedValue(
+        new Response(
+          "event: error\ndata: {\"message\":\"session not found\"}\n\n",
+          { status: 201, headers: { "Content-Type": "text/event-stream" } },
+        ),
+      ),
+    );
+
+    await expect(
+      apiSseRequest("/intake-sessions/intake-1/profile-draft/generate", {
+        method: "POST",
+      }, "test-session"),
+    ).rejects.toMatchObject({ code: "stream_failed", message: "session not found" });
+  });

Also applies to: 90-133

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/api/client.test.ts` at line 2, 在 apiSseRequest 的测试中补充 error
事件用例,模拟服务端发送 event: error 的 SSE 负载,并验证请求以状态码 500、错误消息直接取自负载的预期结果结束;保持现有
completed 和 stream_incomplete 测试不变。
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@frontend/src/api/client.ts`:
- Around line 151-157: Update userMessageFor to handle stream_missing,
invalid_stream, stream_interrupted, and stream_incomplete with dedicated
user-facing messages before the generic status === 0 fallback. Ensure each
status-0 stream error preserves its specific failure reason instead of
displaying the network-connection message.

In `@src/api/routes/intake_sessions.rs`:
- Around line 105-150: The SSE error contract must preserve structured status or
error-code information so the frontend can localize messages and branch on
specific statuses. In src/api/routes/intake_sessions.rs lines 105-150, update
the generate_intake_profile_draft error path and shared
sse_event/intake_sse_response flow to serialize fields derived from ApiError
instead of only error.to_string(); apply the same change to
start_ai_initial_review at lines 248-262. In frontend/src/api/client.ts lines
187-197, consume the new status/error-code fields, pass them through
userMessageFor, and retain specific handling for statuses such as 403, 404, and
409 rather than displaying payload.message directly.
- Around line 105-121: Update the SSE error path around
generate_intake_profile_draft and sse_event to serialize structured ApiError
information, including its error code and applicable HTTP status, instead of
error.to_string(). Update apiSseRequest to preserve that classification rather
than forcing stream_failed/500, and have FamilyIntakeForm map the code through
userMessageFor so all displayed messages are localized, including Internal and
Database errors.

In `@src/integrations/ai_gateway.rs`:
- Around line 859-866: The AI gateway diagnostic logs expose sensitive request
and response contents through native.body(), raw_response, and streamed chunk
logging. Update the relevant logging paths to remove complete payloads and
record only redacted/truncated data or safe metadata such as byte length, field
counts, or a content hash, covering both non-streaming and streaming responses
while preserving provider/model/protocol diagnostics.

---

Outside diff comments:
In `@deploy/preview/nginx.conf`:
- Around line 34-38: 限制 nginx 配置中 location ^~ /_AMapService/ 的代理范围,仅允许
LocationConfirmationPicker 所需的明确请求路径,拒绝其他 AMap 上游路径;同时为该路由配置现有 nginx
限流机制(limit_req 或等效方案),保留 GET-only 访问控制并确保请求仍能正常转发。

---

Nitpick comments:
In `@deploy/preview/nginx.conf`:
- Around line 22-23: 将 proxy_buffering 和 proxy_request_buffering 的配置从整个 /api/
路由范围移除,并仅应用到实际需要流式传输的 SSE 路由;保留普通 API 的请求和响应缓冲,除非配置中明确确认所有端点都需要流式传输。

In `@frontend/src/api/client.test.ts`:
- Line 2: 在 apiSseRequest 的测试中补充 error 事件用例,模拟服务端发送 event: error 的 SSE
负载,并验证请求以状态码 500、错误消息直接取自负载的预期结果结束;保持现有 completed 和 stream_incomplete 测试不变。
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: fa362aab-3889-4d05-a198-a0c0b6356e40

📥 Commits

Reviewing files that changed from the base of the PR and between af36dc5 and f4e9cdf.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (8)
  • Cargo.toml
  • deploy/preview/nginx.conf
  • frontend/src/api/client.test.ts
  • frontend/src/api/client.ts
  • frontend/src/api/intake.ts
  • frontend/src/pages/FamilyIntakeForm.tsx
  • src/api/routes/intake_sessions.rs
  • src/integrations/ai_gateway.rs

Comment on lines +151 to +157
if (!response.body) {
throw new ApiClientError(
0,
"stream_missing",
userMessageFor(0, "stream_missing"),
);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

新增的流错误码始终显示“网络连接失败”,掩盖真实原因。

userMessageFor 的第一条判断是 if (status === 0) return "网络连接失败,请检查服务连接后重试。";,该判断在检查 code 参数之前就返回。这里的四种新错误码——stream_missing(Line 151-157)、invalid_stream(Line 181-186)、stream_interrupted(Line 203-207)、stream_incomplete(Line 212-217)——全部以 status 0 调用 userMessageFor,因此都得到同一条网络错误提示。

这些失败原因并不相同。例如 invalid_stream 表示服务端返回了无法解析的 JSON,是服务端问题,不是网络问题。向用户提示“检查网络连接”会造成误导,也会让用户或客服无法根据提示区分真实故障。

建议在 userMessageFor 中为这些错误码补充专属分支,而不是依赖 status === 0 的通用回退。

As per path instructions for frontend/**: "Focus on frontend/backend API contract compatibility, authentication state, validation, data exposure, and user-visible failure paths."

💡 示例修复方向
 function userMessageFor(
   status: number,
   code: string,
   hasSession = false,
 ): string {
-  if (status === 0) return "网络连接失败,请检查服务连接后重试。";
+  if (code === "stream_missing") return "服务未能返回结果流,请稍后重试。";
+  if (code === "invalid_stream") return "服务返回的数据格式异常,请稍后重试或联系支持。";
+  if (code === "stream_interrupted") return "连接在处理过程中中断,请稍后重试。";
+  if (code === "stream_incomplete") return "未能获取到完整结果,请稍后重试。";
+  if (status === 0) return "网络连接失败,请检查服务连接后重试。";
   if (status === 400 || code === "validation_error")

Also applies to: 181-186, 203-207, 212-217

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/api/client.ts` around lines 151 - 157, Update userMessageFor to
handle stream_missing, invalid_stream, stream_interrupted, and stream_incomplete
with dedicated user-facing messages before the generic status === 0 fallback.
Ensure each status-0 stream error preserves its specific failure reason instead
of displaying the network-connection message.

Source: Path instructions

Comment on lines +105 to +121
tokio::spawn(async move {
let event = match intake_session_service::generate_intake_profile_draft(
&state.db,
&auth,
&session_id,
&state.ai_gateway,
)
.await?,
))
.await
{
Ok(draft) => sse_event("completed", json!(draft)),
Err(error) => sse_event("error", json!({ "message": error.to_string() })),
};
let _ = sender.send(event).await;
});

Ok(intake_sse_response(receiver))
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Inspect ApiError's Display/ResponseError implementation.
ast-grep run --pattern 'impl std::fmt::Display for ApiError { $$$ }' --lang rust src
ast-grep run --pattern 'impl ResponseError for ApiError { $$$ }' --lang rust src
rg -n -C5 'enum ApiError' src

Repository: AG-Angui/Angui

Length of output: 2553


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- src/api/error.rs ---'
cat -n src/api/error.rs

printf '%s\n' '--- intake_sessions.rs relevant sections ---'
sed -n '1,180p' src/api/routes/intake_sessions.rs | cat -n

printf '%s\n' '--- frontend apiSseRequest ---'
sed -n '150,215p' frontend/src/api/client.ts | cat -n

printf '%s\n' '--- FamilyIntakeForm error handling ---'
rg -n -C8 'apiSseRequest|cause\.status|stream_failed|status === 409|intake' frontend/src/pages/FamilyIntakeForm.tsx frontend/src/api/client.ts

printf '%s\n' '--- generate_intake_profile_draft call sites ---'
rg -n -C5 'generate_intake_profile_draft|intake_sse_response|sse_event' src frontend

Repository: AG-Angui/Angui

Length of output: 36270


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- service function definitions ---'
sed -n '350,455p' src/application/services/intake_session_service.rs | cat -n
sed -n '200,275p' src/api/routes/intake_sessions.rs | cat -n

printf '%s\n' '--- ApiError constructions in intake service ---'
rg -n -C4 'ApiError::(Validation|NotFound|Conflict|Unauthorized|Forbidden|RateLimited|Internal|Database)|return Err|map_err' src/application/services/intake_session_service.rs

printf '%s\n' '--- frontend intake API definitions ---'
sed -n '1,260p' frontend/src/api/intake.ts | cat -n

printf '%s\n' '--- frontend SSE call sites ---'
rg -n -C8 'generateIntake|startIntakeAiInitialReview|apiSseRequest' frontend/src

Repository: AG-Angui/Angui

Length of output: 50370


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- client error mapping ---'
sed -n '1,125p' frontend/src/api/client.ts | cat -n

printf '%s\n' '--- relevant intake API definitions ---'
rg -n -A12 -B3 'startIntakeAiInitialReview|getIntakeAiInitialReview|generateIntakeDraft' frontend/src/api/intake.ts

printf '%s\n' '--- exact page calls and nearby catches ---'
rg -n -A28 -B18 'generateIntakeDraft|startIntakeAiInitialReview' frontend/src/pages/FamilyIntakeForm.tsx

printf '%s\n' '--- page error-message helper and status branches ---'
rg -n -A18 -B8 'function messageFrom|const messageFrom|messageFrom\\(|cause\\.status' frontend/src/pages/FamilyIntakeForm.tsx

Repository: AG-Angui/Angui

Length of output: 9524


🏁 Script executed:

#!/bin/bash
set -eu

rg -n -F -A20 -B8 'function messageFrom' frontend/src/pages/FamilyIntakeForm.tsx frontend/src
rg -n -F -A12 -B6 'messageFrom =' frontend/src/pages/FamilyIntakeForm.tsx frontend/src

Repository: AG-Angui/Angui

Length of output: 9062


为 SSE 错误保留结构化错误分类,并使用本地化消息。

两个 SSE 路由只发送 error.to_string();apiSseRequest 随后固定生成 500/stream_failed,FamilyIntakeForm 直接展示该文本。这样会丢失 Forbidden、NotFound 和 Conflict 的状态及错误码。请发送 ApiError 的 code(必要时包含状态码),并在前端通过 userMessageFor 映射消息。当前 Internal 和 Database 文本虽已泛化,但字符串变体仍会透传未本地化的技术文本。

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/api/routes/intake_sessions.rs` around lines 105 - 121, Update the SSE
error path around generate_intake_profile_draft and sse_event to serialize
structured ApiError information, including its error code and applicable HTTP
status, instead of error.to_string(). Update apiSseRequest to preserve that
classification rather than forcing stream_failed/500, and have FamilyIntakeForm
map the code through userMessageFor so all displayed messages are localized,
including Internal and Database errors.

Source: Path instructions

Comment on lines +105 to 150
tokio::spawn(async move {
let event = match intake_session_service::generate_intake_profile_draft(
&state.db,
&auth,
&session_id,
&state.ai_gateway,
)
.await?,
))
.await
{
Ok(draft) => sse_event("completed", json!(draft)),
Err(error) => sse_event("error", json!({ "message": error.to_string() })),
};
let _ = sender.send(event).await;
});

Ok(intake_sse_response(receiver))
}

fn intake_sse_response(receiver: mpsc::Receiver<web::Bytes>) -> HttpResponse {
let events = stream::unfold(
(receiver, tokio::time::interval(Duration::from_secs(10))),
|(mut receiver, mut heartbeat)| async move {
tokio::select! {
event = receiver.recv() => event.map(|event| {
(Ok::<_, Error>(event), (receiver, heartbeat))
}),
_ = heartbeat.tick() => Some((
Ok(web::Bytes::from_static(b": keep-alive\n\n")),
(receiver, heartbeat),
)),
}
},
);

HttpResponse::Created()
.insert_header((header::CONTENT_TYPE, "text/event-stream"))
.insert_header((header::CACHE_CONTROL, "no-cache"))
.insert_header(("X-Accel-Buffering", "no"))
.streaming(events)
}

fn sse_event(event: &str, payload: serde_json::Value) -> web::Bytes {
let payload = serde_json::to_string(&payload)
.unwrap_or_else(|_| "{\"message\":\"internal service error\"}".to_owned());
web::Bytes::from(format!("event: {event}\ndata: {payload}\n\n"))
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

SSE error 事件契约丢失状态分类,前端始终显示未本地化的原始文本。 根因在后端:error 事件只携带 error.to_string(),不携带任何状态或错误码;前端据此始终返回状态 500、直接透传该文本作为用户提示。

  • src/api/routes/intake_sessions.rs#L105-L150:在 generate_intake_profile_draft 的错误分支和共享的 sse_event/intake_sse_response 辅助函数中,补充结构化的状态或错误码字段(例如从 ApiError 派生),而不是只序列化 error.to_string()。
  • src/api/routes/intake_sessions.rs#L248-L262:start_ai_initial_review 使用了相同的模式,需要同步应用上述修复。
  • frontend/src/api/client.ts#L187-L197:改用后端新增的状态/错误码字段,通过 userMessageFor 重新映射为本地化提示文本,而不是直接使用 payload.message,并恢复对具体状态码(如 403/404/409)的分支能力。
📍 Affects 2 files
  • src/api/routes/intake_sessions.rs#L105-L150 (this comment)
  • src/api/routes/intake_sessions.rs#L248-L262
  • frontend/src/api/client.ts#L187-L197
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/api/routes/intake_sessions.rs` around lines 105 - 150, The SSE error
contract must preserve structured status or error-code information so the
frontend can localize messages and branch on specific statuses. In
src/api/routes/intake_sessions.rs lines 105-150, update the
generate_intake_profile_draft error path and shared
sse_event/intake_sse_response flow to serialize fields derived from ApiError
instead of only error.to_string(); apply the same change to
start_ai_initial_review at lines 248-262. In frontend/src/api/client.ts lines
187-197, consume the new status/error-code fields, pass them through
userMessageFor, and retain specific handling for statuses such as 403, 404, and
409 rather than displaying payload.message directly.

Comment on lines +859 to +866
log::debug!(
target: "angui::ai_gateway::diagnostic",
"AI provider request: provider_id={}, model={}, protocol={:?}, body={}",
route.provider_id,
route.model,
route.protocol,
native.body(),
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

诊断日志记录了完整的 AI 请求体与响应体,存在 PII 泄露风险。

  • Line 859-866:记录 native.body(),即发送给 AI 供应商的完整请求体。该请求体包含家属提交的走失人员体貌、衣着、健康注意事项等描述性个人信息。
  • Line 897-909:非成功响应时记录供应商返回的原始文本 raw_response。
  • Line 925-940:非流式成功响应时,同样记录完整的 raw_response,其中包含 AI 生成的走失人员画像草稿。
  • Line 1011、1025-1031:流式响应场景下,记录每一个原始 SSE 分块 String::from_utf8_lossy(&chunk),同样可能包含部分画像内容。

这些内容都属于 log 的调试级别,只有在该 target 的调试日志被启用时才会被记录。但一旦生产环境为了排查问题临时调高日志级别,家属提交的敏感个人信息(可能包含健康相关内容)就会被完整写入应用日志。系统已经通过 AiRequest.redaction_policy_version 体现出对这类数据的治理意识,这里的诊断日志绕开了该治理机制,把同样的数据额外复制进访问范围更广、留存时间通常更长的应用日志中。

建议在记录前对请求体/响应体做脱敏或截断处理,或只记录摘要信息(如字段数量、字节长度、内容哈希),避免记录完整原始内容。

As per path instructions for src/**: "Review Rust/Actix code for authentication, authorization, privacy, error handling, concurrency, API compatibility, and data-loss risks."

Also applies to: 897-909, 925-940, 1011-1011, 1025-1031

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/integrations/ai_gateway.rs` around lines 859 - 866, The AI gateway
diagnostic logs expose sensitive request and response contents through
native.body(), raw_response, and streamed chunk logging. Update the relevant
logging paths to remove complete payloads and record only redacted/truncated
data or safe metadata such as byte length, field counts, or a content hash,
covering both non-streaming and streaming responses while preserving
provider/model/protocol diagnostics.

Source: Path instructions

@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown

Preview ready

The preview for this build is available at https://pr-179-ec9d3ede2283.angui.cg8.site.

Temporary demo accounts (all use the password ea1937e7ee65363e5ad1ee5bb795bca81078ef4e1460c6b0):

Role Email
Family family@demo.invalid
Commander commander@demo.invalid
Volunteer volunteer@demo.invalid
Learner learner@demo.invalid
Administrator admin@demo.invalid

These accounts and all preview data belong only to this deployment. The next preview deployment resets the SQLite database and invalidates these credentials.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/api/intake_session_confirm_post.rs`:
- Around line 175-182: 扩展 intake session SSE 测试覆盖:在现有成功场景中断言 started.session_id
等于 session_id,并保留 completed 数据断言;新增 start_ai_initial_review 返回 Err 时的测试,断言 SSE
error.message,避免使用 AiExecutionResult::Failed(其会走规则回退);补充仅含 started、缺少 completed
以及多段 data: 的边界测试,并按预期验证解析失败或 data 合并行为。
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: b36c46fd-8927-43e9-9889-dcf81c309fa1

📥 Commits

Reviewing files that changed from the base of the PR and between 4b8275f and ec9d3ed.

📒 Files selected for processing (2)
  • tests/api/intake_session_confirm_post.rs
  • tests/support/mod.rs

Comment on lines +175 to +182
assert_eq!(
initial_review
.headers()
.get(header::CONTENT_TYPE)
.and_then(|value| value.to_str().ok()),
Some("text/event-stream")
);
let initial_body = read_sse_completed_json(initial_review).await;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -C 8 \
  'read_sse_completed_json|read_sse_.*error|sse_event\("(started|completed|error)"|event: (started|completed|error)' \
  tests src

Repository: AG-Angui/Angui

Length of output: 9376


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- route structure and SSE producer ---'
ast-grep outline src/api/routes/intake_sessions.rs
sed -n '80,135p' src/api/routes/intake_sessions.rs

printf '%s\n' '--- SSE helper and related tests ---'
sed -n '185,230p' tests/support/mod.rs
sed -n '130,205p' tests/api/intake_session_confirm_post.rs
sed -n '270,390p' tests/api/intake_session_confirm_post.rs

printf '%s\n' '--- all relevant event construction and parser usage ---'
rg -n -C 5 'fn sse_event|sse_event\(|read_sse_' src tests

Repository: AG-Angui/Angui

Length of output: 20174


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- initial-review route and service call graph ---'
sed -n '225,267p' src/api/routes/intake_sessions.rs
rg -n -C 8 'generate_intake_initial_review|StartIntakeAiInitialReviewRequest|ai_gateway|Mock.*Gateway|failure|error' src tests

printf '%s\n' '--- test support and gateway setup ---'
rg -n -C 8 'struct TestContext|impl TestContext|app_state|AiGateway|ai_gateway|mock' tests/support tests

Repository: AG-Angui/Angui

Length of output: 50371


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- exact initial-review implementation and gateway behavior ---'
rg -n '^(pub )?async fn (start_ai_initial_review|generate_intake_profile_draft)|generate_intake_profile_draft|from_configurations|struct TestContext|fn new\(' src/application src/ai_gateway.rs tests/support/mod.rs
sed -n '1,95p' tests/support/mod.rs
sed -n '1,220p' src/ai_gateway.rs

Repository: AG-Angui/Angui

Length of output: 668


🏁 Script executed:

#!/bin/bash
set -euo pipefail

gateway_file="$(git ls-files | rg '(^|/)ai_gateway\.rs$' | head -n 1)"
printf 'gateway_file=%s\n' "$gateway_file"
sed -n '25,65p' tests/support/mod.rs
sed -n '360,510p' src/application/services/intake_session_service.rs
if [ -n "$gateway_file" ]; then
  sed -n '1,240p' "$gateway_file"
fi

Repository: AG-Angui/Angui

Length of output: 17094


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- remaining initial-review service logic ---'
sed -n '500,590p' src/application/services/intake_session_service.rs

printf '%s\n' '--- gateway construction, routing, and execution ---'
rg -n -C 10 'impl AiGateway|from_configurations|pub async fn execute|AiExecutionResult::Failed|AiExecutionResult::Degraded|NoProviderConfigured' src/integrations/ai_gateway.rs

Repository: AG-Angui/Angui

Length of output: 30845


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
import json

def read_sse_completed_json(stream):
    for frame in stream.split("\n\n"):
        event = next(
            (line.removeprefix("event:").strip()
             for line in frame.splitlines()
             if line.startswith("event:")),
            None,
        )
        if event != "completed":
            continue
        payload = "\n".join(
            line.removeprefix("data:").lstrip()
            for line in frame.splitlines()
            if line.startswith("data:")
        )
        return json.loads(payload)
    raise RuntimeError("SSE response should contain a completed event")

cases = {
    "started_only": "event: started\ndata: {\"session_id\":\"s\"}\n\n",
    "completed_missing": "event: started\ndata: {\"session_id\":\"s\"}\n\n",
    "completed_multiple_data": (
        "event: completed\n"
        "data: {\"status\":\n"
        "data: \"awaiting_family_review\"}\n\n"
    ),
}
for name, stream in cases.items():
    try:
        value = read_sse_completed_json(stream)
        print(name, "=>", value)
    except Exception as exc:
        print(name, "=>", type(exc).__name__, str(exc))
PY

Repository: AG-Angui/Angui

Length of output: 371


补充 SSE 事件的正向、负向和边界覆盖。

当前测试只断言 completed。补充以下断言:

  • 正向:断言 started.session_id 等于 session_id,并保留 completed 数据断言。
  • 负向:使 start_ai_initial_review 返回 Err,然后断言 error.message。AiExecutionResult::Failed 当前会转换为规则回退,不会触发 SSE error。
  • 边界:覆盖仅有 started、缺少 completed 和多段 data: 的响应。当前解析器对前两种情况失败,并会合并多段 data:。
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/api/intake_session_confirm_post.rs` around lines 175 - 182, 扩展 intake
session SSE 测试覆盖:在现有成功场景中断言 started.session_id 等于 session_id,并保留 completed
数据断言;新增 start_ai_initial_review 返回 Err 时的测试,断言 SSE error.message,避免使用
AiExecutionResult::Failed(其会走规则回退);补充仅含 started、缺少 completed 以及多段 data:
的边界测试,并按预期验证解析失败或 data 合并行为。

Source: Path instructions

@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown

Preview ready

The preview for this build is available at https://pr-179-ec9d3ede2283.angui.cg8.site.

Temporary demo accounts (all use the password 4be8f4c6ec04c6175c9782872742f94ab798fd27c9b9b38d):

Role Email
Family family@demo.invalid
Commander commander@demo.invalid
Volunteer volunteer@demo.invalid
Learner learner@demo.invalid
Administrator admin@demo.invalid

These accounts and all preview data belong only to this deployment. The next preview deployment resets the SQLite database and invalidates these credentials.

@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown

Preview ready

The preview for this build is available at https://pr-179-ec9d3ede2283.angui.cg8.site.

Temporary demo accounts (all use the password 41763bf5154092333574063a00b6b17a06e1762c6b5efa90):

Role Email
Family family@demo.invalid
Commander commander@demo.invalid
Volunteer volunteer@demo.invalid
Learner learner@demo.invalid
Administrator admin@demo.invalid

These accounts and all preview data belong only to this deployment. The next preview deployment resets the SQLite database and invalidates these credentials.

- Support and normalize provider-specific profile_candidates output
- Require fixed nullable source citations for every profile field
- Validate excerpts against cited family answers with diagnostics
- Add compatibility, provenance, prompt, and schema tests
- Load local environment files before application initialization

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
src/application/services/intake_session_service.rs (1)

2226-2288: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

建议补充两条测试用例。

当前测试覆盖兼容形状与摘录不可验证的拒绝。以下两条路径未覆盖:

  • canonical 形状解码:确认 decode_profile_extraction_output 返回 used_provider_compatibility_shape == false。
  • 跨字段来源冲突:name 与 gender 引用不同 source_answer_field 时,combine_profile_candidates 必须返回 physical_description candidates must cite the same family answer field。

第二条是新增的强约束,回退行为依赖它。

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/application/services/intake_session_service.rs` around lines 2226 - 2288,
补充两条测试覆盖未验证路径:在 profile_extraction 测试中使用 canonical 输出调用
decode_profile_extraction_output,并断言 used_provider_compatibility_shape 为
false;另增加跨字段来源冲突测试,让 name 与 gender 引用不同的 source_answer_field,调用
combine_profile_candidates,并断言返回 “physical_description candidates must cite the
same family answer field”。
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/application/services/intake_session_service.rs`:
- Around line 2226-2288: 补充两条测试覆盖未验证路径:在 profile_extraction 测试中使用 canonical 输出调用
decode_profile_extraction_output,并断言 used_provider_compatibility_shape 为
false;另增加跨字段来源冲突测试,让 name 与 gender 引用不同的 source_answer_field,调用
combine_profile_candidates,并断言返回 “physical_description candidates must cite the
same family answer field”。

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 06e8d709-0d64-4507-816b-2f60d04f72eb

📥 Commits

Reviewing files that changed from the base of the PR and between ec9d3ed and c1256b3.

📒 Files selected for processing (2)
  • src/application/services/intake_session_service.rs
  • src/main.rs

@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown

Preview ready

The preview for this build is available at https://pr-179-c1256b3d1493.angui.cg8.site.

Temporary demo accounts (all use the password 97e7ce3e91072eae0930fd2116b9574f9379ed87208b0eb7):

Role Email
Family family@demo.invalid
Commander commander@demo.invalid
Volunteer volunteer@demo.invalid
Learner learner@demo.invalid
Administrator admin@demo.invalid

These accounts and all preview data belong only to this deployment. The next preview deployment resets the SQLite database and invalidates these credentials.

…ng fields

- Set `Content-Type: application/json` for streamed requests with non-FormData bodies
- Preserve existing headers and authorization while setting SSE acceptance headers
- Allow initial review issues to reference configured fields that remain unanswered
- Restrict review fields and source fields to the configured intake answer fields
- Log rejected AI review output before falling back to rule-based issues
- Add coverage for streamed JSON headers and initial review validation behavior

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
frontend/src/api/client.ts (1)

195-203: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

统一 SSE 错误响应契约并保留错误码。

后端发送顶层 { "message": ... },而 JSON API 使用 { "error": { "code", "message" } }。客户端将所有 SSE 错误转换为 500/stream_failed,因此会丢失 unauthorized、forbidden、not_found 和 conflict,且无法触发会话过期处理。为 SSE 错误增加稳定的 code 和状态信息,并在客户端使用 userMessageFor 映射用户消息,不要直接显示服务端 message。

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/api/client.ts` around lines 195 - 203, 更新 SSE 的 error 处理分支,使其按
JSON API 契约读取顶层 error 对象中的 code 和状态信息,而不是仅读取顶层 message。保留后端返回的
unauthorized、forbidden、not_found、conflict 等错误码及对应状态创建 ApiClientError,并通过
userMessageFor 映射用户提示,避免直接展示服务端 message;同时兼容统一后的 SSE 错误响应结构。

Source: Path instructions

🧹 Nitpick comments (1)
frontend/src/api/client.test.ts (1)

90-155: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

补充 SSE 失败路径的契约测试。

当前新增测试覆盖 completed、keep-alive、请求头和 stream_incomplete。未看到 error 事件、无效 JSON 和 reader.read() 失败的测试。请至少补充这些场景,并断言 ApiClientError.code,以锁定用户可见的错误处理契约。

As per path instructions: 对 frontend/**,重点检查 frontend/backend API contract compatibility、authentication state、validation、data exposure 和 user-visible failure paths。

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/api/client.test.ts` around lines 90 - 155, 在 apiSseRequest
测试组中补充 SSE 失败路径契约测试:覆盖 error 事件、data 无效 JSON,以及 reader.read() 失败,并分别断言抛出的
ApiClientError.code 为对应的用户可见错误码。保持现有 completed、keep-alive、请求头和 stream_incomplete
测试不变。

Source: Path instructions

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@frontend/src/api/client.ts`:
- Around line 195-203: 更新 SSE 的 error 处理分支,使其按 JSON API 契约读取顶层 error 对象中的 code
和状态信息,而不是仅读取顶层 message。保留后端返回的 unauthorized、forbidden、not_found、conflict
等错误码及对应状态创建 ApiClientError,并通过 userMessageFor 映射用户提示,避免直接展示服务端 message;同时兼容统一后的
SSE 错误响应结构。

---

Nitpick comments:
In `@frontend/src/api/client.test.ts`:
- Around line 90-155: 在 apiSseRequest 测试组中补充 SSE 失败路径契约测试:覆盖 error 事件、data 无效
JSON,以及 reader.read() 失败,并分别断言抛出的 ApiClientError.code 为对应的用户可见错误码。保持现有
completed、keep-alive、请求头和 stream_incomplete 测试不变。

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: afe8a637-44ea-4207-97e7-0cd20db0674c

📥 Commits

Reviewing files that changed from the base of the PR and between c1256b3 and 779121f.

📒 Files selected for processing (3)
  • frontend/src/api/client.test.ts
  • frontend/src/api/client.ts
  • src/application/services/intake_session_service.rs

- load AMap PlaceSearch alongside geocoding services
- search addresses, landmarks, and venues before opening map confirmation
- show selectable POI results and center the picker on selection
- handle empty queries, unavailable services, and unlocatable results
- reset search state when clearing a confirmed location
- move clue location picker beside clue content in the workspace form
- add coverage for searching and selecting a location result

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@frontend/src/components/LocationConfirmationPicker.tsx`:
- Around line 425-435: 使 LocationConfirmationPicker 中的搜索结果始终对应当前关键词:在
searchPlaces 处理空关键词时先使 searchOperationRef 递增并清空 searchResults,再显示提示并返回;在更新
searchQuery 的处理逻辑中同步使正在执行的搜索失效并清空旧结果。保留现有请求回调的操作编号校验,确保关键词修改或提交空关键词后旧响应不会重新显示结果。
- Around line 263-280: 在 openPickerAtCoordinates 中,当 map.current
不存在且用户重新选择结果时递增地图初始化版本状态,并将该状态加入负责创建地图的 useEffect
依赖,使失效后的初始化重新执行;保持已有地图实例时的行为不变,并补充地图加载期间连续选择两个搜索结果仍能完成初始化的测试。
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 05a9215d-25f1-480b-a44d-8d9af2ab4cd2

📥 Commits

Reviewing files that changed from the base of the PR and between 779121f and ee20ba2.

📒 Files selected for processing (3)
  • frontend/src/components/LocationConfirmationPicker.test.tsx
  • frontend/src/components/LocationConfirmationPicker.tsx
  • frontend/src/pages/CaseWorkspacePage.tsx
🚧 Files skipped from review as they are similar to previous changes (2)
  • frontend/src/pages/CaseWorkspacePage.tsx
  • frontend/src/components/LocationConfirmationPicker.test.tsx

Comment on lines +263 to +280
const openPickerAtCoordinates = useCallback(
(longitude: number, latitude: number) => {
++operationRef.current;
const next: CandidateLocation = {
longitude,
latitude,
address: "",
precision: "approximate",
};
candidateRef.current = next;
setCandidate(next);
setIsPickerOpen(true);
setMessage("");
setSearchResults([]);
marker.current?.setPosition([longitude, latitude]);
map.current?.panTo([longitude, latitude]);
if (map.current) resolveAddress(longitude, latitude);
},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

重新选择搜索结果时,重新启动地图初始化。

当首次地图初始化仍在等待时,用户可以再次选择搜索结果。Line 265 会使现有初始化失效。此时 isPickerOpen 仍为 true,map.current 仍为 null,useEffect 不会重新执行。地图不会创建,canConfirm 会一直为 false。

当 map.current 不存在时,增加一个地图初始化版本状态并将其加入 effect 依赖。每次选择新结果时递增该版本。补充“地图加载期间连续选择两个结果”的测试。

建议修复
+  const [mapInitializationRevision, setMapInitializationRevision] = useState(0);
+
   const openPickerAtCoordinates = useCallback(
     (longitude: number, latitude: number) => {
       // ...
       marker.current?.setPosition([longitude, latitude]);
       map.current?.panTo([longitude, latitude]);
-      if (map.current) resolveAddress(longitude, latitude);
+      if (map.current) {
+        resolveAddress(longitude, latitude);
+      } else {
+        setMapInitializationRevision((revision) => revision + 1);
+      }
     },
     [resolveAddress],
   );
 
-  }, [chooseCoordinates, isPickerOpen, resolveAddress]);
+  }, [chooseCoordinates, isPickerOpen, mapInitializationRevision, resolveAddress]);

As per path instructions,需检查 frontend/** 的用户可见失败路径。

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const openPickerAtCoordinates = useCallback(
(longitude: number, latitude: number) => {
++operationRef.current;
const next: CandidateLocation = {
longitude,
latitude,
address: "",
precision: "approximate",
};
candidateRef.current = next;
setCandidate(next);
setIsPickerOpen(true);
setMessage("");
setSearchResults([]);
marker.current?.setPosition([longitude, latitude]);
map.current?.panTo([longitude, latitude]);
if (map.current) resolveAddress(longitude, latitude);
},
const [mapInitializationRevision, setMapInitializationRevision] = useState(0);
const openPickerAtCoordinates = useCallback(
(longitude: number, latitude: number) => {
+operationRef.current;
const next: CandidateLocation = {
longitude,
latitude,
address: "",
precision: "approximate",
};
candidateRef.current = next;
setCandidate(next);
setIsPickerOpen(true);
setMessage("");
setSearchResults([]);
marker.current?.setPosition([longitude, latitude]);
map.current?.panTo([longitude, latitude]);
if (map.current) {
resolveAddress(longitude, latitude);
} else {
setMapInitializationRevision((revision) => revision + 1);
}
},
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/components/LocationConfirmationPicker.tsx` around lines 263 -
280, 在 openPickerAtCoordinates 中,当 map.current
不存在且用户重新选择结果时递增地图初始化版本状态,并将该状态加入负责创建地图的 useEffect
依赖,使失效后的初始化重新执行;保持已有地图实例时的行为不变,并补充地图加载期间连续选择两个搜索结果仍能完成初始化的测试。

Source: Path instructions

Comment thread frontend/src/components/LocationConfirmationPicker.tsx
- invalidate pending place searches when the input value changes
- clear displayed results and loading state for changed or empty queries
- show empty-query validation without retaining previous search results
- add coverage for stale asynchronous search responses

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
frontend/src/components/LocationConfirmationPicker.test.tsx (1)

330-336: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

补充已显示结果在清空关键词后被移除的断言。

此处清空关键词前没有已显示的搜索结果。当前断言不能证明清空操作会移除最新搜索结果。若组件保留最新结果,此测试仍会通过。

先完成“新关键词”的搜索并断言结果显示。然后清空输入,并断言该结果已消失。

建议的测试补充
+    fireEvent.click(screen.getByRole("button", { name: "搜索地点" }));
+    await act(async () => {
+      pendingSearch?.("complete", {
+        poiList: {
+          pois: [{
+            id: "current-place",
+            name: "新搜索结果",
+            location: { getLng: () => 116.41, getLat: () => 39.93 },
+          }],
+        },
+      });
+    });
+    expect(screen.getByText("新搜索结果")).toBeInTheDocument();
+
     fireEvent.change(screen.getByRole("textbox", { name: "搜索地点" }), {
       target: { value: "" },
     });
+    expect(screen.queryByText("新搜索结果")).not.toBeInTheDocument();
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/components/LocationConfirmationPicker.test.tsx` around lines 330
- 336, 更新 LocationConfirmationPicker
测试中的搜索流程:先输入新的关键词并点击“搜索地点”,断言对应搜索结果已显示;随后清空输入并再次搜索,断言该结果已从文档中移除,同时保留现有的空关键词提示断言。
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@frontend/src/components/LocationConfirmationPicker.test.tsx`:
- Around line 330-336: 更新 LocationConfirmationPicker
测试中的搜索流程:先输入新的关键词并点击“搜索地点”,断言对应搜索结果已显示;随后清空输入并再次搜索,断言该结果已从文档中移除,同时保留现有的空关键词提示断言。

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: b0dce574-ccf0-43a7-9936-8473c250a256

📥 Commits

Reviewing files that changed from the base of the PR and between ee20ba2 and 8d40190.

📒 Files selected for processing (2)
  • frontend/src/components/LocationConfirmationPicker.test.tsx
  • frontend/src/components/LocationConfirmationPicker.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • frontend/src/components/LocationConfirmationPicker.tsx

- Force PostCSS to use version 8.5.23 through Yarn resolutions
- Upgrade Undici to version 7.29.0 for all matching dependency ranges
- Update the lockfile with the new Undici package metadata and integrity hash
@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown

Preview ready

The preview for this build is available at https://pr-179-445aa2044826.angui.cg8.site.

Temporary demo accounts (all use the password 5ef569eb62b3a4bf225e7adf167b3a1506d28b255a311133):

Role Email
Family family@demo.invalid
Commander commander@demo.invalid
Volunteer volunteer@demo.invalid
Learner learner@demo.invalid
Administrator admin@demo.invalid

These accounts and all preview data belong only to this deployment. The next preview deployment resets the SQLite database and invalidates these credentials.

@cg8-5712
cg8-5712 merged commit 2abb812 into main Aug 4, 2026
15 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants