# ============================================================ # 知识资产库(moduleId: knowledge-assets, 可重生 ✅, 黄金测试 tests/golden/test_m3_knowledge.py) # plan.md §8.1:四类资产(算法知识/企业 SOP/历史案例/工艺知识), # 版本化 + 入库审批标记 + 检索强制带出处。 # M3 形态:单文件 JSON + 种子资产;接口稳定,后续可换向量库不改调用方。 # ============================================================ from __future__ import annotations # 前向类型引用 import json # 序列化 import os # 路径 import tempfile # 原子写 import threading # 互斥 import uuid # 资产 ID from datetime import datetime # 时间戳 from typing import Any # 类型标注 from server.timeutil import fmt_dt # 时间格式化 # 存储路径(与世界状态同目录) _PATH_ENV = "APS_KNOWLEDGE_PATH" def _seed_assets() -> list[dict[str, Any]]: """种子知识资产(演示语料;真实项目由导入管线 §8.4 入库)。""" now = fmt_dt(datetime.now()) # 统一时间戳 def asset(kind: str, title: str, content: str, tags: list[str]) -> dict[str, Any]: return { # 资产结构(DatasetAsset 的知识侧近亲) "assetId": uuid.uuid4().hex[:10], # 资产 ID(@知识 引用用) "kind": kind, # algorithm / sop / case / process "title": title, # 标题(引用与出处展示) "content": content, # 正文(M3 单块;后续分 chunk) "tags": tags, # 检索加权标签 "version": "v1", # 版本号(重复入库递增) "approved": True, # 审批标记(§8.1 入库需审批) "createdAt": now, # 入库时间 } return [ asset("sop", "换线标准SOP", "产线换型必须遵守:① 同产品族连续生产优先,减少换线;② 换线作业标准 30 分钟," "跨产品族换线 60 分钟;③ 每日 16:00 后不安排换线(夜班技师不足);" "④ 换线前必须完成上一批次尾检,未尾检不得拆线。", ["换线", "SOP", "产品族", "夜班"]), asset("sop", "插单审批流程", "VIP 客户插单:计划员评估影响(延迟/成本)→ 主管审批 → 发布新版本。" "非 VIP 插单原则上排入下一个计划周期;确需插入的走例外审批,须附影响评估报告。", ["插单", "审批", "VIP", "例外"]), asset("algorithm", "交期优先策略说明", "DELIVERY_FIRST 按交期升序 + 客户等级加权排序订单,优先保障最紧交期。" "适用:交付压力大、订单交期集中。代价:可能牺牲产能均衡与换线次数。" "参数:客户等级权重(VIP=3/重要=2/普通=1),同交期按下单时间先到先排。", ["交期优先", "DELIVERY_FIRST", "策略", "权重"]), asset("algorithm", "产能均衡策略说明", "CAPACITY_BALANCE 以产线负荷方差最小为目标分配订单,避免忙闲不均。" "适用:多产线可互替、瓶颈不固定。代价:个别紧急订单可能被排后。" "建议与交期承诺看板联用,均衡后逐单核对交付风险。", ["产能均衡", "CAPACITY_BALANCE", "负荷", "方差"]), asset("process", "产线A设备能力", "产线A(L001)适合小批量多品种:SMT 贴片 + 波峰焊;单班产能 400 标准件/日;" "维保窗口每周三 08:00-10:00 固定停机;夜班仅一班技师,复杂换型不排夜班。", ["产线A", "L001", "产能", "维保"]), asset("case", "6月底比亚迪插单复盘", "6月28日比亚迪 VIP 插单 2000 件:采用交期优先重排,牺牲产线B均衡度 12%," "全部订单按期交付。经验:VIP 插单后应立即跑一次沙盒对比,确认对存量订单影响 <5% 再发布。", ["比亚迪", "插单", "复盘", "沙盒"]), ] class KnowledgeStore: """知识资产仓:加载/列出/取回/新增(单文件 JSON + 原子写;P0 读 / P1 新增)。""" def __init__(self, path: str | None = None) -> None: """初始化:加载资产,空库自动播种。权力等级 P0(构造只读+首次播种)。""" self.path = path or os.environ.get(_PATH_ENV, "server/data/knowledge.json") # 存储路径 self._lock = threading.Lock() # 并发保护 self.assets: list[dict[str, Any]] = self._load() # 资产列表 if not self.assets: # 空库播种 self.assets = _seed_assets() # 种子资产 self._write() # 落盘 def _load(self) -> list[dict[str, Any]]: """从磁盘加载;缺失/损坏返回空(触发播种)。""" try: with open(self.path, "r", encoding="utf-8") as f: # 读文件 return json.load(f).get("assets", []) # 取资产数组 except (FileNotFoundError, json.JSONDecodeError): # 缺失/损坏 return [] def _write(self) -> None: """原子写盘(tmp + os.replace,与 WorldStore 同法)。""" os.makedirs(os.path.dirname(self.path) or ".", exist_ok=True) # 确保目录 fd, tmp = tempfile.mkstemp(dir=os.path.dirname(self.path) or ".", suffix=".tmp") # 临时文件 try: with os.fdopen(fd, "w", encoding="utf-8") as f: # 写临时文件 json.dump({"assets": self.assets}, f, ensure_ascii=False, indent=1) # 序列化 os.replace(tmp, self.path) # 原子替换 except BaseException: # 失败清理 if os.path.exists(tmp): os.unlink(tmp) raise def list_meta(self) -> list[dict[str, Any]]: """资产元信息清单(不含正文;知识面板/命令面板数据源,P0)。""" return [{k: a[k] for k in ("assetId", "kind", "title", "version", "createdAt")} for a in self.assets] def get(self, asset_id: str) -> dict[str, Any] | None: """按 ID 取完整资产(@知识 引用解析用,P0)。""" return next((a for a in self.assets if a["assetId"] == asset_id), None) def find_by_title(self, title: str) -> dict[str, Any] | None: """按标题模糊取资产(@知识:换线标准SOP 的解析路径,P0)。""" t = title.strip() # 清理输入 return next((a for a in self.assets if t and t in a["title"]), None) def add(self, kind: str, title: str, content: str, tags: list[str] | None = None) -> dict[str, Any]: """新增资产(P1:只新增知识,不碰世界状态;报告入库走此口 §9.10 规则3)。""" with self._lock: # 串行化写 existing = self.find_by_title(title) # 同名视为新版本 version = "v1" # 默认首版 if existing and existing["title"] == title: # 完全同名 version = f"v{int(existing['version'].lstrip('v')) + 1}" # 版本递增 asset = { # 组装资产 "assetId": uuid.uuid4().hex[:10], "kind": kind, "title": title, "content": content, "tags": tags or [], "version": version, "approved": kind != "report", # 报告类默认待审(对外须人工批 §9.10 规则4) "createdAt": fmt_dt(datetime.now()), } self.assets.append(asset) # 入库 self._write() # 落盘 return asset # 返回 # ---------------- 进程级单例 ---------------- _store: KnowledgeStore | None = None # 单例槽 def get_knowledge() -> KnowledgeStore: """取知识库单例(gateway/workflow 共用)。""" global _store if _store is None: # 首次创建 _store = KnowledgeStore() return _store