feat: 游戏化学习(挑战岛 / Quest Island)— 面向 7-15 岁儿童 - #8
Conversation
- 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)
📝 WalkthroughWalkthrough新增儿童档案、角色权限、公共主题、题目生成、游戏化进度、Kids API、TTS、WebSocket 任务流程和前端学习界面。新增对应的存储、测试、主题数据、本地化资源及构建脚本。 Changes儿童学习平台
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
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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,这段时间内按钮可点击但无反馈,儿童会重复点击。
FeedbackOverlay的onComplete(第 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.json与web/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维护isSpeaking。playBase64Audio分支不设置该状态。使用后端音频时,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,没有为横幅预留高度。横幅出现时会覆盖页面标题。请在showReminder为true时给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.learned为true时用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 NoneAlso 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 >= 0,min的结果始终是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_items与quest_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这些测试没有覆盖路由逻辑。
TestMapsListAPI与TestProgressAPI的类名指向GET /kids/maps和GET /kids/progress/{profile_id},但测试体只调用store与pin的服务层函数。路由中的所有权校验、PIN 校验、404 与 403 分支都没有被执行。第 276 行的assert loaded.guardian_user_id != "guardian-002"是恒真断言,不提供信息。另外,第 40-57 行定义的
mock_guardian_user与mock_kid_userfixture 在本文件中没有被使用。
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
handleThemeClick的async与try/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.success与kidsTheme.colors.warning在kidsTheme.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 valueLLM 失败时重复构造相同提示词。
_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_llm的except 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) >= 1,test_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.yaml的encourage列表有 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.py中init_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
📒 Files selected for processing (78)
deeptutor/api/main.pydeeptutor/api/routers/kids.pydeeptutor/api/routers/profiles.pydeeptutor/api/routers/voice_kids.pydeeptutor/capabilities/quest/__init__.pydeeptutor/capabilities/quest/capability.pydeeptutor/capabilities/quest/forging.pydeeptutor/capabilities/quest/grading.pydeeptutor/capabilities/quest/hints.pydeeptutor/capabilities/quest/prompts/en/forging_10-12.yamldeeptutor/capabilities/quest/prompts/en/forging_13-15.yamldeeptutor/capabilities/quest/prompts/en/forging_7-9.yamldeeptutor/capabilities/quest/prompts/en/hints.yamldeeptutor/capabilities/quest/prompts/zh/forging_10-12.yamldeeptutor/capabilities/quest/prompts/zh/forging_13-15.yamldeeptutor/capabilities/quest/prompts/zh/forging_7-9.yamldeeptutor/capabilities/quest/prompts/zh/hints.yamldeeptutor/capabilities/quest/quest_cache.pydeeptutor/capabilities/quest/questing.pydeeptutor/core/context.pydeeptutor/knowledge/kb_types.pydeeptutor/knowledge/manager.pydeeptutor/multi_user/kid_context.pydeeptutor/runtime/bootstrap/builtin_capabilities.pydeeptutor/services/gamification/__init__.pydeeptutor/services/gamification/badges.pydeeptutor/services/gamification/engine.pydeeptutor/services/gamification/metrics.pydeeptutor/services/gamification/models.pydeeptutor/services/gamification/public_themes.pydeeptutor/services/gamification/safety.pydeeptutor/services/gamification/sources.pydeeptutor/services/gamification/store.pydeeptutor/services/kid_profiles/__init__.pydeeptutor/services/kid_profiles/guard.pydeeptutor/services/kid_profiles/models.pydeeptutor/services/kid_profiles/pin.pydeeptutor/services/kid_profiles/store.pyscripts/build_public_kb.pytests/api/test_kids.pytests/api/test_profiles.pytests/api/test_role_guard.pytests/capabilities/quest/test_forging.pytests/capabilities/quest/test_grading.pytests/capabilities/quest/test_hints.pytests/capabilities/quest/test_questing.pytests/services/gamification/test_badges.pytests/services/gamification/test_engine.pytests/services/gamification/test_engine_aggregation.pytests/services/gamification/test_metrics.pytests/services/gamification/test_public_themes.pytests/services/gamification/test_quest_cache.pytests/services/gamification/test_safety.pytests/services/gamification/test_sources.pytests/services/gamification/test_store.pyweb/app/(kids)/home/page.tsxweb/app/(kids)/layout.tsxweb/app/(kids)/map/[mapId]/page.tsxweb/app/(kids)/play/[levelId]/page.tsxweb/app/(kids)/rewards/page.tsxweb/features/kids/components/BadgeGrid.tsxweb/features/kids/components/FeedbackOverlay.tsxweb/features/kids/components/HintPanel.tsxweb/features/kids/components/LevelSummary.tsxweb/features/kids/components/MapNode.tsxweb/features/kids/components/ParentDashboard.tsxweb/features/kids/components/QuestionCard.tsxweb/features/kids/components/RestReminder.tsxweb/features/kids/components/StreakCalendar.tsxweb/features/kids/hooks/useKidProfile.tsweb/features/kids/hooks/useKidQuest.tsweb/features/kids/hooks/useTTS.tsweb/features/kids/store/kidsStore.tsweb/features/kids/theme/kidsTheme.tsweb/lib/kids-api.tsweb/lib/kids-types.tsweb/locales/en/kids.jsonweb/locales/zh/kids.json
| return ( | ||
| <KidProfileContext.Provider value={value}> | ||
| {children} | ||
| </KidProfileContext.Provider> | ||
| ); |
There was a problem hiding this comment.
🩺 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
| return ( | ||
| <KidsStoreContext.Provider value={value}> | ||
| {children} | ||
| </KidsStoreContext.Provider> | ||
| ); |
There was a problem hiding this comment.
🎯 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
- 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
There was a problem hiding this comment.
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 后向下取整”,并限定在本课的1到9范围内。建议修改
- 而且,结果的十位数也有规律: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
⛔ Files ignored due to path filters (1)
uv.lockis excluded by!**/*.lock
📒 Files selected for processing (65)
.gitignore.workbuddy/memory/2026-08-08.mddata/public_kb/dino-world/01-what-are-dinosaurs.mddata/public_kb/dino-world/02-triassic-period.mddata/public_kb/dino-world/03-jurassic-giants.mddata/public_kb/dino-world/04-dinosaur-diets.mddata/public_kb/dino-world/05-great-extinction.mddata/public_kb/dino-world/manifest.yamldata/public_kb/multiplication-kingdom/01-times-2.mddata/public_kb/multiplication-kingdom/02-times-5.mddata/public_kb/multiplication-kingdom/03-times-10.mddata/public_kb/multiplication-kingdom/04-times-tables-tricks.mddata/public_kb/multiplication-kingdom/05-real-world-problems.mddata/public_kb/multiplication-kingdom/manifest.yamldata/public_kb/solar-system/01-the-sun.mddata/public_kb/solar-system/02-inner-planets.mddata/public_kb/solar-system/03-earth-and-moon.mddata/public_kb/solar-system/04-outer-planets.mddata/public_kb/solar-system/05-asteroids-comets.mddata/public_kb/solar-system/manifest.yamldeeptutor/api/main.pydeeptutor/api/routers/kids.pydeeptutor/api/routers/profiles.pydeeptutor/api/routers/voice_kids.pydeeptutor/capabilities/quest/__init__.pydeeptutor/capabilities/quest/capability.pydeeptutor/capabilities/quest/forging.pydeeptutor/capabilities/quest/grading.pydeeptutor/capabilities/quest/hints.pydeeptutor/capabilities/quest/quest_cache.pydeeptutor/capabilities/quest/questing.pydeeptutor/knowledge/manager.pydeeptutor/services/gamification/__init__.pydeeptutor/services/gamification/badges.pydeeptutor/services/gamification/engine.pydeeptutor/services/gamification/metrics.pydeeptutor/services/gamification/safety.pydeeptutor/services/gamification/store.pydeeptutor/services/kid_profiles/__init__.pydeeptutor/services/kid_profiles/guard.pydeeptutor/services/kid_profiles/store.pyscripts/build_public_kb.pytests/api/test_kids.pytests/api/test_profiles.pytests/api/test_role_guard.pytests/capabilities/quest/test_forging.pytests/capabilities/quest/test_grading.pytests/capabilities/quest/test_hints.pytests/capabilities/quest/test_questing.pytests/core/test_capabilities_runtime.pytests/services/gamification/test_badges.pytests/services/gamification/test_engine.pytests/services/gamification/test_engine_aggregation.pytests/services/gamification/test_metrics.pytests/services/gamification/test_quest_cache.pytests/services/gamification/test_safety.pytests/services/gamification/test_sources.pytests/services/gamification/test_store.pyweb/app/(kids)/play/[levelId]/page.tsxweb/features/kids/components/QuestionCard.tsxweb/features/kids/hooks/useKidProfile.tsxweb/features/kids/store/kidsStore.tsxweb/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
| 很多人以为恐龙是巨大的蜥蜴,其实不是的。恐龙和现在的蜥蜴、鳄鱼不一样,它们是一类独特的爬行动物。 | ||
|
|
||
| 恐龙也不是鱼龙、蛇颈龙或者翼龙——虽然它们也生活在恐龙的时代,但它们不是真正的恐龙。真正的恐龙只生活在陆地上。 |
There was a problem hiding this comment.
🎯 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,将始祖鸟描述为早期鸟类恐龙或鸟类近亲,并将“恐龙变成了鸟”改为“鸟类是仍存活的恐龙谱系”。
|
|
||
| ## 肉食恐龙:凶猛的猎手 | ||
|
|
||
| 肉食恐龙也叫"兽脚类恐龙",它们是恐龙世界里的猎手。它们的特点是: |
There was a problem hiding this comment.
🎯 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.
| 肉食恐龙也叫"兽脚类恐龙",它们是恐龙世界里的猎手。它们的特点是: | |
| 许多肉食恐龙属于兽脚类;兽脚类也包括鸟类等不同食性的成员。 |
🤖 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,
更新该段开头的分类表述,将“肉食恐龙也叫‘兽脚类恐龙’”改为“许多肉食恐龙属于兽脚类”,以区分食性名称与分类群;保留后续关于其猎手特点的内容。
| 11乘以两位数也有技巧:把两个数字相加放在中间! | ||
| - 11 × 23 → 2 + 3 = 5 → **253** | ||
| - 11 × 42 → 4 + 2 = 6 → **462** |
There was a problem hiding this comment.
🎯 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.
| 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。
| - **峡谷**:火星上有巨大的水手大峡谷,长四千多公里,比地球上的大峡谷大得多。 | ||
| - **水**:科学家发现火星上有冰,主要在两极地区。很久以前火星上可能有液态水。 | ||
|
|
||
| 火星是目前人类最想探索的行星。多个国家已经向火星发送了探测器,中国的"天问一号"和美国的"毅力号"都在火星上进行科学研究。 |
There was a problem hiding this comment.
🎯 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.
| 火星是目前人类最想探索的行星。多个国家已经向火星发送了探测器,中国的"天问一号"和美国的"毅力号"都在火星上进行科学研究。 | |
| 中国的"天问一号"和美国的"毅力号"都曾开展火星科学探测;当前状态请按任务分别更新。 |
🤖 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,
更新火星段落中提及“天问一号”和“毅力号”的句子,不要将天问一号描述为仍在进行科学研究;改用过去时准确表述其已完成计划中的科学探测任务,并保留或单独明确毅力号当前任务状态。
| # 外行星:气体巨星 | ||
|
|
||
| 太阳系的后四颗行星——木星、土星、天王星和海王星——被称为"外行星"或"巨行星"。它们与内行星完全不同:它们巨大无比,主要由气体组成,没有坚硬的表面可以登陆。 |
There was a problem hiding this comment.
🎯 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
行附近的相关表述,避免将四颗行星都描述为主要由氢氦组成或均属于气体巨星。
| 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"] |
There was a problem hiding this comment.
🎯 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_kbRepository: 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 200Repository: 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 350Repository: 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))
PYRepository: 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 的英文可用性,避免英文界面混用中文知识上下文。
| 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"] |
There was a problem hiding this comment.
🗄️ 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_kbRepository: 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}")
PYRepository: hubo1989/DeepTutor
Length of output: 20509
让 source_files 生效并校验文件是否存在。
scripts/build_public_kb.py、deeptutor/services/gamification/public_themes.py、deeptutor/api/routers/kids.py 和 deeptutor/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
上,移除依赖目录全局扫描或文件名顺序的匹配逻辑。
| reason = ( | ||
| response.split(":", 1)[-1].strip() if ":" in response else "LLM flagged content" | ||
| ) |
There was a problem hiding this comment.
🔒 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.
| // 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); | ||
| } | ||
| }, []); |
There was a problem hiding this comment.
🎯 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 -50Repository: 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)
PYRepository: hubo1989/DeepTutor
Length of output: 11324
不要依赖空 deps 来清除同类型题目的交互状态。
QuestionCard 在 PlayPage 中按 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.
| 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", | ||
| }); |
There was a problem hiding this comment.
🗄️ 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"。
游戏化学习(挑战岛 / Quest Island)
面向 7-15 岁儿童的游戏化学习功能,把"学知识"包装成"闯关挑战",支持个人知识库和公共知识库两类内容来源。
交付概览
需求覆盖(F0-F11)
kid_questCapability 三阶段流水线(sourcing → forging → questing)public公共知识库类型 + 3 个内置主题(恐龙世界/太阳系/乘法王国)关键设计
personal:<profile_id>:<kb_name>与public:<theme_id>永不碰撞文件结构
配套文档
docs/specs/gamified-learning-spec.mddocs/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 排除,部署时需单独打包Summary by CodeRabbit