相对第一版 46fc976 的完整变更。组员迁移对照表见 docs/20。
一、对外契约对齐 docs/05(破坏性,共 4 处,组员需按 docs/20 调整)
1) 配置发布端点改为文档规定的复数资源名:submit→validations、
approve→reviews(需 body decision)、activate→activations、
rollback→rollbacks;第一版这 4 个动词式路径 docs/05 从未定义过。
2) 错误码由 8 个笼统码改为 15 个具体语义码(FORBIDDEN→AGENT_PERMISSION_DENIED、
UNAUTHORIZED→AUTHENTICATION_REQUIRED、CONFLICT→RESOURCE_VERSION_CONFLICT、
RESOURCE_NOT_FOUND→RUN_NOT_FOUND/SESSION_NOT_FOUND 等),
输入类错误状态码 400→422。
3) POST /api/v1/agent-runs 与 GET /api/v1/agent-runs/{run_id} 统一为
{data, meta} 信封(data 内字段名与语义未变)。
4) 错误响应体统一为 {error:{code,message,retryable,field_errors}, meta:{trace_id}},
不再返回 FastAPI 默认的 {"detail": ...}。
二、数据库基线与约束
新增 39 张表的基线迁移(链根)与联合唯一键纠偏(4 张表、删 8 增 4,幂等收敛);
撤下 config_release 的双人复核 CHECK(应用层已允许自审,审核节点保留,
自审如实写入 reviewer_id);记忆 active key 生成列与唯一键;
activate 开始记录 supersedes_release_id 使版本链可追溯。
docs/00 基线未修改,未重命名或删除任何表与字段。
三、修复会静默出错或无报错的缺陷
- 跑完集成测试后平台会静默失去生效配置:清理只删自己创建的版本,却没有恢复被它
顶成 superseded 的原生效版本,且审计一并删除因而完全无痕,表现为所有工具被拒
但没有任何报错。已修清理逻辑并加恢复。
- Worker 单轮异常导致进程退出;记忆抽取调用方的“事务已开始”异常;
召回缓存丢失 degraded 标记;连接时区未生效导致 created_at/updated_at 差 8 小时;
.env 与 os.getenv 密钥来源分裂导致“没有可用的已批准模型端点”。
- 记忆信号识别漏判与跨键误命中;SSE 未带 Accept 的协商行为。
四、功能补齐
记忆链路 P1/P2/P3(抽取、受控词表、召回与缓存、生命周期级联及投影事件)、
fin_* 场内交易只读 ORM 层、agent_intent_config 状态流转并在运行期真正生效、
限流(Redis 固定窗口、故障一律放行)、游标校验、trace_id 中间件、
示例业务 Agent fund_query_demo 与一键端到端验证脚本,以及审计/指纹/迁移状态工具。
五、文档与验证
新增 docs/19(业务 Agent 接入实操)、docs/20(第一版迁移指南)与 docs/evidence 证据;
docs/01/02/06/08/09/17 同步实现现状。
验证结果:ruff 通过、mypy 103 文件无错、unit+contract 447 passed、
integration 29 passed、acceptance_check --production 7 PASS、
demo_agent_e2e 9/9 PASS(含失败关闭反证)。
162 lines
6.5 KiB
Python
162 lines
6.5 KiB
Python
"""迁移状态诊断:比对库实际结构、`alembic_version` 记录与迁移链 head,给出对齐建议。
|
||
|
||
方案 A 把 39 张基线表补进 Alembic 链首(`20260909_baseline_schema`)后,存在三类库:
|
||
|
||
1. **空库**:直接 `alembic upgrade head` 全量建库;
|
||
2. **由 Alembic 迁移建起的库**(如本地 `jr`):`alembic_version` 已位于新链上,
|
||
直接 `upgrade head` 即可,Alembic 不会重跑祖先迁移;
|
||
3. **由 DBA 脚本手工建库、`alembic_version` 为空**的库:直接 `upgrade head` 会尝试重建
|
||
已存在的表而失败,**必须先 `alembic stamp <与当前结构相符的版本>`**。
|
||
|
||
本脚本只读诊断,不执行任何 DDL,也不代替人工执行 stamp。它按"结构特征"由新到旧推断
|
||
库实际所处的版本,从而给出可执行的建议命令。
|
||
|
||
用法:
|
||
python tools/migration_state_check.py
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import sys
|
||
from pathlib import Path
|
||
|
||
from sqlalchemy import create_engine, text
|
||
from sqlalchemy.engine import Connection
|
||
|
||
ROOT = Path(__file__).resolve().parents[1]
|
||
sys.path.insert(0, str(ROOT))
|
||
|
||
from alembic.config import Config # noqa: E402
|
||
from alembic.script import ScriptDirectory # noqa: E402
|
||
|
||
from app.core.config import get_settings # noqa: E402
|
||
|
||
# 由新到旧排列:每个版本引入一个可探测的结构特征。
|
||
FEATURES: list[tuple[str, str, str]] = [
|
||
(
|
||
"20260909_memory_active_key",
|
||
"memory_unit.active_memory_key 生成列",
|
||
"""SELECT COUNT(*) FROM information_schema.COLUMNS
|
||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'memory_unit'
|
||
AND COLUMN_NAME = 'active_memory_key'""",
|
||
),
|
||
(
|
||
"20260909_constraint_fix",
|
||
"fin_market_price 联合唯一键",
|
||
"""SELECT COUNT(*) FROM information_schema.STATISTICS
|
||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'fin_market_price'
|
||
AND INDEX_NAME = 'uk_fin_market_price_product_id_trade_date'""",
|
||
),
|
||
(
|
||
"20260909_api_receipt",
|
||
"api_request_receipt 表",
|
||
"""SELECT COUNT(*) FROM information_schema.TABLES
|
||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'api_request_receipt'""",
|
||
),
|
||
(
|
||
"20260909_session",
|
||
"svc_conversation_session 表",
|
||
"""SELECT COUNT(*) FROM information_schema.TABLES
|
||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'svc_conversation_session'""",
|
||
),
|
||
(
|
||
"20260909_outbox_delivery",
|
||
"outbox_delivery 表",
|
||
"""SELECT COUNT(*) FROM information_schema.TABLES
|
||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'outbox_delivery'""",
|
||
),
|
||
(
|
||
"20260909_agent_platform_v31",
|
||
"agent_run 表",
|
||
"""SELECT COUNT(*) FROM information_schema.TABLES
|
||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'agent_run'""",
|
||
),
|
||
(
|
||
"20260909_baseline_schema",
|
||
"sys_user 表",
|
||
"""SELECT COUNT(*) FROM information_schema.TABLES
|
||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'sys_user'""",
|
||
),
|
||
]
|
||
|
||
|
||
def satisfied(connection: Connection, statement: str) -> bool:
|
||
return bool(connection.execute(text(statement)).scalar())
|
||
|
||
|
||
def infer_revision(connection: Connection) -> tuple[str | None, str | None]:
|
||
"""按结构特征推断库所处版本,返回 (revision, 命中的特征描述)。"""
|
||
for revision, description, statement in FEATURES:
|
||
if satisfied(connection, statement):
|
||
return revision, description
|
||
return None, None
|
||
|
||
|
||
def main() -> int:
|
||
config = Config(str(ROOT / "alembic.ini"))
|
||
heads = sorted(ScriptDirectory.from_config(config).get_heads())
|
||
engine = create_engine(get_settings().mysql_dsn.replace("mysql+asyncmy", "mysql+pymysql"))
|
||
with engine.connect() as connection:
|
||
# 空库连 alembic_version 表都还没有,必须先探测再查询,否则报 1146。
|
||
has_version_table = bool(
|
||
connection.execute(
|
||
text(
|
||
"SELECT COUNT(*) FROM information_schema.TABLES "
|
||
"WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'alembic_version'"
|
||
)
|
||
).scalar()
|
||
)
|
||
recorded = (
|
||
connection.execute(text("SELECT version_num FROM alembic_version")).scalars().all()
|
||
if has_version_table
|
||
else []
|
||
)
|
||
tables = connection.execute(
|
||
text(
|
||
"SELECT COUNT(*) FROM information_schema.TABLES "
|
||
"WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME <> 'alembic_version'"
|
||
)
|
||
).scalar()
|
||
inferred, feature = infer_revision(connection)
|
||
engine.dispose()
|
||
|
||
print(f"迁移链 head : {', '.join(heads) or '(none)'}")
|
||
print(f"alembic_version : {', '.join(recorded) if recorded else '(空,未记录任何版本)'}")
|
||
print(f"业务表数量 : {tables}")
|
||
print(f"结构推断版本 : {inferred or '无法匹配已知版本'}"
|
||
+ (f"(命中特征:{feature})" if feature else ""))
|
||
print()
|
||
|
||
if not recorded and not tables:
|
||
print("判定:空库。")
|
||
print("建议:alembic upgrade head")
|
||
return 0
|
||
|
||
if not recorded and tables:
|
||
if inferred is None:
|
||
print("判定:库中有表但无版本记录,且结构不符合任何已知版本。")
|
||
print("建议:人工核对库结构后再决定 stamp 目标;不要直接 upgrade(会重建已有表而失败)。")
|
||
return 1
|
||
print("判定:库由脚本或人工建起,没有 Alembic 版本记录。")
|
||
print("直接 upgrade 会尝试重建已存在的表并失败,必须先对齐版本记录:")
|
||
print(f"建议:alembic stamp {inferred}")
|
||
print(" alembic upgrade head")
|
||
return 0
|
||
|
||
if set(recorded) == set(heads):
|
||
print("判定:版本记录已位于 head,无需升级。")
|
||
print("建议:alembic current 复核;结构差异用 tools/audit_constraints.py 检查。")
|
||
return 0
|
||
|
||
print("判定:版本记录不在 head。")
|
||
print("建议:alembic current # 确认当前版本")
|
||
print(" alembic upgrade head")
|
||
if inferred is not None and inferred not in recorded:
|
||
print(f"注意:结构特征显示库可能已处于 {inferred},但版本记录为 {', '.join(recorded)};")
|
||
print(" 若二者不符,请先核对结构再决定是否 stamp,不要盲目 upgrade。")
|
||
return 0
|
||
|
||
|
||
if __name__ == "__main__":
|
||
raise SystemExit(main())
|