zizi c154ca9085 配置与数据库:迁移 V0056–V0060 与流程模板
- 迁移:调用结算与配置验证(V0056)、来源当前授权(V0057)、成品补充验证回执(V0058)、检索索引代次(V0059)、
  评测单元复用来源(V0060);新表按既有约定加只追加守卫与角色授权。
- 流程模板与载入口径同步(内联保护字段改为登记类型约束);旧库迁移工具链按新表结构对齐。
- 配置:提供方模板与运行配置同步角色策略版本;角色策略白名单新增 qwen3.8-flash(见收尾报告待裁决项:
  该模型精确身份与独立性尚未核验,且策略版本号未随白名单升版)。
2026-09-18 01:15:25 +08:00

294 lines
14 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""旧源只读快照、纯分流与隔离目标候选导入;不调用模型、确认或切换生产。"""
from __future__ import annotations
import argparse
import hashlib
import os
import sys
import tempfile
from collections import Counter
from pathlib import Path
from 建立映射 import 稳定JSON, 读取JSON, 旧快照, 映射配置, 检查目标快照, 迁移错误
from 知识记录分流 import 分流批次
文件上限 = 16 * 1024 * 1024
def _读文件(路径: Path):
with 路径.open("rb") as 文件:
内容 = 文件.read(文件上限 + 1)
if len(内容) > 文件上限:
raise 迁移错误("input_too_large", "快照或映射超过16MiB,请显式分批,不截断输入")
return 读取JSON(内容.decode("utf-8"))
def _保存文件(路径: Path, 文本: str):
临时 = None
try:
with tempfile.NamedTemporaryFile(
mode="w", encoding="utf-8", dir=路径.parent, prefix=".迁移分流-", delete=False
) as 文件:
临时 = Path(文件.name)
文件.write(文本)
文件.flush()
os.fsync(文件.fileno())
try:
# 同目录硬链接保证完整可见且不覆盖已有文件;临时文件默认仅当前用户可读写。
os.link(临时, 路径)
except FileExistsError:
if 路径.read_text(encoding="utf-8") != 文本:
raise 迁移错误("output_conflict", "输出已存在且内容不同,请另选路径") from None
finally:
if 临时 is not None:
临时.unlink(missing_ok=True)
def _导出源(值) -> int:
源 = 值.连接说明 if 值.command == "读取PG" else 值.副本
输入路径 = {源.resolve()}
if 值.主键范围:
输入路径.add(值.主键范围.resolve())
if 值.输出.resolve() in 输入路径:
raise 迁移错误("output_is_input", "输出不能覆盖连接说明、旧库副本或范围文件")
范围 = _读文件(值.主键范围) if 值.主键范围 else {}
参数 = {
"数据集": 值.数据集,
"表": tuple(值.表),
"快照版本": 值.快照版本,
"最大行数": 值.最大行数,
"范围": 范围,
}
if 值.command == "读取PG":
from 读取旧PG import 读取旧库 as 读取PG
结果 = 读取PG(源, schema=值.schema, 租户=值.租户, **参数)
else:
from 读取旧SQLite import 读取旧库 as 读取SQLite
结果 = 读取SQLite(源, **参数)
_保存文件(值.输出, 稳定JSON([r.导出() for r in 结果]) + "\n")
print(f"只读快照:{len(结果)}条(未导入目标库)")
return 0
def _文件命令(值) -> int:
from 读取旧文件 import 核对文件保全, 读取文件集
try:
if 值.command == "读取文件":
结果 = 读取文件集(值.源根, _读文件(值.清单), 数据集=值.数据集, 输出目录=值.输出目录)
print(f"文件保全:{len(结果)}项(未写业务候选、未确认)")
return 0
if 值.输出.resolve().is_relative_to(值.文件回执.parent.resolve()):
raise 迁移错误("output_is_input", "文件核对报告不得写入被核对保全根")
结果 = {
**核对文件保全(值.文件回执),
"stage": "file_preservation_only",
"production_switch_authorized": False,
"package_acceptance_claimed": False,
}
_保存文件(值.输出, 稳定JSON(结果) + "\n")
print(
"文件保全核对:" + ("通过" if 结果["passed"] else "有缺口") + ";不代表PG对象或整包通过"
)
return 0 if 结果["passed"] else 2
except (迁移错误, OSError, ValueError, TypeError, KeyError) as 错:
代码 = 错.code if isinstance(错, 迁移错误) else "file_operation_failed"
raise 迁移错误(代码, "文件命令未完成;不回显原文、编码内容或凭据") from None
def _检查实例命令(值) -> int:
from muse.共享.错误 import Muse错误
from muse.配置 import 数据库引用
from 导入新库 import 检查实例
try:
if 值.输出.resolve() in {值.端点记录.resolve(), 值.管理引用.resolve()}:
raise 迁移错误("output_is_input", "实例报告不能覆盖端点记录或凭据引用")
结果 = 检查实例(
端点记录=值.端点记录,
管理引用=数据库引用("受控存储", str(值.管理引用)),
)
_保存文件(值.输出, 稳定JSON(结果) + "\n")
except (迁移错误, Muse错误, OSError, ValueError, TypeError, KeyError) as 错:
代码 = 错.code if isinstance(错, 迁移错误) else "target_inspect_failed"
raise 迁移错误(代码, "只读实例检查未完成;不回显连接串、原始错误或凭据") from None
print("只读实例与数据库身份已保存;未授权迁移、未创建或修改数据库")
return 0
def _初始化目标命令(值) -> int:
from muse.配置 import 数据库引用
from 导入新库 import 初始化目标
回执 = 初始化目标(
允许实例=值.允许实例,
管理引用=数据库引用("受控存储", str(值.管理引用)),
维护模板=数据库引用("受控存储", str(值.维护模板)),
应用模板=数据库引用("受控存储", str(值.应用模板)),
输出目录=值.输出目录,
)
print("已创建隔离目标:" + 回执["database"] + "(未导入业务候选)")
return 0
def _目标命令(值) -> int:
from muse.启动 import 构建
from muse.接入.创作操作 import 创作身份
from muse.配置 import 数据库引用, 读取配置
from 切换检查 import 检查切换条件
from 导入新库 import 导入快照
from 核对结果 import 核对迁移结果
输入路径 = {
p.resolve() for p in (值.快照, 值.映射, 值.配置, 值.允许实例, 值.管理引用, 值.目标回执)
}
if 值.文件回执:
输入路径.add(值.文件回执.resolve())
if 值.输出.resolve().is_relative_to(值.文件回执.parent.resolve()):
raise 迁移错误("output_is_input", "目标报告不得写入文件保全根")
if getattr(值, "当前快照", None):
输入路径.add(值.当前快照.resolve())
if getattr(值, "来源回放", None):
输入路径.add(值.来源回放.resolve())
if 值.输出.resolve() in 输入路径:
raise 迁移错误("output_is_input", "报告不能覆盖任何输入、配置或回执")
输入 = _读文件(值.快照)
if not isinstance(输入, list) or not all(isinstance(r, dict) for r in 输入):
raise 迁移错误("snapshot_invalid", "快照必须是记录数组")
快照 = [旧快照.从载荷(r) for r in 输入]
检查目标快照(快照)
当前 = None
if 值.command == "切换检查":
当前输入 = _读文件(值.当前快照)
if not isinstance(当前输入, list) or not all(isinstance(r, dict) for r in 当前输入):
raise 迁移错误("snapshot_invalid", "当前快照必须是记录数组")
当前 = [旧快照.从载荷(r) for r in 当前输入]
检查目标快照(当前, 范围名称="当前")
映射 = 映射配置(_读文件(值.映射))
配置 = 读取配置(值.配置)
if 配置.HTTP is None:
raise 迁移错误("target_denied", "目标命令需要已有本地作者身份配置,不采用映射作者作为认证")
装配, 身份 = 构建(配置), 创作身份(配置.HTTP.作者ID, 配置)
参数 = {
"允许实例": 值.允许实例,
"管理引用": 数据库引用("受控存储", str(值.管理引用)),
"目标回执": 值.目标回执,
"文件回执": 值.文件回执,
}
# 常驻数据库工厂只在生命周期内开池;目标命令借的还是同一个受控目标。
with 装配.生命周期():
if 值.command == "导入目标":
结果 = 导入快照(装配, 身份, 快照, 映射, **参数)
通过 = 结果["all_accounted"] and not 结果["states"].get("quarantined")
elif 值.command == "核对目标":
请求 = _读文件(值.来源回放) if 值.来源回放 else None
结果 = 核对迁移结果(装配, 身份, 快照, 映射, 来源回放=请求, **参数)
通过 = 结果["data_checks_passed"]
else:
assert 当前 is not None
结果 = 检查切换条件(装配, 身份, 快照, 当前, 映射, **参数)
通过 = 结果["data_checks_passed"]
_保存文件(值.输出, 稳定JSON(结果) + "\n")
print(
值.command
+ (":本范围数据检查通过" if 通过 else ":有隔离或未闭合项")
+ ";未确认候选、未授权生产切换"
)
return 0 if 通过 else 2
def main(argv: list[str] | None = None) -> int:
参数 = argparse.ArgumentParser(description=__doc__)
子命令 = 参数.add_subparsers(dest="command", required=True)
分流 = 子命令.add_parser("分流", help="只读快照与映射,写出分类计划,不导入数据库")
分流.add_argument("--快照", type=Path, required=True)
分流.add_argument("--映射", type=Path, required=True)
分流.add_argument("--输出", type=Path, required=True)
for 名, 源参数 in (("读取PG", "--连接说明"), ("读取SQLite", "--副本")):
读取 = 子命令.add_parser(名, help="只读明确旧源,输出完整原快照,不导入或确认")
读取.add_argument(源参数, type=Path, required=True)
读取.add_argument("--数据集", required=True)
读取.add_argument("--表", action="append", required=True)
读取.add_argument("--快照版本", required=True)
读取.add_argument("--最大行数", type=int, default=10000)
读取.add_argument("--主键范围", type=Path)
读取.add_argument("--输出", type=Path, required=True)
if 名 == "读取PG":
读取.add_argument("--schema", required=True)
作用域 = 读取.add_mutually_exclusive_group(required=True)
作用域.add_argument("--租户", type=int)
作用域.add_argument("--全局", action="store_true")
文件 = 子命令.add_parser("读取文件", help="按明确清单只读源根,生成私有原字节保全与快照")
for 名 in ("源根", "清单", "输出目录"):
文件.add_argument("--" + 名, type=Path, required=True)
文件.add_argument("--数据集", required=True)
核对文件 = 子命令.add_parser("核对文件", help="只读核对实际副本和文件回执,不宣称PG或整包通过")
for 名 in ("文件回执", "输出"):
核对文件.add_argument("--" + 名, type=Path, required=True)
实例 = 子命令.add_parser("检查实例", help="只读核对明确端点的实例与数据库目录身份")
for 名 in ("端点记录", "管理引用", "输出"):
实例.add_argument("--" + 名, type=Path, required=True)
初始化 = 子命令.add_parser("初始化目标", help="只在获准实例新建随机目标库,不接受既有库")
for 名 in ("允许实例", "管理引用", "维护模板", "应用模板", "输出目录"):
初始化.add_argument("--" + 名, type=Path, required=True)
for 名 in ("导入目标", "核对目标", "切换检查"):
目标 = 子命令.add_parser(名, help="隔离目标候选写入或只读对账,不确认和切换")
for 字段 in ("快照", "映射", "配置", "允许实例", "管理引用", "目标回执", "输出"):
目标.add_argument("--" + 字段, type=Path, required=True)
目标.add_argument("--文件回执", type=Path)
if 名 == "核对目标":
目标.add_argument("--来源回放", type=Path)
if 名 == "切换检查":
目标.add_argument("--当前快照", type=Path, required=True)
值 = 参数.parse_args(argv)
try:
if 值.command == "检查实例":
return _检查实例命令(值)
if 值.command in {"读取文件", "核对文件"}:
return _文件命令(值)
if 值.command in {"初始化目标", "导入目标", "核对目标", "切换检查"}:
from muse.共享.错误 import Muse错误
try:
return _初始化目标命令(值) if 值.command == "初始化目标" else _目标命令(值)
except Muse错误 as 错:
raise 迁移错误(
错.错误码, "目标命令未完成;未确认或切换,错误细节不回显正文或凭据"
) from None
if 值.command != "分流":
return _导出源(值)
if 值.输出.resolve() in {值.快照.resolve(), 值.映射.resolve()}:
raise 迁移错误("output_is_input", "输出不能覆盖快照或映射")
输入, 配置 = _读文件(值.快照), _读文件(值.映射)
if not isinstance(输入, list) or not all(isinstance(r, dict) for r in 输入):
raise 迁移错误("snapshot_invalid", "快照文件必须是记录对象数组")
if not isinstance(配置, dict):
raise 迁移错误("mapping_invalid", "映射文件必须是对象")
快照 = [旧快照.从载荷(r) for r in 输入]
映射 = 映射配置(配置)
结果 = 分流批次(快照, 映射)
统计 = dict(Counter(r.disposition for r in 结果))
计划 = {
"schema_version": 1,
"stage": "classification_only",
"snapshot_hash": hashlib.sha256(稳定JSON(输入).encode()).hexdigest(),
"mapping_hash": hashlib.sha256(稳定JSON(映射.值).encode()).hexdigest(),
"source_count": len(输入),
"distinct_source_count": len({r.source_key for r in 结果}),
"dispositions": 统计,
"results": [r.导出() for r in 结果],
}
_保存文件(值.输出, 稳定JSON(计划) + "\n")
print("分流计划(未导入数据库):" + 稳定JSON(统计))
return 2 if 统计.get("隔离", 0) else 0
except (迁移错误, OSError, ValueError) as 错:
print(str(错), file=sys.stderr)
return 1
if __name__ == "__main__":
raise SystemExit(main())