dsh-engineering-ui 0.0.0-stage → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +772 -3
- package/engineering/plugins/dsh-engineering-ui/cordis.patch.yml +5 -0
- package/engineering/plugins/dsh-engineering-ui/icon.svg +14 -0
- package/engineering/plugins/dsh-engineering-ui/lib/client.js +8259 -0
- package/engineering/plugins/dsh-engineering-ui/lib/defense-sign.js +1552 -0
- package/engineering/plugins/dsh-engineering-ui/lib/index.js +5633 -0
- package/engineering/plugins/dsh-engineering-ui/lib/job-wait-guard.js +236 -0
- package/engineering/plugins/dsh-engineering-ui/lib/mem-guard.js +331 -0
- package/engineering/plugins/dsh-engineering-ui/lib/webots.js +114 -0
- package/engineering/plugins/dsh-engineering-ui/locale/en.json +6 -0
- package/engineering/plugins/dsh-engineering-ui/locale/zh.json +6 -0
- package/engineering/plugins/dsh-engineering-ui/preset-engineering.patch.yml +601 -0
- package/engineering/skills/assembly-orchestration/SKILL.md +651 -0
- package/engineering/skills/cad-workflow/SKILL.md +132 -0
- package/engineering/skills/mode-selection/SKILL.md +660 -0
- package/engineering/skills/physics-in-loop/SKILL.md +238 -0
- package/engineering/skills/physics-in-loop/references/feadocs.md +142 -0
- package/engineering/skills/solidworks-bridge/SKILL.md +445 -0
- package/engineering/skills/sw-design/SKILL.md +112 -0
- package/engineering/skills/sw-design/references/autocad-workflow.md +81 -0
- package/engineering/skills/sw-design/references/gbt-drafting.md +71 -0
- package/engineering/skills/sw-to-cad/SKILL.md +121 -0
- package/engineering/skills/webots-simulation/SKILL.md +58 -0
- package/engineering/tools/LICENSE.Auto-SolidWorks +21 -0
- package/engineering/tools/_plugin_paths.py +93 -0
- package/engineering/tools/_root.py +330 -0
- package/engineering/tools/_store.py +446 -0
- package/engineering/tools/ac_bridge.py +459 -0
- package/engineering/tools/ac_rules.json +21 -0
- package/engineering/tools/ac_validate.py +699 -0
- package/engineering/tools/ask_user.py +887 -0
- package/engineering/tools/build_arm1.py +46 -0
- package/engineering/tools/build_assembly.py +94 -0
- package/engineering/tools/build_base.py +57 -0
- package/engineering/tools/build_housing.py +70 -0
- package/engineering/tools/build_structural.py +43 -0
- package/engineering/tools/build_transmission.py +69 -0
- package/engineering/tools/cadx.lsp +128 -0
- package/engineering/tools/choice_contract.py +61 -0
- package/engineering/tools/conn_state.py +1671 -0
- package/engineering/tools/defense_gate.py +3374 -0
- package/engineering/tools/drawing_input.py +314 -0
- package/engineering/tools/env_check.py +467 -0
- package/engineering/tools/examples/build_axis.py +142 -0
- package/engineering/tools/examples/build_cup.py +56 -0
- package/engineering/tools/examples/build_plate.py +58 -0
- package/engineering/tools/examples/build_sphere.py +30 -0
- package/engineering/tools/examples/build_transition_duct.py +381 -0
- package/engineering/tools/gate_integrity.py +401 -0
- package/engineering/tools/mode2_flow.py +3589 -0
- package/engineering/tools/mode_checker.py +77 -0
- package/engineering/tools/mode_gate.py +8013 -0
- package/engineering/tools/part_registry.py +4266 -0
- package/engineering/tools/physics/__init__.py +50 -0
- package/engineering/tools/physics/_compat.py +12 -0
- package/engineering/tools/physics/design_state.py +142 -0
- package/engineering/tools/physics/domain_validator.py +751 -0
- package/engineering/tools/physics/fatigue.py +489 -0
- package/engineering/tools/physics/fea_solver.py +1163 -0
- package/engineering/tools/physics/feapy_solver.py +314 -0
- package/engineering/tools/physics/gb_materials.json +1228 -0
- package/engineering/tools/physics/geometry_gate.py +337 -0
- package/engineering/tools/physics/load_case.py +951 -0
- package/engineering/tools/physics/material_db.py +874 -0
- package/engineering/tools/physics/mesh_adapter.py +321 -0
- package/engineering/tools/physics/refine_rules.py +272 -0
- package/engineering/tools/physics/rules/gb_base_rules.json +377 -0
- package/engineering/tools/physics/rules/gb_drafting_rules.json +57 -0
- package/engineering/tools/physics/rules/gb_electrical_rules.json +25 -0
- package/engineering/tools/physics/rules/gb_fasteners_rules.json +73 -0
- package/engineering/tools/physics/rules/gb_housing_rules.json +41 -0
- package/engineering/tools/physics/rules/gb_manufacturing_rules.json +25 -0
- package/engineering/tools/physics/rules/gb_material_verify_rules.json +72 -0
- package/engineering/tools/physics/rules/gb_materials_rules.json +377 -0
- package/engineering/tools/physics/rules/gb_safety_rules.json +89 -0
- package/engineering/tools/physics/rules/gb_springs_rules.json +41 -0
- package/engineering/tools/physics/rules/gb_standards_registry.json +1646 -0
- package/engineering/tools/physics/rules/gb_structural_rules.json +201 -0
- package/engineering/tools/physics/rules/gb_surface_rules.json +25 -0
- package/engineering/tools/physics/rules/gb_transmission_rules.json +233 -0
- package/engineering/tools/physics/rules/housing_rules.json +48 -0
- package/engineering/tools/physics/rules/manifest.json +54 -0
- package/engineering/tools/physics/rules/mold_rules.json +48 -0
- package/engineering/tools/physics/rules/structural_rules.json +56 -0
- package/engineering/tools/physics/rules/transmission_rules.json +48 -0
- package/engineering/tools/physics/simulation_report.py +829 -0
- package/engineering/tools/physics_bridge.py +2685 -0
- package/engineering/tools/profile-manifest.ps1 +72 -0
- package/engineering/tools/run_regression.py +109 -0
- package/engineering/tools/settings.py +419 -0
- package/engineering/tools/solidworks-modeling.md +519 -0
- package/engineering/tools/sw_bridge.py +8215 -0
- package/engineering/tools/sw_guard.py +1066 -0
- package/engineering/tools/sw_surface.py +87 -0
- package/engineering/tools/swapi.py +14766 -0
- package/engineering/tools/verify_sync.py +218 -0
- package/engineering/tools/visual_check.py +2589 -0
- package/engineering/tools/webots_env.py +121 -0
- package/engineering/tools/webots_mcp/__init__.py +3 -0
- package/engineering/tools/webots_mcp/__main__.py +3 -0
- package/engineering/tools/webots_mcp/assembly.py +391 -0
- package/engineering/tools/webots_mcp/bootstrap.py +55 -0
- package/engineering/tools/webots_mcp/controller_worker.py +124 -0
- package/engineering/tools/webots_mcp/evidence.py +288 -0
- package/engineering/tools/webots_mcp/fake_worker.py +69 -0
- package/engineering/tools/webots_mcp/fault_resources.py +121 -0
- package/engineering/tools/webots_mcp/host.py +45 -0
- package/engineering/tools/webots_mcp/ledger.py +73 -0
- package/engineering/tools/webots_mcp/owned_job.py +70 -0
- package/engineering/tools/webots_mcp/physical_gate.py +187 -0
- package/engineering/tools/webots_mcp/physics.py +152 -0
- package/engineering/tools/webots_mcp/pipeline.py +108 -0
- package/engineering/tools/webots_mcp/recipes.py +129 -0
- package/engineering/tools/webots_mcp/repair.py +132 -0
- package/engineering/tools/webots_mcp/repair_trial.py +375 -0
- package/engineering/tools/webots_mcp/runtime.py +311 -0
- package/engineering/tools/webots_mcp/server.py +217 -0
- package/engineering/tools/webots_mcp/sw_export.py +509 -0
- package/engineering/tools/webots_mcp/transport.py +156 -0
- package/engineering/tools/workflow_gate.py +6134 -0
- package/package.json +64 -4
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""_store.py — 工程模式【状态目录单一事实源】(P0-3 写侧修复)
|
|
3
|
+
|
|
4
|
+
═══════════════════════════════════════════════════════════════════════════
|
|
5
|
+
【要解决的问题】状态双副本分裂
|
|
6
|
+
───────────────────────────────────────────────────────────────────────────
|
|
7
|
+
工程模式在磁盘上存在【两份副本】:
|
|
8
|
+
· 源码/工作区副本:<repo>\\engineering\\tools\\
|
|
9
|
+
· 安装副本: %DSH_HOME%\\.agent-presets\\engineering\\tools\\
|
|
10
|
+
两者都含完整脚本。而原实现里每个模块都用
|
|
11
|
+
_BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
12
|
+
把状态写到【自己所在的那份】——
|
|
13
|
+
· 从安装副本跑 mode_gate → 写安装副本的 mode_state.json
|
|
14
|
+
· 从工作区跑 workflow_gate → 写工作区的 workflow_state.json
|
|
15
|
+
于是同一次任务的状态被劈成两半:step、房间表、reports/ 凭据、防线 runtime
|
|
16
|
+
全部各写各的。实测表现:
|
|
17
|
+
· workflow_state.json:工作区 step=user_selected vs 安装 step=depth_asked
|
|
18
|
+
· mode_state.json :工作区 1 个房间 vs 安装 0 个房间
|
|
19
|
+
· reports/ :工作区有 physics/domain 凭据,安装副本一个都没有
|
|
20
|
+
· 宿主守卫读安装副本 → 把"已完成"判成"未完成",反复 steer 拦住对话结束
|
|
21
|
+
|
|
22
|
+
【为什么不能只改读取侧】
|
|
23
|
+
只在读取侧做"取 mtime 最新"只是治标:写入仍在分叉,两个文件会持续互相
|
|
24
|
+
追赶,谁最后被写谁"赢",判定随调用顺序漂移。必须在【写入侧】收敛到唯一
|
|
25
|
+
目录,读取侧才有意义。
|
|
26
|
+
|
|
27
|
+
【本模块的解法】所有状态/凭据/报告的落点统一由本模块决定:
|
|
28
|
+
① 环境变量 DSH_STATE_DIR(最高优先,便于测试与多实例隔离)
|
|
29
|
+
② 环境变量 DSH_ENGINEERING_ROOT 推导(+ /tools)
|
|
30
|
+
③ _root.py 探测到的工程模式根(+ /tools)
|
|
31
|
+
④ 本文件所在目录(最终兜底,永不返回 None)
|
|
32
|
+
|
|
33
|
+
【重要兼容】状态目录与【脚本所在目录】解耦后,脚本仍可能从任一副本被调用;
|
|
34
|
+
因此本模块只负责"状态放哪",不改变脚本自身的可执行性。
|
|
35
|
+
═══════════════════════════════════════════════════════════════════════════
|
|
36
|
+
"""
|
|
37
|
+
from __future__ import annotations
|
|
38
|
+
|
|
39
|
+
import os
|
|
40
|
+
import sys
|
|
41
|
+
|
|
42
|
+
ENV_STATE_DIR = "DSH_STATE_DIR"
|
|
43
|
+
ENV_ROOT = "DSH_ENGINEERING_ROOT"
|
|
44
|
+
|
|
45
|
+
_cached = None
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _here():
|
|
49
|
+
return os.path.dirname(os.path.abspath(__file__))
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _from_env_state_dir():
|
|
53
|
+
v = (os.environ.get(ENV_STATE_DIR) or "").strip()
|
|
54
|
+
if v:
|
|
55
|
+
return os.path.abspath(v)
|
|
56
|
+
return None
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _from_env_root():
|
|
60
|
+
v = (os.environ.get(ENV_ROOT) or "").strip()
|
|
61
|
+
if not v:
|
|
62
|
+
return None
|
|
63
|
+
try:
|
|
64
|
+
# 允许 DSH_ENGINEERING_ROOT 指到 engineering 或 engineering\tools
|
|
65
|
+
cand = os.path.abspath(v)
|
|
66
|
+
if os.path.basename(cand).lower() == "tools":
|
|
67
|
+
return cand
|
|
68
|
+
return os.path.join(cand, "tools")
|
|
69
|
+
except Exception:
|
|
70
|
+
return None
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _from_root_module():
|
|
74
|
+
"""用 _root.py 的权威探测结果(不引入循环依赖:_root 不 import 本模块)。"""
|
|
75
|
+
try:
|
|
76
|
+
import _root
|
|
77
|
+
td = _root.tools_dir(required=False)
|
|
78
|
+
if td and os.path.isdir(td):
|
|
79
|
+
return os.path.abspath(td)
|
|
80
|
+
except Exception:
|
|
81
|
+
pass
|
|
82
|
+
return None
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _installed_tools_dir():
|
|
86
|
+
"""宿主所在的那份 tools 目录(安装副本)。
|
|
87
|
+
|
|
88
|
+
── 为什么它优先级很高 ────────────────────────────────────────────────
|
|
89
|
+
宿主(DSH 进程)是防线的【信任根】与守卫所在处:
|
|
90
|
+
· 它按【自己加载插件的那份 tools 目录】读 reports/*.physics.json 等凭据;
|
|
91
|
+
· turn-stopping 守卫也按那份读 workflow_state.json / mode_state.json。
|
|
92
|
+
因此【安装副本】才是状态与凭据的天然权威落点;工作区副本按 cmd_doctor
|
|
93
|
+
的既定说明"仅作分发,不应承载运行时状态"。
|
|
94
|
+
若 Python 侧把凭据写到工作区,宿主永远读不到 → 判定"缺少凭据"(P0-3)。
|
|
95
|
+
"""
|
|
96
|
+
home = os.environ.get("USERPROFILE") or os.environ.get("HOME") or ""
|
|
97
|
+
dsh_home = (os.environ.get("DSH_HOME") or "").strip()
|
|
98
|
+
cands = []
|
|
99
|
+
if dsh_home:
|
|
100
|
+
cands.append(os.path.join(dsh_home, ".agent-presets", "engineering", "tools"))
|
|
101
|
+
cands.append(os.path.join(dsh_home, "engineering", "tools"))
|
|
102
|
+
if home:
|
|
103
|
+
cands.append(os.path.join(home, ".dsh", ".agent-presets", "engineering", "tools"))
|
|
104
|
+
cands.append(os.path.join(home, ".dsh", "engineering", "tools"))
|
|
105
|
+
for c in cands:
|
|
106
|
+
try:
|
|
107
|
+
if os.path.isdir(c):
|
|
108
|
+
return os.path.abspath(c)
|
|
109
|
+
except Exception:
|
|
110
|
+
continue
|
|
111
|
+
return None
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _score(path):
|
|
115
|
+
"""候选目录的"可信度"打分:状态文件越新越多,越可能是当前真相。"""
|
|
116
|
+
if not path or not os.path.isdir(path):
|
|
117
|
+
return (-1, 0.0)
|
|
118
|
+
n = 0
|
|
119
|
+
newest = 0.0
|
|
120
|
+
for fn in ("workflow_state.json", "mode_state.json",
|
|
121
|
+
"TASK_FINISHED.json", "artifacts_registry.json",
|
|
122
|
+
"platform_status.json", "sw_state.json"):
|
|
123
|
+
try:
|
|
124
|
+
st = os.stat(os.path.join(path, fn))
|
|
125
|
+
n += 1
|
|
126
|
+
if st.st_mtime > newest:
|
|
127
|
+
newest = st.st_mtime
|
|
128
|
+
except Exception:
|
|
129
|
+
continue
|
|
130
|
+
return (n, newest)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def state_dir(required: bool = False) -> str:
|
|
134
|
+
"""返回【唯一】的状态目录(缓存)。
|
|
135
|
+
|
|
136
|
+
优先级(高 → 低,严格按序取第一个存在的):
|
|
137
|
+
① DSH_STATE_DIR(显式指定,测试/多实例隔离用)
|
|
138
|
+
② 安装副本 tools(宿主/守卫/信任根所在,天然权威)
|
|
139
|
+
③ DSH_ENGINEERING_ROOT
|
|
140
|
+
④ _root.py 的工程根探测
|
|
141
|
+
⑤ 本文件所在目录(兜底,必然存在,故永不返回 None)
|
|
142
|
+
|
|
143
|
+
── 【P0-3 修复·禁止跨目录回退】──────────────────────────────────────
|
|
144
|
+
这里【严格按优先级取第一个存在的目录】,不做"按 mtime 挑最新"、
|
|
145
|
+
也不因某目录暂无状态文件就跳去下一个。原因与 Node 侧同:
|
|
146
|
+
跨目录回退等于让状态在部署之间互相污染 —— 宿主守卫会因此读到
|
|
147
|
+
另一个部署的"任务已完成",该拦不拦(实测已复现)。
|
|
148
|
+
契约:认哪个目录就只认那个目录。
|
|
149
|
+
"""
|
|
150
|
+
global _cached
|
|
151
|
+
if _cached and os.path.isdir(_cached):
|
|
152
|
+
return _cached
|
|
153
|
+
|
|
154
|
+
for fn in (_from_env_state_dir, _installed_tools_dir,
|
|
155
|
+
_from_env_root, _from_root_module, _here):
|
|
156
|
+
try:
|
|
157
|
+
c = fn()
|
|
158
|
+
except Exception:
|
|
159
|
+
c = None
|
|
160
|
+
if c and os.path.isdir(c):
|
|
161
|
+
_cached = os.path.abspath(c)
|
|
162
|
+
return _cached
|
|
163
|
+
_cached = _here()
|
|
164
|
+
return _cached
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def state_path(name: str) -> str:
|
|
168
|
+
"""状态目录下的某个文件绝对路径。"""
|
|
169
|
+
return os.path.join(state_dir(), name)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def subdir(name: str) -> str:
|
|
173
|
+
"""状态目录下的子目录路径(不自动创建)。"""
|
|
174
|
+
return os.path.join(state_dir(), name)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def reset_cache():
|
|
178
|
+
"""清除缓存(测试用;环境变量变化后调用)。"""
|
|
179
|
+
global _cached
|
|
180
|
+
_cached = None
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _shadow_dirs():
|
|
184
|
+
"""除权威目录外的其他 tools 副本(可能承载着"还没搬过来"的新状态)。"""
|
|
185
|
+
out = []
|
|
186
|
+
try:
|
|
187
|
+
import _root
|
|
188
|
+
for c in (_root.tools_dir(required=False), _here()):
|
|
189
|
+
if not c:
|
|
190
|
+
continue
|
|
191
|
+
ap = os.path.abspath(c)
|
|
192
|
+
if os.path.isdir(ap) and os.path.normcase(ap) != os.path.normcase(os.path.abspath(state_dir())):
|
|
193
|
+
if ap not in out:
|
|
194
|
+
out.append(ap)
|
|
195
|
+
except Exception:
|
|
196
|
+
pass
|
|
197
|
+
# 环境变量声明的工作区
|
|
198
|
+
try:
|
|
199
|
+
v = (os.environ.get(ENV_ROOT) or "").strip()
|
|
200
|
+
if v:
|
|
201
|
+
ap = os.path.abspath(v if os.path.basename(v).lower() == "tools"
|
|
202
|
+
else os.path.join(v, "tools"))
|
|
203
|
+
if os.path.isdir(ap) and ap not in out and \
|
|
204
|
+
os.path.normcase(ap) != os.path.normcase(os.path.abspath(state_dir())):
|
|
205
|
+
out.append(ap)
|
|
206
|
+
except Exception:
|
|
207
|
+
pass
|
|
208
|
+
return out
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
# 需要跟随状态一起搬迁的运行时文件(凭据/报告/台账)
|
|
212
|
+
MIGRATE_FILES = (
|
|
213
|
+
"workflow_state.json", "mode_state.json", "TASK_FINISHED.json",
|
|
214
|
+
"artifacts_registry.json", "platform_status.json", "sw_state.json",
|
|
215
|
+
"design_params.json", "defense_bypass.json",
|
|
216
|
+
)
|
|
217
|
+
MIGRATE_SUBDIRS = ("reports", "heartbeats", "load_cases")
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def migrate_state(dry_run=False):
|
|
221
|
+
"""把"影子副本"里【更新】的状态搬到权威目录(P0-3 数据保全)。
|
|
222
|
+
|
|
223
|
+
── 为什么需要 ────────────────────────────────────────────────────────
|
|
224
|
+
收敛写入点之后,此前写在另一份副本里的状态不会自动跟过来;若直接忽略,
|
|
225
|
+
用户会看到"任务进度凭空回退"(实测:工作区 step=user_selected,
|
|
226
|
+
安装副本 step=depth_asked —— 回退了整整两个阶段)。
|
|
227
|
+
本函数在【权威目录那份更旧】时,把影子副本的文件复制过来。
|
|
228
|
+
|
|
229
|
+
安全约束:
|
|
230
|
+
· 只在新 > 旧 时覆盖(按 mtime 比较),绝不回退数据;
|
|
231
|
+
· 覆盖前先把权威目录的旧文件备份为 <name>.bak-migrate-<时间戳>;
|
|
232
|
+
· 子目录逐文件比较,同样只搬更新的;
|
|
233
|
+
· dry_run=True 只报告不落盘。
|
|
234
|
+
"""
|
|
235
|
+
import shutil
|
|
236
|
+
import time as _t
|
|
237
|
+
|
|
238
|
+
dst_dir = state_dir()
|
|
239
|
+
stamp = _t.strftime("%Y%m%d-%H%M%S")
|
|
240
|
+
moved, skipped, backed_up = [], [], []
|
|
241
|
+
|
|
242
|
+
def _newer(src, dst):
|
|
243
|
+
try:
|
|
244
|
+
if not os.path.exists(dst):
|
|
245
|
+
return True
|
|
246
|
+
return os.path.getmtime(src) > os.path.getmtime(dst) + 1.0
|
|
247
|
+
except Exception:
|
|
248
|
+
return False
|
|
249
|
+
|
|
250
|
+
def _do_file(src, dst):
|
|
251
|
+
if not _newer(src, dst):
|
|
252
|
+
skipped.append(os.path.basename(dst))
|
|
253
|
+
return
|
|
254
|
+
if dry_run:
|
|
255
|
+
moved.append(os.path.basename(dst))
|
|
256
|
+
return
|
|
257
|
+
try:
|
|
258
|
+
if os.path.exists(dst):
|
|
259
|
+
bak = dst + ".bak-migrate-" + stamp
|
|
260
|
+
shutil.copy2(dst, bak)
|
|
261
|
+
backed_up.append(os.path.basename(bak))
|
|
262
|
+
os.makedirs(os.path.dirname(dst) or dst_dir, exist_ok=True)
|
|
263
|
+
shutil.copy2(src, dst)
|
|
264
|
+
moved.append(os.path.basename(dst))
|
|
265
|
+
except Exception as e:
|
|
266
|
+
skipped.append("%s(失败:%r)" % (os.path.basename(dst), e))
|
|
267
|
+
|
|
268
|
+
for sh in _shadow_dirs():
|
|
269
|
+
for fn in MIGRATE_FILES:
|
|
270
|
+
sp = os.path.join(sh, fn)
|
|
271
|
+
if os.path.isfile(sp):
|
|
272
|
+
_do_file(sp, os.path.join(dst_dir, fn))
|
|
273
|
+
for sub in MIGRATE_SUBDIRS:
|
|
274
|
+
sd = os.path.join(sh, sub)
|
|
275
|
+
if not os.path.isdir(sd):
|
|
276
|
+
continue
|
|
277
|
+
for root, _dirs, files in os.walk(sd):
|
|
278
|
+
for fn in files:
|
|
279
|
+
sp = os.path.join(root, fn)
|
|
280
|
+
rel = os.path.relpath(sp, sh)
|
|
281
|
+
_do_file(sp, os.path.join(dst_dir, rel))
|
|
282
|
+
|
|
283
|
+
return {
|
|
284
|
+
"ok": True,
|
|
285
|
+
"dry_run": bool(dry_run),
|
|
286
|
+
"state_dir": dst_dir,
|
|
287
|
+
"shadow_dirs": _shadow_dirs(),
|
|
288
|
+
"migrated": sorted(set(moved)),
|
|
289
|
+
"skipped_newer_or_equal": sorted(set(skipped)),
|
|
290
|
+
"backups": backed_up,
|
|
291
|
+
"note": ("只搬【更新】的文件;权威目录的旧版本已就地备份为 "
|
|
292
|
+
"*.bak-migrate-<时间戳>(可用 --dry-run 先预览)"),
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def report() -> dict:
|
|
297
|
+
"""诊断用:当前选择与全部候选的评分。"""
|
|
298
|
+
explicit = _from_env_state_dir()
|
|
299
|
+
cands = []
|
|
300
|
+
for fn, label in ((_from_env_state_dir, "env:DSH_STATE_DIR"),
|
|
301
|
+
(_installed_tools_dir, "installed(~/.dsh/.agent-presets)"),
|
|
302
|
+
(_from_env_root, "env:DSH_ENGINEERING_ROOT"),
|
|
303
|
+
(_from_root_module, "_root.tools_dir()"),
|
|
304
|
+
(_here, "module dir")):
|
|
305
|
+
try:
|
|
306
|
+
c = fn()
|
|
307
|
+
except Exception:
|
|
308
|
+
c = None
|
|
309
|
+
cands.append({"from": label, "path": c, "score": _score(c)})
|
|
310
|
+
return {
|
|
311
|
+
"state_dir": state_dir(),
|
|
312
|
+
"explicit": explicit,
|
|
313
|
+
"exists": os.path.isdir(state_dir()),
|
|
314
|
+
"candidates": cands,
|
|
315
|
+
"note": ("所有状态/凭据/报告统一写入 state_dir,消除双副本分裂(P0-3)"),
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def sync_report() -> dict:
|
|
320
|
+
import time as _time
|
|
321
|
+
|
|
322
|
+
d = state_dir()
|
|
323
|
+
out = {
|
|
324
|
+
"ok": True,
|
|
325
|
+
"checked": False,
|
|
326
|
+
"in_sync": None,
|
|
327
|
+
"state_dir": d,
|
|
328
|
+
"workflow": None,
|
|
329
|
+
"mode": None,
|
|
330
|
+
"cross_file": None,
|
|
331
|
+
"error": None,
|
|
332
|
+
"note": "",
|
|
333
|
+
"at": _time.strftime("%Y-%m-%d %H:%M:%S"),
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
def _read_json(name):
|
|
337
|
+
import json as _json
|
|
338
|
+
path = os.path.join(d, name)
|
|
339
|
+
if not os.path.isfile(path):
|
|
340
|
+
return None, "%s 不存在: %s" % (name, path)
|
|
341
|
+
try:
|
|
342
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
343
|
+
return _json.load(f), None
|
|
344
|
+
except Exception as e:
|
|
345
|
+
return None, "%s 解析失败: %r" % (name, e)
|
|
346
|
+
|
|
347
|
+
wf, wf_err = _read_json("workflow_state.json")
|
|
348
|
+
ms, ms_err = _read_json("mode_state.json")
|
|
349
|
+
if wf_err or ms_err:
|
|
350
|
+
out["ok"] = False
|
|
351
|
+
out["error"] = "; ".join([x for x in (wf_err, ms_err) if x])
|
|
352
|
+
out["note"] = ("跨文件一致性检查未执行(缺文件或损坏)—— "
|
|
353
|
+
"in_sync 保持 None,请勿当作已同步。")
|
|
354
|
+
return out
|
|
355
|
+
|
|
356
|
+
if not isinstance(wf, dict) or not isinstance(ms, dict):
|
|
357
|
+
out["ok"] = False
|
|
358
|
+
out["error"] = "状态文件根节点不是对象"
|
|
359
|
+
out["note"] = "跨文件一致性检查未执行,in_sync 保持 None。"
|
|
360
|
+
return out
|
|
361
|
+
|
|
362
|
+
# ── workflow_state 侧:门禁认定的完成房间 ────────────────────────────
|
|
363
|
+
wf_rooms = wf.get("done_rooms")
|
|
364
|
+
if not isinstance(wf_rooms, list):
|
|
365
|
+
# 兼容旧结构:有些版本把完成信息记在 rooms 里
|
|
366
|
+
wf_rooms = []
|
|
367
|
+
_r = wf.get("rooms")
|
|
368
|
+
if isinstance(_r, dict):
|
|
369
|
+
for k, v in _r.items():
|
|
370
|
+
if isinstance(v, dict) and v.get("done"):
|
|
371
|
+
wf_rooms.append(k)
|
|
372
|
+
wf_task = wf.get("task_id")
|
|
373
|
+
wf_step = wf.get("step")
|
|
374
|
+
|
|
375
|
+
# ── mode_state 侧:状态机记录的已结束房间 ────────────────────────────
|
|
376
|
+
ms_rooms = ms.get("rooms") if isinstance(ms.get("rooms"), dict) else {}
|
|
377
|
+
ms_done = sorted([k for k, v in ms_rooms.items()
|
|
378
|
+
if isinstance(v, dict) and (v.get("ended_at") or 0) > 0])
|
|
379
|
+
ms_active = sorted([k for k, v in ms_rooms.items()
|
|
380
|
+
if isinstance(v, dict) and v.get("active")])
|
|
381
|
+
ms_failed = sorted([k for k, v in ms_rooms.items()
|
|
382
|
+
if isinstance(v, dict) and v.get("failed_at")])
|
|
383
|
+
|
|
384
|
+
wf_done = sorted(set(str(x) for x in wf_rooms))
|
|
385
|
+
only_wf = [x for x in wf_done if x not in ms_done]
|
|
386
|
+
only_ms = [x for x in ms_done if x not in wf_done]
|
|
387
|
+
in_sync = (not only_wf) and (not only_ms)
|
|
388
|
+
|
|
389
|
+
out["checked"] = True
|
|
390
|
+
out["in_sync"] = bool(in_sync)
|
|
391
|
+
out["workflow"] = {
|
|
392
|
+
"task_id": wf_task,
|
|
393
|
+
"step": wf_step,
|
|
394
|
+
"done_rooms": wf_done,
|
|
395
|
+
"count": len(wf_done),
|
|
396
|
+
}
|
|
397
|
+
out["mode"] = {
|
|
398
|
+
"done_rooms": ms_done,
|
|
399
|
+
"active_rooms": ms_active,
|
|
400
|
+
"failed_rooms": ms_failed,
|
|
401
|
+
"count": len(ms_done),
|
|
402
|
+
}
|
|
403
|
+
out["cross_file"] = {
|
|
404
|
+
"only_in_workflow": only_wf,
|
|
405
|
+
"only_in_mode": only_ms,
|
|
406
|
+
"identical": bool(in_sync),
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
if in_sync:
|
|
410
|
+
out["note"] = ("跨文件一致性:两侧已完成房间完全一致(%d 个)。"
|
|
411
|
+
% len(wf_done))
|
|
412
|
+
else:
|
|
413
|
+
_parts = []
|
|
414
|
+
if only_wf:
|
|
415
|
+
_parts.append("workflow_state 认为已完成、但 mode_state 无结束时间: %s"
|
|
416
|
+
% ", ".join(only_wf))
|
|
417
|
+
if only_ms:
|
|
418
|
+
_parts.append("mode_state 已结束、但 workflow_state 未登记: %s"
|
|
419
|
+
% ", ".join(only_ms))
|
|
420
|
+
out["note"] = "跨文件不一致 —— " + ";".join(_parts)
|
|
421
|
+
return out
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
def _ensure_importable():
|
|
425
|
+
"""把状态目录加入 sys.path,供 import mode_gate / defense_gate 等使用。"""
|
|
426
|
+
d = state_dir()
|
|
427
|
+
if d and d not in sys.path:
|
|
428
|
+
sys.path.insert(0, d)
|
|
429
|
+
return d
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
if __name__ == "__main__":
|
|
433
|
+
import json
|
|
434
|
+
import sys as _sys
|
|
435
|
+
|
|
436
|
+
args = [a for a in _sys.argv[1:]]
|
|
437
|
+
if "--migrate" in args or "--dry-run" in args:
|
|
438
|
+
_out = migrate_state(dry_run=("--dry-run" in args))
|
|
439
|
+
else:
|
|
440
|
+
_out = report()
|
|
441
|
+
# Windows 控制台默认 GBK,中文会乱码 —— 强制 UTF-8 输出
|
|
442
|
+
try:
|
|
443
|
+
_sys.stdout.reconfigure(encoding="utf-8")
|
|
444
|
+
except Exception:
|
|
445
|
+
pass
|
|
446
|
+
print(json.dumps(_out, ensure_ascii=False, indent=2))
|