伴学插件接入 OS 本体感知并完善笔记与语音交互#1619
Conversation
Implement the complete study_companion plugin capabilities: Notebook System: - Note data model, SQLite schema, FTS5 search, CRUD store (store_notebook.py, store_schema.py) - Plugin entries: note create/update/query/cross-module search (entry_notebook.py) - Frontend UI: note_card, note_editor, note_search, notebook_panel - AI note expansion and summarization (tutor_llm_agent_notebook.py) - KaTeX math rendering with full font set and CSS/JS - jieba Chinese text segmentation for search (pyproject.toml dep) Voice Interaction: - Voice transcript filter with subject detection and keyword matching (voice_filter.py) - Voice contracts for cancel_response / prime_context actions (voice_contracts.py) - Integration with neko voice bridge infrastructure Localization: - 10+ language i18n support for notebook, voice, UI entries This is Part 2 of the original PR Project-N-E-K-O#1582 split. Depends on Part 1 (voice bridge + plugin dispatch infrastructure).
- Add awareness_runner.py: main loop tying OCR snapshots, OS signals, and supervision together with configurable push-to-LLM intervals - Add config_loader.py: centralized config builder extracting awareness, supervision, pomodoro, OCR, LLM, and FSRS settings from raw config - Add ActivitySnapshotService: server-side service exposing host activity tracker snapshots via messaging with lanlan-name resolution - Enhance supervision with OS-level foreground category for distraction detection and system idle time awareness - Extend activity tracker with get_snapshot_sync and enriched snapshots - Add deskpet plugin scaffold - Consolidate code: move awareness/supervision logic out of __init__.py and models.py into focused modules (-806 lines net) - Expand test coverage across awareness, supervision, voice filter, notebook, and activity snapshot handler tests
…al tracker The awareness loop already has its own UserActivityTracker instance. Walking the cross-process SDK→handler→service chain to reach another tracker on the same machine returns identical OS signals from the shared SystemSignalCollector singleton. Drop the pipe — it adds 8 framework-layer files to the diff for no functional gain. - Delete activity_snapshot_service.py and activity_snapshot handler - Revert protocols / context / SDK / registry to main - Simplify _read_awareness_activity_snapshot to call local tracker directly (12 lines instead of 28) - Remove test_awareness_tick_prefers_host_activity_snapshot (obsolete)
- Add 'unknown' to is_active() and summarize() exclusion sets in ActivityBuffer, matching _record_awareness_snapshot which already treats app_type='unknown' as idle. Fixes false-positive active detection when OS signals classify foreground as unknown. - Limit active_from_idle recovery to inactivity level only in SupervisionController.observe_activity. Previously mouse movement would clear distraction detection even when the user was still in a gaming/entertainment app during focus mode.
CRITICAL: Add _OS_CATEGORY_TO_APP_TYPE mapping table so OS-layer foreground_category values (gaming) are normalised to the OCR/app-title vocabulary (game) before writing into ActivityBuffer.app_type. Prevents mixed-taxonomy keys in app_distribution / current_app summaries. HIGH: Remove 'casual_browsing' from _DISTRACTION_FOREGROUND_CATEGORIES. WindowObservation.category never produces this value — it is an ActivityState, not a foreground-category string. Detected gaming + entertainment still covers the real distraction surface. HIGH: Remove idle_warning_minutes dead config field. Defined, clamped, and loaded in 6 call sites but never consumed by any runtime code path. MEDIUM: Label private-app activity as activity_type='private' instead of 'idle'. The user IS actively using the private app (password manager / banking); is_active() correctly returns True. Exclude 'private' from focus-minute aggregation in summarize().
Add _consecutive_os_read_failures counter in _AwarenessRunnerMixin. The first 3 consecutive failures log at WARNING level; the 4th and beyond escalate to ERROR with an explicit message about potential permanent tracker / collector unavailability. A successful read resets the counter. Previously all exceptions were swallowed identically as a single warning per tick — a permanently-broken tracker would degrade silently forever without surfacing the failure.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (3)
🚧 Files skipped from review as they are similar to previous changes (2)
Walkthrough此 PR 增加 Activity Tracker 的快照开关、添加 DeskPet 插件,并大幅扩展 Study Companion(笔记存储与 UI、语音过滤/仲裁、OCR/awareness 循环、LLM 笔记助手、数学渲染与大量测试与 i18n)。喵。 ChangesActivity Tracker 快照控制
DeskPet 桌面宠物插件
Study Companion:笔记、语音、awareness、UI、静态资源、LM 调用
Sequence Diagram(s)(此更改包含多条交互流,已在隐藏审查栈分层描述,无额外序列图于此。) Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Poem
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 3200e94053
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| ) | ||
| conn.execute( | ||
| """ | ||
| CREATE TABLE IF NOT EXISTS notes ( |
There was a problem hiding this comment.
Add notebook data to the purge path
This commit adds persistent user-owned note data, but StudyStore.purge_all() still iterates the fixed _PURGE_TABLES list in plugin/plugins/study_companion/store_maintenance.py and that list does not include notes or notebooks. In any reset/purge flow, all of the older study, habit, and memory rows are deleted while saved notebook notes remain in the database, which breaks the documented “delete all user data rows” behavior and can leak private notes after a user expects a purge.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Actionable comments posted: 10
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
main_logic/activity/tracker.py (1)
538-545:⚠️ Potential issue | 🟠 Major | ⚡ Quick winpending 字段构建逻辑不一致,可能返回陈旧状态喵~
主人,这里有个逻辑问题要注意喵!在正常富集路径(
include_enrichment=True)中,lines 543-544 无条件构建work_break_pending和anti_slack_pending:work_break_pending=self._build_work_break_pending(), anti_slack_pending=self._build_anti_slack_pending(),但是在
include_enrichment=False路径(lines 528-533),这两个字段是根据tick_followups条件构建的:work_break_pending=( self._build_work_break_pending() if tick_followups else None ), anti_slack_pending=( self._build_anti_slack_pending() if tick_followups else None ),这导致不一致的行为喵:
问题场景: 当调用
get_snapshot(include_enrichment=True, tick_followups=False)时:
- Line 503-504:
_tick_break_reminders不会被调用(因为tick_followups=False)- Lines 543-544:pending 字段仍然会从内部状态
self._work_break_pending和self._anti_slack_pending构建- 结果:返回的是陈旧的 pending 状态(上次 tick 时留下的),而不是当前的
这违反了
tick_followups=False的设计意图:"read-only pollers that must not advance break-reminder / anti-slack pending state"。如果调用者不想推进状态(不 tick),那返回陈旧的 pending 状态会造成误导喵~🔧 建议的修复喵
让富集路径的 pending 字段构建也遵循
tick_followups条件判断:return dc_replace( snap, activity_scores=dict(self._activity_scores_cache), activity_guess=self._activity_guess_cache, open_threads=list(self._open_threads_cache), - work_break_pending=self._build_work_break_pending(), - anti_slack_pending=self._build_anti_slack_pending(), + work_break_pending=( + self._build_work_break_pending() if tick_followups else None + ), + anti_slack_pending=( + self._build_anti_slack_pending() if tick_followups else None + ), )这样就和
include_enrichment=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 `@main_logic/activity/tracker.py` around lines 538 - 545, The snapshot enrichment path currently always calls _build_work_break_pending() and _build_anti_slack_pending() which can return stale pending state even when tick_followups is False; update the get_snapshot logic so that both include_enrichment=True and include_enrichment=False paths apply the same tick_followups guard—i.e., only call _build_work_break_pending() and _build_anti_slack_pending() when tick_followups is truthy (otherwise set those fields to None) so the method respects the tick_followups flag and does not advance or expose pending state when ticking is disabled.
🧹 Nitpick comments (6)
tests/test_activity_tracker_followup.py (1)
768-807: ⚡ Quick win测试覆盖还可以更完善一些喵~
主人,这个测试写得不错喵,但有几个小建议:
缺少 pending 字段的断言: 测试验证了富集字段(
activity_scores、activity_guess、open_threads)被清空,但没有显式断言work_break_pending和anti_slack_pending为None。虽然代码逻辑(lines 528-533)确实会把它们设为None,但显式断言能让测试意图更清晰喵~参数组合覆盖不全: 当前测试只验证了
include_enrichment=False, tick_followups=False的情况,建议补充以下组合的测试:
include_enrichment=True, tick_followups=False— 验证有富集但不 tick 的场景include_enrichment=False, tick_followups=True— 验证无富集但会 tick 的场景include_enrichment=True, tick_followups=True— 验证默认行为(虽然这个可能在其他测试中已覆盖)这样能确保新参数的所有分支逻辑都被验证到喵~
💡 建议的补充断言喵
在现有测试中补充 pending 字段断言:
assert snap.activity_scores == {} assert snap.activity_guess == "" assert snap.open_threads == [] +assert snap.work_break_pending is None +assert snap.anti_slack_pending is None新增覆盖其他组合的测试(示例框架):
def test_tracker_enrichment_without_ticking(): """include_enrichment=True, tick_followups=False should return enrichment but no pending.""" # ... setup tracker with cached enrichment ... snap = asyncio.run( tracker.get_snapshot( now=100.0, include_enrichment=True, tick_followups=False, ) ) # Should have enrichment assert snap.activity_scores != {} assert snap.activity_guess != "" # But no pending (because not ticking) assert snap.work_break_pending is None assert snap.anti_slack_pending is None🤖 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/test_activity_tracker_followup.py` around lines 768 - 807, The test test_tracker_rule_only_snapshot_skips_enrichment_loop is missing explicit assertions for the pending fields and lacks coverage for other include_enrichment/tick_followups combinations; update this test to assert that snap.work_break_pending and snap.anti_slack_pending are None after calling UserActivityTracker.get_snapshot(... include_enrichment=False, tick_followups=False), and add additional small tests (or parametrize) that call tracker.get_snapshot with the other three combinations (include_enrichment=True/False × tick_followups=True/False) to assert expected behaviors: when include_enrichment=True the enrichment fields (tracker._activity_scores_cache/_activity_guess_cache/_open_threads_cache) surface in snap, when tick_followups=True pending fields may be set (assert accordingly) and when False they remain None; reference UserActivityTracker, get_snapshot, and the snapshot fields activity_scores, activity_guess, open_threads, work_break_pending, anti_slack_pending to locate and modify the tests.plugin/plugins/study_companion/static/math-parser.js (1)
93-95: ⚡ Quick winKaTeX 数学模式下
\lt/\gt的尾随空格大多无影响,可选清理更规范喵。
normalizeLatexForKatex里将</>替换为\lt/\gt:在 KaTeX 的数学模式解析中,绝大多数空白会被当作不重要并被吞掉,因此多余空格通常不会引入意外间距喵。\lt/\gt也只是</>的语义别名;不过把尾随空格去掉能让输出更贴近常见写法、也更利于后续维护喵。🔧 建议的修复方案
function normalizeLatexForKatex(value) { - return String(value || '').replace(/</g, '\\lt ').replace(/>/g, '\\gt '); + return String(value || '').replace(/</g, '\\lt').replace(/>/g, '\\gt'); }🤖 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 `@plugin/plugins/study_companion/static/math-parser.js` around lines 93 - 95, The normalizeLatexForKatex function currently replaces '<' and '>' with '\lt ' and '\gt ' (including a trailing space); remove the trailing spaces so replacements produce '\lt' and '\gt' instead, i.e., update the replacement strings in normalizeLatexForKatex to '\\lt' and '\\gt' to produce cleaner KaTeX math-mode output while keeping the existing String(value || '') conversion and replace calls.plugin/plugins/study_companion/constants.py (1)
15-26: ⚡ Quick win确认:
LLM_OPERATION_EXPAND_NOTE/LLM_OPERATION_SUMMARIZE_TO_NOTE未加入SUPPORTED_LLM_OPERATIONS不会触发当前校验喵
SUPPORTED_LLM_OPERATIONS的唯一校验在plugin/plugins/study_companion/llm_prompts.py的build_operation_messages()(operation 不在集合就 raise),但expand_note/summarize_to_note的调用都在plugin/plugins/study_companion/tutor_llm_agent_notebook.py里直接构造messages并调用_call_model,不走build_operation_messages(),所以不会被拒绝喵~(可选)建议在注释/命名上明确这两个 operation 属于 notebook 直调路径,避免未来误接到
build_operation_messages()导致 ValueError 喵~🤖 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 `@plugin/plugins/study_companion/constants.py` around lines 15 - 26, SUPPORTED_LLM_OPERATIONS is missing LLM_OPERATION_EXPAND_NOTE and LLM_OPERATION_SUMMARIZE_TO_NOTE so calls that should be validated by build_operation_messages() (in plugin/plugins/study_companion/llm_prompts.py) may be inconsistent with direct notebook calls in tutor_llm_agent_notebook.py; add LLM_OPERATION_EXPAND_NOTE and LLM_OPERATION_SUMMARIZE_TO_NOTE to the SUPPORTED_LLM_OPERATIONS frozenset and/or add a clarifying comment above those two constants stating they are invoked directly by tutor_llm_agent_notebook.py (not via build_operation_messages()) so future maintainers don’t accidentally rely on build_operation_messages() validation.plugin/plugins/study_companion/surfaces/note_card.tsx (1)
19-19: 💤 Low value建议移除 props 接口中的
key字段喵~React 的
key是保留属性,应该由父组件在渲染列表时直接传递(如<NoteCard key={note.id} note={note} />),而不需要在NoteCardProps接口中显式定义喵。虽然这不会导致运行时错误,但可能让其他开发者困惑,也不符合 React 惯例喵~✨ 建议的修改
type NoteCardProps = { - key?: string; note: NoteItem; selected?: boolean; onSelect?: (note: NoteItem) => void; };🤖 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 `@plugin/plugins/study_companion/surfaces/note_card.tsx` at line 19, Remove the reserved React "key" prop from the NoteCard props interface (remove the "key?: string" from NoteCardProps in note_card.tsx) and update any internal references to that prop; ensure callers pass key directly when rendering lists (e.g. <NoteCard key={...} note={...} />) rather than relying on a key field on the NoteCard props or component.plugin/plugins/study_companion/screen_classifier.py (1)
118-129: 💤 Low value浏览器关键词前导空格可能漏匹配以浏览器名开头的标题喵~
" chrome"和" firefox"使用前导空格来避免匹配 "Chromium" 等误报,这个思路很聪明喵!但是如果标题以浏览器名开头(比如"Chrome - Settings"),就匹配不到了喵~这只是个边缘情况,人家觉得现在的实现已经够用了,如果以后用户反馈再考虑加入无空格版本也不迟喵❤️
🤖 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 `@plugin/plugins/study_companion/screen_classifier.py` around lines 118 - 129, _BROWSER_TITLE_KEYWORDS contains entries with a leading space (e.g., " chrome", " firefox") so titles that start with the browser name (like "Chrome - Settings") won't match; update the matching approach by either adding no-space variants (e.g., "chrome", "firefox") to _BROWSER_TITLE_KEYWORDS or change the title-check logic to perform word-boundary matching (e.g., using regex \b...\\b) when scanning titles so both leading-space and start-of-string cases are detected.plugin/tests/unit/plugins/test_study_companion_code_health.py (1)
59-65: ⚡ Quick win这个健康测试把约束放大到整个仓库了喵。
这里读取仓库根的
requirements.txt/pyproject.toml,再全局禁止scipy和PyWavelets,会把 Study Companion 的局部清理变成 monorepo 级禁令喵。以后哪怕别的插件合法引入这两个通用库,也会被这个测试误伤,和本 PR 的屏幕 hash 移除目标无关喵。更稳一点的做法是只检查你们这次明确移除的直接链路(比如
imagehash),或者把断言范围收窄到study_companion自身的引用面喵。🤖 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 `@plugin/tests/unit/plugins/test_study_companion_code_health.py` around lines 59 - 65, The test test_screen_hash_dependency_and_fields_are_removed currently reads the monorepo root and asserts absence of broad names (forbidden_dependencies) in top-level requirements.txt/pyproject.toml which wrongly globalizes the check; narrow the scope by either (A) limiting forbidden_dependencies to the specific package removed (e.g., "imagehash") and/or (B) changing the inspected files from repo_root / "requirements.txt" and repo_root / "pyproject.toml" to the Study Companion package files under _STUDY_COMPANION_ROOT (or its own pyproject/requirements) so the assertion only targets study_companion's direct dependencies; update the variables in the test function test_screen_hash_dependency_and_fields_are_removed accordingly.
🤖 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 `@plugin/plugins/deskpet/__init__.py`:
- Line 193: The logged messages use str.format-style placeholders like
"cpu={:.1f}%" but the logger expects printf-style formatting; update the
self.logger.info calls (e.g., the instances calling self.logger.info("DeskPet
speak(stressed): cpu={:.1f}%", cpu) and the similar call at the other location)
to use printf-style format specifiers and escape the literal percent sign (e.g.,
use "%.1f%%" as the format for cpu) so the cpu value is interpolated by the
logger consistently with the other "%s" usages.
In `@plugin/plugins/study_companion/entry_notebook.py`:
- Around line 511-529: The create_memory_card branch should guard against the
memory deck capability being unavailable: before calling
self._memory_deck_store.get_or_create_default_deck (and upsert_item), check that
self._memory_deck_store is truthy and exposes the needed methods; if not, return
Err(SdkError("memory deck capability unavailable", code="MEMORY_NOT_AVAILABLE"))
so the caller gets a clear, typed error instead of an AttributeError; keep the
existing asyncio.to_thread calls and returned payload (Ok({...})) when the store
is present.
In `@plugin/plugins/study_companion/i18n/ja.json`:
- Around line 199-251: The JSON values for all notebook/note keys (e.g.,
entries.notebook_create.name, entries.notebook_create.description,
entries.notebook_list.*, entries.notebook_update.*, entries.notebook_delete.*,
entries.note_upsert.*, entries.note_get.*, entries.note_delete.*,
entries.note_list.*, entries.note_search_all.*, entries.note_ai_expand.*,
entries.note_ai_generate.*, entries.note_highlight_action.*, and ui.* keys like
ui.surface.notebook_panel, ui.notebook.new_note, ui.notebook.search_placeholder,
ui.notebook.empty, ui.notebook.tab_notes, etc.) are still in English; replace
each English string with the correct Japanese translation (matching the style of
other locale files such as ko.json/pt.json) so Japanese users see localized
text—for example change "Create Study Notebook" to "学習ノートブックを作成" and similarly
localize all other entries listed in the diff. Ensure you only modify the values
(keep the keys intact) and validate the JSON after edits.
In `@plugin/plugins/study_companion/static/katex-render.js`:
- Around line 17-32: The current falsy check in renderMathPart incorrectly
treats empty string as missing value and produces odd output like
"<code>$$$$</code>"; update renderMathPart so it treats only null/undefined as
missing (e.g., replace the if (!part.value) check with a null/undefined check
such as part.value === null || part.value === undefined) and add a special-case
for truly empty LaTeX content: if part.value.trim() === '' then return an empty
string (or otherwise skip rendering) for display-mode math; keep the rest of the
flow (calling normalizeLatexForKatex, window.katex.renderToString, and the catch
fallback using escapeHTML) unchanged.
In `@plugin/plugins/study_companion/study_ocr_pipeline.py`:
- Around line 189-201: 当前逻辑用 `status = "ok" if title or ocr_text or jpeg_bytes
else "empty"` 会把仅有 `jpeg_bytes`(只有图片没有语义信息)的快照误判为可用;请修改该判定以排除仅靠图片就认为可用的情况 —— 在构建
LightweightSnapshot 前更新 `status` 的条件,改为仅在有语义信息时标记为 "ok"(例如至少存在 `title` 或
`ocr_text` 或 有意义的 `activity_type`/`app_type`),不要把 `jpeg_bytes` 单独作为判定真值;保持后续对
`jpeg_base64`(通过 `_jpeg_data_url`)与 `jpeg_metadata` 的赋值不变。
- Around line 224-234: When extract_text() raises and the code returns a full
LightweightSnapshot with status="ocr_failed", it causes to_activity_snapshot()
to yield None and the frame is treated as unavailable; instead, catch the OCR
exception in the except block and degrade gracefully by returning a
LightweightSnapshot that preserves title and/or jpeg_bytes (e.g., keep
window_title=title and jpeg_bytes/jpeg_base64/jpeg_metadata) but mark the
snapshot as a partial/ocr_failed type so to_activity_snapshot() can still
produce a title-only activity; update the except branch around extract_text() to
include the existing title and image fields rather than discarding them (refer
to extract_text(), to_activity_snapshot(), LightweightSnapshot,
status="ocr_failed", title, jpeg_bytes, jpeg_base64).
- Around line 390-398: 函数 _capture_fullscreen 目前只调用 ImageGrab.grab() 并在异常时退回到
pyautogui.screenshot(),两者都未显式支持多显示器/虚拟桌面,导致副屏截图丢失;修改为优先使用
ImageGrab.grab(all_screens=True)(或等效参数)以捕获所有显示器,并在 PIL 不可用或该调用失败时改为使用
MSS(或其他支持多屏/虚拟桌面的库)作为回退,移除或不要使用仅捕获主屏的 pyautogui.screenshot(),确保
_capture_fullscreen 明确返回包含所有显示器内容的整幅图像以供后续 OCR 使用。
In `@plugin/plugins/study_companion/surfaces/note_editor.tsx`:
- Around line 203-231: The cleanup logic in the useEffect silently swallows
errors from the background save (the void callPlugin('study_note_upsert', ...)
.catch(() => undefined) in the return cleanup) so failures are lost; update the
catch to at minimum log the error (e.g., console.error or processLogger.error)
with context including draft.noteId / notebookId and indicate it occurred during
unmount-save, while keeping the call in the same cleanup block that reads
latestDraft.current and savedSnapshot.current; do not change the save flow
otherwise, just replace the empty catch handler with a handler that logs the
error and any identifying draft info.
In `@plugin/plugins/study_companion/surfaces/notebook_panel.tsx`:
- Around line 136-138: The Refresh button calls the async function refresh()
without awaiting or handling rejections, causing unhandled promise rejections
when callPlugin inside refresh rejects; update the button handler to call
refresh() safely (e.g., await refresh() inside an async onClick or call
refresh().catch(...)) and surface errors to the user or log them consistently
with other actions (createNote/deleteNote) — catch the error from refresh() and
use the same error handling path used by createNote/deleteNote (show UI feedback
or processLogger) so failures are not swallowed.
In `@plugin/plugins/study_companion/tutor_llm_agent.py`:
- Around line 503-524: The code sets call_type_group from requested_model_group
before fallback logic, which can later switch model_group back to "agent" (e.g.,
missing vision or requested model config) causing billing to be recorded against
the wrong group; after determining the final model_group and api_config (inside
the branch handling has_image and the else branch that falls back to agent when
configs are missing, including the requested_model_group check), assign
call_type_group = model_group (or set call_type_group only once after all
fallback decisions), and remove the earlier pre-fallback assignment so
set_call_type() and subsequent token/fee attribution use the actual resolved
model_group; update the same pattern at the other occurrence referenced (around
line 564) as well.
---
Outside diff comments:
In `@main_logic/activity/tracker.py`:
- Around line 538-545: The snapshot enrichment path currently always calls
_build_work_break_pending() and _build_anti_slack_pending() which can return
stale pending state even when tick_followups is False; update the get_snapshot
logic so that both include_enrichment=True and include_enrichment=False paths
apply the same tick_followups guard—i.e., only call _build_work_break_pending()
and _build_anti_slack_pending() when tick_followups is truthy (otherwise set
those fields to None) so the method respects the tick_followups flag and does
not advance or expose pending state when ticking is disabled.
---
Nitpick comments:
In `@plugin/plugins/study_companion/constants.py`:
- Around line 15-26: SUPPORTED_LLM_OPERATIONS is missing
LLM_OPERATION_EXPAND_NOTE and LLM_OPERATION_SUMMARIZE_TO_NOTE so calls that
should be validated by build_operation_messages() (in
plugin/plugins/study_companion/llm_prompts.py) may be inconsistent with direct
notebook calls in tutor_llm_agent_notebook.py; add LLM_OPERATION_EXPAND_NOTE and
LLM_OPERATION_SUMMARIZE_TO_NOTE to the SUPPORTED_LLM_OPERATIONS frozenset and/or
add a clarifying comment above those two constants stating they are invoked
directly by tutor_llm_agent_notebook.py (not via build_operation_messages()) so
future maintainers don’t accidentally rely on build_operation_messages()
validation.
In `@plugin/plugins/study_companion/screen_classifier.py`:
- Around line 118-129: _BROWSER_TITLE_KEYWORDS contains entries with a leading
space (e.g., " chrome", " firefox") so titles that start with the browser name
(like "Chrome - Settings") won't match; update the matching approach by either
adding no-space variants (e.g., "chrome", "firefox") to _BROWSER_TITLE_KEYWORDS
or change the title-check logic to perform word-boundary matching (e.g., using
regex \b...\\b) when scanning titles so both leading-space and start-of-string
cases are detected.
In `@plugin/plugins/study_companion/static/math-parser.js`:
- Around line 93-95: The normalizeLatexForKatex function currently replaces '<'
and '>' with '\lt ' and '\gt ' (including a trailing space); remove the trailing
spaces so replacements produce '\lt' and '\gt' instead, i.e., update the
replacement strings in normalizeLatexForKatex to '\\lt' and '\\gt' to produce
cleaner KaTeX math-mode output while keeping the existing String(value || '')
conversion and replace calls.
In `@plugin/plugins/study_companion/surfaces/note_card.tsx`:
- Line 19: Remove the reserved React "key" prop from the NoteCard props
interface (remove the "key?: string" from NoteCardProps in note_card.tsx) and
update any internal references to that prop; ensure callers pass key directly
when rendering lists (e.g. <NoteCard key={...} note={...} />) rather than
relying on a key field on the NoteCard props or component.
In `@plugin/tests/unit/plugins/test_study_companion_code_health.py`:
- Around line 59-65: The test test_screen_hash_dependency_and_fields_are_removed
currently reads the monorepo root and asserts absence of broad names
(forbidden_dependencies) in top-level requirements.txt/pyproject.toml which
wrongly globalizes the check; narrow the scope by either (A) limiting
forbidden_dependencies to the specific package removed (e.g., "imagehash")
and/or (B) changing the inspected files from repo_root / "requirements.txt" and
repo_root / "pyproject.toml" to the Study Companion package files under
_STUDY_COMPANION_ROOT (or its own pyproject/requirements) so the assertion only
targets study_companion's direct dependencies; update the variables in the test
function test_screen_hash_dependency_and_fields_are_removed accordingly.
In `@tests/test_activity_tracker_followup.py`:
- Around line 768-807: The test
test_tracker_rule_only_snapshot_skips_enrichment_loop is missing explicit
assertions for the pending fields and lacks coverage for other
include_enrichment/tick_followups combinations; update this test to assert that
snap.work_break_pending and snap.anti_slack_pending are None after calling
UserActivityTracker.get_snapshot(... include_enrichment=False,
tick_followups=False), and add additional small tests (or parametrize) that call
tracker.get_snapshot with the other three combinations
(include_enrichment=True/False × tick_followups=True/False) to assert expected
behaviors: when include_enrichment=True the enrichment fields
(tracker._activity_scores_cache/_activity_guess_cache/_open_threads_cache)
surface in snap, when tick_followups=True pending fields may be set (assert
accordingly) and when False they remain None; reference UserActivityTracker,
get_snapshot, and the snapshot fields activity_scores, activity_guess,
open_threads, work_break_pending, anti_slack_pending to locate and modify the
tests.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 3f0a1001-b597-4530-9707-e53b19f05b86
⛔ Files ignored due to path filters (62)
plugin/plugins/study_companion/static/fonts/KaTeX_AMS-Regular.ttfis excluded by!**/*.ttfplugin/plugins/study_companion/static/fonts/KaTeX_AMS-Regular.woffis excluded by!**/*.woffplugin/plugins/study_companion/static/fonts/KaTeX_AMS-Regular.woff2is excluded by!**/*.woff2plugin/plugins/study_companion/static/fonts/KaTeX_Caligraphic-Bold.ttfis excluded by!**/*.ttfplugin/plugins/study_companion/static/fonts/KaTeX_Caligraphic-Bold.woffis excluded by!**/*.woffplugin/plugins/study_companion/static/fonts/KaTeX_Caligraphic-Bold.woff2is excluded by!**/*.woff2plugin/plugins/study_companion/static/fonts/KaTeX_Caligraphic-Regular.ttfis excluded by!**/*.ttfplugin/plugins/study_companion/static/fonts/KaTeX_Caligraphic-Regular.woffis excluded by!**/*.woffplugin/plugins/study_companion/static/fonts/KaTeX_Caligraphic-Regular.woff2is excluded by!**/*.woff2plugin/plugins/study_companion/static/fonts/KaTeX_Fraktur-Bold.ttfis excluded by!**/*.ttfplugin/plugins/study_companion/static/fonts/KaTeX_Fraktur-Bold.woffis excluded by!**/*.woffplugin/plugins/study_companion/static/fonts/KaTeX_Fraktur-Bold.woff2is excluded by!**/*.woff2plugin/plugins/study_companion/static/fonts/KaTeX_Fraktur-Regular.ttfis excluded by!**/*.ttfplugin/plugins/study_companion/static/fonts/KaTeX_Fraktur-Regular.woffis excluded by!**/*.woffplugin/plugins/study_companion/static/fonts/KaTeX_Fraktur-Regular.woff2is excluded by!**/*.woff2plugin/plugins/study_companion/static/fonts/KaTeX_Main-Bold.ttfis excluded by!**/*.ttfplugin/plugins/study_companion/static/fonts/KaTeX_Main-Bold.woffis excluded by!**/*.woffplugin/plugins/study_companion/static/fonts/KaTeX_Main-Bold.woff2is excluded by!**/*.woff2plugin/plugins/study_companion/static/fonts/KaTeX_Main-BoldItalic.ttfis excluded by!**/*.ttfplugin/plugins/study_companion/static/fonts/KaTeX_Main-BoldItalic.woffis excluded by!**/*.woffplugin/plugins/study_companion/static/fonts/KaTeX_Main-BoldItalic.woff2is excluded by!**/*.woff2plugin/plugins/study_companion/static/fonts/KaTeX_Main-Italic.ttfis excluded by!**/*.ttfplugin/plugins/study_companion/static/fonts/KaTeX_Main-Italic.woffis excluded by!**/*.woffplugin/plugins/study_companion/static/fonts/KaTeX_Main-Italic.woff2is excluded by!**/*.woff2plugin/plugins/study_companion/static/fonts/KaTeX_Main-Regular.ttfis excluded by!**/*.ttfplugin/plugins/study_companion/static/fonts/KaTeX_Main-Regular.woffis excluded by!**/*.woffplugin/plugins/study_companion/static/fonts/KaTeX_Main-Regular.woff2is excluded by!**/*.woff2plugin/plugins/study_companion/static/fonts/KaTeX_Math-BoldItalic.ttfis excluded by!**/*.ttfplugin/plugins/study_companion/static/fonts/KaTeX_Math-BoldItalic.woffis excluded by!**/*.woffplugin/plugins/study_companion/static/fonts/KaTeX_Math-BoldItalic.woff2is excluded by!**/*.woff2plugin/plugins/study_companion/static/fonts/KaTeX_Math-Italic.ttfis excluded by!**/*.ttfplugin/plugins/study_companion/static/fonts/KaTeX_Math-Italic.woffis excluded by!**/*.woffplugin/plugins/study_companion/static/fonts/KaTeX_Math-Italic.woff2is excluded by!**/*.woff2plugin/plugins/study_companion/static/fonts/KaTeX_SansSerif-Bold.ttfis excluded by!**/*.ttfplugin/plugins/study_companion/static/fonts/KaTeX_SansSerif-Bold.woffis excluded by!**/*.woffplugin/plugins/study_companion/static/fonts/KaTeX_SansSerif-Bold.woff2is excluded by!**/*.woff2plugin/plugins/study_companion/static/fonts/KaTeX_SansSerif-Italic.ttfis excluded by!**/*.ttfplugin/plugins/study_companion/static/fonts/KaTeX_SansSerif-Italic.woffis excluded by!**/*.woffplugin/plugins/study_companion/static/fonts/KaTeX_SansSerif-Italic.woff2is excluded by!**/*.woff2plugin/plugins/study_companion/static/fonts/KaTeX_SansSerif-Regular.ttfis excluded by!**/*.ttfplugin/plugins/study_companion/static/fonts/KaTeX_SansSerif-Regular.woffis excluded by!**/*.woffplugin/plugins/study_companion/static/fonts/KaTeX_SansSerif-Regular.woff2is excluded by!**/*.woff2plugin/plugins/study_companion/static/fonts/KaTeX_Script-Regular.ttfis excluded by!**/*.ttfplugin/plugins/study_companion/static/fonts/KaTeX_Script-Regular.woffis excluded by!**/*.woffplugin/plugins/study_companion/static/fonts/KaTeX_Script-Regular.woff2is excluded by!**/*.woff2plugin/plugins/study_companion/static/fonts/KaTeX_Size1-Regular.ttfis excluded by!**/*.ttfplugin/plugins/study_companion/static/fonts/KaTeX_Size1-Regular.woffis excluded by!**/*.woffplugin/plugins/study_companion/static/fonts/KaTeX_Size1-Regular.woff2is excluded by!**/*.woff2plugin/plugins/study_companion/static/fonts/KaTeX_Size2-Regular.ttfis excluded by!**/*.ttfplugin/plugins/study_companion/static/fonts/KaTeX_Size2-Regular.woffis excluded by!**/*.woffplugin/plugins/study_companion/static/fonts/KaTeX_Size2-Regular.woff2is excluded by!**/*.woff2plugin/plugins/study_companion/static/fonts/KaTeX_Size3-Regular.ttfis excluded by!**/*.ttfplugin/plugins/study_companion/static/fonts/KaTeX_Size3-Regular.woffis excluded by!**/*.woffplugin/plugins/study_companion/static/fonts/KaTeX_Size3-Regular.woff2is excluded by!**/*.woff2plugin/plugins/study_companion/static/fonts/KaTeX_Size4-Regular.ttfis excluded by!**/*.ttfplugin/plugins/study_companion/static/fonts/KaTeX_Size4-Regular.woffis excluded by!**/*.woffplugin/plugins/study_companion/static/fonts/KaTeX_Size4-Regular.woff2is excluded by!**/*.woff2plugin/plugins/study_companion/static/fonts/KaTeX_Typewriter-Regular.ttfis excluded by!**/*.ttfplugin/plugins/study_companion/static/fonts/KaTeX_Typewriter-Regular.woffis excluded by!**/*.woffplugin/plugins/study_companion/static/fonts/KaTeX_Typewriter-Regular.woff2is excluded by!**/*.woff2plugin/plugins/study_companion/static/katex.min.jsis excluded by!**/*.min.jsuv.lockis excluded by!**/*.lock
📒 Files selected for processing (61)
main_logic/activity/tracker.pyplugin/plugins/deskpet/__init__.pyplugin/plugins/deskpet/plugin.tomlplugin/plugins/study_companion/__init__.pyplugin/plugins/study_companion/awareness_buffer.pyplugin/plugins/study_companion/awareness_runner.pyplugin/plugins/study_companion/config_loader.pyplugin/plugins/study_companion/constants.pyplugin/plugins/study_companion/doc_exporter.pyplugin/plugins/study_companion/entry_export_support.pyplugin/plugins/study_companion/entry_knowledge_entries.pyplugin/plugins/study_companion/entry_neko_commands.pyplugin/plugins/study_companion/entry_notebook.pyplugin/plugins/study_companion/i18n/en.jsonplugin/plugins/study_companion/i18n/es.jsonplugin/plugins/study_companion/i18n/ja.jsonplugin/plugins/study_companion/i18n/ko.jsonplugin/plugins/study_companion/i18n/pt.jsonplugin/plugins/study_companion/i18n/ru.jsonplugin/plugins/study_companion/i18n/zh-CN.jsonplugin/plugins/study_companion/i18n/zh-TW.jsonplugin/plugins/study_companion/models.pyplugin/plugins/study_companion/onboarding.mdplugin/plugins/study_companion/plugin.tomlplugin/plugins/study_companion/screen_classifier.pyplugin/plugins/study_companion/service.pyplugin/plugins/study_companion/static/index.htmlplugin/plugins/study_companion/static/katex-render.jsplugin/plugins/study_companion/static/katex.min.cssplugin/plugins/study_companion/static/main.jsplugin/plugins/study_companion/static/math-parser.jsplugin/plugins/study_companion/static/style.cssplugin/plugins/study_companion/store_notebook.pyplugin/plugins/study_companion/store_schema.pyplugin/plugins/study_companion/study_ocr_pipeline.pyplugin/plugins/study_companion/supervision.pyplugin/plugins/study_companion/surfaces/memory_shared.tsplugin/plugins/study_companion/surfaces/note_card.tsxplugin/plugins/study_companion/surfaces/note_editor.tsxplugin/plugins/study_companion/surfaces/note_search.tsxplugin/plugins/study_companion/surfaces/notebook_panel.tsxplugin/plugins/study_companion/surfaces/study_panel.tsxplugin/plugins/study_companion/tutor_llm_agent.pyplugin/plugins/study_companion/tutor_llm_agent_notebook.pyplugin/plugins/study_companion/voice_contracts.pyplugin/plugins/study_companion/voice_filter.pyplugin/tests/unit/plugins/test_awareness_buffer.pyplugin/tests/unit/plugins/test_study_companion.pyplugin/tests/unit/plugins/test_study_companion_code_health.pyplugin/tests/unit/plugins/test_study_companion_neko_commands.pyplugin/tests/unit/plugins/test_study_companion_phase9_ux.pyplugin/tests/unit/plugins/test_study_companion_study_ocr_pipeline.pyplugin/tests/unit/plugins/test_study_companion_voice_bridge.pyplugin/tests/unit/plugins/test_study_companion_voice_contracts.pyplugin/tests/unit/plugins/test_study_companion_voice_filter.pyplugin/tests/unit/plugins/test_study_companion_voice_scenarios.pyplugin/tests/unit/plugins/test_study_notebook.pyplugin/tests/unit/plugins/test_supervision.pypyproject.tomlrequirements.txttests/test_activity_tracker_followup.py
💤 Files with no reviewable changes (1)
- requirements.txt
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d1ffe23114
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| try: | ||
| result = subprocess.run( | ||
| ["xdotool", "getactivewindow", "getwindowname"], | ||
| capture_output=True, | ||
| check=False, | ||
| text=True, | ||
| timeout=1.0, | ||
| ) | ||
| return str(result.stdout or "").strip() | ||
| import pygetwindow | ||
|
|
||
| window = pygetwindow.getActiveWindow() | ||
| return str(getattr(window, "title", "") or "").strip() |
There was a problem hiding this comment.
Preserve Linux active-window title lookup
On Linux installs created from pyproject.toml, pygetwindow is only declared for sys_platform == 'win32', so this fallback import fails and _get_active_window_title() now returns an empty title after the macOS/Windows branches. This regresses the previous Linux xdotool getactivewindow getwindowname path, and with the default title_first lightweight awareness mode (or whenever OS signals are unavailable) study awareness can no longer classify the foreground app from the active window title.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (1)
plugin/plugins/study_companion/study_ocr_pipeline.py (1)
408-420:⚠️ Potential issue | 🟠 Major | ⚡ Quick win别把
ImageGrab.grab(all_screens=True)当成跨平台多屏修复喵。Pillow 文档把
all_screens明确标成 Windows OS only,源码里也只有 Win32 分支会真正消费这个参数;所以在 macOS/Linux 上如果ImageGrab.grab()成功,这里就不会落到下面的mss路径,副屏内容仍可能被漏掉,awareness / OCR 还是会读错喵。建议改成非 Windows 直接走mss(或统一用支持虚拟桌面的实现),不要把ImageGrab成功当成“已覆盖全部显示器”喵。 (pillow.readthedocs.io)🐾 可参考的最小修正喵
`@staticmethod` def _capture_fullscreen() -> Any: - try: - from PIL import ImageGrab - - return ImageGrab.grab(all_screens=True) - except Exception: - import mss - from PIL import Image - - with mss.mss() as sct: - monitor = sct.monitors[0] - shot = sct.grab(monitor) - return Image.frombytes("RGB", shot.size, shot.rgb) + if sys.platform == "win32": + try: + from PIL import ImageGrab + + return ImageGrab.grab(all_screens=True) + except Exception: + pass + import mss + + with mss.mss() as sct: + monitor = sct.monitors[0] + shot = sct.grab(monitor) + return Image.frombytes("RGB", shot.size, shot.rgb)🤖 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 `@plugin/plugins/study_companion/study_ocr_pipeline.py` around lines 408 - 420, The _capture_fullscreen function incorrectly assumes ImageGrab.grab(all_screens=True) is multi-monitor on all OSes; change it to branch on the OS instead: if platform.system() == "Windows" use PIL.ImageGrab.grab(all_screens=True) (keeping the same call) and for non-Windows platforms always use mss (with PIL.Image.frombytes on the grabbed shot) so you don't treat a successful ImageGrab call on macOS/Linux as covering all screens; reference symbols: _capture_fullscreen, ImageGrab.grab, and mss to locate and update the branch logic.
🤖 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 `@plugin/tests/unit/plugins/test_study_habit_store.py`:
- Line 111: The assertion `assert note.id` is placed after `purge_all()` and
therefore only checks the in-memory note object, not that creation succeeded;
either move the assertion to before the `purge_all()` call (so it validates that
`create_note` produced an id) or remove the assertion entirely if you rely on
`create_note` raising on failure; locate the `create_note(...)` call and the
subsequent `purge_all()` invocation and adjust by moving or deleting the `assert
note.id` accordingly.
---
Duplicate comments:
In `@plugin/plugins/study_companion/study_ocr_pipeline.py`:
- Around line 408-420: The _capture_fullscreen function incorrectly assumes
ImageGrab.grab(all_screens=True) is multi-monitor on all OSes; change it to
branch on the OS instead: if platform.system() == "Windows" use
PIL.ImageGrab.grab(all_screens=True) (keeping the same call) and for non-Windows
platforms always use mss (with PIL.Image.frombytes on the grabbed shot) so you
don't treat a successful ImageGrab call on macOS/Linux as covering all screens;
reference symbols: _capture_fullscreen, ImageGrab.grab, and mss to locate and
update the branch logic.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 4261644a-3388-4bd2-8777-c24cffc4898e
📒 Files selected for processing (21)
main_logic/activity/tracker.pyplugin/plugins/deskpet/__init__.pyplugin/plugins/study_companion/constants.pyplugin/plugins/study_companion/entry_notebook.pyplugin/plugins/study_companion/i18n/ja.jsonplugin/plugins/study_companion/screen_classifier.pyplugin/plugins/study_companion/static/katex-render.jsplugin/plugins/study_companion/static/math-parser.jsplugin/plugins/study_companion/store_maintenance.pyplugin/plugins/study_companion/study_ocr_pipeline.pyplugin/plugins/study_companion/surfaces/note_card.tsxplugin/plugins/study_companion/surfaces/note_editor.tsxplugin/plugins/study_companion/surfaces/notebook_panel.tsxplugin/plugins/study_companion/tutor_llm_agent.pyplugin/tests/unit/plugins/test_awareness_buffer.pyplugin/tests/unit/plugins/test_study_companion.pyplugin/tests/unit/plugins/test_study_companion_code_health.pyplugin/tests/unit/plugins/test_study_companion_study_ocr_pipeline.pyplugin/tests/unit/plugins/test_study_habit_store.pyplugin/tests/unit/plugins/test_study_notebook.pytests/test_activity_tracker_followup.py
💤 Files with no reviewable changes (1)
- plugin/plugins/study_companion/surfaces/note_card.tsx
✅ Files skipped from review due to trivial changes (1)
- plugin/plugins/study_companion/i18n/ja.json
🚧 Files skipped from review as they are similar to previous changes (14)
- plugin/plugins/study_companion/constants.py
- plugin/plugins/study_companion/static/math-parser.js
- plugin/tests/unit/plugins/test_awareness_buffer.py
- plugin/plugins/study_companion/screen_classifier.py
- plugin/plugins/deskpet/init.py
- plugin/plugins/study_companion/surfaces/notebook_panel.tsx
- plugin/tests/unit/plugins/test_study_companion.py
- plugin/plugins/study_companion/static/katex-render.js
- plugin/tests/unit/plugins/test_study_notebook.py
- plugin/plugins/study_companion/tutor_llm_agent.py
- main_logic/activity/tracker.py
- plugin/plugins/study_companion/entry_notebook.py
- plugin/tests/unit/plugins/test_study_companion_study_ocr_pipeline.py
- plugin/plugins/study_companion/surfaces/note_editor.tsx
|
这个 PR 主要把“伴学插件”升级成一个更完整的学习陪伴系统: 核心改动有几块: 伴学插件接入 OS 活动感知:前台窗口、应用分类、系统空闲、隐私状态。 原来的伴学更多依赖 OCR 和聊天上下文判断用户状态,问题比较明显: OCR 不稳定,成本也高。 对程序做了什么修改 主程序侧: 扩展了 UserActivityTracker 的快照能力,让插件能复用已有 OS 活动信号。 新增 awareness_runner.py 承载 OS 感知循环。 主要影响范围在: main_logic/activity/tracker.py 侵入度大不大 整体看:功能范围大,但架构侵入度中等偏低。 原因是: 改动文件很多,功能面很大,所以 PR 体量不小。 |
变更概述
本 PR 为伴学插件补齐 OS 本体感知能力,并将感知逻辑收敛到插件内部使用,避免依赖跨进程快照管线。同时完善笔记系统、语音唤醒过滤、监督提醒和前端笔记面板。
主要改动
UserActivityTracker的 OS 活动信号,用于识别前台应用、隐私状态、空闲/离开状态与学习上下文。awareness_runner.py,将配置构建逻辑拆入config_loader.py,降低主入口复杂度。验证
221 passed:study notebook / OCR / companion / voice filter / neko commands348 passed:code health / server handlers / activity tracker / SDK29 passed:voice bridge / voice scenarios / event busruff F401,F821通过git diff --check通过risk_level=low,affected_processes=[]Summary by CodeRabbit
新功能
修复与改进