Skip to content

伴学插件接入 OS 本体感知并完善笔记与语音交互#1619

Open
MomiJiSan wants to merge 10 commits into
Project-N-E-K-O:mainfrom
MomiJiSan:feat/study-companion-os-awareness
Open

伴学插件接入 OS 本体感知并完善笔记与语音交互#1619
MomiJiSan wants to merge 10 commits into
Project-N-E-K-O:mainfrom
MomiJiSan:feat/study-companion-os-awareness

Conversation

@MomiJiSan
Copy link
Copy Markdown
Contributor

@MomiJiSan MomiJiSan commented Jun 3, 2026

变更概述

本 PR 为伴学插件补齐 OS 本体感知能力,并将感知逻辑收敛到插件内部使用,避免依赖跨进程快照管线。同时完善笔记系统、语音唤醒过滤、监督提醒和前端笔记面板。

主要改动

  • 接入 UserActivityTracker 的 OS 活动信号,用于识别前台应用、隐私状态、空闲/离开状态与学习上下文。
  • 将 awareness 运行逻辑拆入 awareness_runner.py,将配置构建逻辑拆入 config_loader.py,降低主入口复杂度。
  • 移除跨进程 activity snapshot pipe,改为插件内本地 tracker 复用。
  • 增强监督逻辑:支持 OS 空闲离开、分心恢复、unknown app 过滤和 private 状态保护。
  • 新增/完善学习笔记功能:notebook store、AI 扩写/总结、笔记搜索、笔记 UI 面板、多语言文案与 KaTeX 渲染资源。
  • 增强语音交互:角色名过滤、取消/打断命令、语音上下文注入和失败路径日志。
  • 修复 silent failure:FTS 搜索失败、logger 异常、OS activity 读取失败、worker 多次崩溃等路径现在有明确错误或 UI 提示。
  • 去除不再需要的图片 hash 依赖,降低感知链路依赖复杂度。

验证

  • 221 passed:study notebook / OCR / companion / voice filter / neko commands
  • 348 passed:code health / server handlers / activity tracker / SDK
  • 29 passed:voice bridge / voice scenarios / event bus
  • ruff F401,F821 通过
  • git diff --check 通过
  • GitNexus detect_changes:risk_level=lowaffected_processes=[]

Summary by CodeRabbit

  • 新功能

    • 完整的笔记本/笔记管理界面:创建/编辑/搜索/导出(支持按 note_ids)与多语言文案、笔记编辑器与搜索面板、入门引导。
    • AI 辅助笔记:扩写与生成(含超时与降级处理)。
    • 数学公式渲染:内置解析器与 KaTeX 支持,回复与笔记中正确渲染公式。
    • 语音转写仲裁与上下文构建;新增 DeskPet 桌面宠物插件。
  • 修复与改进

    • 语音过滤、awareness/监督与前台分类、空闲/干扰检测逻辑优化。
    • 本地存储与检索改进:SQLite 存储、FTS/回退搜索、导出和统计支持。
    • 活动快照接口新增标志以更灵活控制 enrichment 与 follow-ups;若干无障碍与静态资源改进(ARIA/样式/静态脚本)。

MomiJiSan added 8 commits June 2, 2026 22:35
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.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jun 3, 2026

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 727c0626-fe3f-4107-9733-b87579482ba0

📥 Commits

Reviewing files that changed from the base of the PR and between d1ffe23 and 81eba64.

📒 Files selected for processing (3)
  • plugin/plugins/study_companion/study_ocr_pipeline.py
  • plugin/tests/unit/plugins/test_study_companion_study_ocr_pipeline.py
  • plugin/tests/unit/plugins/test_study_habit_store.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • plugin/tests/unit/plugins/test_study_habit_store.py
  • plugin/tests/unit/plugins/test_study_companion_study_ocr_pipeline.py

Walkthrough

此 PR 增加 Activity Tracker 的快照开关、添加 DeskPet 插件,并大幅扩展 Study Companion(笔记存储与 UI、语音过滤/仲裁、OCR/awareness 循环、LLM 笔记助手、数学渲染与大量测试与 i18n)。喵。

Changes

Activity Tracker 快照控制

Layer / File(s) Summary
快照 API 参数扩展
main_logic/activity/tracker.py
UserActivityTracker.get_snapshot 增加 include_enrichmenttick_followups 控制 enrichment 缓存合并、activity_guess 循环启动与 pending 推进;_ensure_collector_started 新增 start_activity_guess_loop 参数。

DeskPet 桌面宠物插件

Layer / File(s) Summary
插件实现与配置
plugin/plugins/deskpet/__init__.py, plugin/plugins/deskpet/plugin.toml
新增 DeskPet 插件:周期采样 CPU/内存、EMA 平滑、RunCat 速度换算、压力态检测、状态上报與吐槽消息逻辑,及 check_cpu 入口与插件元数据配置。

Study Companion:笔记、语音、awareness、UI、静态资源、LM 调用

Layer / File(s) Summary
核心与配置
plugin/plugins/study_companion/__init__.py, plugin/plugins/study_companion/config_loader.py, plugin/plugins/study_companion/plugin.toml
引入 NotebookStore、VoiceFilter、handle_voice_transcript 事件与 mixin 组合;新增配置解析 build_config 与 plugin.toml 的若干 study/awareness/supervision 设置。
存储层与 DB 模式
plugin/plugins/study_companion/store_notebook.py, plugin/plugins/study_companion/store_schema.py, plugin/plugins/study_companion/store_maintenance.py
新增 notebooks/notes 表、FTS5 索引與触发器;实现 NotebookStore 完整 CRUD、FTS/LIST 回退、导出 Markdown、检索/计数/会话上下文等。
笔记插件入口与导出
plugin/plugins/study_companion/entry_notebook.py, plugin/plugins/study_companion/entry_export_support.py, plugin/plugins/study_companion/entry_knowledge_entries.py, plugin/plugins/study_companion/doc_exporter.py
新增 _NotebookEntriesMixin 与多项 @plugin_entry:notebook/note CRUD、AI 扩写/生成、highlight 动作;DocExporter 支持按 note_ids 导出;知识图谱注入 note_count。
LLM 笔记助手
plugin/plugins/study_companion/tutor_llm_agent.py, plugin/plugins/study_companion/tutor_llm_agent_notebook.py, plugin/plugins/study_companion/constants.py
支持 model_group_override 与 timeout;新增 expand_note/summarize_to_note,含结构校验、降级回退與多语言 headings,並注册相应 LLM 操作常量。
语音合约与过滤
plugin/plugins/study_companion/voice_contracts.py, plugin/plugins/study_companion/voice_filter.py
新增语音转写动作合约与仲裁函数、实现 VoiceFilter(名字窗口、question intent、OCR 重叠/数字匹配抑制、上下文构建、safe_cancel_response),并在插件中暴露 handle_voice_transcript 入口。
awareness 循环与 OCR 管道
plugin/plugins/study_companion/awareness_runner.py, plugin/plugins/study_companion/study_ocr_pipeline.py, plugin/plugins/study_companion/awareness_buffer.py, plugin/plugins/study_companion/supervision.py
新增 _AwarenessRunnerMixin 驱动异步感知循环,改进 LightweightSnapshot、JPEG 迭代编码与 OCR 提取策略,替换 ActivityBuffer 去重判定并扩展 Supervision 返回字段(含 idle_seconds/foreground_category/distraction)。
前端组件与静态资源
plugin/plugins/study_companion/surfaces/*.tsx, plugin/plugins/study_companion/static/*, plugin/plugins/study_companion/i18n/*
新增 NotebookPanel/NoteEditor/NoteSearch/NoteCard、math-parser.js、katex-render.js、katex.min.css、style.css 与 index.html 更新;在多语言包中添加学习笔记相关键并移除旧的 awareness 词条。
服务/状态/维护
plugin/plugins/study_companion/service.py, plugin/plugins/study_companion/store_maintenance.py
build_status_payload 新增 recent_notes 字段;purge_all 扩展清理 notes/notebooks 表。
测试与代码健康
plugin/tests/unit/plugins/*, tests/*
新增/调整大量单元测试覆盖 notebook 存储、voice_filter/voice_contracts、awareness/ocr/supervision、tracker snapshot flags、phase9 静态资源与 i18n 校验等。

Sequence Diagram(s)

(此更改包含多条交互流,已在隐藏审查栈分层描述,无额外序列图于此。)

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Poem

喵~快照有新开关了,后台可以偷懒喵,
桌宠盯着 CPU 喵,会说“你也太忙喵”,
笔记长了数据库喵,FTS 检索又飞快喵,
语音会找名字喵,OCR 与 KaTeX 一起闪喵,
测试和 i18n 全套到位喵,陪你学习不孤单喵。

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 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 (
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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 👍 / 👎.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

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 win

pending 字段构建逻辑不一致,可能返回陈旧状态喵~

主人,这里有个逻辑问题要注意喵!在正常富集路径(include_enrichment=True)中,lines 543-544 无条件构建 work_break_pendinganti_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) 时:

  1. Line 503-504:_tick_break_reminders 不会被调用(因为 tick_followups=False
  2. Lines 543-544:pending 字段仍然会从内部状态 self._work_break_pendingself._anti_slack_pending 构建
  3. 结果:返回的是陈旧的 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

测试覆盖还可以更完善一些喵~

主人,这个测试写得不错喵,但有几个小建议:

  1. 缺少 pending 字段的断言: 测试验证了富集字段(activity_scoresactivity_guessopen_threads)被清空,但没有显式断言 work_break_pendinganti_slack_pendingNone。虽然代码逻辑(lines 528-533)确实会把它们设为 None,但显式断言能让测试意图更清晰喵~

  2. 参数组合覆盖不全: 当前测试只验证了 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 win

KaTeX 数学模式下 \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.pybuild_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,再全局禁止 scipyPyWavelets,会把 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

📥 Commits

Reviewing files that changed from the base of the PR and between f99b5ab and 3200e94.

⛔ Files ignored due to path filters (62)
  • plugin/plugins/study_companion/static/fonts/KaTeX_AMS-Regular.ttf is excluded by !**/*.ttf
  • plugin/plugins/study_companion/static/fonts/KaTeX_AMS-Regular.woff is excluded by !**/*.woff
  • plugin/plugins/study_companion/static/fonts/KaTeX_AMS-Regular.woff2 is excluded by !**/*.woff2
  • plugin/plugins/study_companion/static/fonts/KaTeX_Caligraphic-Bold.ttf is excluded by !**/*.ttf
  • plugin/plugins/study_companion/static/fonts/KaTeX_Caligraphic-Bold.woff is excluded by !**/*.woff
  • plugin/plugins/study_companion/static/fonts/KaTeX_Caligraphic-Bold.woff2 is excluded by !**/*.woff2
  • plugin/plugins/study_companion/static/fonts/KaTeX_Caligraphic-Regular.ttf is excluded by !**/*.ttf
  • plugin/plugins/study_companion/static/fonts/KaTeX_Caligraphic-Regular.woff is excluded by !**/*.woff
  • plugin/plugins/study_companion/static/fonts/KaTeX_Caligraphic-Regular.woff2 is excluded by !**/*.woff2
  • plugin/plugins/study_companion/static/fonts/KaTeX_Fraktur-Bold.ttf is excluded by !**/*.ttf
  • plugin/plugins/study_companion/static/fonts/KaTeX_Fraktur-Bold.woff is excluded by !**/*.woff
  • plugin/plugins/study_companion/static/fonts/KaTeX_Fraktur-Bold.woff2 is excluded by !**/*.woff2
  • plugin/plugins/study_companion/static/fonts/KaTeX_Fraktur-Regular.ttf is excluded by !**/*.ttf
  • plugin/plugins/study_companion/static/fonts/KaTeX_Fraktur-Regular.woff is excluded by !**/*.woff
  • plugin/plugins/study_companion/static/fonts/KaTeX_Fraktur-Regular.woff2 is excluded by !**/*.woff2
  • plugin/plugins/study_companion/static/fonts/KaTeX_Main-Bold.ttf is excluded by !**/*.ttf
  • plugin/plugins/study_companion/static/fonts/KaTeX_Main-Bold.woff is excluded by !**/*.woff
  • plugin/plugins/study_companion/static/fonts/KaTeX_Main-Bold.woff2 is excluded by !**/*.woff2
  • plugin/plugins/study_companion/static/fonts/KaTeX_Main-BoldItalic.ttf is excluded by !**/*.ttf
  • plugin/plugins/study_companion/static/fonts/KaTeX_Main-BoldItalic.woff is excluded by !**/*.woff
  • plugin/plugins/study_companion/static/fonts/KaTeX_Main-BoldItalic.woff2 is excluded by !**/*.woff2
  • plugin/plugins/study_companion/static/fonts/KaTeX_Main-Italic.ttf is excluded by !**/*.ttf
  • plugin/plugins/study_companion/static/fonts/KaTeX_Main-Italic.woff is excluded by !**/*.woff
  • plugin/plugins/study_companion/static/fonts/KaTeX_Main-Italic.woff2 is excluded by !**/*.woff2
  • plugin/plugins/study_companion/static/fonts/KaTeX_Main-Regular.ttf is excluded by !**/*.ttf
  • plugin/plugins/study_companion/static/fonts/KaTeX_Main-Regular.woff is excluded by !**/*.woff
  • plugin/plugins/study_companion/static/fonts/KaTeX_Main-Regular.woff2 is excluded by !**/*.woff2
  • plugin/plugins/study_companion/static/fonts/KaTeX_Math-BoldItalic.ttf is excluded by !**/*.ttf
  • plugin/plugins/study_companion/static/fonts/KaTeX_Math-BoldItalic.woff is excluded by !**/*.woff
  • plugin/plugins/study_companion/static/fonts/KaTeX_Math-BoldItalic.woff2 is excluded by !**/*.woff2
  • plugin/plugins/study_companion/static/fonts/KaTeX_Math-Italic.ttf is excluded by !**/*.ttf
  • plugin/plugins/study_companion/static/fonts/KaTeX_Math-Italic.woff is excluded by !**/*.woff
  • plugin/plugins/study_companion/static/fonts/KaTeX_Math-Italic.woff2 is excluded by !**/*.woff2
  • plugin/plugins/study_companion/static/fonts/KaTeX_SansSerif-Bold.ttf is excluded by !**/*.ttf
  • plugin/plugins/study_companion/static/fonts/KaTeX_SansSerif-Bold.woff is excluded by !**/*.woff
  • plugin/plugins/study_companion/static/fonts/KaTeX_SansSerif-Bold.woff2 is excluded by !**/*.woff2
  • plugin/plugins/study_companion/static/fonts/KaTeX_SansSerif-Italic.ttf is excluded by !**/*.ttf
  • plugin/plugins/study_companion/static/fonts/KaTeX_SansSerif-Italic.woff is excluded by !**/*.woff
  • plugin/plugins/study_companion/static/fonts/KaTeX_SansSerif-Italic.woff2 is excluded by !**/*.woff2
  • plugin/plugins/study_companion/static/fonts/KaTeX_SansSerif-Regular.ttf is excluded by !**/*.ttf
  • plugin/plugins/study_companion/static/fonts/KaTeX_SansSerif-Regular.woff is excluded by !**/*.woff
  • plugin/plugins/study_companion/static/fonts/KaTeX_SansSerif-Regular.woff2 is excluded by !**/*.woff2
  • plugin/plugins/study_companion/static/fonts/KaTeX_Script-Regular.ttf is excluded by !**/*.ttf
  • plugin/plugins/study_companion/static/fonts/KaTeX_Script-Regular.woff is excluded by !**/*.woff
  • plugin/plugins/study_companion/static/fonts/KaTeX_Script-Regular.woff2 is excluded by !**/*.woff2
  • plugin/plugins/study_companion/static/fonts/KaTeX_Size1-Regular.ttf is excluded by !**/*.ttf
  • plugin/plugins/study_companion/static/fonts/KaTeX_Size1-Regular.woff is excluded by !**/*.woff
  • plugin/plugins/study_companion/static/fonts/KaTeX_Size1-Regular.woff2 is excluded by !**/*.woff2
  • plugin/plugins/study_companion/static/fonts/KaTeX_Size2-Regular.ttf is excluded by !**/*.ttf
  • plugin/plugins/study_companion/static/fonts/KaTeX_Size2-Regular.woff is excluded by !**/*.woff
  • plugin/plugins/study_companion/static/fonts/KaTeX_Size2-Regular.woff2 is excluded by !**/*.woff2
  • plugin/plugins/study_companion/static/fonts/KaTeX_Size3-Regular.ttf is excluded by !**/*.ttf
  • plugin/plugins/study_companion/static/fonts/KaTeX_Size3-Regular.woff is excluded by !**/*.woff
  • plugin/plugins/study_companion/static/fonts/KaTeX_Size3-Regular.woff2 is excluded by !**/*.woff2
  • plugin/plugins/study_companion/static/fonts/KaTeX_Size4-Regular.ttf is excluded by !**/*.ttf
  • plugin/plugins/study_companion/static/fonts/KaTeX_Size4-Regular.woff is excluded by !**/*.woff
  • plugin/plugins/study_companion/static/fonts/KaTeX_Size4-Regular.woff2 is excluded by !**/*.woff2
  • plugin/plugins/study_companion/static/fonts/KaTeX_Typewriter-Regular.ttf is excluded by !**/*.ttf
  • plugin/plugins/study_companion/static/fonts/KaTeX_Typewriter-Regular.woff is excluded by !**/*.woff
  • plugin/plugins/study_companion/static/fonts/KaTeX_Typewriter-Regular.woff2 is excluded by !**/*.woff2
  • plugin/plugins/study_companion/static/katex.min.js is excluded by !**/*.min.js
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (61)
  • main_logic/activity/tracker.py
  • plugin/plugins/deskpet/__init__.py
  • plugin/plugins/deskpet/plugin.toml
  • plugin/plugins/study_companion/__init__.py
  • plugin/plugins/study_companion/awareness_buffer.py
  • plugin/plugins/study_companion/awareness_runner.py
  • plugin/plugins/study_companion/config_loader.py
  • plugin/plugins/study_companion/constants.py
  • plugin/plugins/study_companion/doc_exporter.py
  • plugin/plugins/study_companion/entry_export_support.py
  • plugin/plugins/study_companion/entry_knowledge_entries.py
  • plugin/plugins/study_companion/entry_neko_commands.py
  • plugin/plugins/study_companion/entry_notebook.py
  • plugin/plugins/study_companion/i18n/en.json
  • plugin/plugins/study_companion/i18n/es.json
  • plugin/plugins/study_companion/i18n/ja.json
  • plugin/plugins/study_companion/i18n/ko.json
  • plugin/plugins/study_companion/i18n/pt.json
  • plugin/plugins/study_companion/i18n/ru.json
  • plugin/plugins/study_companion/i18n/zh-CN.json
  • plugin/plugins/study_companion/i18n/zh-TW.json
  • plugin/plugins/study_companion/models.py
  • plugin/plugins/study_companion/onboarding.md
  • plugin/plugins/study_companion/plugin.toml
  • plugin/plugins/study_companion/screen_classifier.py
  • plugin/plugins/study_companion/service.py
  • plugin/plugins/study_companion/static/index.html
  • plugin/plugins/study_companion/static/katex-render.js
  • plugin/plugins/study_companion/static/katex.min.css
  • plugin/plugins/study_companion/static/main.js
  • plugin/plugins/study_companion/static/math-parser.js
  • plugin/plugins/study_companion/static/style.css
  • plugin/plugins/study_companion/store_notebook.py
  • plugin/plugins/study_companion/store_schema.py
  • plugin/plugins/study_companion/study_ocr_pipeline.py
  • plugin/plugins/study_companion/supervision.py
  • plugin/plugins/study_companion/surfaces/memory_shared.ts
  • plugin/plugins/study_companion/surfaces/note_card.tsx
  • plugin/plugins/study_companion/surfaces/note_editor.tsx
  • plugin/plugins/study_companion/surfaces/note_search.tsx
  • plugin/plugins/study_companion/surfaces/notebook_panel.tsx
  • plugin/plugins/study_companion/surfaces/study_panel.tsx
  • plugin/plugins/study_companion/tutor_llm_agent.py
  • plugin/plugins/study_companion/tutor_llm_agent_notebook.py
  • plugin/plugins/study_companion/voice_contracts.py
  • plugin/plugins/study_companion/voice_filter.py
  • plugin/tests/unit/plugins/test_awareness_buffer.py
  • plugin/tests/unit/plugins/test_study_companion.py
  • plugin/tests/unit/plugins/test_study_companion_code_health.py
  • plugin/tests/unit/plugins/test_study_companion_neko_commands.py
  • plugin/tests/unit/plugins/test_study_companion_phase9_ux.py
  • plugin/tests/unit/plugins/test_study_companion_study_ocr_pipeline.py
  • plugin/tests/unit/plugins/test_study_companion_voice_bridge.py
  • plugin/tests/unit/plugins/test_study_companion_voice_contracts.py
  • plugin/tests/unit/plugins/test_study_companion_voice_filter.py
  • plugin/tests/unit/plugins/test_study_companion_voice_scenarios.py
  • plugin/tests/unit/plugins/test_study_notebook.py
  • plugin/tests/unit/plugins/test_supervision.py
  • pyproject.toml
  • requirements.txt
  • tests/test_activity_tracker_followup.py
💤 Files with no reviewable changes (1)
  • requirements.txt

Comment thread plugin/plugins/deskpet/__init__.py Outdated
Comment thread plugin/plugins/study_companion/entry_notebook.py
Comment thread plugin/plugins/study_companion/i18n/ja.json Outdated
Comment thread plugin/plugins/study_companion/static/katex-render.js
Comment thread plugin/plugins/study_companion/study_ocr_pipeline.py Outdated
Comment thread plugin/plugins/study_companion/study_ocr_pipeline.py
Comment thread plugin/plugins/study_companion/study_ocr_pipeline.py Outdated
Comment thread plugin/plugins/study_companion/surfaces/note_editor.tsx
Comment thread plugin/plugins/study_companion/surfaces/notebook_panel.tsx Outdated
Comment thread plugin/plugins/study_companion/tutor_llm_agent.py
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 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".

Comment on lines 399 to +403
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()
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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 👍 / 👎.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

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

📥 Commits

Reviewing files that changed from the base of the PR and between 3200e94 and d1ffe23.

📒 Files selected for processing (21)
  • main_logic/activity/tracker.py
  • plugin/plugins/deskpet/__init__.py
  • plugin/plugins/study_companion/constants.py
  • plugin/plugins/study_companion/entry_notebook.py
  • plugin/plugins/study_companion/i18n/ja.json
  • plugin/plugins/study_companion/screen_classifier.py
  • plugin/plugins/study_companion/static/katex-render.js
  • plugin/plugins/study_companion/static/math-parser.js
  • plugin/plugins/study_companion/store_maintenance.py
  • plugin/plugins/study_companion/study_ocr_pipeline.py
  • plugin/plugins/study_companion/surfaces/note_card.tsx
  • plugin/plugins/study_companion/surfaces/note_editor.tsx
  • plugin/plugins/study_companion/surfaces/notebook_panel.tsx
  • plugin/plugins/study_companion/tutor_llm_agent.py
  • plugin/tests/unit/plugins/test_awareness_buffer.py
  • plugin/tests/unit/plugins/test_study_companion.py
  • plugin/tests/unit/plugins/test_study_companion_code_health.py
  • plugin/tests/unit/plugins/test_study_companion_study_ocr_pipeline.py
  • plugin/tests/unit/plugins/test_study_habit_store.py
  • plugin/tests/unit/plugins/test_study_notebook.py
  • tests/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

Comment thread plugin/tests/unit/plugins/test_study_habit_store.py Outdated
@MomiJiSan
Copy link
Copy Markdown
Contributor Author

这个 PR 主要把“伴学插件”升级成一个更完整的学习陪伴系统:
不只是 OCR 看屏幕、回答问题,而是能结合 Neko 主程序的系统活动信号,判断用户当前是在学习、分心、离开、还是处于隐私场景。

核心改动有几块:

伴学插件接入 OS 活动感知:前台窗口、应用分类、系统空闲、隐私状态。
感知逻辑收敛到插件内部,由 awareness_runner.py 统一跑感知循环。
移除跨进程 activity snapshot pipe,改为插件内本地复用 activity tracker。
增强监督提醒:分心提醒、空闲离开提醒、隐私场景跳过截图/OCR。
增加学习笔记能力:笔记本、笔记搜索、AI 扩写/总结、前端笔记面板。
增强语音交互:名字过滤、取消/打断命令、语音上下文注入。
修复一批 silent failure:OS 读取失败、FTS 搜索失败、logger 异常、worker 崩溃等不再静默。
为什么要做

原来的伴学更多依赖 OCR 和聊天上下文判断用户状态,问题比较明显:

OCR 不稳定,成本也高。
它不知道用户是不是已经离开电脑。
它不容易区分“学习用浏览器”和“分心刷网页”。
隐私窗口或敏感应用场景下,如果还继续截图/OCR,风险比较大。
感知逻辑如果散在插件入口、OCR、监督器和跨进程管线里,后续维护会越来越难。
所以这个 PR 的目标是:
让 Neko 主程序继续提供系统级活动信号,伴学插件只负责学习场景解释、监督策略和学习内容沉淀。

对程序做了什么修改

主程序侧:

扩展了 UserActivityTracker 的快照能力,让插件能复用已有 OS 活动信号。
支持按需关闭 enrichment / followup tick,避免伴学读取活动状态时触发额外上下文推进。
主程序继续作为系统活动观测来源,插件不重复造一套窗口/空闲检测逻辑。
伴学插件侧:

新增 awareness_runner.py 承载 OS 感知循环。
新增 config_loader.py 承载配置构建,降低 models.py 复杂度。
调整 study_ocr_pipeline.py、supervision.py、awareness_buffer.py,让 OCR、OS 信号、监督逻辑协同。
增加 notebook / voice 相关 store、entry、UI surface 和测试。
移除不再需要的图片 hash 依赖,降低依赖复杂度。
影响了什么

主要影响范围在:

main_logic/activity/tracker.py
plugin/plugins/study_companion/*
伴学插件前端 UI、笔记系统、语音交互、监督提醒
伴学相关单测和依赖配置
对主程序核心聊天、角色、普通插件运行链路影响较小。主程序侧主要是 activity tracker 的能力增强,不是重写核心架构。

侵入度大不大

整体看:功能范围大,但架构侵入度中等偏低。

原因是:

改动文件很多,功能面很大,所以 PR 体量不小。
但大部分改动集中在 study_companion 插件内部。
主程序侧只扩展 activity tracker 的快照能力,没有把伴学逻辑塞进主程序。
感知逻辑已经从插件入口拆到 runner,后续维护边界更清楚。
隐私策略是收敛的:private 状态下跳过截图/OCR。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant