-
Notifications
You must be signed in to change notification settings - Fork 99
fix: 修复文件存储配置的最大 Key 长度没有生效的问题 #345
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -10,6 +10,7 @@ | |
| from __future__ import annotations | ||
|
|
||
| import copy | ||
| import hashlib | ||
| import shutil | ||
| import uuid | ||
| from pathlib import Path | ||
|
|
@@ -191,6 +192,11 @@ async def update_session(self, session: Session) -> None: | |
| # ------------------------------------------------------------------ | ||
|
|
||
| def _get_session_path(self, save_key: str) -> Path: | ||
| digest = hashlib.sha256(save_key.encode("utf-8")).hexdigest() | ||
| return self.sessions_dir / f"{digest}.jsonl" | ||
|
|
||
| def _get_unhashed_session_path(self, save_key: str) -> Path: | ||
| """Return the session path used before filenames were hashed.""" | ||
| safe_key = safe_filename(save_key.replace(":", "_")) | ||
| return self.sessions_dir / f"{safe_key}.jsonl" | ||
|
|
||
|
|
@@ -199,14 +205,19 @@ def _get_legacy_session_path(self, save_key: str) -> Path: | |
| return self.legacy_sessions_dir / f"{safe_key}.jsonl" | ||
|
|
||
| def _maybe_migrate(self, save_key: str, target: Path) -> None: | ||
| """Move a legacy session file to *target* if one exists and target is absent.""" | ||
| """Move an older session file to *target* when one exists.""" | ||
| if target.exists(): | ||
| return | ||
| legacy = self._get_legacy_session_path(save_key) | ||
| if not legacy.exists(): | ||
| candidates = ( | ||
| self._get_unhashed_session_path(save_key), | ||
| self._get_legacy_session_path(save_key), | ||
| ) | ||
| for source in candidates: | ||
| if not source.exists(): | ||
| continue | ||
| try: | ||
| shutil.move(str(source), str(target)) | ||
| logger.info("Migrated session %s from %s", save_key, source) | ||
| except Exception as exc: # pylint: disable=broad-except | ||
| logger.error("Failed to migrate session %s from %s: %s", save_key, source, exc) | ||
| return | ||
|
Comment on lines
+215
to
223
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 问题: 触发条件: 磁盘同时存在 实际影响: legacy 目录中的历史会话数据被无限期搁置(下次调用会重试第一个候选,仍失败则始终不达 legacy),升级后这些会话恢复不到;无数据丢失但迁移停止,与注释宣称的“依次迁移两个来源”不符。 修正方向: 将循环体内的 |
||
| try: | ||
| shutil.move(str(legacy), str(target)) | ||
| logger.info("Migrated session %s from legacy path", save_key) | ||
| except Exception as exc: # pylint: disable=broad-except | ||
| logger.error("Failed to migrate session %s: %s", save_key, exc) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -27,7 +27,6 @@ | |
| from aiofiles import ospath as aio_ospath | ||
| from nanobot.utils.helpers import safe_filename | ||
| from trpc_agent_sdk.storage import BaseStorage | ||
| from trpc_agent_sdk.storage import DEFAULT_MAX_KEY_LENGTH | ||
|
|
||
| from ..config import FileStorageConfig | ||
| from ._constants import HISTORY_FILENAME | ||
|
|
@@ -113,6 +112,7 @@ async def add(self, db: FileSession, data: FileData | dict[str, Any]) -> None: | |
|
|
||
| @override | ||
| async def delete(self, db: FileSession, key: str, conditions: Optional[FileCondition] = None) -> None: | ||
| self._validate_key(key) | ||
| file_path = await self._resolve_key_path(db.base_dir, key) | ||
| if not await aio_ospath.exists(file_path): | ||
| return | ||
|
|
@@ -140,6 +140,7 @@ async def query(self, | |
|
|
||
| @override | ||
| async def get(self, db: FileSession, key: str) -> Any: | ||
| self._validate_key(key) | ||
|
Comment on lines
142
to
+143
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 问题: 触发条件: 实际影响: 升级后首次 修正方向: 对 |
||
| file_path = await self._resolve_key_path(db.base_dir, key) | ||
| if not await aio_ospath.exists(file_path): | ||
| return None | ||
|
|
@@ -177,12 +178,11 @@ def _key_to_path(base_dir: Path, key: str) -> Path: | |
| def _path_to_key(path: Path) -> str: | ||
| return unquote(path.stem) | ||
|
|
||
| @staticmethod | ||
| def _validate_key(key: str) -> None: | ||
| def _validate_key(self, key: str) -> None: | ||
| if not key: | ||
| raise ValueError("AioFileStorage key cannot be empty") | ||
| if len(key) > DEFAULT_MAX_KEY_LENGTH: | ||
| raise ValueError(f"AioFileStorage key too long: {len(key)} > {DEFAULT_MAX_KEY_LENGTH}") | ||
| if len(key) > self._max_key_length: | ||
| raise ValueError(f"AioFileStorage key too long: {len(key)} > {self._max_key_length}") | ||
| if "/" in key or "\\" in key: | ||
| # Key is logical identifier, not filesystem path. | ||
| raise ValueError("AioFileStorage key must not contain path separators") | ||
|
Comment on lines
+181
to
188
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 问题: 触发条件: 存储类型为 实际影响: 写入持久化永久失败且每次重试必崩; 修正方向: 校验应以 _key_to_path 的最终字节数为准:在 |
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
问题: 新测试
test_add_honors_configured_max_key_length仅用 150 字符 key 验证配置放宽路径,恰好避开 251-255 的崩溃边界,未覆盖“配置上限与文件系统 255 字节文件名限制冲突”的核心场景;test_get_validates_key/test_delete_validates_key只测了超长抛错,未测session:前缀 key 与长 workspace 路径下get校验的回归;迁移测试未覆盖 unhashed 与 legacy 双候选并存的优先级及 move 失败后回退到第二候选。触发条件: 无;为测试缺口。
实际影响: 上述 SEVERE 与 MODERATE 缺陷在测试全绿的情况下合入上线,回归无法被 CI 拦截。
修正方向: 补充 251-255 字符 ASCII key、CJK key、
max_key_length极值(1/200)的写-读往返用例;为_maybe_migrate增加双候选并存与“第一候选失败回退第二候选”的用例。