Dev#1756
Conversation
- 为 memory_profile_admin 增加画像证据查询与纠错动作 - 暴露 WebUI 画像证据接口并接入人物画像页 - 将用户可见 override 文案统一为画像覆写
增加人物画像证据纠错功能,并提高检索表现
- 将 paragraph ngram 索引改为随段落写入、删除、复活路径增量维护 - sparse 检索加载与 fallback 不再触发全量 ngram 回填,索引未就绪时跳过 ngram fallback - 补充 paragraph ngram 增量维护测试,覆盖软删、物理删除、原子删除与读路径回归
perf:提高A_memorix的导入表现,优化后台任务
fix: 提升本地存储写入可靠性
|
@codex review |
|
To use Codex here, create a Codex account and connect to github. |
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (2)
Walkthrough该 PR发布 1.0.0-rc.2:引入 Future Retro 仪表盘样式与存储,数值输入 DraftNumberInput,动态发言频率时间轴与“talk-time”小组件,回复器格式化输出与媒体工具显隐,画像证据查询/纠错前后端闭环,ngram 增量维护与导入并发/超时重构,插件非 Git 更新与评分协议扩展。 Changes1.0.0-rc.2 全栈更新
Sequence Diagram(s)(无) Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Possibly related PRs
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
|
There was a problem hiding this comment.
Actionable comments posted: 11
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/A_memorix/core/utils/web_import_manager.py (1)
2497-2510:⚠️ Potential issue | 🟠 Major | ⚡ Quick win预检超时后子进程没有被回收。
wait_for()超时或这里的异常分支会直接返回,但probe还在后台运行。导入重试几次后会留下孤儿预检进程,持续占用资源。🛠️ 建议在失败路径里显式终止 `probe`
async def _preflight_convert_runtime(self) -> Tuple[bool, str]: """使用当前服务解释器做 convert 依赖预检,避免子进程报错信息不透明。""" probe_code = ( "import importlib\n" "mods=['networkx','scipy','pyarrow']\n" "failed=[]\n" "for m in mods:\n" " try:\n" " importlib.import_module(m)\n" " except Exception as e:\n" " failed.append(f'{m}:{e.__class__.__name__}:{e}')\n" "print('OK' if not failed else ';'.join(failed))\n" ) + probe: Optional[asyncio.subprocess.Process] = None try: - probe = await asyncio.create_subprocess_exec( + probe = await asyncio.create_subprocess_exec( sys.executable, "-c", probe_code, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, ) stdout, stderr = await asyncio.wait_for( probe.communicate(), timeout=self._timeout_config()["convert_preflight_seconds"], ) + except asyncio.TimeoutError: + if probe is not None: + await self._terminate_process(probe) + return False, "依赖预检超时" except Exception as e: + if probe is not None: + await self._terminate_process(probe) return False, f"依赖预检执行失败: {e}"As per coding guidelines, 请重点关注以下方面:异步代码的正确性(async/await 使用是否合理)以及异常处理是否覆盖了边界情况。
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/A_memorix/core/utils/web_import_manager.py` around lines 2497 - 2510, The preflight subprocess `probe` created via `asyncio.create_subprocess_exec` may remain running on asyncio.wait_for timeout or other exceptions; modify the try/except to ensure the child is explicitly terminated: if an exception or timeout occurs (around the `probe.communicate()` call using `self._timeout_config()["convert_preflight_seconds"]`), call `probe.kill()` (or `probe.terminate()` as appropriate) and `await probe.wait()` inside a safe try/except/finally so the subprocess is always reaped before returning the failure tuple; ensure you only call kill/wait when `probe` is defined and not already finished.
🧹 Nitpick comments (6)
dashboard/src/routes/index.tsx (1)
796-857: ⚡ Quick win将新增状态文案改为 i18n key,避免多语言回退。
Line 808 的“读取中”和 Line 859 的“未知”是硬编码中文;同分支其余状态已走
t(...)。建议统一改为翻译键,避免非 zh-CN 语言下出现混杂文案。🤖 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 `@dashboard/src/routes/index.tsx` around lines 796 - 857, Replace the two hardcoded Chinese labels in the status Badge JSX with i18n keys like the other branches: change the "读取中" string in the loading branch and the "未知" string in the unknown branch to use the t(...) translator (e.g. t('home.botStatus.loading') and t('home.botStatus.unknown')) so they match the pattern used in the running/stopped branches; update any surrounding test/usage that expects the literal text if needed.src/webui/routers/plugin/stats_proxy.py (1)
1-9: ⚡ Quick win请按仓库规范调整 import 顺序。
当前
import httpx插在from ... import ...组中间,建议先放完所有from ... import ...,再放import ...。As per coding guidelines
**/*.py: “Import from standard libraries and third-party libraries should follow this order: (1)from ... import ...syntax imports first, (2) directimport ...syntax imports second.”🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/webui/routers/plugin/stats_proxy.py` around lines 1 - 9, Reorder the imports so all "from ... import ..." statements come before direct "import ..." statements: keep "from os import getenv", "from typing import Any, Dict", "from urllib.parse import quote", "from fastapi import APIRouter, HTTPException", "from fastapi.responses import JSONResponse", and "from pydantic import BaseModel, Field, model_validator" together first, and move the "import httpx" line after those; no other changes to names or ordering within each group are required.src/webui/routers/plugin/management.py (1)
1-8: ⚡ Quick win按项目规范重排 imports
当前
from fastapi import ...(第三方)夹在标准库import json/shutil之前,和仓库约定不一致。建议按“from ... import ...先、import ...后;标准库/第三方在前,本地模块在后”统一重排。建议修改
from datetime import datetime from pathlib import Path from typing import Any, Dict, List, Optional -from fastapi import APIRouter, Cookie, HTTPException import json import shutil + import tomlkit +from fastapi import APIRouter, Cookie, HTTPException from src.common.logger import get_logger from src.webui.services.git_mirror_service import get_git_mirror_serviceAs per coding guidelines
**/*.py: “Import from standard libraries and third-party libraries should follow this order: (1) from ... import ... syntax imports first, (2) direct import ... syntax imports second... Import external libraries before local module imports.”🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/webui/routers/plugin/management.py` around lines 1 - 8, Reorder the top-level imports to follow project guidelines: place all "from ... import ..." statements first (keep "from datetime import datetime", "from pathlib import Path", "from typing import Any, Dict, List, Optional" and "from fastapi import APIRouter, Cookie, HTTPException" together), then place direct "import ..." statements (e.g., "import json", "import shutil", "import tomlkit") after them; ensure standard library and third-party groups remain ordered before any local imports and preserve existing import names (use the exact symbols APIRouter, Cookie, HTTPException, json, shutil, tomlkit to locate lines).src/manager/local_store_manager.py (1)
11-22: ⚡ Quick win把
LocalStoreValue收敛成递归 JSON 类型。现在的
str | list | dict | int | float | bool既漏掉了合法的null,也把list/dict的嵌套值类型全抹掉了;load_local_store()读回正常 JSON 时,注解和真实数据会长期不一致。💡 建议修改
-LocalStoreValue: TypeAlias = str | list | dict | int | float | bool +JsonScalar: TypeAlias = str | int | float | bool | None +LocalStoreValue: TypeAlias = JsonScalar | list["LocalStoreValue"] | dict[str, "LocalStoreValue"]As per coding guidelines, "For parameterized generics, use type annotations from the
typingmodule to specify the types of generic parameters, such asList[int]for a list of integers orDict[str, Any]for a dictionary with string keys and any-type values."🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/manager/local_store_manager.py` around lines 11 - 22, LocalStoreValue 当前定义遗漏了 null 且未表达嵌套 JSON 结构,导致注解与 load_local_store() 读回的数据不一致;请把 LocalStoreValue 改为递归的 JSON 类型(包含 None/null,并用 typing 的泛型来限定容器元素),例如使用 Union/TypeAlias + List["LocalStoreValue"] 和 Dict[str, "LocalStoreValue"] 的组合,并确保模块开启或使用前向引用以支持递归类型;更新 LocalStoreValue 的声明以及 LocalStoreManager.store 注解以使用该新类型,保证类型检查和运行时结构一致。dashboard/src/index.css (1)
853-856: 💤 Low value考虑缩小滚动条样式的选择器范围。
使用
*通配符选择器会导致样式引擎对所有后代元素进行样式计算。虽然在data-dashboard-style条件下作用范围有限,但如果 DOM 较大,可能影响渲染性能。可以考虑仅针对需要自定义滚动条的特定容器应用样式,而非全局所有元素。
♻️ 可选的优化方案
:root[data-dashboard-style='future-retro'] .custom-scrollbar, -:root[data-dashboard-style='future-retro'] * { +:root[data-dashboard-style='future-retro'] [data-scrollable], +:root[data-dashboard-style='future-retro'] .overflow-auto, +:root[data-dashboard-style='future-retro'] .overflow-y-auto, +:root[data-dashboard-style='future-retro'] .overflow-x-auto { scrollbar-color: var(--retro-rust) transparent; }🤖 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 `@dashboard/src/index.css` around lines 853 - 856, 当前样式使用了全局通配符选择器 (:root[data-dashboard-style='future-retro'] *) 来设置滚动条颜色,这会对所有后代元素触发样式计算并影响性能;请将选择器范围缩小为仅匹配需要自定义滚动条的容器,例如替换为 :root[data-dashboard-style='future-retro'] .custom-scrollbar 或其他具体类名(如 .scrollable / .panel 等),并确保只在需要的组件上添加该类以避免全局影响,保留原有 :root[data-dashboard-style='future-retro'] .custom-scrollbar 规则用于目标容器。src/A_memorix/core/storage/metadata_store.py (1)
7-19: ⚡ Quick win调整导入分组顺序。
这里把标准库
import ...放在了from ... import ...之前,而且from src...也排在了同级相对导入前面,按当前仓库的导入规范很容易直接触发 Ruff/lint 失败。♻️ 建议调整
-import json -import pickle -import re -import sqlite3 -import time -import uuid from datetime import datetime from pathlib import Path from typing import Any, Dict, List, Optional, Sequence, Tuple, Union -from src.common.logger import get_logger +import json +import pickle +import re +import sqlite3 +import time +import uuid + from ..utils.hash import compute_hash, normalize_text from ..utils.time_parser import normalize_time_meta + +from src.common.logger import get_loggerAs per coding guidelines,
**/*.py: Import from standard libraries and third-party libraries should follow this order: (1)from ... import ...syntax imports first, (2) directimport ...syntax imports second; andsrc/**/*.py: import 顺序需遵循项目规范:标准库/第三方库在前,本地模块在后;本地同级模块使用相对导入,跨目录使用以from src开头的绝对导入。🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/A_memorix/core/storage/metadata_store.py` around lines 7 - 19, The imports in metadata_store.py are mis-ordered and will trigger lint errors; reorder them so: (1) standard-library from-style imports first (e.g., from datetime import datetime, from pathlib import Path, from typing import ...), (2) standard-library plain imports next (e.g., import json, import pickle, import re, import sqlite3, import time, import uuid), (3) third-party libs (none here), and finally (4) local project imports — use absolute imports for cross-package modules (keep from src.common.logger import get_logger) and use relative imports for sibling utils (move from ..utils.hash import compute_hash, normalize_text and from ..utils.time_parser import normalize_time_meta to after the stdlib/third-party block); adjust ordering of those specific symbols (get_logger, compute_hash, normalize_text, normalize_time_meta) 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 `@dashboard/src/components/memory/MemoryProfileManager.tsx`:
- Around line 517-520: The row click currently binds selection to the
non-focusable TableRow (keyed by item.person_id) making keyboard users unable to
change profiles; update the UI so selection is triggered by a focusable control
instead: move the onClick/selectProfile(item.person_id) from TableRow into an
explicit button or link inside the row (e.g., a <button> that shows the profile
and toggles selectedPersonId), or if keeping the row interactive, add
role="button", tabIndex={0} and keyboard handlers (onKeyDown handling
Enter/Space to call selectProfile) and ensure the selected styling still uses
selectedPersonId === item.person_id. Also ensure aria-pressed or aria-current is
set appropriately on the control for screen-reader semantics.
In `@dashboard/src/components/plugin-stats.tsx`:
- Around line 125-140: The current logic allows submission whenever userRating >
0 even if neither the rating nor comment changed; update canSubmit to require
either the rating changed (userRating !== savedUserRating) or the comment
changed (commentChanged) and reuse that same canSubmit flag to control the
submit button disabled state. Also simplify the prepare logic: set
ratingToSubmit to userRating only when userRating > 0 AND userRating !==
savedUserRating, and set commentToSubmit only when commentChanged. Apply the
same fixes to the duplicate block referenced (the other occurrence around the
block at lines 333-336).
In `@dashboard/src/components/ui/scroll-area.tsx`:
- Line 29: The Tailwind "important" prefixes used in the class string should be
converted to the v4 suffix form: update the class literal inside the div in the
ScrollArea component (the cn call that currently contains "!block w-full
!min-w-0") to use "block! w-full min-w-0!" so each utility uses the trailing "!"
suffix; keep contentClassName and children usage unchanged.
In `@dashboard/src/lib/plugin-stats.ts`:
- Around line 394-419: The current hasComment check treats "" or whitespace-only
strings as a provided comment, causing empty comments to be sent and bypassing
the "at least one field" rule; update the hasComment logic to only consider
comment present when comment !== undefined AND String(comment).trim() is
non-empty, and when adding to payload include the trimmed comment (e.g.
payload.comment = String(comment).trim()) so empty/whitespace comments are
ignored and not written to payload; adjust any downstream assumptions about
payload.comment being null/undefined accordingly (references: hasComment,
comment, payload).
In `@dashboard/src/lib/prompt-generator-api.ts`:
- Around line 13-14: The code assigns normalizedError =
result.error.toLowerCase() which can throw if result.error is not a string; in
the normalizedError assignment (near the result.error usage) guard and coerce
safely by checking for null/undefined and ensuring result.error is a string (or
convert via a safe string coercion) before calling toLowerCase, and fallback to
an appropriate default string (e.g., empty string or the serialized error) so
subsequent checks (normalizedError.includes(...)) never throw.
In `@dashboard/src/lib/theme/pipeline.ts`:
- Around line 82-87: 当前逻辑在合并 tokenOverrides 之后又把 futureRetroDarkTokens /
futureRetroLightTokens 合并到 mergedTokens,导致用户的 tokenOverrides 被覆盖;请修改合并顺序:当
(config.dashboardStyle ?? DEFAULT_DASHBOARD_STYLE) === 'future-retro' 时,先用
mergeTokens 将对应的 futureRetroDarkTokens 或 futureRetroLightTokens 合并到
mergedTokens,然后再应用 tokenOverrides(即保证 tokenOverrides
最后合并以覆盖风格默认值);查找并修改涉及的标识符:mergedTokens、mergeTokens、tokenOverrides、futureRetroDarkTokens、futureRetroLightTokens、DEFAULT_DASHBOARD_STYLE、config.dashboardStyle
和 isDark。
In `@dashboard/src/routes/settings/AppearanceTab.tsx`:
- Around line 64-82: The new dashboardStyleOptions array contains hard-coded
Chinese strings for label and description (items with value 'modern' and
'future-retro') instead of using the app i18n; replace those literal strings
with translation keys and call the existing t() function where
dashboardStyleOptions is constructed or consumed so labels/descriptions use
t('settings.appearance.style.modern.label') /
t('settings.appearance.style.modern.description') and equivalent keys for
'future-retro' (and mirror this change for the other occurrences you noted),
ensuring the icon fields (Monitor, ScanLine) and value fields remain unchanged.
In `@src/A_memorix/core/runtime/sdk_memory_kernel.py`:
- Around line 6118-6123: The current code fetches a truncated evidence list via
_profile_evidence_admin (evidence_payload with limit=max(50,...)) and then
checks for normalized_hash inside that page, which can falsely reject valid
hashes when a profile has >50 items; instead, directly resolve the provided
evidence by its hash/evidence_type (parse the paragraph/relationship for the
given hash) and then validate that the resolved evidence still belongs to the
target profile (person_id/person_keyword) using the authoritative ownership
check, rather than relying on presence inside the paginated evidence_payload;
apply the same change pattern for the similar logic at the block covering lines
6127-6137.
In `@src/emoji_system/emoji_manager.py`:
- Around line 509-516: The current startup cleanup deletes any record with
record.no_file_flag or _resolve_existing_emoji_path(record.full_path) is None,
but records with no_file_flag are intentionally kept as description-cache
entries used by delete_emoji(..., keep_desc=True) and hash-based file backfill;
stop removing those. Change the deletion condition to only delete when
_resolve_existing_emoji_path(record.full_path) is None AND record.no_file_flag
is False (or, equivalently, do not delete when record.no_file_flag is True),
ensure session.delete(record) and removed_record_count++ only run for genuinely
broken entries, and preserve records with no_file_flag so downstream backfill
and keep_desc logic can work.
In `@src/manager/local_store_manager.py`:
- Around line 60-71: The JSON load failure handling in LocalStoreManager
currently catches json.JSONDecodeError and ValueError but misses
UnicodeDecodeError, so corrupted/invalid UTF-8 files bypass the recovery path;
update the except clause in the load/init block to also catch UnicodeDecodeError
(e.g., change except (json.JSONDecodeError, ValueError) to except
(json.JSONDecodeError, UnicodeDecodeError, ValueError) as exc) and keep the
existing recovery calls to self._backup_broken_store(file_path), set self.store
= {}, and call self._write_store_atomically(file_path, self.store) so
invalid-UTF8 files are backed up and rebuilt.
In `@src/webui/routers/plugin/stats_proxy.py`:
- Around line 31-37: validate_rating_or_comment 在 RatingRequest 中只检查
model_fields_set 导致传入 comment: null 绕过校验并被 rate_plugin 通过
model_dump(exclude_unset=True) 传给下游;修改 validate_rating_or_comment 使其同时验证 comment
值不是 None 且去空后非空(例如检查 self.comment is not None and self.comment.strip() !=
""),并保留对 rating 的现有检查(has_rating 使用 self.rating is not None),抛出原有的 ValueError
消息以保持行为一致。
---
Outside diff comments:
In `@src/A_memorix/core/utils/web_import_manager.py`:
- Around line 2497-2510: The preflight subprocess `probe` created via
`asyncio.create_subprocess_exec` may remain running on asyncio.wait_for timeout
or other exceptions; modify the try/except to ensure the child is explicitly
terminated: if an exception or timeout occurs (around the `probe.communicate()`
call using `self._timeout_config()["convert_preflight_seconds"]`), call
`probe.kill()` (or `probe.terminate()` as appropriate) and `await probe.wait()`
inside a safe try/except/finally so the subprocess is always reaped before
returning the failure tuple; ensure you only call kill/wait when `probe` is
defined and not already finished.
---
Nitpick comments:
In `@dashboard/src/index.css`:
- Around line 853-856: 当前样式使用了全局通配符选择器
(:root[data-dashboard-style='future-retro'] *)
来设置滚动条颜色,这会对所有后代元素触发样式计算并影响性能;请将选择器范围缩小为仅匹配需要自定义滚动条的容器,例如替换为
:root[data-dashboard-style='future-retro'] .custom-scrollbar 或其他具体类名(如
.scrollable / .panel 等),并确保只在需要的组件上添加该类以避免全局影响,保留原有
:root[data-dashboard-style='future-retro'] .custom-scrollbar 规则用于目标容器。
In `@dashboard/src/routes/index.tsx`:
- Around line 796-857: Replace the two hardcoded Chinese labels in the status
Badge JSX with i18n keys like the other branches: change the "读取中" string in the
loading branch and the "未知" string in the unknown branch to use the t(...)
translator (e.g. t('home.botStatus.loading') and t('home.botStatus.unknown')) so
they match the pattern used in the running/stopped branches; update any
surrounding test/usage that expects the literal text if needed.
In `@src/A_memorix/core/storage/metadata_store.py`:
- Around line 7-19: The imports in metadata_store.py are mis-ordered and will
trigger lint errors; reorder them so: (1) standard-library from-style imports
first (e.g., from datetime import datetime, from pathlib import Path, from
typing import ...), (2) standard-library plain imports next (e.g., import json,
import pickle, import re, import sqlite3, import time, import uuid), (3)
third-party libs (none here), and finally (4) local project imports — use
absolute imports for cross-package modules (keep from src.common.logger import
get_logger) and use relative imports for sibling utils (move from ..utils.hash
import compute_hash, normalize_text and from ..utils.time_parser import
normalize_time_meta to after the stdlib/third-party block); adjust ordering of
those specific symbols (get_logger, compute_hash, normalize_text,
normalize_time_meta) accordingly.
In `@src/manager/local_store_manager.py`:
- Around line 11-22: LocalStoreValue 当前定义遗漏了 null 且未表达嵌套 JSON 结构,导致注解与
load_local_store() 读回的数据不一致;请把 LocalStoreValue 改为递归的 JSON 类型(包含 None/null,并用
typing 的泛型来限定容器元素),例如使用 Union/TypeAlias + List["LocalStoreValue"] 和 Dict[str,
"LocalStoreValue"] 的组合,并确保模块开启或使用前向引用以支持递归类型;更新 LocalStoreValue 的声明以及
LocalStoreManager.store 注解以使用该新类型,保证类型检查和运行时结构一致。
In `@src/webui/routers/plugin/management.py`:
- Around line 1-8: Reorder the top-level imports to follow project guidelines:
place all "from ... import ..." statements first (keep "from datetime import
datetime", "from pathlib import Path", "from typing import Any, Dict, List,
Optional" and "from fastapi import APIRouter, Cookie, HTTPException" together),
then place direct "import ..." statements (e.g., "import json", "import shutil",
"import tomlkit") after them; ensure standard library and third-party groups
remain ordered before any local imports and preserve existing import names (use
the exact symbols APIRouter, Cookie, HTTPException, json, shutil, tomlkit to
locate lines).
In `@src/webui/routers/plugin/stats_proxy.py`:
- Around line 1-9: Reorder the imports so all "from ... import ..." statements
come before direct "import ..." statements: keep "from os import getenv", "from
typing import Any, Dict", "from urllib.parse import quote", "from fastapi import
APIRouter, HTTPException", "from fastapi.responses import JSONResponse", and
"from pydantic import BaseModel, Field, model_validator" together first, and
move the "import httpx" line after those; no other changes to names or ordering
within each group are required.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: a29c969d-5995-45ab-8e2f-8a70c2e32ddb
⛔ Files ignored due to path filters (1)
uv.lockis excluded by!**/*.lock
📒 Files selected for processing (99)
.gitignorechangelogs/changelog.mddashboard/index.htmldashboard/package.jsondashboard/src/components/CodeEditor.tsxdashboard/src/components/CodeEditorImpl.tsxdashboard/src/components/ListFieldEditor.tsxdashboard/src/components/dynamic-form/DynamicField.tsxdashboard/src/components/dynamic-form/__tests__/DynamicField.test.tsxdashboard/src/components/layout/Header.tsxdashboard/src/components/layout/Layout.tsxdashboard/src/components/layout/LogoArea.tsxdashboard/src/components/layout/NavItem.tsxdashboard/src/components/layout/Sidebar.tsxdashboard/src/components/memory/MemoryConfigEditor.tsxdashboard/src/components/memory/MemoryProfileManager.tsxdashboard/src/components/plugin-stats.tsxdashboard/src/components/theme-provider.tsxdashboard/src/components/ui/badge.tsxdashboard/src/components/ui/button.tsxdashboard/src/components/ui/card.tsxdashboard/src/components/ui/dialog.tsxdashboard/src/components/ui/draft-number-input.tsxdashboard/src/components/ui/input.tsxdashboard/src/components/ui/progress.tsxdashboard/src/components/ui/scroll-area.tsxdashboard/src/components/ui/select.tsxdashboard/src/components/ui/switch.tsxdashboard/src/components/ui/tabs.tsxdashboard/src/i18n/locales/zh.jsondashboard/src/index.cssdashboard/src/lib/memory-api.tsdashboard/src/lib/plugin-api/install-flow.tsdashboard/src/lib/plugin-stats.tsdashboard/src/lib/prompt-generator-api.tsdashboard/src/lib/theme-context.tsdashboard/src/lib/theme/pipeline.tsdashboard/src/lib/theme/storage.tsdashboard/src/lib/theme/tokens.tsdashboard/src/lib/version.tsdashboard/src/routes/chat/MessageList.tsxdashboard/src/routes/config/adapter.tsxdashboard/src/routes/config/bot/sections/DreamSection.tsxdashboard/src/routes/config/bot/sections/ExpressionSection.tsxdashboard/src/routes/index.tsxdashboard/src/routes/monitor/index.tsxdashboard/src/routes/monitor/maisaka-monitor.tsxdashboard/src/routes/plugin-config.tsxdashboard/src/routes/resource/__tests__/knowledge-base.test.tsxdashboard/src/routes/resource/knowledge-base.tsxdashboard/src/routes/settings/AppearanceTab.tsxplugins/hello_world_plugin/_manifest.jsonprompts/en-US/mid_term_memory_summary.promptprompts/ja-JP/mid_term_memory_summary.promptprompts/zh-CN/mid_term_memory_summary.promptpyproject.tomlpytests/A_memorix_test/test_feedback_correction_chat_flow.pypytests/A_memorix_test/test_knowledge_fetcher.pypytests/A_memorix_test/test_paragraph_ngram_incremental.pypytests/A_memorix_test/test_query_long_term_memory_tool.pypytests/A_memorix_test/test_web_import_manager_payloads.pypytests/common_test/test_local_store_manager.pypytests/config_test/test_config_base.pypytests/config_test/test_llm_request_hot_reload.pypytests/image_sys_test/emoji_manager_test.pypytests/image_sys_test/test_image_data_model.pypytests/logger.pypytests/test_maisaka_chat_loop_visual_mode.pypytests/test_maisaka_history_post_processor.pypytests/test_maisaka_message_adapter.pypytests/test_maisaka_monitor_protocol.pypytests/test_plugin_runtime.pypytests/test_runtime_business_hooks.pypytests/webui/test_emoji_routes.pypytests/webui/test_memory_routes.pypytests/webui/test_plugin_management_routes.pypytests/webui/test_system_routes.pysrc/A_memorix/CONFIG_REFERENCE.mdsrc/A_memorix/QUICK_START.mdsrc/A_memorix/config_schema.jsonsrc/A_memorix/core/retrieval/sparse_bm25.pysrc/A_memorix/core/runtime/sdk_memory_kernel.pysrc/A_memorix/core/storage/metadata_store.pysrc/A_memorix/core/utils/web_import_manager.pysrc/common/data_models/image_data_model.pysrc/common/logger.pysrc/config/config.pysrc/config/official_configs.pysrc/emoji_system/emoji_manager.pysrc/maisaka/chat_loop_service.pysrc/maisaka/mid_term_memory.pysrc/maisaka/runtime.pysrc/manager/local_store_manager.pysrc/platform_io/manager.pysrc/webui/routers/memory.pysrc/webui/routers/plugin/management.pysrc/webui/routers/plugin/stats_proxy.pysrc/webui/services/git_mirror_service.pytests/test_emoji_maintenance.py
💤 Files with no reviewable changes (10)
- pytests/logger.py
- pytests/test_runtime_business_hooks.py
- pytests/config_test/test_llm_request_hot_reload.py
- pytests/test_maisaka_message_adapter.py
- pytests/A_memorix_test/test_knowledge_fetcher.py
- pytests/A_memorix_test/test_query_long_term_memory_tool.py
- pytests/webui/test_system_routes.py
- pytests/webui/test_emoji_routes.py
- pytests/A_memorix_test/test_feedback_correction_chat_flow.py
- pytests/image_sys_test/emoji_manager_test.py
| <TableRow | ||
| key={item.person_id} | ||
| className={cn('cursor-pointer', selectedPersonId === item.person_id && 'bg-muted/60')} | ||
| onClick={() => setSelectedPersonId(item.person_id)} | ||
| onClick={() => selectProfile(item.person_id)} |
There was a problem hiding this comment.
不要把人物选择只绑定到整行 TableRow 上。
这里的交互目标默认不可聚焦,键盘用户无法从画像库/检索结果切换人物。请把选择动作放到真实的 button/link 上,或至少补齐可聚焦语义和 Enter/Space 键处理。
🤖 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 `@dashboard/src/components/memory/MemoryProfileManager.tsx` around lines 517 -
520, The row click currently binds selection to the non-focusable TableRow
(keyed by item.person_id) making keyboard users unable to change profiles;
update the UI so selection is triggered by a focusable control instead: move the
onClick/selectProfile(item.person_id) from TableRow into an explicit button or
link inside the row (e.g., a <button> that shows the profile and toggles
selectedPersonId), or if keeping the row interactive, add role="button",
tabIndex={0} and keyboard handlers (onKeyDown handling Enter/Space to call
selectProfile) and ensure the selected styling still uses selectedPersonId ===
item.person_id. Also ensure aria-pressed or aria-current is set appropriately on
the control for screen-reader semantics.
| const commentChanged = userComment !== savedUserComment | ||
| const canSubmit = userRating > 0 || commentChanged | ||
|
|
||
| if (!canSubmit) { | ||
| toast({ | ||
| title: '请选择评分', | ||
| description: '至少选择 1 颗星', | ||
| title: '请填写评分或评论', | ||
| description: '可以只评分,也可以只写评论', | ||
| variant: 'destructive', | ||
| }) | ||
| return | ||
| } | ||
|
|
||
| const ratingToSubmit = userRating > 0 && (userRating !== savedUserRating || !commentChanged) | ||
| ? userRating | ||
| : undefined | ||
| const commentToSubmit = commentChanged ? userComment : undefined |
There was a problem hiding this comment.
提交条件仍允许“未改动重复提交”
Line 126 的 canSubmit 只要 userRating > 0 就允许提交,导致“评分和评论都未变”时仍可重复提交。建议改为“评分变化或评论变化”才允许提交,并复用同一条件控制按钮禁用。
建议修改
- const commentChanged = userComment !== savedUserComment
- const canSubmit = userRating > 0 || commentChanged
+ const commentChanged = userComment !== savedUserComment
+ const ratingChanged = userRating !== savedUserRating
+ const canSubmit = ratingChanged || commentChanged
@@
- <Button
- onClick={handleSubmitRating}
- disabled={actionLoading !== null || (userRating === 0 && userComment === savedUserComment)}
- >
+ <Button
+ onClick={handleSubmitRating}
+ disabled={actionLoading !== null || !(userRating !== savedUserRating || userComment !== savedUserComment)}
+ >Also applies to: 333-336
🤖 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 `@dashboard/src/components/plugin-stats.tsx` around lines 125 - 140, The
current logic allows submission whenever userRating > 0 even if neither the
rating nor comment changed; update canSubmit to require either the rating
changed (userRating !== savedUserRating) or the comment changed (commentChanged)
and reuse that same canSubmit flag to control the submit button disabled state.
Also simplify the prepare logic: set ratingToSubmit to userRating only when
userRating > 0 AND userRating !== savedUserRating, and set commentToSubmit only
when commentChanged. Apply the same fixes to the duplicate block referenced (the
other occurrence around the block at lines 333-336).
| )} | ||
| > | ||
| <div className={contentClassName}>{children}</div> | ||
| <div className={cn("!block w-full !min-w-0", contentClassName)}>{children}</div> |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
Tailwind CSS v4 important modifier syntax - is !block valid or should it be block!?
💡 Result:
In Tailwind CSS v4, the correct syntax for the important modifier is to place the exclamation mark (!) at the end of the class name (e.g., block!) [1][2]. This is a change from Tailwind CSS v3, where the modifier was applied by prefixing the class with an exclamation mark (e.g.,!block) [1][3]. While the old prefix syntax is still supported in v4 for backward compatibility, it is deprecated, and you should transition to the suffix syntax [1][2]. Example:
@apply directive, such as within CSS files, the same suffix rule applies [2]:.foo { @apply block!; }
Citations:
- 1: https://tailwindcss.com/docs/upgrade-guide
- 2: How to use !important in v4 tailwindlabs/tailwindcss#15803
- 3: https://stackoverflow.com/questions/68527235/add-both-important-selector-strategy-for-tailwind-configuration
将 Tailwind v4 的 important 语法从前缀迁移为后缀(!block/!min-w-0 → block!/min-w-0!)。
Tailwind v4 重要修饰符推荐使用后缀写法;前缀(!block 等)在 v4 里仍可能兼容但属于弃用用法,建议按规范迁移以避免将来不一致/移除风险。
🔧 建议修复
- <div className={cn("!block w-full !min-w-0", contentClassName)}>{children}</div>
+ <div className={cn("block! w-full min-w-0!", contentClassName)}>{children}</div>📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <div className={cn("!block w-full !min-w-0", contentClassName)}>{children}</div> | |
| <div className={cn("block! w-full min-w-0!", contentClassName)}>{children}</div> |
🤖 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 `@dashboard/src/components/ui/scroll-area.tsx` at line 29, The Tailwind
"important" prefixes used in the class string should be converted to the v4
suffix form: update the class literal inside the div in the ScrollArea component
(the cn call that currently contains "!block w-full !min-w-0") to use "block!
w-full min-w-0!" so each utility uses the trailing "!" suffix; keep
contentClassName and children usage unchanged.
| const hasRating = rating !== undefined && rating !== null | ||
| const hasComment = comment !== undefined | ||
|
|
||
| if (!hasRating && !hasComment) { | ||
| return { success: false, error: '评分和评论至少需要填写一项' } | ||
| } | ||
|
|
||
| if (hasRating && (rating < 1 || rating > 5)) { | ||
| return { success: false, error: '评分必须在 1-5 之间' } | ||
| } | ||
|
|
||
| try { | ||
| const finalUserId = userId || getUserId() | ||
| const payload: { | ||
| plugin_id: string | ||
| user_id: string | ||
| rating?: number | ||
| comment?: string | null | ||
| } = { plugin_id: pluginId, user_id: finalUserId } | ||
|
|
||
| if (hasRating) { | ||
| payload.rating = Number(rating) | ||
| } | ||
| if (hasComment) { | ||
| payload.comment = comment | ||
| } |
There was a problem hiding this comment.
不要把空字符串评论当成“已填写”。
这里的 comment !== undefined 会把 "" 和纯空白字符串都当成有效评论,并直接写进 payload。调用方如果用受控输入框传默认空串,纯评分更新也可能顺带清空评论,同时绕过“至少填写一项”的校验。
💡 建议修改
- const hasRating = rating !== undefined && rating !== null
- const hasComment = comment !== undefined
+ const hasRating = rating !== undefined && rating !== null
+ const hasComment =
+ comment !== undefined && (comment === null || (typeof comment === 'string' && comment.trim() !== ''))
@@
- if (hasComment) {
+ if (hasComment) {
payload.comment = comment
}🤖 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 `@dashboard/src/lib/plugin-stats.ts` around lines 394 - 419, The current
hasComment check treats "" or whitespace-only strings as a provided comment,
causing empty comments to be sent and bypassing the "at least one field" rule;
update the hasComment logic to only consider comment present when comment !==
undefined AND String(comment).trim() is non-empty, and when adding to payload
include the trimmed comment (e.g. payload.comment = String(comment).trim()) so
empty/whitespace comments are ignored and not written to payload; adjust any
downstream assumptions about payload.comment being null/undefined accordingly
(references: hasComment, comment, payload).
| const normalizedError = result.error.toLowerCase() | ||
| if (normalizedError.includes('method not allowed') || normalizedError.includes('405')) { |
There was a problem hiding this comment.
避免在错误归一化里触发二次异常
Line 13 对 result.error 直接调用 toLowerCase() 不够安全;当错误不是字符串时会抛异常,导致本次请求直接失败而不是返回可读错误。
建议修复
- const normalizedError = result.error.toLowerCase()
+ const normalizedError = String(result.error ?? "").toLowerCase()📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const normalizedError = result.error.toLowerCase() | |
| if (normalizedError.includes('method not allowed') || normalizedError.includes('405')) { | |
| const normalizedError = String(result.error ?? "").toLowerCase() | |
| if (normalizedError.includes('method not allowed') || normalizedError.includes('405')) { |
🤖 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 `@dashboard/src/lib/prompt-generator-api.ts` around lines 13 - 14, The code
assigns normalizedError = result.error.toLowerCase() which can throw if
result.error is not a string; in the normalizedError assignment (near the
result.error usage) guard and coerce safely by checking for null/undefined and
ensuring result.error is a string (or convert via a safe string coercion) before
calling toLowerCase, and fallback to an appropriate default string (e.g., empty
string or the serialized error) so subsequent checks
(normalizedError.includes(...)) never throw.
| const dashboardStyleOptions: Array<{ | ||
| value: DashboardStyle | ||
| label: string | ||
| description: string | ||
| icon: typeof Monitor | ||
| }> = [ | ||
| { | ||
| value: 'modern', | ||
| label: '原版 Dashboard', | ||
| description: '保留当前的卡片、圆角、背景和自定义主题能力。', | ||
| icon: Monitor, | ||
| }, | ||
| { | ||
| value: 'future-retro', | ||
| label: '未来复古', | ||
| description: '使用一键包外壳同款纸面颗粒、硬朗描边和切角面板。', | ||
| icon: ScanLine, | ||
| }, | ||
| ] |
There was a problem hiding this comment.
新增的界面风格文案还没接入 i18n。
这个文件已经在用 t(),但这几处新增文案仍然是硬编码中文;切到英文/日文时这里会变成混合语言界面。建议和其他设置项一样走翻译 key。
Also applies to: 408-408, 1037-1039, 1104-1148
🤖 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 `@dashboard/src/routes/settings/AppearanceTab.tsx` around lines 64 - 82, The
new dashboardStyleOptions array contains hard-coded Chinese strings for label
and description (items with value 'modern' and 'future-retro') instead of using
the app i18n; replace those literal strings with translation keys and call the
existing t() function where dashboardStyleOptions is constructed or consumed so
labels/descriptions use t('settings.appearance.style.modern.label') /
t('settings.appearance.style.modern.description') and equivalent keys for
'future-retro' (and mirror this change for the other occurrences you noted),
ensuring the icon fields (Monitor, ScanLine) and value fields remain unchanged.
| evidence_payload = await self._profile_evidence_admin( | ||
| person_id=person_id, | ||
| person_keyword=person_keyword, | ||
| limit=max(50, int(limit or 12)), | ||
| force_refresh=False, | ||
| ) |
There was a problem hiding this comment.
不要用截断后的 evidence 列表校验目标 hash。
这里先用 limit=max(50, int(limit or 12)) 拉一页证据,再在这页里找 normalized_hash。当某个人物的画像证据超过 50 条,或者目标不在当前 top-k 内时,合法的 hash 也会被误判成“当前画像证据中未找到目标 hash”,导致显式纠错失败。既然这里已经拿到了 evidence_type 和 hash,更稳妥的做法是按 hash 直接解析段落/关系,再校验它是否仍属于当前人物画像,而不是依赖截断后的列表。
Also applies to: 6127-6137
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/A_memorix/core/runtime/sdk_memory_kernel.py` around lines 6118 - 6123,
The current code fetches a truncated evidence list via _profile_evidence_admin
(evidence_payload with limit=max(50,...)) and then checks for normalized_hash
inside that page, which can falsely reject valid hashes when a profile has >50
items; instead, directly resolve the provided evidence by its hash/evidence_type
(parse the paragraph/relationship for the given hash) and then validate that the
resolved evidence still belongs to the target profile (person_id/person_keyword)
using the authoritative ownership check, rather than relying on presence inside
the paginated evidence_payload; apply the same change pattern for the similar
logic at the block covering lines 6127-6137.
| if record.no_file_flag or _resolve_existing_emoji_path(record.full_path) is None: | ||
| logger.warning( | ||
| f"[数据库] 已注册表情包缺少实际文件,删除破损注册记录: " | ||
| f"id={record.id}, path={record.full_path}" | ||
| ) | ||
| session.delete(record) | ||
| removed_record_count += 1 | ||
| continue |
There was a problem hiding this comment.
不要把 no_file_flag 记录当成破损记录直接删掉。
delete_emoji(..., keep_desc=True) 和按 hash 回填文件都依赖这类记录保留描述缓存。这里一旦在启动加载时删除它们,重启后就会丢掉缓存元数据,后续也无法走回填恢复。
💡 建议修改
- if record.no_file_flag or _resolve_existing_emoji_path(record.full_path) is None:
+ if record.no_file_flag:
+ logger.debug(
+ f"[数据库] 已注册表情包仅缺少本地文件,保留描述缓存记录: "
+ f"id={record.id}, path={record.full_path}"
+ )
+ continue
+ if _resolve_existing_emoji_path(record.full_path) is None:
logger.warning(
f"[数据库] 已注册表情包缺少实际文件,删除破损注册记录: "
f"id={record.id}, path={record.full_path}"
)
session.delete(record)🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/emoji_system/emoji_manager.py` around lines 509 - 516, The current
startup cleanup deletes any record with record.no_file_flag or
_resolve_existing_emoji_path(record.full_path) is None, but records with
no_file_flag are intentionally kept as description-cache entries used by
delete_emoji(..., keep_desc=True) and hash-based file backfill; stop removing
those. Change the deletion condition to only delete when
_resolve_existing_emoji_path(record.full_path) is None AND record.no_file_flag
is False (or, equivalently, do not delete when record.no_file_flag is True),
ensure session.delete(record) and removed_record_count++ only run for genuinely
broken entries, and preserve records with no_file_flag so downstream backfill
and keep_desc logic can work.
| with file_path.open("r", encoding="utf-8") as f: | ||
| loaded_store = json.load(f) | ||
| if not isinstance(loaded_store, dict): | ||
| raise ValueError("本地存储根节点必须是 JSON 对象") | ||
| self.store = loaded_store | ||
| logger.info("全都记起来了!") | ||
| except (json.JSONDecodeError, ValueError) as exc: | ||
| logger.warning("啊咧?记事本被弄脏了,正在重建记事本......") | ||
| logger.debug(f"本地存储文件无法读取: {exc}") | ||
| self._backup_broken_store(file_path) | ||
| self.store = {} | ||
| with open(self.file_path, "w", encoding="utf-8") as f: | ||
| json.dump({}, f, ensure_ascii=False, indent=4) | ||
| self._write_store_atomically(file_path, self.store) |
There was a problem hiding this comment.
损坏恢复分支还漏掉了非法 UTF-8。
这里把坏 JSON 当成可恢复场景处理了,但文件字节损坏时 json.load() 会抛 UnicodeDecodeError,当前不会走备份/重建逻辑,而是直接让初始化失败。
💡 建议修改
- except (json.JSONDecodeError, ValueError) as exc:
+ except (json.JSONDecodeError, UnicodeDecodeError, ValueError) as exc:
logger.warning("啊咧?记事本被弄脏了,正在重建记事本......")
logger.debug(f"本地存储文件无法读取: {exc}")
self._backup_broken_store(file_path)
self.store = {}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/manager/local_store_manager.py` around lines 60 - 71, The JSON load
failure handling in LocalStoreManager currently catches json.JSONDecodeError and
ValueError but misses UnicodeDecodeError, so corrupted/invalid UTF-8 files
bypass the recovery path; update the except clause in the load/init block to
also catch UnicodeDecodeError (e.g., change except (json.JSONDecodeError,
ValueError) to except (json.JSONDecodeError, UnicodeDecodeError, ValueError) as
exc) and keep the existing recovery calls to
self._backup_broken_store(file_path), set self.store = {}, and call
self._write_store_atomically(file_path, self.store) so invalid-UTF8 files are
backed up and rebuilt.
| @model_validator(mode="after") | ||
| def validate_rating_or_comment(self) -> "RatingRequest": | ||
| has_rating = "rating" in self.model_fields_set and self.rating is not None | ||
| has_comment = "comment" in self.model_fields_set | ||
| if not has_rating and not has_comment: | ||
| raise ValueError("rating 和 comment 至少需要提供一个") | ||
| return self |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
Pydantic v2 中,model_fields_set 是否会包含显式传入 null(None) 的字段?model_validator(mode="after") 内如何可靠判断“字段被设置且值非 None/非空字符串”?
💡 Result:
结论: 1) 在 Pydantic v2 中,model_fields_set 会包含“显式传入的字段名(包含显式传入的 None)”,前提是该字段名在实例创建/校验时确实出现在输入里(即被“显式提供”)。文档明确把它定义为“显式提供时的字段名集合”[1][2];并且 pydantic_fields_set/model_fields_set 是“明确在实例化/验证时指定过的字段名”[3]。 2) 在 @model_validator(mode="after") 里要可靠判断“字段被设置且值非 None/非空字符串”,建议使用: - self.model_fields_set 判断“是否显式传入/设置过该字段”; - 再判断 self. 的值是否满足非 None、且如果是 str 还要非空字符串。 说明与可靠写法: - model_fields_set 的语义是:只关心字段名是否在初始化时被显式提供,而不关心你提供的值到底是什么(例如 None 也算“显式提供过”)[1][2]。因此: - 如果输入中有 foo=None(或 JSON 为 null),那么 'foo' 会在 model_fields_set 里; - 如果输入中根本没给 foo,那么 'foo' 不在 model_fields_set。 - @model_validator(mode="after") 在模型完成校验后运行,入参是已经构建好的 instance;并且 after validator 的函数签名/返回 Self/raise 错误的方式在官方文档中有示例说明[4]。 示例代码(以判断字段被显式设置且值非 None/非空字符串为目标): class M(BaseModel): a: str | None = None @model_validator(mode="after") def check_a(self) -> 'M': # 1) 先用 model_fields_set 判断“是否显式设置过 a” if 'a' in self.model_fields_set: # 2) 再判断值非 None,且若是字符串则非空 if self.a is None or (isinstance(self.a, str) and self.a == ''): raise ValueError('a must be non-null and non-empty when explicitly set') return self 为什么这在 after 模式里“可靠”: - 判断“是否显式传入”用 model_fields_set:它在初始化/验证时就记录了显式给出的字段名[1][3]。 - 判断“值非 None/非空字符串”用 after 阶段的 self.:after 阶段字段已经完成类型校验/转换(例如如果你声明 a: str,则传入的值会变成 str;如果声明 a: str|None,则会允许 None),因此你可以直接对最终值做 None/空串判断。after validator 作为 post-initialization hook 的特性由官方文档描述[4]。 注意事项(避免误判): - 如果字段类型不是 str(或你确实允许非 str 类型),就不要简单用 self.a == '';上面示例用 isinstance(self.a, str) 做了保护。 - 如果你希望把“显式传入空字符串”当作等价于“未设置”,那需要在 before 阶段把 '' 转为 None/或从输入中剔除(这属于业务规则,不是 model_fields_set 的职责)。Pydantic 社区讨论中就有使用 before validator 将空字符串转为 None 的做法思路[5]。 如果你把具体字段定义(类型标注)和希望的“空字符串/None 的业务语义”贴出来(例如:只对 str 字段判空串?还是所有 Optional 字段都判非 None?),我可以把上述模板改成更贴合的版本。
Citations:
- 1: https://pydantic.dev/docs/validation/2.10/concepts/models/
- 2: https://pydantic.dev/docs/validation/latest/concepts/models/
- 3: https://pydantic.dev/docs/validation/2.0/api/pydantic/main/
- 4: https://pydantic.dev/docs/validation/latest/concepts/validators/
- 5: Pydantic model field: convert empty string to None pydantic/pydantic#2687
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="src/webui/routers/plugin/stats_proxy.py"
echo "=== Path check ==="
ls -la "$FILE" || exit 1
echo "=== Imports (top 80 lines) ==="
sed -n '1,90p' "$FILE" | nl -ba -w2 -s': '
echo "=== RatingRequest definition + validator (search) ==="
rg -n "class RatingRequest|validate_rating_or_comment|model_validator" "$FILE" || true
# Print around validator lines (roughly 10 lines before/after the snippet range)
echo "=== Validator area (lines 1-140) ==="
sed -n '1,160p' "$FILE" | nl -ba -w2 -s': ' | sed -n '1,160p'Repository: Mai-with-u/MaiBot
Length of output: 261
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="src/webui/routers/plugin/stats_proxy.py"
echo "=== Imports (top 120 lines) ==="
sed -n '1,120p' "$FILE" | nl -ba -w2 -s': '
echo "=== RatingRequest + validator (search + surrounding) ==="
# Show the whole class if not huge; otherwise show a window around validator
LINE=$(rg -n "class RatingRequest" "$FILE" | head -n1 | cut -d: -f1 || true)
if [ -z "${LINE}" ]; then
echo "No class RatingRequest found"
else
START=$((LINE-20))
END=$((LINE+120))
sed -n "${START},${END}p" "$FILE" | nl -ba -w2 -s': '
fi
# Show exact validator function area
rg -n "def validate_rating_or_comment" "$FILE" || true
VAL=$(rg -n "def validate_rating_or_comment" "$FILE" | head -n1 | cut -d: -f1 || true)
if [ -n "${VAL}" ]; then
START=$((VAL-15))
END=$((VAL+30))
sed -n "${START},${END}p" "$FILE" | nl -ba -w2 -s': '
fiRepository: Mai-with-u/MaiBot
Length of output: 154
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="src/webui/routers/plugin/stats_proxy.py"
echo "=== Imports (top 120 lines) ==="
sed -n '1,120p' "$FILE" | nl -ba -w2 -s': '
echo "=== RatingRequest + validator (search + surrounding) ==="
LINE=$(rg -n "class RatingRequest" "$FILE" | head -n1 | cut -d: -f1 || true)
if [ -n "${LINE}" ]; then
START=$((LINE-20))
END=$((LINE+140))
sed -n "${START},${END}p" "$FILE" | nl -ba -w2 -s': '
fi
echo "=== validate_rating_or_comment exact area ==="
VAL=$(rg -n "def validate_rating_or_comment" "$FILE" | head -n1 | cut -d: -f1 || true)
if [ -n "${VAL}" ]; then
START=$((VAL-15))
END=$((VAL+35))
sed -n "${START},${END}p" "$FILE" | nl -ba -w2 -s': '
fiRepository: Mai-with-u/MaiBot
Length of output: 154
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="src/webui/routers/plugin/stats_proxy.py"
echo "=== File head (1-140) ==="
sed -n '1,140p' "$FILE" | awk '{printf "%4d: %s\n", NR, $0}'
echo "=== Find RatingRequest class ==="
python3 - <<'PY'
import re, pathlib
p=pathlib.Path("src/webui/routers/plugin/stats_proxy.py")
txt=p.read_text(encoding="utf-8")
for m in re.finditer(r'^class\s+RatingRequest\b', txt, flags=re.M):
start=txt[:m.start()].count("\n")+1
print("RatingRequest line:", start)
PY
echo "=== Extract around RatingRequest + validator ==="
python3 - <<'PY'
import re, pathlib
p=pathlib.Path("src/webui/routers/plugin/stats_proxy.py")
lines=p.read_text(encoding="utf-8").splitlines()
def show(a,b):
for i in range(a,b+1):
if 1<=i<=len(lines):
print(f"{i:4d}: {lines[i-1]}")
txt="\n".join(lines)
m=re.search(r'^class\s+RatingRequest\b', txt, flags=re.M)
if not m:
print("No RatingRequest found"); raise SystemExit
start=txt[:m.start()].count("\n")+1
# show a wider window
show(max(1,start-20), min(len(lines), start+220))
vm=re.search(r'def\s+validate_rating_or_comment\b', txt, flags=re.M)
if vm:
vline=txt[:vm.start()].count("\n")+1
print("\n=== validate_rating_or_comment window ===")
show(max(1,vline-30), min(len(lines), vline+60))
else:
print("\nNo validate_rating_or_comment found")
PY
echo "=== Quick check: comment field line(s) and type annotation ==="
rg -n "comment\s*:" "$FILE" || trueRepository: Mai-with-u/MaiBot
Length of output: 12958
修复 RatingRequest 校验:comment=null 可能绕过“至少提供一个”
validate_rating_or_comment 里 has_comment 仅判断字段名是否出现在 model_fields_set;当显式传入 comment: null 且未提供 rating 时会被放行,进而在 rate_plugin 的 model_dump(exclude_unset=True) 下把 comment: None 传给下游。建议校验时同时要求 comment 的值非 None 且去空后非空。
建议修改
`@model_validator`(mode="after")
def validate_rating_or_comment(self) -> "RatingRequest":
has_rating = "rating" in self.model_fields_set and self.rating is not None
- has_comment = "comment" in self.model_fields_set
+ has_comment = (
+ "comment" in self.model_fields_set
+ and self.comment is not None
+ and bool(str(self.comment).strip())
+ )
if not has_rating and not has_comment:
raise ValueError("rating 和 comment 至少需要提供一个")
return self🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/webui/routers/plugin/stats_proxy.py` around lines 31 - 37,
validate_rating_or_comment 在 RatingRequest 中只检查 model_fields_set 导致传入 comment:
null 绕过校验并被 rate_plugin 通过 model_dump(exclude_unset=True) 传给下游;修改
validate_rating_or_comment 使其同时验证 comment 值不是 None 且去空后非空(例如检查 self.comment is
not None and self.comment.strip() != ""),并保留对 rating 的现有检查(has_rating 使用
self.rating is not None),抛出原有的 ValueError 消息以保持行为一致。
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 226f422ae0
ℹ️ About Codex in GitHub
Your team has set up Codex to 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 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if not self.metadata_store.is_paragraph_ngram_ready( | ||
| n=self.config.char_ngram_n, | ||
| conn=self._conn, | ||
| ) | ||
| rows = self.metadata_store.ngram_search_paragraphs( | ||
| tokens=uniq_tokens, | ||
| limit=limit, | ||
| max_doc_len=self.config.max_doc_len, | ||
| conn=self._conn, | ||
| ) | ||
| if rows: | ||
| return rows | ||
| ): | ||
| logger.debug("paragraph ngram 索引未就绪,跳过 ngram fallback") |
There was a problem hiding this comment.
Restore an initialization path for ngram fallback
With the default config (enable_ngram_fallback_index=True, enable_like_fallback=False), fresh or upgraded stores have no paragraph_ngram_meta entry, and I checked repo-wide that ensure_paragraph_ngram_backfilled is now only called from tests. In that state this branch just skips the ngram fallback and immediately returns no substring fallback results, so Chinese/char-ngram queries that miss FTS lose the intended fallback until some out-of-band code initializes the index. Please add a startup/admin backfill path before treating “not ready” as a permanent skip.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (2)
src/maisaka/builtin_tool/__init__.py (1)
130-133: ⚡ Quick win这里可以直接访问配置字段,避免
getattr掩盖配置结构问题这段是固定配置对象读取,建议改为直接属性访问,提高可读性并更早暴露配置结构异常。
As per coding guidelines, "Minimize the use of
getattrandsetattrmethods... if the class instance has the attribute, replace it with direct attribute access."🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/maisaka/builtin_tool/__init__.py` around lines 130 - 133, The code uses getattr to read a fixed config structure (chat_config = getattr(global_config, "chat", None) and bool(getattr(chat_config, "enable_replyer_format_output", False))); replace these with direct attribute access to surface configuration issues and improve readability—for example, check if global_config.chat exists and then directly read global_config.chat.enable_replyer_format_output (e.g., if global_config.chat and global_config.chat.enable_replyer_format_output:) and return False when true; update the branch that handles entry.name in {"send_emoji","send_image"} to use these direct attributes instead of getattr.src/chat/replyer/maisaka_generator_base.py (1)
268-269: ⚡ Quick win改为直接访问已确认存在的配置字段,避免
getattr回退。Line 268 和 Line 445 这里可直接使用
global_config.chat.enable_replyer_format_output,getattr(..., False)会掩盖真实配置问题并增加分支噪音。建议修改
- if not getattr(global_config.chat, "enable_replyer_format_output", False): + if not global_config.chat.enable_replyer_format_output: ... - if getattr(global_config.chat, "enable_replyer_format_output", False): + if global_config.chat.enable_replyer_format_output:As per coding guidelines, "Minimize the use of
getattrandsetattrmethods... if the class instance has the attribute, replace it with direct attribute access."Also applies to: 445-446
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/chat/replyer/maisaka_generator_base.py` around lines 268 - 269, Replace the getattr checks on the Chat config with direct attribute access: where the code tests getattr(global_config.chat, "enable_replyer_format_output", False) (e.g., in the conditional inside the function/method around the locale handling near the check at line 268 and the similar checks around lines 445-446), change them to use global_config.chat.enable_replyer_format_output directly so the code fails fast on missing config and avoids silent fallback; update both places and remove the default False fallback.
🤖 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 `@src/maisaka/builtin_tool/reply.py`:
- Around line 192-194: Wrap the call to
tool_ctx.post_process_reply_message_sequences_async (where reply_sequences is
assigned) in a try/except so any exception is caught; on exception use the same
failure pattern as the send phase by calling build_failure_result with the error
and returning it from handle_tool, otherwise continue to build reply_segments
with build_visible_text_from_sequence and combined_reply_text as before—ensure
the try/except specifically surrounds reply_sequences assignment and subsequent
processing to guarantee a structured failure result instead of an uncaught
exception.
---
Nitpick comments:
In `@src/chat/replyer/maisaka_generator_base.py`:
- Around line 268-269: Replace the getattr checks on the Chat config with direct
attribute access: where the code tests getattr(global_config.chat,
"enable_replyer_format_output", False) (e.g., in the conditional inside the
function/method around the locale handling near the check at line 268 and the
similar checks around lines 445-446), change them to use
global_config.chat.enable_replyer_format_output directly so the code fails fast
on missing config and avoids silent fallback; update both places and remove the
default False fallback.
In `@src/maisaka/builtin_tool/__init__.py`:
- Around line 130-133: The code uses getattr to read a fixed config structure
(chat_config = getattr(global_config, "chat", None) and
bool(getattr(chat_config, "enable_replyer_format_output", False))); replace
these with direct attribute access to surface configuration issues and improve
readability—for example, check if global_config.chat exists and then directly
read global_config.chat.enable_replyer_format_output (e.g., if
global_config.chat and global_config.chat.enable_replyer_format_output:) and
return False when true; update the branch that handles entry.name in
{"send_emoji","send_image"} to use these direct attributes instead of getattr.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 8547105c-59a8-4018-a03f-6f8c0b0e7ad9
📒 Files selected for processing (22)
dashboard/src/routes/monitor/index.tsxdashboard/src/routes/monitor/maisaka-monitor.tsxprompts/en-US/emoji_content_analysis.promptprompts/en-US/emoji_selection.promptprompts/en-US/maisaka_replyer.promptprompts/ja-JP/emoji_content_analysis.promptprompts/ja-JP/emoji_selection.promptprompts/ja-JP/maisaka_replyer.promptprompts/zh-CN/emoji_content_analysis.promptprompts/zh-CN/emoji_selection.promptprompts/zh-CN/maisaka_replyer.promptpytests/test_maisaka_builtin_context.pypytests/test_maisaka_builtin_query_person_profile.pypytests/test_maisaka_monitor_protocol.pysrc/chat/replyer/maisaka_generator_base.pysrc/config/config.pysrc/config/official_configs.pysrc/maisaka/builtin_tool/__init__.pysrc/maisaka/builtin_tool/context.pysrc/maisaka/builtin_tool/reply.pysrc/maisaka/builtin_tool/send_emoji.pysrc/webui/routers/system.py
💤 Files with no reviewable changes (4)
- prompts/ja-JP/emoji_content_analysis.prompt
- prompts/zh-CN/emoji_content_analysis.prompt
- prompts/en-US/emoji_content_analysis.prompt
- dashboard/src/routes/monitor/index.tsx
✅ Files skipped from review due to trivial changes (4)
- prompts/zh-CN/emoji_selection.prompt
- prompts/en-US/emoji_selection.prompt
- prompts/ja-JP/emoji_selection.prompt
- prompts/zh-CN/maisaka_replyer.prompt
🚧 Files skipped from review as they are similar to previous changes (2)
- src/config/config.py
- dashboard/src/routes/monitor/maisaka-monitor.tsx
| reply_sequences = await tool_ctx.post_process_reply_message_sequences_async(reply_text) | ||
| reply_segments = [build_visible_text_from_sequence(sequence) for sequence in reply_sequences] | ||
| combined_reply_text = "".join(reply_segments) |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win
reply_sequences 后处理缺少异常兜底,会导致工具调用直接抛出
Line 192 的异步后处理不在任何异常兜底里;一旦这里抛错,handle_tool 不会返回结构化失败结果。建议和发送阶段一样做 try/except 并返回 build_failure_result。
建议修复
- reply_sequences = await tool_ctx.post_process_reply_message_sequences_async(reply_text)
+ try:
+ reply_sequences = await tool_ctx.post_process_reply_message_sequences_async(reply_text)
+ except Exception:
+ logger.exception(
+ f"{tool_ctx.runtime.log_prefix} 回复后处理失败,目标消息编号={target_message_id}"
+ )
+ return tool_ctx.build_failure_result(
+ invocation.tool_name,
+ "回复后处理失败。",
+ metadata=reply_metadata,
+ )📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| reply_sequences = await tool_ctx.post_process_reply_message_sequences_async(reply_text) | |
| reply_segments = [build_visible_text_from_sequence(sequence) for sequence in reply_sequences] | |
| combined_reply_text = "".join(reply_segments) | |
| try: | |
| reply_sequences = await tool_ctx.post_process_reply_message_sequences_async(reply_text) | |
| except Exception: | |
| logger.exception( | |
| f"{tool_ctx.runtime.log_prefix} 回复后处理失败,目标消息编号={target_message_id}" | |
| ) | |
| return tool_ctx.build_failure_result( | |
| invocation.tool_name, | |
| "回复后处理失败。", | |
| metadata=reply_metadata, | |
| ) | |
| reply_segments = [build_visible_text_from_sequence(sequence) for sequence in reply_sequences] | |
| combined_reply_text = "".join(reply_segments) |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/maisaka/builtin_tool/reply.py` around lines 192 - 194, Wrap the call to
tool_ctx.post_process_reply_message_sequences_async (where reply_sequences is
assigned) in a try/except so any exception is caught; on exception use the same
failure pattern as the send phase by calling build_failure_result with the error
and returning it from handle_tool, otherwise continue to build reply_segments
with build_visible_text_from_sequence and combined_reply_text as before—ensure
the try/except specifically surrounds reply_sequences assignment and subsequent
processing to guarantee a structured failure result instead of an uncaught
exception.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/common/utils/utils_config.py`:
- Around line 510-516: The helper _get_rule_value should not silently coerce
invalid or unparsable rule values to 0.0; change it to return None on parse
failure (catch TypeError/ValueError and return None) and update the
rule-selection logic that consumes _get_rule_value (the codepath that
filters/sorts/chooses rules using the returned float) to explicitly skip rules
where _get_rule_value(...) is None so malformed values do not participate in
winner selection or depress frequencies; make the same change for the other
identical helper usages noted around the 533-547 region.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 540dd810-f3d9-42dd-9872-039809789e42
📒 Files selected for processing (8)
dashboard/src/components/dynamic-form/DynamicField.tsxdashboard/src/routes/config/bot/hooks/ChatSectionHook.tsxdashboard/src/routes/config/bot/hooks/ListItemEditorHookFactory.tsxdashboard/src/routes/config/bot/hooks/complexFieldHooks.tsxdashboard/src/types/config-schema.tspytests/common_test/test_chat_config_utils.pysrc/common/utils/utils_config.pysrc/config/official_configs.py
🚧 Files skipped from review as they are similar to previous changes (1)
- src/config/official_configs.py
| def _get_rule_value(rule) -> float: | ||
| value = rule.get("value") if isinstance(rule, dict) else rule.value | ||
| try: | ||
| return float(value) | ||
| except (TypeError, ValueError): | ||
| return 0.0 | ||
|
|
There was a problem hiding this comment.
无效 value 被静默降为 0.0 会错误覆盖匹配结果
当前 _get_rule_value 在解析失败时返回 0.0,会让格式错误的规则仍参与胜出并把频率压到 0,风险较高。建议解析失败返回 None,并在选择阶段跳过该规则。
建议修改
- def _get_rule_value(rule) -> float:
+ def _get_rule_value(rule) -> Optional[float]:
value = rule.get("value") if isinstance(rule, dict) else rule.value
try:
return float(value)
except (TypeError, ValueError):
- return 0.0
+ return None
@@
def _select_talk_rule_value(rules: list[tuple[Any, int]], now_min: int) -> Optional[float]:
@@
for rule, target_priority in rules:
time_priority = ChatConfigUtils._talk_rule_time_priority(ChatConfigUtils._get_rule_time(rule), now_min)
if time_priority is None:
continue
+ rule_value = ChatConfigUtils._get_rule_value(rule)
+ if rule_value is None:
+ continue
priority = (target_priority, time_priority)
if priority <= selected_priority:
continue
selected_priority = priority
- selected_value = ChatConfigUtils._get_rule_value(rule)
+ selected_value = rule_valueAlso applies to: 533-547
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/common/utils/utils_config.py` around lines 510 - 516, The helper
_get_rule_value should not silently coerce invalid or unparsable rule values to
0.0; change it to return None on parse failure (catch TypeError/ValueError and
return None) and update the rule-selection logic that consumes _get_rule_value
(the codepath that filters/sorts/chooses rules using the returned float) to
explicitly skip rules where _get_rule_value(...) is None so malformed values do
not participate in winner selection or depress frequencies; make the same change
for the other identical helper usages noted around the 533-547 region.
zh-CN目标翻译作为常规 GitHub 编辑面;常规翻译以 Crowdin ->l10n_*PR 回流为准,详见docs/i18n.md请填写以下内容
(删除掉中括号内的空格,并替换为小写的x)
main分支 禁止修改,请确认本次提交的分支 不是main分支src/A_memorix,我确认已阅读src/A_memorix/MODIFICATION_POLICY.md,不涉及则无需勾选其他信息
Summary by CodeRabbit
新功能
Bug 修复
其他改进