feat: MySQL 索引长度兼容 + 启动自动补列 + 未发布徽标与查询加载态
后端: - 索引/唯一键改用 mysql_length 前缀索引,适配 MySQL 767 字节限制(error 1071):knowledge_notes、knowledge_entities、knowledge_graph_blacklist、agent_skills、feedback - engine.py 新增 _ensure_orm_columns_sync:启动时对比 ORM 元数据,自动 ALTER TABLE 补齐缺失的可空/带默认值列及其索引 - 新增 scripts/audit_index_key_lengths.py 审计全部索引键长度 - 更新 backend/CLAUDE.md 迁移说明 前端: - 智能体卡片/技能列表:所有者与管理员可见未发布徽标 - 智能体广场:服务端查询进行中时灰显旧结果并悬浮加载提示 - 排行榜入口从 Header 移到欢迎语旁 - threads/hooks.ts:抽取 updateCachedThreadLists 统一处理数组与分页两种缓存形态 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
da677f87d5
commit
57a87d6322
15 files changed
+330
-102
No files matched your search
@@ -0,0 +1,71 @@
|
||||
"""Audit ORM index/unique-key byte sizes against MySQL InnoDB prefix limits.
|
||||
|
||||
MySQL with the legacy 767-byte InnoDB index-prefix limit (5.6, or 5.7 with
|
||||
COMPACT row format) rejects any index whose key exceeds 767 bytes — and
|
||||
utf8mb4 costs 4 bytes per char, so an indexed VARCHAR key may total at most
|
||||
191 chars. Modern servers (DYNAMIC row format) allow 3072 bytes = 768 chars.
|
||||
|
||||
Run: PYTHONPATH=. .venv/Scripts/python scripts/audit_index_key_lengths.py
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
|
||||
from sqlalchemy import String, UniqueConstraint
|
||||
|
||||
import deerflow.persistence.models # noqa: F401 (registers all ORM models)
|
||||
from deerflow.persistence.base import Base
|
||||
|
||||
LIMIT_OLD = 767
|
||||
LIMIT_NEW = 3072
|
||||
BYTES_PER_CHAR = 4 # utf8mb4
|
||||
|
||||
|
||||
def _col_key_bytes(col) -> int | None:
|
||||
t = col.type
|
||||
# unwrap TypeDecorator
|
||||
impl = getattr(t, "impl", None)
|
||||
if impl is not None:
|
||||
t = impl
|
||||
if isinstance(t, String):
|
||||
return (t.length or 255) * BYTES_PER_CHAR
|
||||
return 8 # ints/datetimes/floats — small fixed size, close enough
|
||||
|
||||
|
||||
def _key_bytes(col, mysql_length) -> int:
|
||||
full = _col_key_bytes(col) or 0
|
||||
if mysql_length is None:
|
||||
return full
|
||||
if isinstance(mysql_length, dict):
|
||||
prefix = mysql_length.get(col.name)
|
||||
else:
|
||||
prefix = mysql_length
|
||||
if prefix is None:
|
||||
return full
|
||||
return min(full, int(prefix) * BYTES_PER_CHAR)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
bad = 0
|
||||
for table in Base.metadata.sorted_tables:
|
||||
keys: list[tuple[str, str, list, object]] = []
|
||||
for idx in table.indexes:
|
||||
keys.append(("INDEX", idx.name, list(idx.columns), idx.dialect_options["mysql"].get("length")))
|
||||
for cons in table.constraints:
|
||||
if isinstance(cons, UniqueConstraint):
|
||||
keys.append(("UNIQUE", cons.name or "?", list(cons.columns), None))
|
||||
for kind, name, cols, mysql_length in keys:
|
||||
total = sum(_key_bytes(c, mysql_length) for c in cols)
|
||||
if total > LIMIT_OLD:
|
||||
bad += 1
|
||||
tag = "EXCEEDS 3072 (fails on ALL MySQL)" if total > LIMIT_NEW else "exceeds 767 (fails on legacy MySQL)"
|
||||
detail = ", ".join(f"{c.name}({getattr(getattr(c.type, 'impl', c.type), 'length', '?')})" for c in cols)
|
||||
print(f"{table.name}: {kind} {name} = {total} bytes [{detail}] -- {tag}")
|
||||
if not bad:
|
||||
print("OK: every index key fits within 767 bytes (utf8mb4 worst case)")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in new issue
Block a user