Skip to content

feat: 游戏化学习(挑战岛 / Quest Island)— 面向 7-15 岁儿童 - #8

Merged
hubo1989 merged 5 commits into
mainfrom
feature/gamified-learning
Aug 11, 2026
Merged

hubo1989 merged 5 commits into
mainfrom
feature/gamified-learning

Conversation

@hubo1989

@hubo1989 hubo1989 commented Aug 9, 2026

Copy link
Copy Markdown
Owner

游戏化学习(挑战岛 / Quest Island)

面向 7-15 岁儿童的游戏化学习功能,把"学知识"包装成"闯关挑战",支持个人知识库和公共知识库两类内容来源。

交付概览

指标 数值
新增/修改文件 78 个
代码行数 15,026
测试数 410 passed, 0 failed
后端新增依赖 0
前端新增依赖 0

需求覆盖(F0-F11)

需求 说明
F0 监护人主账号 + 儿童 profile + PIN 切换 + 角色能力矩阵守卫
F1 kid_quest Capability 三阶段流水线(sourcing → forging → questing)
F2 public 公共知识库类型 + 3 个内置主题(恐龙世界/太阳系/乘法王国)
F3 个人 KB 一键生成挑战地图(SourceResolver 双来源隔离)
F4 关卡解锁与星级评定引擎(确定性纯函数,零 LLM)
F5 XP / 徽章 / streak / 等级系统与 JSON 持久化
F6 答题引导式提示三段式(再想想 → 提示 → 讲解)
F7 Kids 前端:首页 / 地图 / 闯关 / 结算 / 徽章墙
F8 家长只读面板(周报/薄弱知识点/建议复习)+ PIN 进入
F9 儿童安全过滤管线(中英黑名单 + LLM 复检 + 红队测试 ≥98%)
F10 TTS 朗读(7-9 岁默认开,浏览器 fallback)
F11 每日目标与休息提醒(20 分钟可调)+ 指标埋点

关键设计

  • 判定公理:LLM 只管出题/讲解,判分/解锁/星级/XP 全部由确定性纯函数引擎完成
  • LLM 全可选:无 LLM 时用模板占位题 + 浏览器 TTS fallback,流程完整可跑通
  • 角色安全:儿童 profile 无独立登录,PIN 切换,角色矩阵限制(禁支付/分享/KB 删改)
  • 命名空间隔离personal:<profile_id>:<kb_name>public:<theme_id> 永不碰撞

文件结构

后端:
  deeptutor/services/gamification/   — 引擎/徽章/存储/来源/安全/指标/主题
  deeptutor/services/kid_profiles/   — profile/PIN/角色守卫
  deeptutor/capabilities/quest/      — kid_quest Capability(出题/判分/提示/闯关/缓存)
  deeptutor/api/routers/             — profiles / kids / voice_kids API
  deeptutor/multi_user/kid_context.py — 儿童 profile ContextVar

前端:
  web/app/(kids)/                    — 5 个页面(home/map/play/rewards/layout)
  web/features/kids/                 — 10 组件 + 3 hooks + store + theme
  web/lib/                           — kids-api / kids-types
  web/locales/                       — zh/en i18n

数据:
  data/public_kb/                    — 3 主题(各含 manifest.yaml + 5 篇中文语料)

测试:
  tests/services/gamification/       — 引擎/徽章/存储/来源/主题/安全/缓存/指标
  tests/capabilities/quest/          — 出题/判分/提示/闯关
  tests/api/                         — profiles / kids / role_guard

配套文档

  • Spec: docs/specs/gamified-learning-spec.md
  • Plan: docs/specs/gamified-learning-plan.md(1257 行,含追溯矩阵/架构图/时序图/风险登记册)

测试

pytest tests/services/gamification/ tests/api/ tests/capabilities/quest/ --tb=short
# 410 passed in 2.77s

已知事项

  • 公共库数据 data/public_kb/ 被 .gitignore 排除,部署时需单独打包
  • WS 端到端联调待真实环境验证(前端 useKidQuest hook ↔ 后端 StreamBus)
  • 接入真实 LLM 后需做 golden set 出题质量回放(目标 schema 合法率 ≥95%)

Summary by CodeRabbit

  • 新功能
    • 新增儿童学习模式,支持儿童档案创建、切换、删除及监护人 PIN 保护。
    • 提供主题选择、挑战地图、答题闯关、提示、语音朗读和休息提醒。
    • 新增 XP、等级、星级、连续学习、徽章、周报及薄弱主题复习建议。
    • 提供恐龙、乘法和太阳系等公共学习主题。
    • 新增家长进度面板,可查看学习统计与主题表现。
  • 安全性
    • 增加儿童内容安全过滤和角色权限控制。
  • 本地化
    • 新增中文和英文儿童学习界面文案。

- Kid profile system: guardian account + child profiles, PIN switch,
  role-based capability guard matrix
- Gamification engine: XP/stars/unlock/streak/level pure functions,
  6 badges, JSON persistence with atomic writes
- Public knowledge base: PUBLIC_KB_TYPE + 3 themes (dino-world,
  solar-system, multiplication-kingdom), SourceResolver isolation
- kid_quest Capability: 3-stage pipeline (sourcing -> forging ->
  questing), 8 question types, deterministic grading (zero LLM),
  3-stage hints, safety filter, quest cache
- Kids frontend: home/map/play/rewards pages, 7 components,
  WS event state machine, kids theme tokens, zh/en i18n
- Parent dashboard: weekly report, weak topics, recommendations,
  TTS (browser fallback), rest reminder, metrics (JSONL)
- 410 tests passing, 0 failures

Covers: F0 (accounts), F1 (capability), F2 (public KB), F3 (personal
KB maps), F4 (unlock/stars engine), F5 (XP/badges/streak), F6 (hints),
F7 (kids UI), F8 (parent panel), F9 (safety filter), F10 (TTS),
F11 (rest reminder)
@coderabbitai

coderabbitai Bot commented Aug 9, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

新增儿童档案、角色权限、公共主题、题目生成、游戏化进度、Kids API、TTS、WebSocket 任务流程和前端学习界面。新增对应的存储、测试、主题数据、本地化资源及构建脚本。

Changes

儿童学习平台

Layer / File(s) Summary
儿童身份、权限与知识源基础
deeptutor/multi_user/..., deeptutor/services/kid_profiles/..., deeptutor/knowledge/...
新增儿童档案、Guardian PIN、角色能力矩阵、请求上下文和公共知识库保护。
游戏化模型、引擎与持久化
deeptutor/services/gamification/...
新增题目、地图、进度、XP、等级、徽章、周报、指标、安全过滤和 JSON 持久化。
任务生成、提示、判分与结算
deeptutor/capabilities/quest/..., tests/capabilities/quest/...
新增年龄适配题目生成、模板回退、Schema 校验、确定性评分、提示、缓存、任务回合和进度结算。
档案、Kids 与语音 API
deeptutor/api/...
新增档案 CRUD、角色切换、PIN、主题、地图、进度和 TTS 接口,并挂载认证路由。
Kids 前端运行时与交互界面
web/lib/..., web/features/kids/..., web/app/(kids)/...
新增类型、API 客户端、档案上下文、Reducer、WebSocket 与 TTS Hook、学习页面和交互组件。
公共主题内容与教学支持
data/public_kb/..., scripts/build_public_kb.py, web/locales/..., 教学产物/...
新增三个公共主题的双语知识内容、索引构建脚本、本地化资源和数学教学文档。

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Guardian
  participant ProfilesAPI
  participant KidsAPI
  participant KidQuestCapability
  participant GamificationStore
  participant KidsWeb
  Guardian->>ProfilesAPI: create or switch kid profile
  ProfilesAPI-->>KidsWeb: kid profile context
  KidsWeb->>KidsAPI: request themes and map
  KidsAPI->>KidQuestCapability: start quest
  KidQuestCapability->>GamificationStore: load or save progress
  GamificationStore-->>KidsAPI: map and progress state
  KidsAPI-->>KidsWeb: render map and quest data
Loading

Poem

小兔敲键盘,题目蹦出来,
星星落进 XP,徽章排成排。
地图连着知识,提示轻轻来,
PIN 守住小档案,
兔耳朵说:“学习真精彩!”

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.11% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed 标题准确概括了面向 7–15 岁儿童新增游戏化学习功能这一主要变更,简洁且与变更内容一致。
Description check ✅ Passed 描述详细说明了功能范围、受影响模块、测试结果和已知事项,内容与 PR 目标一致,但未完整包含模板中的 Related Issues、Checklist 等章节。
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/gamified-learning

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 5

Note

Due to the large number of review comments, Critical severity comments were prioritized as inline comments.

🟡 Minor comments (19)
deeptutor/api/routers/voice_kids.py-11-14 (1)

11-14: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

文档与实现不一致。

模块文档说明后端语音服务不可用时返回 HTTP 501。实际实现始终返回 HTTP 200,并把 fallback 置为 True。请更新文档,避免调用方按 501 编写处理逻辑。

📝 建议的修改
 When the backend voice service is configured, it delegates to
 :func:`deeptutor.services.voice.synthesize_speech`. When the voice service
-is not available (no TTS provider configured), it returns HTTP 501 so the
-frontend can fall back to the browser-native ``speechSynthesis`` API.
+is not available (no TTS provider configured), it returns HTTP 200 with
+``fallback=True`` so the frontend can fall back to the browser-native
+``speechSynthesis`` API.
🤖 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 `@deeptutor/api/routers/voice_kids.py` around lines 11 - 14, 更新 voice_kids
模块文档,将语音服务未配置时的行为描述为返回 HTTP 200 并设置 fallback=True,不要再说明会返回 HTTP 501;保留已配置语音服务时委托
synthesize_speech 的说明。
web/features/kids/store/kidsStore.ts-142-155 (1)

142-155: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

NEXT_QUESTION 越界时会让页面停留在加载态。

nextIdx >= state.questions.length 时,reducer 仍把 currentIndex 设为越界值并保持 phase: "presenting"play/[levelId]/page.tsx 第 109 行的 store.questions[store.currentIndex] ?? null 会得到 null,页面随后永久显示加载动画。

建议在越界时切换到 level_complete,让状态机自身闭合。

🐛 建议修复
     case "NEXT_QUESTION": {
       const nextIdx = state.currentIndex + 1;
       if (nextIdx >= state.questions.length) {
-        return { ...state, phase: "presenting", currentIndex: nextIdx };
+        return { ...state, phase: "level_complete" };
       }
🤖 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 `@web/features/kids/store/kidsStore.ts` around lines 142 - 155, Update the
NEXT_QUESTION branch in the reducer so that when nextIdx is at or beyond
state.questions.length, it transitions to phase "level_complete" instead of
"presenting". Preserve the existing index advancement and normal-question reset
behavior for in-range questions.
web/app/(kids)/play/[levelId]/page.tsx-296-326 (1)

296-326: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

反馈动画期间点击 "Next Question" 没有任何响应。

第 308 行在 feedback !== null 时静默 return。反馈遮罩持续 1200ms,这段时间内按钮可点击但无反馈,儿童会重复点击。

FeedbackOverlayonComplete(第 323 行)也只清空 feedback,不推进题目。建议让 onComplete 直接调用 handleFeedbackComplete,并在动画期间禁用按钮。

♻️ 建议修复
           onClick={() => {
             if (feedback !== null) {
-              // Wait for animation
               return;
             }
             handleFeedbackComplete();
           }}
+          disabled={feedback !== null}
+          aria-disabled={feedback !== null}
         >
@@
       <FeedbackOverlay
         type={feedback}
         comboCount={store.combo}
-        onComplete={() => {
-          setFeedback(null);
-        }}
+        onComplete={handleFeedbackComplete}
       />
🤖 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 `@web/app/`(kids)/play/[levelId]/page.tsx around lines 296 - 326, Update the
submitted-question button and FeedbackOverlay flow: disable the button while
feedback is active instead of silently returning from its onClick handler, and
have FeedbackOverlay’s onComplete call handleFeedbackComplete so the next
question or results advances after the animation completes. Preserve feedback
state cleanup as part of that completion flow.
web/features/kids/components/HintPanel.tsx-159-174 (1)

159-174: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

阶段指示器固定渲染 3 个点,与实际提示条数不符。

hints 可能少于 3 条。play/[levelId]/page.tsx 中的 demo_q2 只有 2 条提示。此时第 3 个点永远保持灰色,向儿童暗示还存在一条未解锁的提示,但 canAdvance(第 61 行)已经为 false

请按实际提示条数渲染指示点。

🐛 建议修复
       <div className="flex gap-1 mt-3 justify-center">
-        {[0, 1, 2].map((i) => (
+        {Array.from({ length: Math.min(hints.length, 3) }).map((_, i) => (
           <div
             key={i}
🤖 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 `@web/features/kids/components/HintPanel.tsx` around lines 159 - 174, Update
the stage indicator in HintPanel to render one indicator for each entry in hints
instead of the fixed [0, 1, 2] collection. Preserve the existing active-state
styling based on stage while ensuring the number of indicators matches the
actual hint count.
web/app/(kids)/rewards/page.tsx-107-133 (1)

107-133: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

满级时显示不存在的 "Level 11"。

MAX_LEVEL 为 10。当 level 为 10 时:

  • xpForNextLevel(10) 返回最后一个阈值 1680,而 totalXp 已达到或超过 1680。第 113 行因此显示 "0 XP to Level 11" 或负数。
  • 第 129 行显示 "Progress to Level 11"。

请为满级增加单独的文案分支。

🐛 建议修复
+import { MAX_LEVEL } from "`@/lib/kids-types`";
+
+  const isMaxLevel = level >= MAX_LEVEL;
@@
           <div className="text-xs opacity-75">
-            {nextLevelXp - totalXp} XP to Level {level + 1}
+            {isMaxLevel
+              ? "Max level reached!"
+              : `${nextLevelXp - totalXp} XP to Level ${level + 1}`}
           </div>
@@
           <span className="text-sm font-semibold" style={{ color: "var(--foreground)" }}>
-            Progress to Level {level + 1}
+            {isMaxLevel ? "Max level reached" : `Progress to Level ${level + 1}`}
           </span>
🤖 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 `@web/app/`(kids)/rewards/page.tsx around lines 107 - 133, Update the rewards
page level-progress messaging to branch on the MAX_LEVEL state: when level is
10, do not render references to Level 11 or negative/zero remaining XP, and
instead show appropriate max-level copy; preserve the existing next-level
progress text for levels below the cap. Use the existing level and MAX_LEVEL
symbols in the relevant progress labels and XP calculation display.
web/app/(kids)/home/page.tsx-262-270 (1)

262-270: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

主题标题固定优先中文,未使用语言设置。

第 266 行使用 theme.title_zh || theme.title_en。英文用户始终看到中文标题。本 PR 已加入 web/locales/en/kids.jsonweb/locales/zh/kids.json。请按当前语言选择字段。页面内其他文案(例如第 132 行 "Choose a theme to start your adventure"、第 210 行 "Continue Learning")同样是硬编码英文,请一并接入本地化。

🤖 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 `@web/app/`(kids)/home/page.tsx around lines 262 - 270, Update the theme title
rendering in the page’s theme list to select title_zh or title_en according to
the active locale, rather than always preferring Chinese. Also replace the
hardcoded strings “Choose a theme to start your adventure” and “Continue
Learning” with the corresponding keys from the existing kids locale files,
preserving the current English and Chinese translations.
web/features/kids/components/RestReminder.tsx-81-86 (1)

81-86: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

按钮文案承诺 5 分钟,实际重置为完整时长。

按钮文案是 “Keep Going (5 more minutes)”。handleDismiss 在第 84 行把倒计时重置为 durationMinutes * 60,默认是 20 分钟。行为与文案不一致,家长设定的休息节奏会被延后。请重置为 5 分钟,或修改文案。

🐛 建议修复
+const SNOOZE_MINUTES = 5;
+
   const handleDismiss = useCallback(() => {
     setShowPopup(false);
-    // Reset timer for another cycle
-    setSecondsLeft(durationMinutes * 60);
+    // Snooze for a short cycle, matching the button label
+    setSecondsLeft(SNOOZE_MINUTES * 60);
     onDismiss?.();
-  }, [durationMinutes, onDismiss]);
+  }, [onDismiss]);

Also applies to: 179-188

🤖 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 `@web/features/kids/components/RestReminder.tsx` around lines 81 - 86, Update
handleDismiss in RestReminder so the “Keep Going (5 more minutes)” action resets
secondsLeft to a fixed five-minute interval instead of durationMinutes. Keep the
popup dismissal and onDismiss behavior unchanged, and apply the same correction
to the corresponding logic referenced later in the component.
web/features/kids/components/ParentDashboard.tsx-357-372 (1)

357-372: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

AccuracyTrendChart 接收 weeklyReport 但从未使用。

该组件只根据 maps 的完成度绘制柱状图,weeklyReport 参数在函数体内没有引用。同时该图的标题是 “Accuracy Trend”,但绘制的是关卡完成率,而不是正确率。weeklyReport.avg_accuracy 才是正确率数据。请改用正确率数据,或把标题改为 “Topic Completion”,避免误导家长。

🤖 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 `@web/features/kids/components/ParentDashboard.tsx` around lines 357 - 372,
Update AccuracyTrendChart to use weeklyReport.avg_accuracy for the accuracy
trend instead of deriving bars solely from maps completion ratios, and remove
any unused maps dependency if no longer needed; alternatively, if the chart must
remain completion-based, rename its title to “Topic Completion” so it matches
the displayed metric.
web/features/kids/hooks/useTTS.ts-74-84 (1)

74-84: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

切换档案时,未存储的档案会沿用上一个档案的开关状态。

该 effect 只在 localStorage 存在记录时调用 setEnabledState。如果新的 profileId 没有记录,状态保持为上一个档案的值。年龄段默认值(第 61-64 行)也不会重新应用。请在无记录时回退到默认值。

🐛 建议修复
   useEffect(() => {
     const storageKey = `deeptutor:tts:${profileId || "default"}`;
     try {
       const stored = localStorage.getItem(storageKey);
       if (stored !== null) {
         setEnabledState(stored === "true");
+      } else {
+        setEnabledState(defaultEnabled);
       }
     } catch {
       // localStorage may be unavailable
     }
-  }, [profileId]);
+  }, [profileId, defaultEnabled]);
🤖 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 `@web/features/kids/hooks/useTTS.ts` around lines 74 - 84, 更新 useEffect 中基于
profileId 读取 TTS 设置的逻辑:当 localStorage 没有对应 storageKey 的记录时,调用 setEnabledState
以恢复年龄段默认值,而不是保留上一个档案的状态;保留已有记录时的解析行为,并确保切换档案和 localStorage 不可用时都能应用默认值。
web/features/kids/hooks/useTTS.ts-179-183 (1)

179-183: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

后端音频播放路径不更新 isSpeaking

speakWithBrowser 通过 utterance.onstart / onend 维护 isSpeakingplayBase64Audio 分支不设置该状态。使用后端音频时,isSpeaking 始终为 false,依赖它的按钮状态或动画不会变化。请在调用前后设置该状态。

🐛 建议修复
           if (data.audio_base64) {
-            // Play the base64 audio
-            await playBase64Audio(data.audio_base64, data.content_type || "audio/mpeg");
-            return;
+            setIsSpeaking(true);
+            try {
+              await playBase64Audio(data.audio_base64, data.content_type || "audio/mpeg");
+            } finally {
+              setIsSpeaking(false);
+            }
+            return;
           }
🤖 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 `@web/features/kids/hooks/useTTS.ts` around lines 179 - 183, Update the backend
audio branch in speakWithBrowser around playBase64Audio to set isSpeaking to
true before playback and reset it to false after playback completes, including
when playback fails. Preserve the existing return behavior and browser-speech
state handling.
web/app/(kids)/layout.tsx-60-76 (1)

60-76: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

固定定位的横幅会遮挡页面顶部内容。

第 62-63 行的横幅使用 fixed top-0 ... z-50,脱离文档流。第 76 行的 main 只有 py-6,没有为横幅预留高度。横幅出现时会覆盖页面标题。请在 showRemindertrue 时给 main 增加顶部内边距。

🐛 建议修复
-      <main className="max-w-3xl mx-auto px-4 py-6 pb-24">{children}</main>
+      <main
+        className="max-w-3xl mx-auto px-4 py-6 pb-24"
+        style={showReminder ? { paddingTop: "4.5rem" } : undefined}
+      >
+        {children}
+      </main>
🤖 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 `@web/app/`(kids)/layout.tsx around lines 60 - 76, Update the main content
container in the kids layout so that when showReminder is true it receives
additional top padding sufficient to clear the fixed session reminder banner,
while preserving the existing spacing when the banner is hidden.
web/features/kids/components/StreakCalendar.tsx-156-184 (1)

156-184: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

已学习的日期格子对屏幕阅读器不可读。

第 179-183 行在 day.learnedtrue 时用 Flame 图标替换日期数字。该图标没有文本替代内容。屏幕阅读器读到的是空格子,无法区分日期,也无法得知该日已学习。请为每个格子添加 aria-label

🐛 建议修复
                 <div
                   key={day.isoDate}
                   className="flex items-center justify-center text-sm font-medium transition-all"
+                  aria-label={`${day.isoDate}${day.learned ? " learned" : ""}`}
🤖 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 `@web/features/kids/components/StreakCalendar.tsx` around lines 156 - 184,
Update the day cell rendered in StreakCalendar so each grid item has an
aria-label containing its date and learned status, including when day.learned
replaces the date number with the Flame icon. Keep the existing visual rendering
unchanged and derive the label from the existing day data and localization
conventions.
web/features/kids/components/StreakCalendar.tsx-24-30 (1)

24-30: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

统一前后端日期比较的时区基准。

StreakCalendar 用浏览器本地 Date 生成 YYYY-MM-DD,并用该日期与 streakHistory 比较。streak_history 由后端写入,且 daily_xp_date 是服务时区 YYYY-MM-DD。因此非 UTC 时区用户在本地跨零点时可以看到学习日标记偏移一天。前端日历和后端 streak 更新使用同一用户时区基准。

🤖 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 `@web/features/kids/components/StreakCalendar.tsx` around lines 24 - 30, Update
toIsoDate in StreakCalendar to derive YYYY-MM-DD using the same
UTC/service-timezone date basis as streak_history.daily_xp_date instead of
browser-local Date components. Ensure calendar comparisons and displayed streak
markers use this consistent timezone basis.
deeptutor/services/gamification/public_themes.py-98-100 (1)

98-100: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

验证每个 level 定义都是字典。

当前代码只验证 levels 是列表。若 manifest 包含标量元素,deeptutor/api/routers/kids.py_build_quest_map_from_theme() 会对该元素调用 .get() 并抛出 AttributeError。将包含非字典 level 的 manifest 视为无效并跳过,或在这里规范化全部元素。

🤖 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 `@deeptutor/services/gamification/public_themes.py` around lines 98 - 100, 在
public theme 的 levels 校验逻辑中,除验证 levels 为列表外,还要验证每个元素都是字典;若包含非字典元素,则将整个 manifest
视为无效并跳过,或统一规范化为仅保留字典元素,确保 kids.py 的 _build_quest_map_from_theme() 不会对标量调用
.get()。
deeptutor/capabilities/quest/quest_cache.py-27-30 (1)

27-30: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

清洗后的缓存键可能冲突,读取时未做校验。

_sanitize_cache_id 删除所有非字母数字字符。"a/b""ab" 会映射到同一个文件。save_cache 已经把原始键写入 payload["_cache_key"],但 get_cached 没有比对该字段,冲突时会返回错误的 QuestMap。建议读取时校验原始键。

🛡️ 建议修复
     try:
         raw = json.loads(path.read_text(encoding="utf-8"))
+        if raw.get("_cache_key", map_id) != map_id:
+            return None
         return _dict_to_quest_map(raw)
     except (json.JSONDecodeError, OSError, KeyError, TypeError):
         return None

Also applies to: 145-162

🤖 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 `@deeptutor/capabilities/quest/quest_cache.py` around lines 27 - 30, Update
get_cached to validate the stored payload["_cache_key"] against the requested
raw cache key before returning a QuestMap; treat missing or mismatched keys as a
cache miss. Preserve save_cache’s existing storage of the original key and the
current sanitized filename lookup.
deeptutor/capabilities/quest/capability.py-274-288 (1)

274-288: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

缓存键与实际生成使用的题型不一致。

第 274 行用 AGE_BAND_QUESTION_MATRIX.get(age_band, []) 计算 content_hash;未知 age_band 时题型为空列表。第 281 行生成题目时回退到 AGE_BAND_QUESTION_MATRIX["7-9"]。因此不同的未知 age_band 会共用同一个缓存键,且键不反映真实题型。请先解析题型,再用同一份题型计算哈希。

♻️ 建议修复
-        # Check cache
-        content_hash = compute_content_hash(chunks, age_band, AGE_BAND_QUESTION_MATRIX.get(age_band, []))
+        # Check cache
+        q_types = AGE_BAND_QUESTION_MATRIX.get(age_band, AGE_BAND_QUESTION_MATRIX["7-9"])
+        content_hash = compute_content_hash(chunks, age_band, q_types)
         cached_map = get_cached(content_hash)
         if cached_map is not None and cached_map.levels:
             # Return questions from the first cached level
             return list(cached_map.levels[0].questions)
 
         # Generate
-        q_types = AGE_BAND_QUESTION_MATRIX.get(age_band, AGE_BAND_QUESTION_MATRIX["7-9"])
         questions = await self._forging.generate_questions(
🤖 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 `@deeptutor/capabilities/quest/capability.py` around lines 274 - 288,
在生成题目的流程中先计算并确定 q_types,使用与实际生成相同的年龄段回退规则;随后将该 q_types 传入
compute_content_hash,而不是再次使用 AGE_BAND_QUESTION_MATRIX.get(age_band,
[])。保持缓存读取、缓存命中和 _forging.generate_questions 的现有行为不变。
deeptutor/capabilities/quest/grading.py-107-118 (1)

107-118: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

frozenset 比较配对会丢失方向性和重复项。

frozenset(["A","B"]) 等于 frozenset(["B","A"]),所以把左右两栏对调提交也会判对。另外整体用 set 存放,重复配对会被折叠,[["A","1"],["A","1"]][["A","1"]] 判定相同。

如果配对题的左右两栏语义不同,请改用有序元组并对列表排序后比较。

🐛 建议修复
-            ua_set = set()
+            ua_pairs_norm = []
             for pair in ua_pairs:
                 if isinstance(pair, (list, tuple)) and len(pair) >= 2:
-                    ua_set.add(frozenset([str(pair[0]).strip(), str(pair[1]).strip()]))
+                    ua_pairs_norm.append((str(pair[0]).strip(), str(pair[1]).strip()))
 
-            ca_set = set()
+            ca_pairs_norm = []
             for pair in q.matching_pairs:
                 if len(pair) >= 2:
-                    ca_set.add(frozenset([str(pair[0]).strip(), str(pair[1]).strip()]))
+                    ca_pairs_norm.append((str(pair[0]).strip(), str(pair[1]).strip()))
 
-            return ua_set == ca_set
+            return sorted(ua_pairs_norm) == sorted(ca_pairs_norm)
🤖 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 `@deeptutor/capabilities/quest/grading.py` around lines 107 - 118, Update the
matching comparison in the grading logic to preserve pair direction and
duplicate entries: represent each valid pair as an ordered tuple, normalize its
values, sort both answer collections, and compare the resulting lists. Replace
the set/frozenset construction for ua_pairs and q.matching_pairs while retaining
the existing validation of pair shape.
deeptutor/capabilities/quest/hints.py-132-136 (1)

132-136: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

min(0, len(q.hints) - 1) 恒等于 0,多条提示永远用不到。

进入该分支时 q.hints 非空,因此 len(q.hints) - 1 >= 0min 的结果始终是 0。表达式看起来像边界保护,实际只是固定取第一条提示。模块文档描述的是"渐进式提示",但第二条及以后的提示永远不会展示。

请把提示序号作为参数传入,并用 min(idx, len(q.hints) - 1) 收敛。

🐛 建议修复
-    def _structural_hint(self, q: Question) -> str:
+    def _structural_hint(self, q: Question, hint_index: int = 0) -> str:
         """Return a structural hint from the question's hints list."""
         if q.hints:
-            hint_idx = min(0, len(q.hints) - 1)
+            hint_idx = min(max(hint_index, 0), len(q.hints) - 1)
             hint_text = q.hints[hint_idx]
🤖 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 `@deeptutor/capabilities/quest/hints.py` around lines 132 - 136, Update
_structural_hint to accept a hint index parameter and use min(idx, len(q.hints)
- 1) when selecting from q.hints, preserving the existing non-empty-hints branch
while enabling progressive hints beyond the first entry.
deeptutor/capabilities/quest/grading.py-134-145 (1)

134-145: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

第 145 行是死代码,与第 139 行的判断完全相同。

第 139 行已经检查过 ua_stripped in ca_ids。执行到第 145 行时该条件必然为 False,所以这一行永远返回 False。注释说它处理"存文本的情况",但表达式并未做任何不同的比较。

请改为 return False,或补上真正意图的比较(例如对 q.options 做索引反查)。

🐛 建议修复
             # Also check against correct_answer text
             if ua_stripped == q.correct_answer.strip():
                 return True
-            # Check if user answer text is in correct_answer_ids (when they store text)
-            return ua_stripped in ca_ids
+            return False
🤖 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 `@deeptutor/capabilities/quest/grading.py` around lines 134 - 145, Update the
error_correction branch in the grading logic to remove the duplicate
ua_stripped-in-ca_ids check: replace the final return with return False, or
implement a distinct q.options index-to-text lookup if text-stored answer IDs
are intended to be supported.
🧹 Nitpick comments (21)
deeptutor/api/routers/kids.py (2)

249-295: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

校验 source 的取值。

当前只判断 body.source == "public",其余任何值都会进入 personal 分支。前端 web/lib/kids-types.ts:122-127 已把 source 约束为 "public" | "personal"。请在服务端也使用 Literal,让非法取值返回 422,而不是静默走 personal 路径。

♻️ 建议的修改
+from typing import Any, Literal
@@
-    source: str = Field(
+    source: Literal["public", "personal"] = Field(
         default="public",
         description='"public" for a theme pack, "personal" for a KB.',
     )
🤖 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 `@deeptutor/api/routers/kids.py` around lines 249 - 295, Update the request
model or schema defining body.source to use Literal["public", "personal"],
ensuring validation rejects any other value with HTTP 422 before the handler
branches. Preserve the existing public and personal handling in the route that
checks body.source.

376-412: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

避免对同一主题重复读取。

第 377-385 行的循环已对每个 public: 前缀的 map 调用 load_theme。第 403-408 行对同一批 map 再次调用 load_theme。请合并为一次遍历,缓存 theme 结果后同时构建 maps_itemsquest_maps

另外,_build_quest_map_from_theme 接收 age_band 但没有使用它。如果关卡应随年龄段变化,请补上该逻辑;否则请移除该参数。

🤖 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 `@deeptutor/api/routers/kids.py` around lines 376 - 412, 合并构建 maps_items 与
quest_maps 的两次遍历:在处理每个 map_id 时缓存单次 load_theme(map_id 对应主题) 的结果,并复用该结果完成标题、图标及
QuestMap 构建,保持非 public map 的现有行为不变。同时更新 _build_quest_map_from_theme
及其调用方:若关卡内容需要按 profile.age_band 变化,则实际应用该参数;否则移除该参数及所有传递。
tests/api/test_kids.py (1)

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

这些测试没有覆盖路由逻辑。

TestMapsListAPITestProgressAPI 的类名指向 GET /kids/mapsGET /kids/progress/{profile_id},但测试体只调用 storepin 的服务层函数。路由中的所有权校验、PIN 校验、404 与 403 分支都没有被执行。第 276 行的 assert loaded.guardian_user_id != "guardian-002" 是恒真断言,不提供信息。

另外,第 40-57 行定义的 mock_guardian_usermock_kid_user fixture 在本文件中没有被使用。

kids.py 的路由处理函数是普通协程,可以直接 await 调用并传入 mock 依赖。请补充覆盖 get_progress 在档案不属于该 guardian 时返回 403、以及 PIN 错误时返回 401 的用例。

🤖 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/test_kids.py` around lines 204 - 276, Replace the
service-layer-only checks in TestProgressAPI with direct awaited calls to the
kids.py get_progress route, passing mocked dependencies. Add cases asserting a
foreign profile returns 403 and an incorrect PIN returns 401, while preserving
the existing valid-profile setup as needed. Remove the tautological ownership
assertion and unused mock_guardian_user and mock_kid_user fixtures.
web/app/(kids)/home/page.tsx (2)

73-82: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

handleThemeClickasynctry/catch 无作用。

router.push 是同步调用,不会抛出可捕获的导航错误。该函数也不 await 任何内容。可以简化为同步回调。

♻️ 建议简化
   const handleThemeClick = useCallback(
-    async (theme: ThemeSummary) => {
-      try {
-        router.push(`/map/${theme.theme_id}`);
-      } catch {
-        // navigate error — stay on page
-      }
-    },
+    (theme: ThemeSummary) => {
+      router.push(`/map/${theme.theme_id}`);
+    },
     [router],
   );
🤖 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 `@web/app/`(kids)/home/page.tsx around lines 73 - 82, 简化 handleThemeClick
为同步回调,移除 async 修饰符及无效的 try/catch,仅保留对 router.push(`/map/${theme.theme_id}`)
的调用,并维持现有的 router 依赖。

101-117: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

“Retry” 使用整页刷新,可以改为重新执行加载。

第 111 行调用 window.location.reload()。这会丢弃全部客户端状态并重新拉取整个应用包。把加载逻辑提取为可复用函数,并用一个 reloadKey 状态触发 effect 重跑即可。

🤖 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 `@web/app/`(kids)/home/page.tsx around lines 101 - 117, 将 home
页面中的加载逻辑提取为可复用函数,并在组件内增加 reloadKey 状态,使数据加载 effect 依赖该值;将错误状态下 Retry 按钮的
window.location.reload() 改为递增 reloadKey,以重新执行加载而不刷新整页。
web/features/kids/components/ParentDashboard.tsx (1)

124-130: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

|| "#22c55e" 等回退分支是死代码。

kidsTheme.colors.successkidsTheme.colors.warningkidsTheme.ts 中已定义为非空字符串常量。第 124、130、181、225、226、244、397 行的 || 回退永远不会执行。请移除这些回退,保持颜色来源单一。

🤖 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 `@web/features/kids/components/ParentDashboard.tsx` around lines 124 - 130,
Remove the unreachable fallback color expressions from the affected StatCard and
related color usages in ParentDashboard, including success and warning values
and the other referenced lines. Use the non-null constants directly from
kidsTheme.colors so each color has a single source.
web/features/kids/components/RestReminder.tsx (1)

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

避免注入同名的 bounce 关键帧。

Tailwind 已内置 bounce 关键帧,供 animate-bounce 使用;这里的同名全局 @keyframes bounce 会覆盖它,并改变组件外所有 animate-bounce/animation: bounce ... 元素的动画效果。改用不冲突的名称,或直接使用 Tailwind 动画类。

🤖 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 `@web/features/kids/components/RestReminder.tsx` around lines 192 - 198, Rename
the inline `@keyframes` animation in RestReminder to a component-specific name and
update its associated animation reference, or replace it with Tailwind’s
existing animation class. Ensure it no longer defines the global bounce keyframe
or alters other animate-bounce/animation: bounce elements.
tests/capabilities/quest/test_hints.py (1)

81-87: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

表情断言过于脆弱。

ord(c) > 0x1F000 只覆盖补充平面的表情。若鼓励语改用 (U+2728) 或 (U+2B50) 等基本平面符号,断言会失败,但功能没有问题。建议改为断言文案非空并与预期文案集合比对。

🤖 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/capabilities/quest/test_hints.py` around lines 81 - 87, Update
test_english_encourage to remove the fragile Unicode-range emoji check. Keep
asserting that hint is non-empty, then verify it belongs to the expected set of
English encouragement messages, including all valid variants.
tests/services/gamification/test_safety.py (1)

206-221: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

拦截率阈值在当前样本量下等价于 100%。

样本数量在 14 到 40 之间。当样本数小于 50 时,只要漏掉一条,比率就低于 0.98。因此 rate >= 0.98 实际要求全部命中,断言信息会让人误以为允许少量漏检。建议直接断言 blocked == len(samples),或扩大样本集到能表达 98% 语义的规模。

Also applies to: 388-409

🤖 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/services/gamification/test_safety.py` around lines 206 - 221, Update
the violence interception assertions in test_violence_interception_rate and the
additionally affected test block so the small sample sets explicitly require
every sample to be blocked by asserting blocked equals the sample count, rather
than using the misleading 98% rate threshold.
deeptutor/capabilities/quest/capability.py (2)

209-214: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

第三个分支不可达。

第 85 行把 theme_id 默认设为 source_id。因此当 source_id 非空时,elif theme_id: 一定先命中,elif source_id: 永远不执行。请删除该分支,或者让 theme_id 不再默认取 source_id

🤖 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 `@deeptutor/capabilities/quest/capability.py` around lines 209 - 214, 删除构建
handle 的条件分支中 `elif source_id:` 分支,因为 `theme_id` 默认取 `source_id` 时该分支不可达;保留
`SourceResolver.resolve_personal` 和 `SourceResolver.resolve_public(theme_id)`
的现有处理逻辑。

94-105: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

默认学习材料与所选主题无关。

当来源解析失败时,代码固定返回恐龙主题的文本。如果孩子选择的是太阳系或乘法主题,题目内容会与主题不符。建议把默认语料按主题拆分到配置或资源文件,并在无法匹配主题时提示来源不可用。

🤖 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 `@deeptutor/capabilities/quest/capability.py` around lines 94 - 105, 更新 chunks
为空时的默认内容逻辑,避免固定返回恐龙主题材料。根据所选主题从配置或资源中加载对应语料,覆盖太阳系、乘法等主题;无法匹配主题时应提示来源不可用,而不是使用不相关的兜底文本。
deeptutor/capabilities/quest/forging.py (2)

331-355: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

LLM 失败时重复构造相同提示词。

_build_llm_prompt 接收 attempt,但没有在提示词中使用它。三次尝试发送的提示词完全相同,重试无法改善输出质量。请在重试时加入校验失败信息,或删除未使用的 attempt 参数。

🤖 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 `@deeptutor/capabilities/quest/forging.py` around lines 331 - 355, Update
_build_llm_prompt and its call from _generate_with_llm so the attempt value
changes the prompt on retries, incorporating validation-failure context or
another retry-specific instruction; alternatively remove attempt from both the
method signature and call if retries should remain identical.

207-224: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

加固 Question 构造时的类型转换。

第 214 行 int(raw.get("points", 10)) 在 LLM 返回非数字字符串时会抛出 ValueError。第 215 行 raw.get("hints", []) 若返回字符串,会被逐字符拆成提示列表。当前这两种情况只在 _generate_with_llmexcept Exception 中被吞掉,整批题目会被丢弃。建议在此处做防御性转换。

🛡️ 建议修复
+    raw_points = raw.get("points", 10)
+    try:
+        points = int(raw_points)
+    except (TypeError, ValueError):
+        points = 10
+    raw_hints = raw.get("hints", [])
+    if not isinstance(raw_hints, list):
+        raw_hints = [raw_hints] if raw_hints else []
     question = Question(
         question_id=str(raw.get("question_id", _make_qid(q_text))),
         text=q_text,
         question_type=q_type,
         options=options,
         correct_answer=correct_answer,
         explanation=str(raw.get("explanation", "")),
-        points=int(raw.get("points", 10)),
-        hints=[str(h) for h in raw.get("hints", [])],
+        points=points,
+        hints=[str(h) for h in raw_hints],
🤖 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 `@deeptutor/capabilities/quest/forging.py` around lines 207 - 224, 在构造 Question
的 points 和 hints 字段处增加防御性类型转换:points 遇到非数字值时回退到默认分值,避免 int 转换抛出 ValueError;hints
仅在原始值为列表或元组时逐项转换,否则使用空列表,避免字符串被拆分为单字符提示。保持其他字段处理逻辑不变。
tests/capabilities/quest/test_questing.py (2)

27-49: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

MockStreamBus 在答案耗尽时返回空串,会掩盖测试意图。

第 49 行在队列耗尽后无限返回 ""。如果某个用例的答案队列写少了,循环不会失败,而是把剩余题目当作空白作答处理,测试仍可能通过并给出误导性的通过结果。

建议在队列耗尽时抛出异常,强制答案数量与实际交互次数匹配。

♻️ 建议修改
     async def wait_for_input(self, prompt="", source="", stage="", timeout=None) -> str:
-        if self._answer_idx < len(self._answer_queue):
-            answer = self._answer_queue[self._answer_idx]
-            self._answer_idx += 1
-            return answer
-        return ""
+        if self._answer_idx >= len(self._answer_queue):
+            raise AssertionError("answer queue exhausted; the test needs more answers")
+        answer = self._answer_queue[self._answer_idx]
+        self._answer_idx += 1
+        return answer
🤖 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/capabilities/quest/test_questing.py` around lines 27 - 49, Update
MockStreamBus.wait_for_input so it raises an exception when _answer_queue is
exhausted instead of returning an empty string, while preserving the existing
queued-answer behavior and index advancement.

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

提示分级顺序没有被断言。

test_hint_emitted_on_wrong 只断言 len(hints) >= 1test_explanation_emitted_after_exhausting_attempts 只断言 >= 1。模块文档描述的核心行为是"encourage → hint → explain"三级递进,但当前用例无法区分三级提示是否按序发出。如果 hint_stage 的递增逻辑回退,测试仍会通过。

建议对 metadata["stage"] 断言精确序列。

💚 建议补强
         hints = [
             e for e in bus.events
             if e.metadata.get("sub_type") == "hint"
         ]
-        assert len(hints) >= 1  # at least one hint emitted
+        assert [h.metadata["stage"] for h in hints] == [0]
         explanations = [
             e for e in bus.events
             if e.metadata.get("sub_type") == "explanation"
         ]
-        assert len(explanations) >= 1
+        assert len(explanations) == 1
+        assert explanations[0].metadata["question_id"] == "q1"
+        hint_stages = [
+            e.metadata["stage"] for e in bus.events
+            if e.metadata.get("sub_type") == "hint"
+        ]
+        assert hint_stages == [0, 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 `@tests/capabilities/quest/test_questing.py` around lines 230 - 264, Strengthen
test_hint_emitted_on_wrong and
test_explanation_emitted_after_exhausting_attempts to assert the exact
metadata["stage"] progression for the emitted guidance, covering the documented
encourage → hint → explain order rather than only checking event counts.
Preserve the existing event filtering and quest setup while making the
assertions fail if hint_stage ordering regresses.
deeptutor/services/gamification/store.py (1)

22-29: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

注释与代码不一致。

第 22-24 行的注释说明模型在调用时才导入,但第 25-29 行是模块级的立即导入。请删除或更正该注释,避免误导后续维护者。

♻️ 建议修改
-# Import models lazily to avoid circular imports (gamification/__init__.py
-# imports from store.py which imports from models.py). The functions below
-# import ProgressState et al. at call-time.
 from deeptutor.services.gamification.models import (
     LevelProgress,
     MapProgress,
     ProgressState,
 )
🤖 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 `@deeptutor/services/gamification/store.py` around lines 22 - 29, 更新 store.py
顶部关于延迟导入模型的注释,使其准确反映 LevelProgress、MapProgress 和 ProgressState
的模块级立即导入;不要声称这些模型会在函数调用时导入。
deeptutor/capabilities/quest/hints.py (1)

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

硬编码的 3 与文案来源不一致。

第 126 行用 % 3 取模,但 deeptutor/capabilities/quest/prompts/zh/hints.yamlencourage 列表有 4 条文案。若后续改为从 YAML 加载,第 4 条永远不会出现,或在列表变短时触发 IndexError

请改用列表长度取模。

♻️ 建议修改
     def _encourage(self) -> str:
         """Pick an encouragement message using a deterministic rotation."""
-        idx = self._call_count % 3
+        pool = _DEFAULT_ENCOURAGE_ZH if self._language == "zh" else _DEFAULT_ENCOURAGE_EN
+        idx = self._call_count % len(pool)
         self._call_count += 1
-        if self._language == "zh":
-            return _DEFAULT_ENCOURAGE_ZH[idx]
-        return _DEFAULT_ENCOURAGE_EN[idx]
+        return pool[idx]
🤖 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 `@deeptutor/capabilities/quest/hints.py` around lines 124 - 130, Update
_encourage to calculate the rotation index using the length of the selected
encouragement list rather than the hardcoded 3. Ensure both Chinese and English
lists use their actual lengths so all available messages are reachable and
shorter lists cannot cause an IndexError.
deeptutor/services/gamification/safety.py (2)

89-95: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

_mask 重复编译已经预编译过的正则。

第 83-86 行已经把每个词条编译成 re.Pattern 并缓存。第 93 行在每次调用时又重新编译一遍,抵消了预编译的收益。

建议改为按词条查表复用已编译的模式。

♻️ 建议修改
+_PATTERN_BY_TERM: dict[str, re.Pattern[str]] = dict(_TERM_PATTERNS)
+
+
 def _mask(text: str, flagged: list[str]) -> str:
     """Replace flagged terms in *text* with asterisks of equal length."""
     sanitized = text
     for term in flagged:
-        pattern = re.compile(re.escape(term), re.IGNORECASE)
+        pattern = _PATTERN_BY_TERM[term]
         sanitized = pattern.sub("*" * len(term), sanitized)
     return sanitized
🤖 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 `@deeptutor/services/gamification/safety.py` around lines 89 - 95, Update _mask
to reuse the precompiled re.Pattern objects cached for each flagged term instead
of calling re.compile for every term on each invocation. Look up each term’s
cached pattern, preserve case-insensitive matching and equal-length asterisk
replacement, and keep the existing sanitization flow unchanged.

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

age_band 参数从未被使用。

check_question(第 205 行)和 check_explanation(第 245 行)都接收 age_band,但方法体内没有任何一处读取它。check_explanation 实际上只是 SafetyFilter.filter 的转发。

这个签名会让调用方误以为过滤强度随年龄段变化,实际对 7 岁和 15 岁使用完全相同的词表。请二选一:按年龄段分级词表(例如 7-9 岁额外拦截恐怖类词汇),或删除该参数并在文档中说明过滤与年龄无关。

hints 字段未被 check_question 校验的问题,已在 deeptutor/capabilities/quest/hints.py 第 68-87 行的评论中说明。

🤖 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 `@deeptutor/services/gamification/safety.py` around lines 204 - 255, Remove the
unused age_band parameter from SafetyFilter.check_question and
SafetyFilter.check_explanation, update their docstrings and all call sites to
match, and document that filtering is age-independent. Do not modify hints
handling here.
tests/services/gamification/test_store.py (2)

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

该测试没有验证它声称的行为。

docstring 说"含特殊字符的 profile ID 应被清洗",但用例使用的 "safe-123" 不含任何特殊字符,因此没有覆盖 _sanitize_profile_id 的清洗逻辑,也没有覆盖路径穿越场景。

建议补充真实的穿越用例,并断言写入文件确实落在目标目录内。

💚 建议补充用例
 class TestPathSafety:
     def test_safe_profile_id(self, temp_gamification_dir):
         """Profile IDs with special characters should be sanitized."""
         state = ProgressState(profile_id="safe-123")
         save("safe-123", state)
         loaded = load("safe-123")
         assert loaded is not None
         assert loaded.profile_id == "safe-123"
+
+    `@pytest.mark.parametrize`("evil_id", ["../escape", "..%2Fescape", "a/b/c", "kid.123"])
+    def test_traversal_ids_stay_in_dir(self, temp_gamification_dir, evil_id):
+        save(evil_id, ProgressState(profile_id=evil_id))
+        written = list(temp_gamification_dir.rglob("*.json"))
+        assert written, "no file written"
+        for path in written:
+            assert path.parent == temp_gamification_dir
+
+    def test_distinct_ids_do_not_collide(self, temp_gamification_dir):
+        save("a/b", ProgressState(profile_id="a/b", total_xp=1))
+        save("ab", ProgressState(profile_id="ab", total_xp=2))
+        assert load("a/b").total_xp == 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 `@tests/services/gamification/test_store.py` around lines 205 - 212, Update
TestPathSafety.test_safe_profile_id to use a profile ID containing
path-traversal or other special characters, then verify save/load still succeeds
with the sanitized identity. Also assert the persisted file path remains within
temp_gamification_dir, exercising _sanitize_profile_id rather than the current
already-safe identifier.

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

并发测试只验证文件有效,未验证更新是否丢失。

用例断言写入后文件仍可解析,但 5 个线程写入的是各自独立的完整 state,任何一个胜出都能通过断言。真正的风险是"读取-修改-写入"过程中的丢失更新(见 deeptutor/services/gamification/store.pyinit_if_absent 的评论)。

建议补充:多个线程并发调用 init_if_absent + 增量 XP 后,断言最终 total_xp 等于各次增量之和。该用例在加锁前会失败,可作为回归保护。

🤖 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/services/gamification/test_store.py` around lines 163 - 191, 增强
test_concurrent_writes_no_corruption,使多个线程并发调用 init_if_absent 后执行增量 XP
更新,而不是分别写入独立的完整 ProgressState;等待所有线程完成后,断言 load(profile_id).total_xp
等于所有线程增量之和,从而覆盖读取-修改-写入过程中的丢失更新,并保留现有无异常和有效 profile 校验。

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 55fbd4b3-6e8c-4911-9b91-8740c2f68330

📥 Commits

Reviewing files that changed from the base of the PR and between 6880902 and c04e2f9.

📒 Files selected for processing (78)
  • deeptutor/api/main.py
  • deeptutor/api/routers/kids.py
  • deeptutor/api/routers/profiles.py
  • deeptutor/api/routers/voice_kids.py
  • deeptutor/capabilities/quest/__init__.py
  • deeptutor/capabilities/quest/capability.py
  • deeptutor/capabilities/quest/forging.py
  • deeptutor/capabilities/quest/grading.py
  • deeptutor/capabilities/quest/hints.py
  • deeptutor/capabilities/quest/prompts/en/forging_10-12.yaml
  • deeptutor/capabilities/quest/prompts/en/forging_13-15.yaml
  • deeptutor/capabilities/quest/prompts/en/forging_7-9.yaml
  • deeptutor/capabilities/quest/prompts/en/hints.yaml
  • deeptutor/capabilities/quest/prompts/zh/forging_10-12.yaml
  • deeptutor/capabilities/quest/prompts/zh/forging_13-15.yaml
  • deeptutor/capabilities/quest/prompts/zh/forging_7-9.yaml
  • deeptutor/capabilities/quest/prompts/zh/hints.yaml
  • deeptutor/capabilities/quest/quest_cache.py
  • deeptutor/capabilities/quest/questing.py
  • deeptutor/core/context.py
  • deeptutor/knowledge/kb_types.py
  • deeptutor/knowledge/manager.py
  • deeptutor/multi_user/kid_context.py
  • deeptutor/runtime/bootstrap/builtin_capabilities.py
  • deeptutor/services/gamification/__init__.py
  • deeptutor/services/gamification/badges.py
  • deeptutor/services/gamification/engine.py
  • deeptutor/services/gamification/metrics.py
  • deeptutor/services/gamification/models.py
  • deeptutor/services/gamification/public_themes.py
  • deeptutor/services/gamification/safety.py
  • deeptutor/services/gamification/sources.py
  • deeptutor/services/gamification/store.py
  • deeptutor/services/kid_profiles/__init__.py
  • deeptutor/services/kid_profiles/guard.py
  • deeptutor/services/kid_profiles/models.py
  • deeptutor/services/kid_profiles/pin.py
  • deeptutor/services/kid_profiles/store.py
  • scripts/build_public_kb.py
  • tests/api/test_kids.py
  • tests/api/test_profiles.py
  • tests/api/test_role_guard.py
  • tests/capabilities/quest/test_forging.py
  • tests/capabilities/quest/test_grading.py
  • tests/capabilities/quest/test_hints.py
  • tests/capabilities/quest/test_questing.py
  • tests/services/gamification/test_badges.py
  • tests/services/gamification/test_engine.py
  • tests/services/gamification/test_engine_aggregation.py
  • tests/services/gamification/test_metrics.py
  • tests/services/gamification/test_public_themes.py
  • tests/services/gamification/test_quest_cache.py
  • tests/services/gamification/test_safety.py
  • tests/services/gamification/test_sources.py
  • tests/services/gamification/test_store.py
  • web/app/(kids)/home/page.tsx
  • web/app/(kids)/layout.tsx
  • web/app/(kids)/map/[mapId]/page.tsx
  • web/app/(kids)/play/[levelId]/page.tsx
  • web/app/(kids)/rewards/page.tsx
  • web/features/kids/components/BadgeGrid.tsx
  • web/features/kids/components/FeedbackOverlay.tsx
  • web/features/kids/components/HintPanel.tsx
  • web/features/kids/components/LevelSummary.tsx
  • web/features/kids/components/MapNode.tsx
  • web/features/kids/components/ParentDashboard.tsx
  • web/features/kids/components/QuestionCard.tsx
  • web/features/kids/components/RestReminder.tsx
  • web/features/kids/components/StreakCalendar.tsx
  • web/features/kids/hooks/useKidProfile.ts
  • web/features/kids/hooks/useKidQuest.ts
  • web/features/kids/hooks/useTTS.ts
  • web/features/kids/store/kidsStore.ts
  • web/features/kids/theme/kidsTheme.ts
  • web/lib/kids-api.ts
  • web/lib/kids-types.ts
  • web/locales/en/kids.json
  • web/locales/zh/kids.json

Comment thread web/app/(kids)/play/[levelId]/page.tsx
Comment thread web/features/kids/components/QuestionCard.tsx
Comment on lines +124 to +128
return (
<KidProfileContext.Provider value={value}>
{children}
</KidProfileContext.Provider>
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

文件包含 JSX,但扩展名是 .ts,无法编译。

KidProfileProvider 返回 JSX 元素。TypeScript 只在 .tsx 文件中解析 JSX 语法。静态分析在第 125 行和第 127 行报告解析错误,这与该结论一致。请把文件重命名为 useKidProfile.tsx。导入路径 @/features/kids/hooks/useKidProfile 无需修改。

替代方案:保留 .ts 扩展名并改用 createElement。推荐重命名,保持与其他组件一致。

🧰 Tools
🪛 Biome (2.5.6)

[error] 125-125: expected > but instead found value

(parse)


[error] 125-125: Invalid assignment to <KidProfileContext.Provider value

(parse)


[error] 127-127: unterminated regex literal

(parse)

🤖 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 `@web/features/kids/hooks/useKidProfile.ts` around lines 124 - 128, Rename the
file containing KidProfileProvider from useKidProfile.ts to useKidProfile.tsx so
its JSX return compiles, while preserving the existing import path and
implementation.

Source: Linters/SAST tools

Comment on lines +263 to +267
return (
<KidsStoreContext.Provider value={value}>
{children}
</KidsStoreContext.Provider>
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

文件包含 JSX,但扩展名是 .ts,构建会失败。

TypeScript 只在 .tsx 文件中解析 JSX 语法。在 .ts 文件中,<KidsStoreContext.Provider value={value}> 会被解析为类型断言或关系运算符。静态分析给出的三条 parse 错误(264、266 行)正是这个原因。

请把文件重命名为 web/features/kids/store/kidsStore.tsx。导入语句 @/features/kids/store/kidsStore 不需要修改。

另一种方案是用 React.createElement 替换 JSX,但重命名更符合项目中其他组件的写法。

🐛 若选择保留 `.ts` 扩展名的替代改法
-  return (
-    <KidsStoreContext.Provider value={value}>
-      {children}
-    </KidsStoreContext.Provider>
-  );
+  return React.createElement(
+    KidsStoreContext.Provider,
+    { value },
+    children,
+  );
🧰 Tools
🪛 Biome (2.5.6)

[error] 264-264: expected > but instead found value

(parse)


[error] 264-264: Invalid assignment to <KidsStoreContext.Provider value

(parse)


[error] 266-266: unterminated regex literal

(parse)

🤖 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 `@web/features/kids/store/kidsStore.ts` around lines 263 - 267, Rename the file
containing the KidsStoreContext.Provider JSX from kidsStore.ts to kidsStore.tsx,
keeping its exports and implementation unchanged. Do not modify imports that
reference the extensionless kidsStore module path.

Source: Linters/SAST tools

Comment thread web/features/kids/theme/kidsTheme.ts
- Remove unused PUBLIC_KB_TYPE import from manager.py
- Add kid_quest to BUILTIN_CAPABILITY_CLASSES test assertion
- Whitelist data/public_kb/ in .gitignore (themes ship with package)
- Add TYPE_CHECKING import for ProgressState/QuestMap in engine.py
- Rename useKidProfile.ts → .tsx and kidsStore.ts → .tsx (contain JSX)
- Fix NEXT_QUESTION reducer: transition to level_complete on overflow
  instead of leaving UI stuck in presenting with out-of-bounds index
- Fix play page: show 'See Results' button when phase=level_complete;
  FeedbackOverlay.onComplete now calls handleFeedbackComplete directly;
  disable button during feedback animation to prevent duplicate clicks;
  immediately show summary after completing last question
- Add fonts and colors.accent to kidsTheme (ParentDashboard references)
- Implement interactive MatchingInteraction (select-based) and
  OrderingInteraction (tap-to-order) replacing read-only displays
  that leaked correct answers to children
- Update local checkAnswer to handle matching/ordering formats

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 11

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

Caution

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

⚠️ Outside diff range comments (1)
web/app/(kids)/play/[levelId]/page.tsx (1)

117-120: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

在 WebSocket 判分到达前禁用题卡交互。

handleAnswer 发送 quest_answer 后立即返回,但 submitted 只计算 judged_correct / judged_wrong。网络判分返回前,儿童仍可再次提交同一个 question_index 的答案。增加提交等待状态,直到收到 item_judged 或连接/事件错误后再禁用。

🤖 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 `@web/app/`(kids)/play/[levelId]/page.tsx around lines 117 - 120, 在
handleAnswer 的 WebSocket 提交流程中增加按 question_index 跟踪的提交等待状态,发送 quest_answer
后立即标记为 pending,使题卡在收到 item_judged 前不能重复提交;在 item_judged 事件成功处理以及连接或事件错误时清除该状态,并让
submitted 同时包含此等待状态。
🟡 Minor comments (15)
.workbuddy/memory/2026-08-08.md-16-16 (1)

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

修正公共知识库的跟踪状态说明。

第 16 行说明 data/public_kb/.gitignore 排除。当前 .gitignore 已通过 !data/public_kb/ 保留该目录。更新此记录,避免后续打包工作错误地将公共主题数据视为本地未跟踪资源。

🤖 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 @.workbuddy/memory/2026-08-08.md at line 16, 更新记录中关于 data/public_kb/
的跟踪状态说明,移除其被 .gitignore 排除的表述,并明确该公共知识库目录已通过 !data/public_kb/
保留、应视为可跟踪资源;保留现有主题和语料信息。
deeptutor/services/gamification/safety.py-160-163 (1)

160-163: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

为英文词条添加单词边界。

当前子串匹配会把安全文本误判为不安全。例如,"gun" 会匹配 "begun""meth" 会匹配 "method""sex" 会匹配 "Essex"ForgingService 会丢弃这些题目,导致题目数量减少。

对英文词条使用单词边界。中文词条继续使用子串匹配。_mask 也必须使用相同的匹配规则。

🤖 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 `@deeptutor/services/gamification/safety.py` around lines 160 - 163, Update
_TERM_PATTERNS to apply word-boundary matching for English terms while retaining
substring matching for Chinese terms. Ensure _mask uses the same
language-specific matching rules and patterns, so detection and masking remain
consistent. Preserve case-insensitive matching and avoid matching English terms
embedded within larger words.
教学产物/处理说明.md-70-76 (1)

70-76: 🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

移除开发者本机绝对路径。

Line 72 暴露了开发者用户名,并且这些命令无法在其他环境直接执行。请使用仓库根目录、$PROJECT_ROOT 或相对路径。

🤖 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 `@教学产物/处理说明.md` around lines 70 - 76,
移除“常用检查命令”中的开发者本机绝对路径,改用仓库根目录、$PROJECT_ROOT 或相对路径表示执行位置,同时保留 .venv/bin/deeptutor
kb info grade5-math-pilot 命令内容不变。
data/public_kb/solar-system/02-inner-planets.md-12-12 (1)

12-12: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

将水星表述为“几乎没有大气层”。

水星不是完全没有大气,而是拥有极其稀薄的外逸层。请避免“没有大气层”这一绝对表述。(science.nasa.gov)

建议修改
- 因为没有大气层保护,水星白天温度高达四百三十摄氏度
+ 因为几乎没有能保温的大气层,水星白天温度高达四百三十摄氏度
🤖 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 `@data/public_kb/solar-system/02-inner-planets.md` at line 12,
将水星温度说明中的“没有大气层保护”改为“几乎没有大气层保护”,准确体现其拥有极其稀薄外逸层的情况,并保留其余温度描述不变。
data/public_kb/dino-world/02-triassic-period.md-9-9 (1)

9-9: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

改正“似哺乳爬行动物”的分类表述。

“似哺乳爬行动物”是旧称,不能直接等同于“哺乳动物的祖先”。请使用“合弓类动物”,并说明其中部分成员与哺乳动物有较近的亲缘关系。(nhm.ac.uk)

建议修改
- 比如似哺乳爬行动物——它们是哺乳动物的祖先。
+ 比如合弓类动物——其中一些与哺乳动物有较近的亲缘关系。
🤖 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 `@data/public_kb/dino-world/02-triassic-period.md` at line 9,
更新三叠纪早期描述中的“似哺乳爬行动物”表述,改用“合弓类动物”,并说明其中部分成员与哺乳动物具有较近亲缘关系,避免将其直接等同于哺乳动物的祖先。
data/public_kb/solar-system/03-earth-and-moon.md-27-27 (1)

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

删除“大约一秒多”的重复不确定表达。

请使用“约 1.3 秒”或“一秒多”。当前表达同时使用“大约”和“多”,影响文本精度。

建议修改
- 光从月球到地球只需要大约一秒多。
+ 光从月球到地球只需要约 1.3 秒。
🤖 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 `@data/public_kb/solar-system/03-earth-and-moon.md` at line 27,
更新月球到地球光行时间的表述,删除“约/大约”与“多”的重复不确定表达,改为“约 1.3 秒”或“一秒多”,并保持其余距离描述不变。

Source: Linters/SAST tools

data/public_kb/dino-world/04-dinosaur-diets.md-35-37 (1)

35-37: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

把牙齿规则改为启发式判断。

这些句子把牙齿形状与食性写成确定映射,无法覆盖杂食和特殊牙齿。请使用“通常”或“可能”,并说明还需要结合胃内容物、粪化石等证据。(amnh.org)

建议修改
- 尖锐的牙齿属于肉食恐龙,平钝的牙齿属于植食恐龙。
+ 尖锐或平钝的牙齿可以提供食性线索,但不能单独确定食性。
🤖 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 `@data/public_kb/dino-world/04-dinosaur-diets.md` around lines 35 - 37,
将“看牙齿的形状”中的确定性食性映射改为启发式表述,使用“通常”或“可能”说明尖锐、平钝牙齿与食性的关联,并补充牙齿只能作为线索,需结合胃部化石、粪化石等证据判断,同时保留后续两种证据来源。
data/public_kb/solar-system/03-earth-and-moon.md-34-34 (1)

34-34: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

不要把潮汐次数写成所有地点都相同。

许多沿海地区每天有两次涨潮和两次退潮,但部分地区每天只有一次涨潮和一次退潮,或呈现混合潮型。请加入“许多沿海地区”等限定词。(oceanservice.noaa.gov)

建议修改
- 每天有两次涨潮和两次退潮。
+ 许多沿海地区每天有两次涨潮和两次退潮,但有些地区每天只有一次涨潮和一次退潮。
🤖 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 `@data/public_kb/solar-system/03-earth-and-moon.md` at line 34,
更新潮汐说明中的次数表述,避免将每天两次涨潮和两次退潮概括为所有地点的普遍规律;加入“许多沿海地区”等限定词,并保留部分地区存在一次潮汐或混合潮型的准确性。
data/public_kb/solar-system/03-earth-and-moon.md-40-40 (1)

40-40: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

为登月日期注明时区。

NASA 记录的第一次踏月时间是美国东部时间 1969 年 7 月 20 日 22:56;换算为协调世界时已经是 1969 年 7 月 21 日。中文教材应避免省略时区。(nasa.gov)

建议修改
- 1969年7月20日,美国宇航员尼尔·阿姆斯特朗成为第一个踏上月球的人类。
+ 1969年7月20日(美国东部时间;协调世界时为7月21日),美国宇航员尼尔·阿姆斯特朗成为第一个踏上月球的人类。
🤖 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 `@data/public_kb/solar-system/03-earth-and-moon.md` at line 40,
在地球与月球文档中涉及阿姆斯特朗首次踏月的句子,明确注明日期为1969年7月20日美国东部时间22:56,并保留对应的UTC日期为1969年7月21日,避免无时区的日期表述。
data/public_kb/solar-system/03-earth-and-moon.md-28-28 (1)

28-28: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

把“没有空气和水”改为更准确的表述。

月球有极其稀薄的外逸层,极区也存在水冰。请改为“几乎没有大气和地表液态水”,并将“没有风化作用”限定为没有地球式的风雨风化。(science.nasa.gov)

建议修改
- 月球上没有空气和水,所以没有风化作用,脚印可以保存几百万年。
+ 月球上的大气极其稀薄,地表没有稳定的液态水,因此没有地球式的风雨风化;脚印可以保存很长时间。
🤖 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 `@data/public_kb/solar-system/03-earth-and-moon.md` at line 28,
更新月球表面描述中的“没有空气和水”为“几乎没有大气和地表液态水”,并将“没有风化作用”限定为“没有地球式的风雨风化”;保留其余关于脚印可长期保存的表述。
data/public_kb/multiplication-kingdom/05-real-world-problems.md-47-49 (1)

47-49: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

不要把“一周”直接写成 5 天。

Line 47-49 实际统计的是上学的 5 天。请改为“连续 5 天”或“一周上学的 5 天”,避免混淆日历周和上学日。

建议修改
- **例题**:小明每天写作业用30分钟,一周(5天)一共用了多少分钟?
+ **例题**:小明每天写作业用30分钟,连续5天一共用了多少分钟?
🤖 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 `@data/public_kb/multiplication-kingdom/05-real-world-problems.md` around lines
47 - 49, Update the example wording around the “小明每天写作业” problem to explicitly
say “连续5天” or “一周上学的5天” instead of treating “一周” as five days, while preserving
the existing calculation and answer.
data/public_kb/solar-system/01-the-sun.md-33-33 (1)

33-33: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

区分耀斑和日冕物质抛射。

Line 33 将日冕物质抛射描述成“巨大的火焰”。耀斑主要是强烈的辐射爆发;日冕物质抛射则是大量带电等离子体和磁场从太阳喷出。相关粒子还需要与地磁场和大气相互作用,才可能形成极光。 (science.nasa.gov)

建议修改
- 有时候太阳还会喷出巨大的火焰,叫"日冕物质抛射"。这些带电粒子飞向太空,如果到达地球,就会产生美丽的极光。
+ 有时太阳会发生耀斑;日冕物质抛射则是大量带电等离子体和磁场从太阳喷出。相关粒子到达地球并与地磁场和大气作用时,可能形成极光。
🤖 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 `@data/public_kb/solar-system/01-the-sun.md` at line 33,
修改描述太阳喷发的句子,明确区分耀斑与日冕物质抛射:将日冕物质抛射表述为大量带电等离子体及磁场从太阳喷出,而非“巨大的火焰”;同时说明其到达地球后需与地磁场和大气相互作用才可能形成极光。

Source: MCP tools

data/public_kb/multiplication-kingdom/02-times-5.md-32-36 (1)

32-36: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

把十位数字规则改成精确表述。

Line 32 的“大约”不能指导计算。以 5 × 9 = 45 为例,9 ÷ 2 = 4.5,但十位数字是 4。请明确写成“乘数除以 2 后向下取整”,并限定在本课的 19 范围内。

建议修改
- 而且,结果的十位数也有规律:5 × 几的十位数大约就是"几除以2":
+ 结果的十位数字等于乘数除以2后向下取整(适用于1到9):
🤖 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 `@data/public_kb/multiplication-kingdom/02-times-5.md` around lines 32 - 36,
将本课中关于十位数规律的“大约就是‘几除以2’”改为精确表述:对乘数 1 到 9 除以 2 后向下取整,即为结果的十位数字;保留现有示例并确保表述适用于 5 ×
9 = 45。
data/public_kb/solar-system/01-the-sun.md-3-3 (1)

3-3: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

避免使用“所有生命能量”的绝对表述。

Line 3 的“一切生命能量”过于绝对。太阳能是绝大多数地球生命活动的重要能量来源。请改为“几乎所有生命活动的主要能量来源”。NASA 也使用“most life”而不是“all life”。 (science.nasa.gov)

建议修改
- 太阳是太阳系的中心,也是我们地球上一切生命能量的来源。
+ 太阳是太阳系的中心,也是地球上几乎所有生命活动的主要能量来源。
🤖 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 `@data/public_kb/solar-system/01-the-sun.md` at line 3,
将太阳介绍中的绝对表述“一切生命能量”改为“几乎所有生命活动的主要能量来源”,保留其余句子内容不变。

Source: MCP tools

data/public_kb/dino-world/05-great-extinction.md-10-10 (1)

10-10: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

更正撞击能量的数量级。

Line 10 将撞击能量写成“比所有核弹加在一起还要大几百万倍”。这会明显高估。NASA 的公开估计约为全球核武库的 1 万倍。请使用有来源的数量级,或删除该比较。 (science.nasa.gov)

建议修改
- - 撞击产生了巨大的爆炸,威力比所有核弹加在一起还要大几百万倍。
+ - 撞击释放的能量极其巨大,约相当于全球核武库总能量的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 `@data/public_kb/dino-world/05-great-extinction.md` at line 10,
将文档中撞击能量的“比所有核弹加在一起还要大几百万倍”改为有来源支持的数量级表述,采用约为全球核武库一万倍,并保留 NASA
来源链接;若无法准确引用该数量级,则删除这项比较。

Source: MCP tools

🧹 Nitpick comments (1)
tests/capabilities/quest/test_hints.py (1)

150-164: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

覆盖不安全 LLM 解释的回退路径。

第 150-164 行只测试了安全响应。HintService.get_explanation()check_explanation() 拦截 LLM 输出时必须返回已存储的解释。添加该集成测试以锁定儿童内容安全边界。

建议的测试
+    `@pytest.mark.asyncio`
+    async def test_unsafe_llm_response_falls_back_to_stored_explanation(self):
+        class UnsafeLLM:
+            async def complete(self, prompt, **kwargs):
+                return "A gun is shown in this explanation."
+
+        service = HintService(language="zh")
+        q = Question(
+            text="Test",
+            question_type="single_choice",
+            options=["a", "b"],
+            correct_answer="a",
+            explanation="Safe fallback explanation.",
+        )
+
+        result = await service.get_explanation(q, llm_client=UnsafeLLM())
+
+        assert result == "Safe fallback explanation."
🤖 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/capabilities/quest/test_hints.py` around lines 150 - 164, 扩展
test_with_llm,覆盖 MockLLM.complete 返回不通过 HintService.check_explanation()
的不安全解释时的行为;断言 service.get_explanation() 返回题目中已存储的 explanation(“Stored.”),而不是被拦截的
LLM 输出。
🤖 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 `@data/public_kb/dino-world/01-what-are-dinosaurs.md`:
- Around line 17-19: 统一“非鸟类恐龙”和“鸟类”的定义:在
data/public_kb/dino-world/01-what-are-dinosaurs.md:17-19,将“真正的恐龙只生活在陆地上”改为“非鸟类恐龙主要生活在陆地上”,并说明鸟类是现存恐龙;在
data/public_kb/dino-world/03-jurassic-giants.md:26-28,将始祖鸟描述为早期鸟类恐龙或鸟类近亲,并将“恐龙变成了鸟”改为“鸟类是仍存活的恐龙谱系”。

In `@data/public_kb/dino-world/04-dinosaur-diets.md`:
- Line 7:
更新该段开头的分类表述,将“肉食恐龙也叫‘兽脚类恐龙’”改为“许多肉食恐龙属于兽脚类”,以区分食性名称与分类群;保留后续关于其猎手特点的内容。

In `@data/public_kb/multiplication-kingdom/04-times-tables-tricks.md`:
- Around line 67-69: 将两位数乘以 11 的技巧限定为十位与个位之和小于 10 的情况,并在说明或示例中补充总和达到 10
时需要进位的规则,例如 11 × 29 = 319。

In `@data/public_kb/solar-system/02-inner-planets.md`:
- Line 37:
更新火星段落中提及“天问一号”和“毅力号”的句子,不要将天问一号描述为仍在进行科学研究;改用过去时准确表述其已完成计划中的科学探测任务,并保留或单独明确毅力号当前任务状态。

In `@data/public_kb/solar-system/04-outer-planets.md`:
- Line 12:
更新木星卫星介绍中的固定数量“九十五颗”,改用不易过期的非数值表述;同时检查同一文档中土星卫星数量的相关表述并采用相同处理,或补充明确的截至日期和数据来源。保留伽利略卫星及其发现者信息不变。
- Around line 1-3: 修订该文档中外行星的分类描述:明确木星和土星是气体巨星,天王星和海王星是冰巨星;同步更新标题、开头段落及第
28-30、43-45 行附近的相关表述,避免将四颗行星都描述为主要由氢氦组成或均属于气体巨星。

In `@data/public_kb/solar-system/manifest.yaml`:
- Around line 1-31: 为 solar-system manifest 的英文 locale 提供独立的英文知识源或明确的语言映射,确保所有
levels 的 source_files 在英文用户进入时加载英文内容;若无法提供英文知识库,则禁用该 public theme
的英文可用性,避免英文界面混用中文知识上下文。
- Around line 10-31: 公开主题的 level 处理流程未使用 manifest 中的 source_files,且可能错误继承其他级别的
is_boss。更新构建与运行时公开主题加载流程,使每个 level 仅匹配其声明的 source_files,校验每个指定 Markdown
文件存在,并基于该绑定文档生成内容;同时将 is_boss 限定在对应 level 上,移除依赖目录全局扫描或文件名顺序的匹配逻辑。

In `@deeptutor/services/gamification/safety.py`:
- Around line 271-273: Update the LLM moderation flow in the safety-check
function containing the response parsing so that, when llm_client is configured,
only a response strictly indicating “SAFE” returns passed=True; return a failed
moderation result for complete() exceptions and unknown or non-“unsafe”
responses. Preserve the existing pass-through behavior for offline word-list
moderation when llm_client is not configured.

In `@web/features/kids/components/QuestionCard.tsx`:
- Around line 421-431: Update the empty-dependency synchronization effects in
QuestionCard at web/features/kids/components/QuestionCard.tsx:421-431 and
web/features/kids/components/QuestionCard.tsx:507-519 so they respond to the
current question ID or relevant question data changes. Reset the internal state
of MatchingInteraction and OrderingInteraction when the rendered question
changes, while preserving synchronization for the active question’s
selectedValue.

In `@web/features/kids/hooks/useKidProfile.tsx`:
- Around line 89-99: 更新 useKidProfile 中的档案选择逻辑,不要默认使用
data[0];应从服务端会话的活动档案或明确的档案选择状态确定当前儿童档案,确保多档案及 PIN 切换后使用正确的 profileId。构造
setProfile 数据时保留并验证后端返回的 role,避免固定为 "kid"。

---

Outside diff comments:
In `@web/app/`(kids)/play/[levelId]/page.tsx:
- Around line 117-120: 在 handleAnswer 的 WebSocket 提交流程中增加按 question_index
跟踪的提交等待状态,发送 quest_answer 后立即标记为 pending,使题卡在收到 item_judged 前不能重复提交;在
item_judged 事件成功处理以及连接或事件错误时清除该状态,并让 submitted 同时包含此等待状态。

---

Minor comments:
In @.workbuddy/memory/2026-08-08.md:
- Line 16: 更新记录中关于 data/public_kb/ 的跟踪状态说明,移除其被 .gitignore 排除的表述,并明确该公共知识库目录已通过
!data/public_kb/ 保留、应视为可跟踪资源;保留现有主题和语料信息。

In `@data/public_kb/dino-world/02-triassic-period.md`:
- Line 9:
更新三叠纪早期描述中的“似哺乳爬行动物”表述,改用“合弓类动物”,并说明其中部分成员与哺乳动物具有较近亲缘关系,避免将其直接等同于哺乳动物的祖先。

In `@data/public_kb/dino-world/04-dinosaur-diets.md`:
- Around line 35-37:
将“看牙齿的形状”中的确定性食性映射改为启发式表述,使用“通常”或“可能”说明尖锐、平钝牙齿与食性的关联,并补充牙齿只能作为线索,需结合胃部化石、粪化石等证据判断,同时保留后续两种证据来源。

In `@data/public_kb/dino-world/05-great-extinction.md`:
- Line 10: 将文档中撞击能量的“比所有核弹加在一起还要大几百万倍”改为有来源支持的数量级表述,采用约为全球核武库一万倍,并保留 NASA
来源链接;若无法准确引用该数量级,则删除这项比较。

In `@data/public_kb/multiplication-kingdom/02-times-5.md`:
- Around line 32-36: 将本课中关于十位数规律的“大约就是‘几除以2’”改为精确表述:对乘数 1 到 9 除以 2
后向下取整,即为结果的十位数字;保留现有示例并确保表述适用于 5 × 9 = 45。

In `@data/public_kb/multiplication-kingdom/05-real-world-problems.md`:
- Around line 47-49: Update the example wording around the “小明每天写作业” problem to
explicitly say “连续5天” or “一周上学的5天” instead of treating “一周” as five days, while
preserving the existing calculation and answer.

In `@data/public_kb/solar-system/01-the-sun.md`:
- Line 33:
修改描述太阳喷发的句子,明确区分耀斑与日冕物质抛射:将日冕物质抛射表述为大量带电等离子体及磁场从太阳喷出,而非“巨大的火焰”;同时说明其到达地球后需与地磁场和大气相互作用才可能形成极光。
- Line 3: 将太阳介绍中的绝对表述“一切生命能量”改为“几乎所有生命活动的主要能量来源”,保留其余句子内容不变。

In `@data/public_kb/solar-system/02-inner-planets.md`:
- Line 12: 将水星温度说明中的“没有大气层保护”改为“几乎没有大气层保护”,准确体现其拥有极其稀薄外逸层的情况,并保留其余温度描述不变。

In `@data/public_kb/solar-system/03-earth-and-moon.md`:
- Line 27: 更新月球到地球光行时间的表述,删除“约/大约”与“多”的重复不确定表达,改为“约 1.3 秒”或“一秒多”,并保持其余距离描述不变。
- Line 34:
更新潮汐说明中的次数表述,避免将每天两次涨潮和两次退潮概括为所有地点的普遍规律;加入“许多沿海地区”等限定词,并保留部分地区存在一次潮汐或混合潮型的准确性。
- Line 40:
在地球与月球文档中涉及阿姆斯特朗首次踏月的句子,明确注明日期为1969年7月20日美国东部时间22:56,并保留对应的UTC日期为1969年7月21日,避免无时区的日期表述。
- Line 28:
更新月球表面描述中的“没有空气和水”为“几乎没有大气和地表液态水”,并将“没有风化作用”限定为“没有地球式的风雨风化”;保留其余关于脚印可长期保存的表述。

In `@deeptutor/services/gamification/safety.py`:
- Around line 160-163: Update _TERM_PATTERNS to apply word-boundary matching for
English terms while retaining substring matching for Chinese terms. Ensure _mask
uses the same language-specific matching rules and patterns, so detection and
masking remain consistent. Preserve case-insensitive matching and avoid matching
English terms embedded within larger words.

In `@教学产物/处理说明.md`:
- Around line 70-76: 移除“常用检查命令”中的开发者本机绝对路径,改用仓库根目录、$PROJECT_ROOT
或相对路径表示执行位置,同时保留 .venv/bin/deeptutor kb info grade5-math-pilot 命令内容不变。

---

Nitpick comments:
In `@tests/capabilities/quest/test_hints.py`:
- Around line 150-164: 扩展 test_with_llm,覆盖 MockLLM.complete 返回不通过
HintService.check_explanation() 的不安全解释时的行为;断言 service.get_explanation()
返回题目中已存储的 explanation(“Stored.”),而不是被拦截的 LLM 输出。
🪄 Autofix

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: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: e9d6a34f-2f30-4c09-84e6-c5564c20eff9

📥 Commits

Reviewing files that changed from the base of the PR and between c04e2f9 and 98b9c5f.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (65)
  • .gitignore
  • .workbuddy/memory/2026-08-08.md
  • data/public_kb/dino-world/01-what-are-dinosaurs.md
  • data/public_kb/dino-world/02-triassic-period.md
  • data/public_kb/dino-world/03-jurassic-giants.md
  • data/public_kb/dino-world/04-dinosaur-diets.md
  • data/public_kb/dino-world/05-great-extinction.md
  • data/public_kb/dino-world/manifest.yaml
  • data/public_kb/multiplication-kingdom/01-times-2.md
  • data/public_kb/multiplication-kingdom/02-times-5.md
  • data/public_kb/multiplication-kingdom/03-times-10.md
  • data/public_kb/multiplication-kingdom/04-times-tables-tricks.md
  • data/public_kb/multiplication-kingdom/05-real-world-problems.md
  • data/public_kb/multiplication-kingdom/manifest.yaml
  • data/public_kb/solar-system/01-the-sun.md
  • data/public_kb/solar-system/02-inner-planets.md
  • data/public_kb/solar-system/03-earth-and-moon.md
  • data/public_kb/solar-system/04-outer-planets.md
  • data/public_kb/solar-system/05-asteroids-comets.md
  • data/public_kb/solar-system/manifest.yaml
  • deeptutor/api/main.py
  • deeptutor/api/routers/kids.py
  • deeptutor/api/routers/profiles.py
  • deeptutor/api/routers/voice_kids.py
  • deeptutor/capabilities/quest/__init__.py
  • deeptutor/capabilities/quest/capability.py
  • deeptutor/capabilities/quest/forging.py
  • deeptutor/capabilities/quest/grading.py
  • deeptutor/capabilities/quest/hints.py
  • deeptutor/capabilities/quest/quest_cache.py
  • deeptutor/capabilities/quest/questing.py
  • deeptutor/knowledge/manager.py
  • deeptutor/services/gamification/__init__.py
  • deeptutor/services/gamification/badges.py
  • deeptutor/services/gamification/engine.py
  • deeptutor/services/gamification/metrics.py
  • deeptutor/services/gamification/safety.py
  • deeptutor/services/gamification/store.py
  • deeptutor/services/kid_profiles/__init__.py
  • deeptutor/services/kid_profiles/guard.py
  • deeptutor/services/kid_profiles/store.py
  • scripts/build_public_kb.py
  • tests/api/test_kids.py
  • tests/api/test_profiles.py
  • tests/api/test_role_guard.py
  • tests/capabilities/quest/test_forging.py
  • tests/capabilities/quest/test_grading.py
  • tests/capabilities/quest/test_hints.py
  • tests/capabilities/quest/test_questing.py
  • tests/core/test_capabilities_runtime.py
  • tests/services/gamification/test_badges.py
  • tests/services/gamification/test_engine.py
  • tests/services/gamification/test_engine_aggregation.py
  • tests/services/gamification/test_metrics.py
  • tests/services/gamification/test_quest_cache.py
  • tests/services/gamification/test_safety.py
  • tests/services/gamification/test_sources.py
  • tests/services/gamification/test_store.py
  • web/app/(kids)/play/[levelId]/page.tsx
  • web/features/kids/components/QuestionCard.tsx
  • web/features/kids/hooks/useKidProfile.tsx
  • web/features/kids/store/kidsStore.tsx
  • web/features/kids/theme/kidsTheme.ts
  • 教学产物/五年级数学教学小样.md
  • 教学产物/处理说明.md
💤 Files with no reviewable changes (1)
  • deeptutor/knowledge/manager.py
🚧 Files skipped from review as they are similar to previous changes (31)
  • deeptutor/services/gamification/init.py
  • tests/services/gamification/test_badges.py
  • web/features/kids/theme/kidsTheme.ts
  • deeptutor/services/kid_profiles/init.py
  • deeptutor/api/main.py
  • deeptutor/capabilities/quest/init.py
  • tests/services/gamification/test_metrics.py
  • tests/capabilities/quest/test_forging.py
  • tests/services/gamification/test_engine_aggregation.py
  • deeptutor/capabilities/quest/quest_cache.py
  • tests/services/gamification/test_store.py
  • tests/services/gamification/test_engine.py
  • deeptutor/services/gamification/badges.py
  • tests/api/test_role_guard.py
  • tests/api/test_profiles.py
  • deeptutor/api/routers/voice_kids.py
  • tests/capabilities/quest/test_grading.py
  • deeptutor/services/kid_profiles/guard.py
  • deeptutor/capabilities/quest/grading.py
  • scripts/build_public_kb.py
  • tests/services/gamification/test_quest_cache.py
  • deeptutor/api/routers/profiles.py
  • deeptutor/services/kid_profiles/store.py
  • deeptutor/capabilities/quest/hints.py
  • deeptutor/capabilities/quest/forging.py
  • deeptutor/capabilities/quest/questing.py
  • deeptutor/capabilities/quest/capability.py
  • deeptutor/services/gamification/store.py
  • deeptutor/services/gamification/metrics.py
  • tests/api/test_kids.py
  • deeptutor/services/gamification/engine.py

Comment on lines +17 to +19
很多人以为恐龙是巨大的蜥蜴,其实不是的。恐龙和现在的蜥蜴、鳄鱼不一样,它们是一类独特的爬行动物。

恐龙也不是鱼龙、蛇颈龙或者翼龙——虽然它们也生活在恐龙的时代,但它们不是真正的恐龙。真正的恐龙只生活在陆地上。

Copy link
Copy Markdown

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

统一“非鸟类恐龙”和“鸟类”的定义。

鸟类属于现存恐龙,始祖鸟则是早期鸟类恐龙或近亲,不应将所有恐龙写成只生活在陆地上,也不应将始祖鸟确定为现代鸟类的直接祖先。(nhm.ac.uk)

  • data/public_kb/dino-world/01-what-are-dinosaurs.md#L17-L19: 将“真正的恐龙只生活在陆地上”改为“非鸟类恐龙主要生活在陆地上”,并说明鸟类是现存恐龙。
  • data/public_kb/dino-world/03-jurassic-giants.md#L26-L28: 将始祖鸟改为早期鸟类恐龙或鸟类近亲,并将“恐龙变成了鸟”改为“鸟类是仍存活的恐龙谱系”。
📍 Affects 2 files
  • data/public_kb/dino-world/01-what-are-dinosaurs.md#L17-L19 (this comment)
  • data/public_kb/dino-world/03-jurassic-giants.md#L26-L28
🤖 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 `@data/public_kb/dino-world/01-what-are-dinosaurs.md` around lines 17 - 19,
统一“非鸟类恐龙”和“鸟类”的定义:在
data/public_kb/dino-world/01-what-are-dinosaurs.md:17-19,将“真正的恐龙只生活在陆地上”改为“非鸟类恐龙主要生活在陆地上”,并说明鸟类是现存恐龙;在
data/public_kb/dino-world/03-jurassic-giants.md:26-28,将始祖鸟描述为早期鸟类恐龙或鸟类近亲,并将“恐龙变成了鸟”改为“鸟类是仍存活的恐龙谱系”。


## 肉食恐龙:凶猛的猎手

肉食恐龙也叫"兽脚类恐龙",它们是恐龙世界里的猎手。它们的特点是:

Copy link
Copy Markdown

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

不要把“兽脚类”当作“肉食恐龙”的同义词。

“兽脚类”是分类群,不是食性名称。许多兽脚类是肉食动物,但鸟类也属于兽脚类,且该类群并非全部肉食。请改为“许多肉食恐龙属于兽脚类”。(digitalcollections.amnh.org)

建议修改
- 肉食恐龙也叫"兽脚类恐龙",它们是恐龙世界里的猎手。
+ 许多肉食恐龙属于兽脚类;兽脚类也包括鸟类等不同食性的成员。
📝 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
肉食恐龙也叫"兽脚类恐龙",它们是恐龙世界里的猎手。它们的特点是:
许多肉食恐龙属于兽脚类;兽脚类也包括鸟类等不同食性的成员。
🤖 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 `@data/public_kb/dino-world/04-dinosaur-diets.md` at line 7,
更新该段开头的分类表述,将“肉食恐龙也叫‘兽脚类恐龙’”改为“许多肉食恐龙属于兽脚类”,以区分食性名称与分类群;保留后续关于其猎手特点的内容。

Comment on lines +67 to +69
11乘以两位数也有技巧:把两个数字相加放在中间!
- 11 × 23 → 2 + 3 = 5 → **253**
- 11 × 42 → 4 + 2 = 6 → **462**

Copy link
Copy Markdown

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

限定 11 乘两位数的简便规则。

Line 67-69 将规则写成适用于所有两位数。但当数字和达到 10 时需要进位。例如 11 × 29 = 319,不能直接把 2 + 9 = 11 放在中间。请限定“两个数字之和小于 10”,或补充进位规则。

建议修改
- 11乘以两位数也有技巧:把两个数字相加放在中间!
+ 当两位数的两个数字之和小于10时,11乘以它可以把数字和放在中间:
  - 11 × 23 → 2 + 3 = 5 → **253**
  - 11 × 42 → 4 + 2 = 6 → **462**
+ 如果数字和达到10,需要按进位规则计算,例如 11 × 29 = 319。
📝 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
11乘以两位数也有技巧:把两个数字相加放在中间!
- 11 × 23 → 2 + 3 = 5 → **253**
- 11 × 42 → 4 + 2 = 6 → **462**
当两位数的两个数字之和小于10时,11乘以它可以把数字和放在中间:
- 11 × 23 → 2 + 3 = 5 → **253**
- 11 × 42 → 4 + 2 = 6 → **462**
如果数字和达到10,需要按进位规则计算,例如 11 × 29 = 319。
🤖 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 `@data/public_kb/multiplication-kingdom/04-times-tables-tricks.md` around lines
67 - 69, 将两位数乘以 11 的技巧限定为十位与个位之和小于 10 的情况,并在说明或示例中补充总和达到 10 时需要进位的规则,例如 11 × 29
= 319。

- **峡谷**:火星上有巨大的水手大峡谷,长四千多公里,比地球上的大峡谷大得多。
- **水**:科学家发现火星上有冰,主要在两极地区。很久以前火星上可能有液态水。

火星是目前人类最想探索的行星。多个国家已经向火星发送了探测器,中国的"天问一号"和美国的"毅力号"都在火星上进行科学研究。

Copy link
Copy Markdown

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 37 将天问一号和毅力号都描述为正在进行科学研究。中国科学院在 2022 年 6 月 30 日报道,天问一号轨道器和巡视器已完成计划中的科学探测任务。请改用过去时,或为每个任务添加明确的更新时间。(english.cas.cn)

建议修改
- 中国的"天问一号"和美国的"毅力号"都在火星上进行科学研究。
+ 中国的"天问一号"和美国的"毅力号"都曾开展火星科学探测;当前状态请按任务分别更新。
📝 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
火星是目前人类最想探索的行星。多个国家已经向火星发送了探测器,中国的"天问一号"和美国的"毅力号"都在火星上进行科学研究
中国的"天问一号"和美国的"毅力号"都曾开展火星科学探测;当前状态请按任务分别更新
🤖 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 `@data/public_kb/solar-system/02-inner-planets.md` at line 37,
更新火星段落中提及“天问一号”和“毅力号”的句子,不要将天问一号描述为仍在进行科学研究;改用过去时准确表述其已完成计划中的科学探测任务,并保留或单独明确毅力号当前任务状态。

Comment on lines +1 to +3
# 外行星:气体巨星

太阳系的后四颗行星——木星、土星、天王星和海王星——被称为"外行星"或"巨行星"。它们与内行星完全不同:它们巨大无比,主要由气体组成,没有坚硬的表面可以登陆。

Copy link
Copy Markdown

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 3、Line 29 和 Lines 43-45 将四颗外行星都描述为气体巨星或主要由氢氦组成。NASA 将木星和土星分类为气体巨星,将天王星和海王星分类为冰巨星。该错误会影响整篇主题的核心分类。(science.nasa.gov)

建议修改
- # 外行星:气体巨星
+ # 外行星:气体巨星与冰巨星

- 太阳系的后四颗行星——木星、土星、天王星和海王星——被称为"外行星"或"巨行星"。它们与内行星完全不同:它们巨大无比,主要由气体组成,没有坚硬的表面可以登陆。
+ 距离太阳较远的四颗行星——木星、土星、天王星和海王星——统称为外行星或巨行星。木星和土星是气体巨星;天王星和海王星是冰巨星。四者都没有可供登陆的固体表面。

- 天王星主要由氢、氦和甲烷组成。
+ 天王星是冰巨星;其大气含氢、氦和甲烷,内部还含有水、氨等成分。

- 主要由气体组成(氢和氦),没有坚硬表面。
+ 木星、土星主要由氢和氦组成;天王星、海王星是含水、氨、甲烷等成分较多的冰巨星。四者都没有可供登陆的固体表面。

Also applies to: 28-30, 43-45

🧰 Tools
🪛 LanguageTool

[uncategorized] ~3-~3: 单纯方位词与名词结合不加“的”,您的意思是否是:"太阳系后 或 太阳系以后 或 太阳系的后"边
Context: # 外行星:气体巨星 太阳系的后四颗行星——木星、土星、天王星和海王星——被称为"外行星"或"巨行星"。它们与...

(wa2)


[uncategorized] ~3-~3: 量词“颗”不能修饰“行星”。
Context: # 外行星:气体巨星 太阳系的后四颗行星——木星、土星、天王星和海王星——被称为"外行星"或"巨行星"。它们与内行星完全...

(wa5)

🤖 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 `@data/public_kb/solar-system/04-outer-planets.md` around lines 1 - 3,
修订该文档中外行星的分类描述:明确木星和土星是气体巨星,天王星和海王星是冰巨星;同步更新标题、开头段落及第 28-30、43-45
行附近的相关表述,避免将四颗行星都描述为主要由氢氦组成或均属于气体巨星。

Comment on lines +1 to +31
theme_id: solar-system
title:
zh: "太阳系"
en: "Solar System"
age_band: "10-12"
description:
zh: "探索八大行星和太阳系的奥秘!"
en: "Explore the eight planets and the mysteries of the solar system!"
icon: "🪐"
levels:
- id: lvl_001
title: { zh: "太阳——我们的恒星", en: "The Sun" }
difficulty: "入门"
source_files: ["01-the-sun.md"]
- id: lvl_002
title: { zh: "内行星:岩石世界", en: "Inner Planets" }
difficulty: "进阶"
source_files: ["02-inner-planets.md"]
- id: lvl_003
title: { zh: "地球与月球", en: "Earth and Moon" }
difficulty: "进阶"
source_files: ["03-earth-and-moon.md"]
- id: lvl_004
title: { zh: "外行星:气体巨星", en: "Outer Planets" }
difficulty: "精通"
source_files: ["04-outer-planets.md"]
- id: lvl_005
title: { zh: "小行星与彗星", en: "Asteroids and Comets" }
difficulty: "精通"
is_boss: true
source_files: ["05-asteroids-comets.md"]

Copy link
Copy Markdown

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 预期:英文 locale 使用英文知识源,或明确经过翻译处理。
rg -n -C 6 'locale|language|source_files|public_kb|title\.en|title\.zh' \
  deeptutor web tests data/public_kb

Repository: hubo1989/DeepTutor

Length of output: 50375


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== public_themes relevant sections =="
sed -n '1,220p' deeptutor/services/gamification/public_themes.py

echo
echo "== theme files =="
find data/public_kb/solar-system -maxdepth 2 -type f | sort

echo
echo "== solar manifest =="
cat -n data/public_kb/solar-system/manifest.yaml

echo
echo "== references to load_theme / PublicTheme / levels source_files =="
rg -n "load_theme\(|PublicTheme|source_files|levels:" deeptutor web tests -g '*.py' -g '*.ts' -g '*.tsx' -g '*.vue' -g '*.json' | head -n 200

Repository: hubo1989/DeepTutor

Length of output: 15025


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== kids router relevant sections =="
sed -n '140,420p' deeptutor/api/routers/kids.py

echo
echo "== search source_files construction/serialization =="
rg -n -C 4 "source_files|LevelSummaryModel|QuestMap|language|language|locale" deeptutor/api deeptutor/services deeptutor/knowledge deeptutor/capabilities/web -g '*.py' | head -n 300

echo
echo "== search language/directive around quest/kids =="
rg -n -C 4 "language_directive|append_language_directive|getPublicThemes|getMaps|locale|language" web/app '(web)' web/features web/lib deeptutor/api/routers/kids.py deeptutor/capabilities/quest deeptutor/services/gamification -g '*.py' -g '*.tsx' -g '*.ts' | head -n 350

Repository: hubo1989/DeepTutor

Length of output: 29225


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== quest language context construction =="
sed -n '200,380p' deeptutor/capabilities/quest/questing.py
sed -n '1,220p' deeptutor/capabilities/quest/questing.py

echo
echo "== web kids source_files / language usage =="
rg -n -C 5 "source_files|question|knowledge_context|language|locale|getPublicThemes|getMaps|kids" web -g '*.tsx' -g '*.ts' | head -n 350

echo
echo "== deterministic manifest/source parsing model =="
python3 - <<'PY'
from pathlib import Path
data=[]
for path in Path('data/public_kb/solar-system').glob('*.md'):
    txt=path.read_text(encoding='utf-8')
    data.append((str(path), txt.splitlines()[0] or '<empty>'))
print(data)
print('no English source files:', all('.en.' not in p and not l.startswith('##') for p,l in data))
PY

Repository: hubo1989/DeepTutor

Length of output: 36931


为英文 locale 补充独立知识源或禁用英文 public theme。

data/public_kb/solar-system 中的每个 level source_files 都指向中文 markdown,且该目录没有英文知识库或语言映射;英文用户若通过 public theme 进入关卡,会得到中文知识上下文并可能用英文界面混合。

🤖 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 `@data/public_kb/solar-system/manifest.yaml` around lines 1 - 31, 为
solar-system manifest 的英文 locale 提供独立的英文知识源或明确的语言映射,确保所有 levels 的 source_files
在英文用户进入时加载英文内容;若无法提供英文知识库,则禁用该 public theme 的英文可用性,避免英文界面混用中文知识上下文。

Comment on lines +10 to +31
levels:
- id: lvl_001
title: { zh: "太阳——我们的恒星", en: "The Sun" }
difficulty: "入门"
source_files: ["01-the-sun.md"]
- id: lvl_002
title: { zh: "内行星:岩石世界", en: "Inner Planets" }
difficulty: "进阶"
source_files: ["02-inner-planets.md"]
- id: lvl_003
title: { zh: "地球与月球", en: "Earth and Moon" }
difficulty: "进阶"
source_files: ["03-earth-and-moon.md"]
- id: lvl_004
title: { zh: "外行星:气体巨星", en: "Outer Planets" }
difficulty: "精通"
source_files: ["04-outer-planets.md"]
- id: lvl_005
title: { zh: "小行星与彗星", en: "Asteroids and Comets" }
difficulty: "精通"
is_boss: true
source_files: ["05-asteroids-comets.md"]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 预期:构建流程读取 source_files,并按 level 绑定文档。
rg -n -C 8 'source_files|_collect_source_files|levels|is_boss|manifest' \
  scripts deeptutor tests data/public_kb

Repository: hubo1989/DeepTutor

Length of output: 50375


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== candidate files for public_kb/kids =="
git ls-files | rg -n 'public_kb|public_themes|kids|build_public_kb|gamification|manifest' | head -200

echo
echo "== focused build/load references =="
rg -n --max-count 120 'load_theme|_build_quest_map_from_theme|source_files|levels:|def collect|glob|is_boss|public_kb|manifest\.yaml' deeptutor scripts tests data/public_kb \
  | rg -n -C 6 'load_theme|_build_quest_map_from_theme|source_files|public_kb|manifest\.yaml|collect|glob|is_boss'

Repository: hubo1989/DeepTutor

Length of output: 50374


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== scripts/build_public_kb.py =="
cat -n scripts/build_public_kb.py | sed -n '1,140p'

echo
echo "== deeptutor/services/gamification/public_themes.py =="
cat -n deeptutor/services/gamification/public_themes.py | sed -n '1,180p'

echo
echo "== deeptutor/api/routers/kids.py relevant build route =="
cat -n deeptutor/api/routers/kids.py | sed -n '140,210p'
cat -n deeptutor/api/routers/kids.py | sed -n '235,290p'

echo
echo "== deeptutor/capabilities/quest/capability.py relevant corpus loading =="
cat -n deeptutor/capabilities/quest/capability.py | sed -n '210,270p'

echo
echo "== behavioral scan: source_files field consumers = public_kb sources =="
python3 - <<'PY'
from pathlib import Path
for p in Path('deeptutor').rglob('*.py'):
    txt = p.read_text(encoding='utf-8', errors='ignore')
    if 'source_files' in txt and 'public_kb' in txt:
        print(f"-- {p} --")
        for i,line in enumerate(txt.splitlines(), 1):
            if 'source_files' in line or 'public_kb' in line:
                print(f"{i}: {line}")
PY

Repository: hubo1989/DeepTutor

Length of output: 20509


source_files 生效并校验文件是否存在。

scripts/build_public_kb.pydeeptutor/services/gamification/public_themes.pydeeptutor/api/routers/kids.pydeeptutor/capabilities/quest/capability.py 的公开主题路径都没有读取 level 的 source_files。当前匹配规则会认为任意主题目录下全局出现一次的 is_boss: true 都可能被用作对应级 is_boss,且构建/运行时都依赖 Markdown 文件名顺序,而不是 manifest 声明的级文档绑定。应使这些路径按每个 level 的 source_files 过滤并校验指定文件存在。

🤖 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 `@data/public_kb/solar-system/manifest.yaml` around lines 10 - 31, 公开主题的 level
处理流程未使用 manifest 中的 source_files,且可能错误继承其他级别的 is_boss。更新构建与运行时公开主题加载流程,使每个 level
仅匹配其声明的 source_files,校验每个指定 Markdown 文件存在,并基于该绑定文档生成内容;同时将 is_boss 限定在对应 level
上,移除依赖目录全局扫描或文件名顺序的匹配逻辑。

Comment on lines +271 to +273
reason = (
response.split(":", 1)[-1].strip() if ":" in response else "LLM flagged content"
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

当配置 llm_client 时,对审核失败采用拒绝策略。

complete() 抛出异常时,代码会返回 passed=True。当响应不是以 "unsafe" 开头时,代码也会返回 passed=True。此行为会在 LLM 审核不可用或响应格式异常时放行未被词表命中的不安全内容。

仅当响应严格表示 "SAFE" 时返回通过。对异常和未知响应返回失败结果。未配置 llm_client 的离线词表模式可以继续返回通过。

建议修改
-            response_lower = response.strip().lower()
+            response_lower = response.strip().casefold()
+            if response_lower == "safe":
+                return FilterResult(passed=True, sanitized_text=text)
             if response_lower.startswith("unsafe"):
                 reason = (
                     response.split(":", 1)[-1].strip() if ":" in response else "LLM flagged content"
                 )
                 return FilterResult(
@@
                     sanitized_text=text,
                 )
+            return FilterResult(
+                passed=False,
+                reason="LLM review returned an unexpected response",
+                sanitized_text=text,
+            )
         except Exception:
-            # If the LLM call fails, fall back to wordlist-only result.
-            pass
-
-        return FilterResult(passed=True, sanitized_text=text)
+            return FilterResult(
+                passed=False,
+                reason="LLM review unavailable",
+                sanitized_text=text,
+            )
🤖 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 `@deeptutor/services/gamification/safety.py` around lines 271 - 273, Update the
LLM moderation flow in the safety-check function containing the response parsing
so that, when llm_client is configured, only a response strictly indicating
“SAFE” returns passed=True; return a failed moderation result for complete()
exceptions and unknown or non-“unsafe” responses. Preserve the existing
pass-through behavior for offline word-list moderation when llm_client is not
configured.

Comment on lines +421 to +431
// Sync from external selectedValue (e.g. after re-render)
useEffect(() => {
if (selectedValue && selectedValue.includes(";;")) {
const parsed: Record<string, string> = {};
for (const part of selectedValue.split(";;")) {
const [l, r] = part.split("|");
if (l && r) parsed[l] = r;
}
setSelections(parsed);
}
}, []);

Copy link
Copy Markdown

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '30,145p' web/features/kids/components/QuestionCard.tsx
rg -n -C 4 'renderQuestionBody|question_id|<QuestionCard' \
  web/features/kids/components/QuestionCard.tsx \
  'web/app/(kids)/play/[levelId]/page.tsx'

Repository: hubo1989/DeepTutor

Length of output: 6659


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== QuestionCard outline =="
ast-grep outline web/features/kids/components/QuestionCard.tsx --view compact || true

echo "== Rendered relevant blocks =="
sed -n '144,560p' web/features/kids/components/QuestionCard.tsx

echo "== QuestionCard usages with keys =="
rg -n -C 5 '<QuestionCard|renderQuestionBody|question_id|useCurrentQuestion|currentIndex' web/app web/features/kids || true

echo "== Deterministic React behavior probe =="
node - <<'JS'
function run(initial, effects, transitions) {
  let selections = initial.selections;
  let available = initial.available;
  let userOrder = initial.userOrder;
  const events = [];
  function log(...args) { events.push(args.join(" ")); }
  for (const transition of transitions) {
    if (transition.action) {
      transition.action(selections, available, userOrder);
    }
    for (const e of effects) {
      log(`after ${transition.question.question_id}:${transition.question.question_type}: ${e.name} with {selectedValue=${e.selectedValue}, sequence=...}`);
      if (transition.question.question_type === 'matching' && e.name === 'syncMatching' && e.selectedValue && e.selectedValue.includes(';;')) {
        const parsed = {};
        for (const part of e.selectedValue.split(';;')) {
          const [l, r] = part.split('|');
          if (l && r) parsed[l] = r;
        }
        selections = parsed;
      } else if (transition.question.question_type === 'matching' && e.name === 'initMatching') {
        selections = {};
      } else if (transition.question.question_type === 'ordering' && e.name === 'initOrdering') {
        available = [...transition.question.ordering_sequence].sort(() => Math.random() - 0.5);
        userOrder = [];
      } else if (transition.question.question_type === 'ordering' && e.name === 'syncOrdering' && e.selectedValue && e.selectedValue.includes(';;')) {
        userOrder = e.selectedValue.split(';;');
        available = [];
      }
      log(`state: matchingSelections=${JSON.stringify(selections)}, orderAvailable=${JSON.stringify(available)}, orderUser=${JSON.stringify(userOrder)}`);
    }
  }
  return events;
}

const effects = [
  { name: 'matchInitAndSync', selectedValue: '' },
  { name: 'matchSync', selectedValue: 'a|1;;b|2' },
  { name: 'orderingInitAndSync', sequence: ['x','y'], selectedValue: '' },
  { name: 'orderingInitWithSequence', sequence: ['x','y'], selectedValue: '' },
  { name: 'orderingInitAndSyncFromPrevious', sequence: ['x','y'], selectedValue: 'p;;q' }
];

const q1 = { question_id: 'q1', question_type: 'matching' };
const q2 = { question_id: 'q2', question_type: 'matching' };
const o1 = { question_id: 'o1', question_type: 'ordering', ordering_sequence: ['x','y'] };

console.log('empty deps, same type, same component instance:');
for (const e of ["q1->q2", "o1->o2"]) {
  console.log(e, [...run(e.startsWith('o')
    ? { selections: {}, available: ['x','y'], userOrder: ['p','q'] }
    : { selections: {'prev': 'prev'}, available: [], userOrder: [] }, effects, [{ question: e.startsWith('o') ? o1 : q1, action: undefined }, {question: e.startsWith('o') ? o1 : q2}])].join('\n'));
}

console.log('keyed by question_id, same component instance impossible, must remount; effects should run in new instance, likely state resets');
JS

echo "== Files likely containing current question hook/state =="
fd -a '.*\.(ts|tsx)$' web | xargs rg -l 'currentQuestion|useCurrentQuestion|currentIndex|setcurrentIndex' | head -50

Repository: hubo1989/DeepTutor

Length of output: 36092


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== order section =="
sed -n '480,620p' web/features/kids/components/QuestionCard.tsx

echo "== current page imports and QuestionCard context =="
sed -n '1,130p' 'web/app/(kids)/play/[levelId]/page.tsx'
sed -n '220,280p' 'web/app/(kids)/play/[levelId]/page.tsx'

echo "== matching all QuestionCard props and keys =="
python3 - <<'PY'
from pathlib import Path
import re
for p in Path('web').rglob('*QuestionCard.ts*'):
    print('FILE', p)
    s=p.read_text()
    for i,line in enumerate(s.splitlines(),1):
        if '<QuestionCard' in line or 'key=' in line and 'QuestionCard' in s[max(0,i-5):i+5]:
            print(i,line.strip())
print('ALL QuestionCard usages:')
for q in list(Path('web').rglob('*QuestionCard*')):
    print(q)
PY

Repository: hubo1989/DeepTutor

Length of output: 11324


不要依赖空 deps 来清除同类型题目的交互状态。

QuestionCardPlayPage 中按 currentQuestion 直接渲染且没有传入 key。匹配或排序组件的空 deps effect 不会在新题目上重新运行,因此前一题的选择会通过 props 保留到下一题。按题目 ID 或相关题目数据变化重置 MatchingInteraction / OrderingInteraction 的内部状态。

📍 Affects 1 file
  • web/features/kids/components/QuestionCard.tsx#L421-L431 (this comment)
  • web/features/kids/components/QuestionCard.tsx#L507-L519
🤖 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 `@web/features/kids/components/QuestionCard.tsx` around lines 421 - 431, Update
the empty-dependency synchronization effects in QuestionCard at
web/features/kids/components/QuestionCard.tsx:421-431 and
web/features/kids/components/QuestionCard.tsx:507-519 so they respond to the
current question ID or relevant question data changes. Reset the internal state
of MatchingInteraction and OrderingInteraction when the rendered question
changes, while preserving synchronization for the active question’s
selectedValue.

Comment on lines +89 to +99
if (!cancelled) {
// Use the first profile as the active one (UI can add a switcher)
if (data.length > 0) {
const p = data[0];
setProfile({
profileId: p.profile_id,
nickname: p.nickname,
avatar: p.avatar || "🧒",
ageBand: p.age_band || "7-9",
role: "kid",
});

Copy link
Copy Markdown

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

不要把列表首项当作活动儿童档案。

Line 91 无条件使用 data[0]。Line 98 还丢弃了后端返回的 role。多档案或 PIN 切换后,web/app/(kids)/play/[levelId]/page.tsx 会把错误的 profileId 发送到任务连接,导致进度写入错误的儿童档案。

请从服务端会话的活动档案或明确的档案选择状态获取档案,并验证后保留 role

🤖 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 `@web/features/kids/hooks/useKidProfile.tsx` around lines 89 - 99, 更新
useKidProfile 中的档案选择逻辑,不要默认使用 data[0];应从服务端会话的活动档案或明确的档案选择状态确定当前儿童档案,确保多档案及 PIN
切换后使用正确的 profileId。构造 setProfile 数据时保留并验证后端返回的 role,避免固定为 "kid"。

@hubo1989
hubo1989 merged commit ec3e24b into main Aug 11, 2026
12 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

Development

Successfully merging this pull request may close these issues.

1 participant