2026-07-28 02:12:46 +08:00
|
|
|
|
# ============================================================
|
|
|
|
|
|
# 项目深度分析(moduleId: domain-project-analyze, 可重生 ✅)
|
|
|
|
|
|
# 工程目录 Excel/SQL → 写入当前项目世界 → 明细表 + 齐备度 + 知识库补洞 plan
|
|
|
|
|
|
# ============================================================
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
|
|
import os
|
|
|
|
|
|
import re
|
|
|
|
|
|
from typing import Any
|
|
|
|
|
|
|
|
|
|
|
|
from server.aps_domain.readiness import check_readiness
|
|
|
|
|
|
|
|
|
|
|
|
World = dict[str, Any]
|
|
|
|
|
|
|
2026-08-20 11:39:21 +08:00
|
|
|
|
_TABLE_EXT_RE = re.compile(r"\.(xlsx|xlsm|csv|txt)$", re.I)
|
|
|
|
|
|
_INPUT_EXT_RE = re.compile(r"\.(xlsx|xlsm|csv|txt|sql)$", re.I)
|
|
|
|
|
|
|
2026-07-28 02:12:46 +08:00
|
|
|
|
|
|
|
|
|
|
def _project_ctx(session_id: str | None) -> tuple[dict | None, str]:
|
|
|
|
|
|
try:
|
|
|
|
|
|
from server.state.projects import get_project_store
|
|
|
|
|
|
snap = get_project_store().snapshot(include_messages=False)
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
return None, ""
|
|
|
|
|
|
sessions = {s["id"]: s for s in (snap.get("sessions") or []) if isinstance(s, dict)}
|
|
|
|
|
|
projects = {p["id"]: p for p in (snap.get("projects") or []) if isinstance(p, dict)}
|
|
|
|
|
|
sess = sessions.get(session_id or "") or {}
|
|
|
|
|
|
proj = projects.get(sess.get("projectId") or "") if sess.get("projectId") else None
|
|
|
|
|
|
work = (proj.get("workDir") or "").strip() if proj else ""
|
|
|
|
|
|
return proj, work
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-08-20 11:39:21 +08:00
|
|
|
|
def _input_path_from_query(query: str) -> str:
|
|
|
|
|
|
"""从自然语言中提取当前机器上真实存在的文件/目录路径。"""
|
|
|
|
|
|
text = str(query or "").strip()
|
|
|
|
|
|
if not text:
|
|
|
|
|
|
return ""
|
|
|
|
|
|
candidates: list[str] = []
|
|
|
|
|
|
for match in re.finditer(r'["“”\']([^"“”\']+)["“”\']', text):
|
|
|
|
|
|
candidates.append(match.group(1))
|
|
|
|
|
|
candidates.extend(re.findall(
|
|
|
|
|
|
r"[A-Za-z]:[\\/][^\r\n\"'<>|]+?\.(?:xlsx|xlsm|csv|txt|sql)",
|
|
|
|
|
|
text,
|
|
|
|
|
|
flags=re.I,
|
|
|
|
|
|
))
|
|
|
|
|
|
# 没带引号时,也允许整句本身就是一个目录。
|
|
|
|
|
|
candidates.append(text)
|
|
|
|
|
|
for raw in candidates:
|
|
|
|
|
|
value = raw.strip().strip("`,。;;、")
|
|
|
|
|
|
if os.path.isfile(value) and _INPUT_EXT_RE.search(value):
|
|
|
|
|
|
return os.path.abspath(value)
|
|
|
|
|
|
if os.path.isdir(value):
|
|
|
|
|
|
return os.path.abspath(value)
|
|
|
|
|
|
return ""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _direct_table_report(
|
|
|
|
|
|
world: World,
|
|
|
|
|
|
paths: list[str],
|
|
|
|
|
|
*,
|
|
|
|
|
|
project_name: str,
|
|
|
|
|
|
work_dir: str,
|
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
|
"""直接路径旁路:复用 preview_file,但不依赖工作区里的 workDir。"""
|
|
|
|
|
|
from server.aps_domain.folder_pack import KIND_CN
|
|
|
|
|
|
from server.aps_domain.importers import preview_file
|
|
|
|
|
|
|
|
|
|
|
|
required = ("orders", "materials", "routing", "equipment")
|
|
|
|
|
|
files: list[dict[str, Any]] = []
|
|
|
|
|
|
batches: list[dict[str, Any]] = []
|
|
|
|
|
|
kind_counts: dict[str, int] = {}
|
|
|
|
|
|
for path in paths:
|
|
|
|
|
|
name = os.path.basename(path)
|
|
|
|
|
|
try:
|
|
|
|
|
|
with open(path, "rb") as source:
|
|
|
|
|
|
preview = preview_file(name, source.read(), world, soft=True)
|
|
|
|
|
|
file_batches = preview.get("batches") or []
|
|
|
|
|
|
ok_count = int(preview.get("totalOk") or 0)
|
|
|
|
|
|
error_count = int(preview.get("totalErrors") or 0)
|
|
|
|
|
|
primary_kind = next(
|
|
|
|
|
|
(str(batch.get("kind")) for batch in file_batches if batch.get("okCount")),
|
|
|
|
|
|
str((file_batches[0] if file_batches else {}).get("kind") or ""),
|
|
|
|
|
|
)
|
|
|
|
|
|
field_map = next(
|
|
|
|
|
|
(list(batch.get("fieldMap") or []) for batch in file_batches if batch.get("fieldMap")),
|
|
|
|
|
|
[],
|
|
|
|
|
|
)
|
|
|
|
|
|
errors: list[str] = []
|
|
|
|
|
|
warnings: list[str] = []
|
|
|
|
|
|
headers: list[str] = []
|
|
|
|
|
|
samples: list[str] = []
|
|
|
|
|
|
sheets: list[dict[str, Any]] = []
|
|
|
|
|
|
for batch in file_batches:
|
|
|
|
|
|
kind = str(batch.get("kind") or primary_kind or "materials")
|
|
|
|
|
|
count = int(batch.get("okCount") or 0)
|
|
|
|
|
|
if count:
|
|
|
|
|
|
kind_counts[kind] = kind_counts.get(kind, 0) + count
|
|
|
|
|
|
errors.extend(str(item) for item in (batch.get("errors") or [])[:2])
|
|
|
|
|
|
warnings.extend(str(item) for item in (batch.get("warnings") or [])[:2])
|
|
|
|
|
|
if not headers:
|
|
|
|
|
|
headers = [str(item) for item in (batch.get("headersRaw") or []) if item][:8]
|
|
|
|
|
|
for row in (batch.get("okRows") or [])[:1]:
|
|
|
|
|
|
if isinstance(row, dict):
|
|
|
|
|
|
samples.append(";".join(f"{k}={v}" for k, v in list(row.items())[:6]))
|
|
|
|
|
|
sheets.append({
|
|
|
|
|
|
"sheet": batch.get("sheet"), "kind": kind,
|
|
|
|
|
|
"kindCn": KIND_CN.get(kind, kind), "okCount": count,
|
|
|
|
|
|
"errorCount": int(batch.get("errorCount") or 0),
|
|
|
|
|
|
})
|
|
|
|
|
|
if count:
|
|
|
|
|
|
batches.append({
|
|
|
|
|
|
"kind": kind, "sheet": batch.get("sheet") or name,
|
|
|
|
|
|
"okRows": batch.get("okRows") or [], "okCount": count,
|
|
|
|
|
|
"errorCount": int(batch.get("errorCount") or 0),
|
|
|
|
|
|
"errors": (batch.get("errors") or [])[:8],
|
|
|
|
|
|
"warnings": (batch.get("warnings") or [])[:5],
|
|
|
|
|
|
"fieldMap": batch.get("fieldMap") or [], "sourceFile": name,
|
|
|
|
|
|
})
|
|
|
|
|
|
fmap_text = "、".join(
|
|
|
|
|
|
f"{item.get('source')}→{item.get('target')}" for item in field_map
|
|
|
|
|
|
)
|
|
|
|
|
|
issue = (errors or warnings or ([f"字段:{fmap_text}"] if fmap_text else []))
|
|
|
|
|
|
files.append({
|
|
|
|
|
|
"name": name, "size": os.path.getsize(path),
|
|
|
|
|
|
"status": "ok" if ok_count and not error_count else ("warn" if ok_count else "fail"),
|
|
|
|
|
|
"kind": primary_kind, "kindCn": KIND_CN.get(primary_kind, primary_kind or "未识别"),
|
|
|
|
|
|
"okCount": ok_count, "errorCount": error_count,
|
|
|
|
|
|
"errors": errors, "warnings": warnings, "headers": headers,
|
|
|
|
|
|
"samples": samples, "sheets": sheets, "fieldMap": field_map,
|
|
|
|
|
|
"fieldMapText": fmap_text, "issue": issue[0] if issue else "",
|
|
|
|
|
|
})
|
|
|
|
|
|
except Exception as exc: # noqa: BLE001
|
|
|
|
|
|
files.append({
|
|
|
|
|
|
"name": name, "size": os.path.getsize(path) if os.path.isfile(path) else 0,
|
|
|
|
|
|
"status": "fail", "kind": "", "kindCn": "读取失败",
|
|
|
|
|
|
"okCount": 0, "errorCount": 1, "errors": [str(exc)],
|
|
|
|
|
|
"warnings": [], "headers": [], "samples": [], "sheets": [],
|
|
|
|
|
|
"fieldMap": [], "fieldMapText": "", "issue": str(exc),
|
|
|
|
|
|
})
|
|
|
|
|
|
coverage = {kind: kind_counts.get(kind, 0) > 0 for kind in required}
|
|
|
|
|
|
coverage_detail = [{
|
|
|
|
|
|
"key": kind, "label": KIND_CN[kind], "ok": coverage[kind],
|
|
|
|
|
|
"count": kind_counts.get(kind, 0), "fieldMap": [],
|
|
|
|
|
|
"note": (f"读到 {kind_counts[kind]} 行" if coverage[kind] else "还没有有效数据"),
|
|
|
|
|
|
} for kind in required]
|
|
|
|
|
|
missing = [KIND_CN[kind] for kind in required if not coverage[kind]]
|
|
|
|
|
|
return {
|
|
|
|
|
|
"ok": True, "projectName": project_name, "workDir": work_dir,
|
|
|
|
|
|
"files": files, "coverage": coverage, "coverageDetail": coverage_detail,
|
|
|
|
|
|
"kindCounts": kind_counts, "missing": missing, "softMissing": [],
|
|
|
|
|
|
"canSchedule": not missing, "batches": batches,
|
|
|
|
|
|
"totalOk": sum(int(item.get("okCount") or 0) for item in files),
|
|
|
|
|
|
"totalErrors": sum(int(item.get("errorCount") or 0) for item in files),
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-07-28 02:12:46 +08:00
|
|
|
|
def _fill_from_knowledge(world: World, product_codes: list[str]) -> list[dict[str, Any]]:
|
|
|
|
|
|
"""缺工艺时用行业模板/知识库自动补;返回动作清单。"""
|
|
|
|
|
|
actions: list[dict[str, Any]] = []
|
|
|
|
|
|
try:
|
|
|
|
|
|
from server.knowledge.routing_templates import recommend_templates, apply_template_to_product
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
return actions
|
|
|
|
|
|
routes = {r.get("productCode") for r in (world.get("flexRoutings") or [])}
|
|
|
|
|
|
mats = {m.get("code") for m in (world.get("flexMaterials") or [])}
|
|
|
|
|
|
for pc in product_codes:
|
|
|
|
|
|
if not pc:
|
|
|
|
|
|
continue
|
|
|
|
|
|
if pc not in mats:
|
|
|
|
|
|
world.setdefault("flexMaterials", []).append({
|
|
|
|
|
|
"id": len(world.get("flexMaterials") or []) + 1,
|
|
|
|
|
|
"code": pc, "name": pc, "type": "FINISHED_PRODUCT", "unit": "件",
|
|
|
|
|
|
"stock": 0, "inTransit": 0, "safetyStock": 0, "procurementLeadTime": 0,
|
|
|
|
|
|
})
|
|
|
|
|
|
actions.append({"type": "material_stub", "productCode": pc,
|
|
|
|
|
|
"detail": f"物料主数据缺失,已建产品档案占位 {pc}"})
|
|
|
|
|
|
if pc in routes:
|
|
|
|
|
|
continue
|
|
|
|
|
|
name = next((m.get("name") for m in (world.get("flexMaterials") or [])
|
|
|
|
|
|
if m.get("code") == pc), pc)
|
|
|
|
|
|
try:
|
|
|
|
|
|
recs = recommend_templates(f"{pc} {name}") or []
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
recs = []
|
|
|
|
|
|
if not recs:
|
|
|
|
|
|
actions.append({"type": "need_routing", "productCode": pc,
|
|
|
|
|
|
"detail": f"产品 {pc} 无工艺,知识库也无匹配模板,需客户提供工艺路线"})
|
|
|
|
|
|
continue
|
|
|
|
|
|
tpl = recs[0]
|
|
|
|
|
|
code = tpl.get("code") or tpl.get("templateCode")
|
|
|
|
|
|
try:
|
|
|
|
|
|
apply_template_to_product(world, code, pc)
|
|
|
|
|
|
actions.append({
|
|
|
|
|
|
"type": "template_applied", "productCode": pc,
|
|
|
|
|
|
"template": code, "templateName": tpl.get("name"),
|
|
|
|
|
|
"detail": f"产品 {pc} 缺工艺 → 已用知识库模板「{tpl.get('name') or code}」生成",
|
|
|
|
|
|
})
|
|
|
|
|
|
routes.add(pc)
|
|
|
|
|
|
except Exception as exc:
|
|
|
|
|
|
actions.append({"type": "template_failed", "productCode": pc,
|
|
|
|
|
|
"detail": f"模板 {code} 应用失败:{exc}"})
|
|
|
|
|
|
return actions
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def analyze_project_deep(
|
|
|
|
|
|
world: World,
|
|
|
|
|
|
session_id: str | None = None,
|
|
|
|
|
|
*,
|
|
|
|
|
|
apply_sql: bool = True,
|
|
|
|
|
|
query: str = "",
|
2026-08-20 11:39:21 +08:00
|
|
|
|
next_id=None,
|
2026-07-28 02:12:46 +08:00
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
|
"""深度分析当前项目:目录包(SQL/Excel)+ 世界主数据 + 知识库补洞。"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
from server.agent_core.progress import emit_thinking
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
def emit_thinking(*_a, **_k): # type: ignore
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
proj, work = _project_ctx(session_id)
|
2026-08-20 11:39:21 +08:00
|
|
|
|
explicit_input = _input_path_from_query(query)
|
|
|
|
|
|
if explicit_input:
|
|
|
|
|
|
work = explicit_input if os.path.isdir(explicit_input) else os.path.dirname(explicit_input)
|
|
|
|
|
|
if proj is None:
|
|
|
|
|
|
proj = {
|
|
|
|
|
|
"name": os.path.basename(work.rstrip("\\/")) or "当前项目",
|
|
|
|
|
|
"workDir": work,
|
|
|
|
|
|
}
|
2026-07-28 02:12:46 +08:00
|
|
|
|
sources: list[str] = []
|
2026-08-20 11:39:21 +08:00
|
|
|
|
source_paths: list[str] = []
|
2026-07-28 02:12:46 +08:00
|
|
|
|
sql_preview: dict[str, Any] | None = None
|
|
|
|
|
|
applied: dict[str, int] = {}
|
|
|
|
|
|
kb_actions: list[dict[str, Any]] = []
|
|
|
|
|
|
|
2026-08-20 11:39:21 +08:00
|
|
|
|
def apply_folder_batches(report: dict[str, Any] | None) -> None:
|
|
|
|
|
|
batches = (report or {}).get("batches") or []
|
|
|
|
|
|
if not batches or next_id is None:
|
|
|
|
|
|
return
|
|
|
|
|
|
from server.aps_domain.importers import apply_import_commit
|
|
|
|
|
|
|
|
|
|
|
|
counts = apply_import_commit(world, next_id, batches)
|
|
|
|
|
|
for key, value in counts.items():
|
|
|
|
|
|
applied[key] = applied.get(key, 0) + int(value or 0)
|
|
|
|
|
|
|
2026-07-28 02:12:46 +08:00
|
|
|
|
emit_thinking("定位工程目录", (proj or {}).get("name") or "当前项目", pct=2)
|
|
|
|
|
|
if work and os.path.isdir(work):
|
2026-08-20 11:39:21 +08:00
|
|
|
|
names = ([os.path.basename(explicit_input)] if os.path.isfile(explicit_input)
|
|
|
|
|
|
else sorted(os.listdir(work)))
|
2026-07-28 02:12:46 +08:00
|
|
|
|
sqls = [n for n in names if n.lower().endswith(".sql") and not n.startswith("~$")]
|
2026-08-20 11:39:21 +08:00
|
|
|
|
xls = [n for n in names if _TABLE_EXT_RE.search(n) and not n.startswith("~$")]
|
|
|
|
|
|
source_paths = [
|
|
|
|
|
|
os.path.join(work, name) for name in (*sqls, *xls)
|
|
|
|
|
|
if os.path.isfile(os.path.join(work, name))
|
|
|
|
|
|
]
|
|
|
|
|
|
folder = None
|
2026-07-28 02:12:46 +08:00
|
|
|
|
emit_thinking(
|
|
|
|
|
|
"清点目录文件",
|
|
|
|
|
|
f"SQL {len(sqls)} · Excel/CSV {len(xls)}",
|
|
|
|
|
|
pct=5,
|
|
|
|
|
|
)
|
|
|
|
|
|
if sqls:
|
|
|
|
|
|
from server.importers.sql_pack import preview_sql_file, apply_sql_pack_to_world
|
|
|
|
|
|
# 优先最大的 sql(完整库)
|
|
|
|
|
|
sqls_sorted = sorted(sqls, key=lambda n: os.path.getsize(os.path.join(work, n)), reverse=True)
|
|
|
|
|
|
path = os.path.join(work, sqls_sorted[0])
|
|
|
|
|
|
sources.append(f"SQL:{sqls_sorted[0]}")
|
|
|
|
|
|
try:
|
|
|
|
|
|
sql_preview = preview_sql_file(path)
|
|
|
|
|
|
if apply_sql and sql_preview.get("flex"):
|
|
|
|
|
|
emit_thinking("写入项目世界", "订单 / 物料 / 工艺 / 设备", pct=93)
|
|
|
|
|
|
# SQL 包是工程目录真相源,始终整包覆盖,避免旧错误工艺残留
|
|
|
|
|
|
applied = apply_sql_pack_to_world(
|
|
|
|
|
|
world, sql_preview["flex"], replace=True)
|
|
|
|
|
|
emit_thinking(
|
|
|
|
|
|
"入库完成",
|
|
|
|
|
|
"、".join(f"{k}{v}" for k, v in applied.items()) or "无新增",
|
|
|
|
|
|
status="done", pct=95,
|
|
|
|
|
|
)
|
|
|
|
|
|
except Exception as exc:
|
|
|
|
|
|
sql_preview = {"error": str(exc), "filename": sqls_sorted[0]}
|
|
|
|
|
|
emit_thinking("SQL 解析失败", str(exc), status="warn")
|
2026-08-20 11:39:21 +08:00
|
|
|
|
elif xls:
|
|
|
|
|
|
# 无 SQL 时:识别 MOM 主数据收集表并写入世界(否则深度分析永远是 0)
|
|
|
|
|
|
mom_applied = False
|
|
|
|
|
|
try:
|
|
|
|
|
|
from server.importers.mom_pack import is_mom_workbook, import_mom_excel
|
|
|
|
|
|
mom_files = [
|
|
|
|
|
|
n for n in xls
|
|
|
|
|
|
if is_mom_workbook(os.path.join(work, n))
|
|
|
|
|
|
]
|
|
|
|
|
|
# 文件名含 MOM/主数据 优先;否则对全部 xlsx 试探
|
|
|
|
|
|
candidates = mom_files or xls
|
|
|
|
|
|
for n in candidates:
|
|
|
|
|
|
path = os.path.join(work, n)
|
|
|
|
|
|
if not is_mom_workbook(path):
|
|
|
|
|
|
continue
|
|
|
|
|
|
sources.append(f"MOM:{n}")
|
|
|
|
|
|
emit_thinking("识别 MOM 主数据表", n, pct=90)
|
|
|
|
|
|
result = import_mom_excel(world, path, replace=True)
|
|
|
|
|
|
applied = result.get("applied") or {}
|
|
|
|
|
|
mom_applied = True
|
|
|
|
|
|
emit_thinking(
|
|
|
|
|
|
"MOM 入库完成",
|
|
|
|
|
|
"、".join(f"{k}{v}" for k, v in applied.items()) or "无新增",
|
|
|
|
|
|
status="done", pct=95,
|
|
|
|
|
|
)
|
|
|
|
|
|
stats = result.get("stats") or {}
|
|
|
|
|
|
if stats.get("note"):
|
|
|
|
|
|
sources.append(str(stats["note"]))
|
|
|
|
|
|
break
|
|
|
|
|
|
except Exception as exc:
|
|
|
|
|
|
emit_thinking("MOM Excel 解析失败", str(exc), status="warn")
|
|
|
|
|
|
if not mom_applied:
|
|
|
|
|
|
sources.append(f"Excel×{len(xls)}")
|
|
|
|
|
|
try:
|
|
|
|
|
|
emit_thinking("解析并写入 Excel/CSV", f"{len(xls)} 个表格", pct=96)
|
|
|
|
|
|
if explicit_input:
|
|
|
|
|
|
folder = _direct_table_report(
|
|
|
|
|
|
world,
|
|
|
|
|
|
[os.path.join(work, name) for name in xls],
|
|
|
|
|
|
project_name=(proj or {}).get("name") or "当前项目",
|
|
|
|
|
|
work_dir=work,
|
|
|
|
|
|
)
|
|
|
|
|
|
else:
|
|
|
|
|
|
from server.aps_domain.folder_pack import analyze_work_dir
|
|
|
|
|
|
folder = analyze_work_dir(world, session_id)
|
|
|
|
|
|
apply_folder_batches(folder)
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
folder = None
|
|
|
|
|
|
else:
|
|
|
|
|
|
try:
|
|
|
|
|
|
if explicit_input:
|
|
|
|
|
|
folder = _direct_table_report(
|
|
|
|
|
|
world,
|
|
|
|
|
|
[os.path.join(work, name) for name in xls],
|
|
|
|
|
|
project_name=(proj or {}).get("name") or "当前项目",
|
|
|
|
|
|
work_dir=work,
|
|
|
|
|
|
)
|
|
|
|
|
|
else:
|
|
|
|
|
|
from server.aps_domain.folder_pack import analyze_work_dir
|
|
|
|
|
|
folder = analyze_work_dir(world, session_id)
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
folder = None
|
|
|
|
|
|
else:
|
|
|
|
|
|
folder = None
|
|
|
|
|
|
|
|
|
|
|
|
if sqls and xls:
|
2026-07-28 02:12:46 +08:00
|
|
|
|
sources.append(f"Excel×{len(xls)}")
|
|
|
|
|
|
try:
|
|
|
|
|
|
emit_thinking("核对 Excel/CSV", f"{len(xls)} 个表格", pct=96)
|
2026-08-20 11:39:21 +08:00
|
|
|
|
if explicit_input:
|
|
|
|
|
|
folder = _direct_table_report(
|
|
|
|
|
|
world,
|
|
|
|
|
|
[os.path.join(work, name) for name in xls],
|
|
|
|
|
|
project_name=(proj or {}).get("name") or "当前项目",
|
|
|
|
|
|
work_dir=work,
|
|
|
|
|
|
)
|
|
|
|
|
|
else:
|
|
|
|
|
|
from server.aps_domain.folder_pack import analyze_work_dir
|
|
|
|
|
|
folder = analyze_work_dir(world, session_id)
|
|
|
|
|
|
apply_folder_batches(folder)
|
2026-07-28 02:12:46 +08:00
|
|
|
|
except Exception:
|
|
|
|
|
|
folder = None
|
|
|
|
|
|
else:
|
|
|
|
|
|
folder = None
|
|
|
|
|
|
emit_thinking("未挂工程目录", "仅分析当前项目世界里的数据", status="warn", pct=10)
|
|
|
|
|
|
|
|
|
|
|
|
# 知识库补洞:缺工艺的产品
|
|
|
|
|
|
emit_thinking("检查齐备度与知识库", "缺工艺时自动匹配行业模板", pct=97)
|
|
|
|
|
|
active = [o for o in (world.get("flexOrders") or [])
|
|
|
|
|
|
if (o.get("status") or "") not in ("DONE", "CANCELLED")]
|
|
|
|
|
|
pcs = list({str(o.get("productCode") or "") for o in active})
|
|
|
|
|
|
kb_actions = _fill_from_knowledge(world, pcs)
|
|
|
|
|
|
if kb_actions:
|
|
|
|
|
|
emit_thinking(
|
|
|
|
|
|
"知识库补洞",
|
|
|
|
|
|
f"{len(kb_actions)} 项(模板/占位/待客户补)",
|
|
|
|
|
|
status="done", pct=98,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
ready = check_readiness(world)
|
|
|
|
|
|
emit_thinking("整理分析报告", f"订单 {len(active)} 张", status="done", pct=99)
|
|
|
|
|
|
mats = world.get("flexMaterials") or []
|
|
|
|
|
|
routes = world.get("flexRoutings") or []
|
|
|
|
|
|
equip = [e for e in (world.get("flexEquipment") or []) if e.get("status") == "RUNNING"]
|
|
|
|
|
|
bom = world.get("flexBom") or []
|
|
|
|
|
|
|
|
|
|
|
|
order_rows = [{
|
|
|
|
|
|
"orderNo": o.get("orderNo"),
|
|
|
|
|
|
"productCode": o.get("productCode"),
|
|
|
|
|
|
"productName": o.get("productName") or next(
|
|
|
|
|
|
(m.get("name") for m in mats if m.get("code") == o.get("productCode")), ""),
|
|
|
|
|
|
"quantity": o.get("quantity"),
|
|
|
|
|
|
"dueDate": o.get("dueDate"),
|
|
|
|
|
|
"customerName": o.get("customerName") or "",
|
|
|
|
|
|
"ready": next((r["ready"] for r in ready["orders"] if r["orderNo"] == o.get("orderNo")), False),
|
|
|
|
|
|
} for o in active[:80]]
|
|
|
|
|
|
|
|
|
|
|
|
# 主数据抽样:订单涉及的产品优先
|
|
|
|
|
|
need_codes = {r["productCode"] for r in order_rows if r.get("productCode")}
|
|
|
|
|
|
mat_rows = []
|
|
|
|
|
|
for m in mats:
|
|
|
|
|
|
if need_codes and m.get("code") not in need_codes and len(mat_rows) >= 40:
|
|
|
|
|
|
continue
|
|
|
|
|
|
if m.get("code") in need_codes or len(mat_rows) < 40:
|
|
|
|
|
|
mat_rows.append({
|
|
|
|
|
|
"code": m.get("code"), "name": m.get("name"),
|
|
|
|
|
|
"type": m.get("type"), "unit": m.get("unit"), "stock": m.get("stock"),
|
|
|
|
|
|
})
|
|
|
|
|
|
if len(mat_rows) >= 60:
|
|
|
|
|
|
break
|
|
|
|
|
|
|
|
|
|
|
|
route_rows = []
|
|
|
|
|
|
for r in routes:
|
|
|
|
|
|
if need_codes and r.get("productCode") not in need_codes and len(route_rows) >= 40:
|
|
|
|
|
|
continue
|
|
|
|
|
|
route_rows.append({
|
|
|
|
|
|
"productCode": r.get("productCode"),
|
|
|
|
|
|
"seq": r.get("seq"),
|
|
|
|
|
|
"operationCode": r.get("operationCode"),
|
|
|
|
|
|
"operationName": r.get("operationName") or r.get("operationCode"),
|
|
|
|
|
|
"stdMin": r.get("stdTimePerUnit"),
|
|
|
|
|
|
})
|
|
|
|
|
|
if len(route_rows) >= 80:
|
|
|
|
|
|
break
|
|
|
|
|
|
|
|
|
|
|
|
equip_rows = [{
|
|
|
|
|
|
"code": e.get("code"), "name": e.get("name"),
|
|
|
|
|
|
"capabilities": "、".join((e.get("capabilities") or [])[:6]),
|
|
|
|
|
|
} for e in equip[:40]]
|
|
|
|
|
|
|
|
|
|
|
|
# 缺口 plan:先库后客户(同类缺口合并,避免刷屏)
|
|
|
|
|
|
plan: list[str] = []
|
|
|
|
|
|
need_rt = [a for a in kb_actions if a.get("type") == "need_routing"]
|
|
|
|
|
|
applied_tpl = [a for a in kb_actions if a.get("type") == "template_applied"]
|
|
|
|
|
|
other_kb = [a for a in kb_actions if a.get("type") not in ("need_routing", "template_applied")]
|
|
|
|
|
|
if applied_tpl:
|
|
|
|
|
|
plan.append(f"✓ 已用知识库模板补工艺 {len(applied_tpl)} 个产品")
|
|
|
|
|
|
for a in applied_tpl[:5]:
|
|
|
|
|
|
plan.append(f" · {a.get('productCode')} ← {a.get('templateName') or a.get('template')}")
|
|
|
|
|
|
if len(applied_tpl) > 5:
|
|
|
|
|
|
plan.append(f" · …另有 {len(applied_tpl) - 5} 个")
|
|
|
|
|
|
if need_rt:
|
|
|
|
|
|
samples = "、".join(str(a.get("productCode") or "") for a in need_rt[:8])
|
|
|
|
|
|
more = f" 等共 {len(need_rt)} 个" if len(need_rt) > 8 else f"({len(need_rt)} 个)"
|
|
|
|
|
|
plan.append(
|
|
|
|
|
|
f"△ SQL/知识库仍未挂上工艺的产品:{samples}{more}。"
|
|
|
|
|
|
f"若现场库本有工艺,请重新「分析项目」(工艺按订单 craftl 关联,不是中间件料号)"
|
|
|
|
|
|
)
|
|
|
|
|
|
for a in other_kb[:6]:
|
|
|
|
|
|
plan.append(f"· {a['detail']}")
|
|
|
|
|
|
s = ready["summary"]
|
|
|
|
|
|
if s.get("blocked"):
|
|
|
|
|
|
plan.append(f"△ 仍有 {s['blocked']} 张单齐备度未过,见订单明细「是否可排」列")
|
|
|
|
|
|
if not active:
|
|
|
|
|
|
plan.append("△ 没有订单:请在工程目录放订单表/SQL,或说「新建订单」")
|
|
|
|
|
|
if not equip:
|
|
|
|
|
|
plan.append("△ 没有可用设备:请提供设备台账,或从 SQL 的 md_equipment 导入")
|
|
|
|
|
|
if not plan:
|
|
|
|
|
|
plan.append("✓ 主数据与知识库已齐,可直接说「给我排产」")
|
|
|
|
|
|
|
|
|
|
|
|
proj_name = (proj or {}).get("name") or "当前项目"
|
|
|
|
|
|
md_lines = [
|
|
|
|
|
|
f"**项目深度分析 · {proj_name}**",
|
|
|
|
|
|
"",
|
|
|
|
|
|
f"数据来源:{'、'.join(sources) if sources else '当前项目世界(未挂工程目录文件)'}",
|
|
|
|
|
|
]
|
2026-08-20 11:39:21 +08:00
|
|
|
|
# 绑定当前登录用户
|
|
|
|
|
|
try:
|
|
|
|
|
|
from server.knowledge.ingest import bind_world_to_identity, ingest_project_analyze_report
|
|
|
|
|
|
owner_meta = bind_world_to_identity(world, sources=sources)
|
|
|
|
|
|
md_lines.append(
|
|
|
|
|
|
f"归属:{owner_meta.get('ownerFullname') or owner_meta.get('ownerUsername')} "
|
|
|
|
|
|
f"(用户 {owner_meta.get('ownerUserId')} · 租户 {str(owner_meta.get('tenantUuid') or '')[:8]}…)"
|
|
|
|
|
|
)
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
owner_meta = {}
|
|
|
|
|
|
ingest_project_analyze_report = None # type: ignore
|
|
|
|
|
|
|
2026-07-28 02:12:46 +08:00
|
|
|
|
if applied:
|
|
|
|
|
|
md_lines.append(
|
2026-08-20 11:39:21 +08:00
|
|
|
|
"已入库:"
|
2026-07-28 02:12:46 +08:00
|
|
|
|
+ "、".join(f"{k} {v}" for k, v in applied.items())
|
|
|
|
|
|
)
|
|
|
|
|
|
if sql_preview and sql_preview.get("fieldMapText"):
|
|
|
|
|
|
md_lines.append(f"SQL 包识别:{sql_preview['fieldMapText']}")
|
|
|
|
|
|
md_lines += [
|
|
|
|
|
|
"",
|
|
|
|
|
|
f"订单 **{len(active)}** · 物料 **{len(mats)}** · 工艺步 **{len(routes)}** · "
|
|
|
|
|
|
f"设备 **{len(equip)}** · BOM **{len(bom)}**",
|
|
|
|
|
|
"",
|
|
|
|
|
|
"### 订单明细(本项目)",
|
|
|
|
|
|
"",
|
|
|
|
|
|
"| 订单号 | 产品 | 名称 | 数量 | 交期 | 客户 | 可排 |",
|
|
|
|
|
|
"| --- | --- | --- | ---: | --- | --- | --- |",
|
|
|
|
|
|
]
|
|
|
|
|
|
for r in order_rows[:30]:
|
|
|
|
|
|
md_lines.append(
|
|
|
|
|
|
f"| {r['orderNo']} | {r['productCode']} | {str(r.get('productName') or '')[:16]} "
|
|
|
|
|
|
f"| {r['quantity']} | {r['dueDate'] or '—'} | {str(r.get('customerName') or '')[:12]} "
|
|
|
|
|
|
f"| {'✓' if r['ready'] else '×'} |"
|
|
|
|
|
|
)
|
|
|
|
|
|
if len(order_rows) > 30:
|
|
|
|
|
|
md_lines.append(f"| … | 另有 {len(order_rows) - 30} 张 | | | | | |")
|
|
|
|
|
|
|
|
|
|
|
|
md_lines += [
|
|
|
|
|
|
"",
|
|
|
|
|
|
"### 主数据(产品/物料抽样)",
|
|
|
|
|
|
"",
|
|
|
|
|
|
"| 编码 | 名称 | 类型 | 单位 | 库存 |",
|
|
|
|
|
|
"| --- | --- | --- | --- | ---: |",
|
|
|
|
|
|
]
|
|
|
|
|
|
for m in mat_rows[:25]:
|
|
|
|
|
|
md_lines.append(
|
|
|
|
|
|
f"| {m['code']} | {str(m.get('name') or '')[:20]} | {m.get('type')} "
|
|
|
|
|
|
f"| {m.get('unit')} | {m.get('stock') or 0} |"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
md_lines += ["", "### 补数 Plan(先知识库/数据库,再找客户)", ""]
|
|
|
|
|
|
for i, p in enumerate(plan[:12], 1):
|
|
|
|
|
|
md_lines.append(f"{i}. {p}")
|
|
|
|
|
|
|
2026-08-20 11:39:21 +08:00
|
|
|
|
result = {
|
2026-07-28 02:12:46 +08:00
|
|
|
|
"ok": True,
|
|
|
|
|
|
"projectName": proj_name,
|
|
|
|
|
|
"workDir": work,
|
|
|
|
|
|
"sources": sources,
|
2026-08-20 11:39:21 +08:00
|
|
|
|
"sourcePaths": source_paths,
|
2026-07-28 02:12:46 +08:00
|
|
|
|
"applied": applied,
|
2026-08-20 11:39:21 +08:00
|
|
|
|
"owner": {
|
|
|
|
|
|
"userId": owner_meta.get("ownerUserId"),
|
|
|
|
|
|
"username": owner_meta.get("ownerUsername"),
|
|
|
|
|
|
"fullname": owner_meta.get("ownerFullname"),
|
|
|
|
|
|
"tenantUuid": owner_meta.get("tenantUuid"),
|
|
|
|
|
|
} if owner_meta else None,
|
2026-07-28 02:12:46 +08:00
|
|
|
|
"sqlPreview": {
|
|
|
|
|
|
"filename": (sql_preview or {}).get("filename"),
|
|
|
|
|
|
"fieldMapText": (sql_preview or {}).get("fieldMapText"),
|
|
|
|
|
|
"tables": (sql_preview or {}).get("tables") or [],
|
|
|
|
|
|
"error": (sql_preview or {}).get("error"),
|
|
|
|
|
|
} if sql_preview else None,
|
2026-08-20 11:39:21 +08:00
|
|
|
|
"folder": folder,
|
2026-07-28 02:12:46 +08:00
|
|
|
|
"orders": order_rows,
|
|
|
|
|
|
"materials": mat_rows,
|
|
|
|
|
|
"routings": route_rows,
|
|
|
|
|
|
"equipment": equip_rows,
|
|
|
|
|
|
"kbActions": kb_actions,
|
|
|
|
|
|
"plan": plan,
|
|
|
|
|
|
"readiness": ready,
|
|
|
|
|
|
"summary": {
|
|
|
|
|
|
"orders": len(active), "materials": len(mats),
|
|
|
|
|
|
"routings": len(routes), "equipment": len(equip), "bom": len(bom),
|
|
|
|
|
|
"ready": s.get("ready", 0), "blocked": s.get("blocked", 0),
|
|
|
|
|
|
},
|
|
|
|
|
|
"canSchedule": s.get("ready", 0) > 0 and not ready.get("globalIssues"),
|
|
|
|
|
|
"markdown": "\n".join(md_lines),
|
|
|
|
|
|
}
|
2026-08-20 11:39:21 +08:00
|
|
|
|
|
|
|
|
|
|
# 分析结果写入当前租户知识库(平台知识模块,按登录租户隔离)
|
|
|
|
|
|
knowledge_ingest: dict[str, Any] = {}
|
|
|
|
|
|
try:
|
|
|
|
|
|
has_data = bool(
|
|
|
|
|
|
result["summary"]["materials"]
|
|
|
|
|
|
or result["summary"]["orders"]
|
|
|
|
|
|
or result["summary"]["equipment"]
|
|
|
|
|
|
or result["summary"]["bom"]
|
|
|
|
|
|
or result.get("sourcePaths")
|
|
|
|
|
|
)
|
|
|
|
|
|
if ingest_project_analyze_report is not None and has_data:
|
|
|
|
|
|
knowledge_ingest = ingest_project_analyze_report(result, world=world)
|
|
|
|
|
|
if knowledge_ingest.get("assetId"):
|
|
|
|
|
|
md_lines.append("")
|
|
|
|
|
|
md_lines.append(
|
|
|
|
|
|
f"已写入知识库:《{knowledge_ingest.get('title')}》"
|
|
|
|
|
|
f"({knowledge_ingest.get('chunkCount')} 段,{knowledge_ingest.get('version')})"
|
|
|
|
|
|
)
|
|
|
|
|
|
result["markdown"] = "\n".join(md_lines)
|
|
|
|
|
|
except Exception as exc:
|
|
|
|
|
|
knowledge_ingest = {"error": str(exc)}
|
|
|
|
|
|
result["knowledgeIngest"] = knowledge_ingest
|
|
|
|
|
|
return result
|