修复显式数据源串用旧数据

支持通过目录和文件名加载标准 APS JSON 数据包;表格目录改为先导入独立候选世界,未读取到文件或数据不完整时拒绝排产,避免沿用当前会话数据。补充 JSON 解析、成功排产和空目录防串数据测试。
This commit is contained in:
ssk 2026-09-04 16:55:33 +08:00
parent cd8806f649
commit b8fb217c09
3 changed files with 121 additions and 24 deletions

View File

@ -210,7 +210,7 @@ def _is_imperative_schedule(text: str) -> bool:
def _extract_explicit_data_source(text: str) -> dict[str, str]: def _extract_explicit_data_source(text: str) -> dict[str, str]:
"""Extract an absolute data directory/file pair from a chat command.""" """Extract an absolute data directory/file pair from a chat command."""
t = str(text or "") t = str(text or "")
ext = r"(?:xlsx|xlsm|csv|txt|sql)" ext = r"(?:xlsx|xlsm|csv|txt|sql|json)"
file_match = re.search(rf"([A-Za-z]:[\\/][^\r\n\"'<>|,,。;;]*?\.{ext})", t, re.I) file_match = re.search(rf"([A-Za-z]:[\\/][^\r\n\"'<>|,,。;;]*?\.{ext})", t, re.I)
dir_match = re.search(r"([A-Za-z]:[\\/][^\r\n\"'<>|,,。;;]+?)\s*目录下", t, re.I) dir_match = re.search(r"([A-Za-z]:[\\/][^\r\n\"'<>|,,。;;]+?)\s*目录下", t, re.I)
data_dir = "" data_dir = ""
@ -261,7 +261,7 @@ def parse_fast(text: str, world: World) -> IntentResult | None:
params["drawingAction"] = "analyze" params["drawingAction"] = "analyze"
return hit("data.analyze", params) return hit("data.analyze", params)
# 带真实表格/SQL 路径的口令优先按数据处理,不能被句尾「知识库」误吞。 # 带真实表格/SQL 路径的口令优先按数据处理,不能被句尾「知识库」误吞。
has_data_path = bool(re.search(r"[A-Za-z]:[\\/].*\.(xlsx|xlsm|csv|txt|sql)\b", t, re.I)) has_data_path = bool(re.search(r"[A-Za-z]:[\\/].*\.(xlsx|xlsm|csv|txt|sql|json)\b", t, re.I))
explicit_data_algorithm_schedule = bool( explicit_data_algorithm_schedule = bool(
re.search(r"(?:使用|用).+?数据(?:包)?", t, re.I) re.search(r"(?:使用|用).+?数据(?:包)?", t, re.I)
and re.search(r"(?:算法|\b(?:EDD|SPT|PRIORITY|FIFO|LPT|CR|ATC)\b|\boptimize\b)", t, re.I) and re.search(r"(?:算法|\b(?:EDD|SPT|PRIORITY|FIFO|LPT|CR|ATC)\b|\boptimize\b)", t, re.I)

View File

@ -1807,7 +1807,6 @@ def _run_flex(store: WorldStore, intent: IntentResult, actor: str,
data_dir = str(intent.params.get("dataDir") or "").strip() data_dir = str(intent.params.get("dataDir") or "").strip()
data_file = str(intent.params.get("dataFile") or "").strip() data_file = str(intent.params.get("dataFile") or "").strip()
if data_path or data_dir: if data_path or data_dir:
from server.aps_domain.project_analyze import analyze_project_deep
source_query = data_path or (os.path.join(data_dir, data_file) if data_file else data_dir) source_query = data_path or (os.path.join(data_dir, data_file) if data_file else data_dir)
if data_file and not os.path.isfile(source_query): if data_file and not os.path.isfile(source_query):
return AgentReply(text=f"数据文件不存在:{source_query},没有执行排产。") return AgentReply(text=f"数据文件不存在:{source_query},没有执行排产。")
@ -1815,27 +1814,61 @@ def _run_flex(store: WorldStore, intent: IntentResult, actor: str,
return AgentReply(text=f"数据目录不存在:{data_dir},没有执行排产。") return AgentReply(text=f"数据目录不存在:{data_dir},没有执行排产。")
if data_path and not data_file and not os.path.isfile(data_path) and not os.path.isdir(data_path): if data_path and not data_file and not os.path.isfile(data_path) and not os.path.isdir(data_path):
return AgentReply(text=f"数据路径不存在:{data_path},没有执行排产。") return AgentReply(text=f"数据路径不存在:{data_path},没有执行排产。")
try: if os.path.isfile(source_query) and source_query.lower().endswith(".json"):
source_report = analyze_project_deep( from server.state.packs import load_pack
store.data, session_id, apply_sql=True, try:
query=source_query, next_id=store.next_id, candidate_world = load_pack(source_query)
) except (OSError, ValueError, TypeError) as exc:
except (OSError, PermissionError, ValueError) as exc: return AgentReply(text=f"APS 数据包读取失败:{exc},没有执行排产。")
return AgentReply(text=f"数据文件读取失败:{exc}") required = ("flexOrders", "flexRoutings", "flexEquipment")
if not source_report.get("ok"): if not isinstance(candidate_world, dict) or not all(candidate_world.get(key) for key in required):
return AgentReply(text=f"数据目录分析失败:{source_report.get('error') or '目录不可用'}") return AgentReply(
if not source_report.get("canSchedule"): text="该 JSON 不是可排产的 APS 数据包:至少需要订单、工艺和设备数据,没有执行排产。",
missing = (source_report.get("plan") or [])[:3] )
detail = ";".join(str(item) for item in missing) store.data = candidate_world
return AgentReply( store._reset_counters()
text=(f"数据已读取,但暂时不能排产:缺少可排所需的订单、工艺或设备信息。" data_pack = {
+ (f"\n{detail}" if detail else "")), "file": os.path.basename(source_query),
) "name": os.path.splitext(os.path.basename(source_query))[0],
data_source = { "path": source_query,
"directory": source_report.get("workDir") or data_dir, }
"file": data_file or (os.path.basename(data_path) if data_path else None), data_source = {
"paths": source_report.get("sourcePaths") or [], "directory": os.path.dirname(source_query),
} "file": os.path.basename(source_query),
"paths": [os.path.abspath(source_query)],
}
else:
from server.aps_domain.project_analyze import analyze_project_deep
from server.state.seed import empty_world
candidate_world = empty_world()
try:
source_report = analyze_project_deep(
candidate_world, session_id, apply_sql=True,
query=source_query, next_id=store.next_id,
)
except (OSError, PermissionError, ValueError) as exc:
return AgentReply(text=f"数据文件读取失败:{exc}")
if not source_report.get("ok"):
return AgentReply(text=f"数据目录分析失败:{source_report.get('error') or '目录不可用'}")
source_paths = source_report.get("sourcePaths") or []
if not source_paths:
return AgentReply(
text="指定位置没有找到可读取的排产数据文件,没有执行排产。",
)
if not source_report.get("canSchedule"):
missing = (source_report.get("plan") or [])[:3]
detail = ";".join(str(item) for item in missing)
return AgentReply(
text=(f"数据已读取,但暂时不能排产:缺少可排所需的订单、工艺或设备信息。"
+ (f"\n{detail}" if detail else "")),
)
store.data = candidate_world
store._reset_counters()
data_source = {
"directory": source_report.get("workDir") or data_dir,
"file": data_file or (os.path.basename(data_path) if data_path else None),
"paths": source_paths,
}
data_pack_ref = str(intent.params.get("dataPackRef") or "").strip() data_pack_ref = str(intent.params.get("dataPackRef") or "").strip()
if data_pack_ref: if data_pack_ref:
from server.state.packs import load_pack, resolve_pack_reference from server.state.packs import load_pack, resolve_pack_reference

View File

@ -5,6 +5,7 @@ from pathlib import Path
from server.agent_core.intent import parse_fast from server.agent_core.intent import parse_fast
from server.aps_domain.workflow import handle_intent from server.aps_domain.workflow import handle_intent
from server.contracts import IntentResult
from server.state.packs import load_pack, resolve_pack_reference from server.state.packs import load_pack, resolve_pack_reference
from server.state.store import WorldStore from server.state.store import WorldStore
@ -49,6 +50,21 @@ def test_chat_command_parses_directory_file_and_algorithm_slots():
assert result.params["engine"] == "OPTIMIZE" assert result.params["engine"] == "OPTIMIZE"
def test_chat_command_parses_directory_json_pack_and_algorithm_slots():
result = parse_fast(
r"使用 D:\aps-data 目录下的 optimize-simulation-v1.json 数据用 EDD 算法生成排产方案",
{},
)
assert result is not None
assert result.intent == "flex.schedule"
assert result.params["dataDir"] == r"D:\aps-data"
assert result.params["dataFile"] == "optimize-simulation-v1.json"
assert result.params["dataPath"].endswith(r"aps-data\optimize-simulation-v1.json")
assert result.params["sortMode"] == "EDD"
assert result.params["engine"] == "OPTIMIZE"
def test_chat_command_loads_pack_and_materializes_optimize_result(tmp_path, monkeypatch): def test_chat_command_loads_pack_and_materializes_optimize_result(tmp_path, monkeypatch):
monkeypatch.setenv("APS_DB_DISABLED", "1") monkeypatch.setenv("APS_DB_DISABLED", "1")
store = WorldStore(path=str(tmp_path / "world.json")) store = WorldStore(path=str(tmp_path / "world.json"))
@ -71,6 +87,54 @@ def test_chat_command_loads_pack_and_materializes_optimize_result(tmp_path, monk
assert store.data["flexScheduleVersions"][-1]["algorithmId"] == "optimize.edd" assert store.data["flexScheduleVersions"][-1]["algorithmId"] == "optimize.edd"
def test_explicit_json_pack_path_materializes_optimize_result(tmp_path, monkeypatch):
monkeypatch.setenv("APS_DB_DISABLED", "1")
store = WorldStore(path=str(tmp_path / "world.json"))
intent = IntentResult(
intent="flex.schedule",
params={
"dataDir": str(PACK_PATH.parent),
"dataFile": PACK_PATH.name,
"dataPath": str(PACK_PATH),
"engine": "OPTIMIZE",
"sortMode": "EDD",
},
confidence=0.95,
source="RULE_FAST",
)
reply = asyncio.run(handle_intent(store, "round88-json", intent, actor="test"))
block = next(block for block in reply.blocks if block.type == "flex-schedule")
assert block.props["dataPack"]["file"] == PACK_PATH.name
assert block.props["dataSource"]["file"] == PACK_PATH.name
assert block.props["dataSource"]["paths"] == [str(PACK_PATH.resolve())]
assert block.props["algorithm"]["rule"] == "EDD"
assert block.props["stats"]["vlCount"] == 10
assert block.props["stats"]["woCount"] == 72
def test_empty_explicit_directory_does_not_reuse_current_world(tmp_path, monkeypatch):
monkeypatch.setenv("APS_DB_DISABLED", "1")
store = WorldStore(path=str(tmp_path / "world.json"))
store.data = load_pack(str(PACK_PATH))
store._reset_counters()
before_versions = len(store.data.get("flexScheduleVersions") or [])
empty_dir = tmp_path / "empty-data"
empty_dir.mkdir()
intent = IntentResult(
intent="flex.schedule",
params={"dataDir": str(empty_dir), "engine": "OPTIMIZE", "sortMode": "EDD"},
confidence=0.95,
source="RULE_FAST",
)
reply = asyncio.run(handle_intent(store, "round88-empty", intent, actor="test"))
assert "没有找到可读取的排产数据文件" in reply.text
assert len(store.data.get("flexScheduleVersions") or []) == before_versions
def test_unknown_pack_fails_closed_without_creating_schedule(tmp_path, monkeypatch): def test_unknown_pack_fails_closed_without_creating_schedule(tmp_path, monkeypatch):
monkeypatch.setenv("APS_DB_DISABLED", "1") monkeypatch.setenv("APS_DB_DISABLED", "1")
store = WorldStore(path=str(tmp_path / "world.json")) store = WorldStore(path=str(tmp_path / "world.json"))