@hupan56/wlkj 3.3.12 → 3.3.14

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.
Files changed (103) hide show
  1. package/bin/cli.js +1 -23
  2. package/package.json +29 -29
  3. package/templates/qoder/agents/design-agent.md +20 -0
  4. package/templates/qoder/agents/spec-generator.md +21 -0
  5. package/templates/qoder/commands/optional/wl-spec.md +2 -12
  6. package/templates/qoder/commands/optional/wl-status.md +0 -8
  7. package/templates/qoder/commands/wl-code.md +0 -8
  8. package/templates/qoder/commands/wl-commit.md +1 -11
  9. package/templates/qoder/commands/wl-init.md +129 -129
  10. package/templates/qoder/commands/wl-prd.md +0 -24
  11. package/templates/qoder/commands/wl-search.md +11 -26
  12. package/templates/qoder/commands/wl-task.md +1 -11
  13. package/templates/qoder/commands/wl-test.md +0 -8
  14. package/templates/qoder/config.yaml +8 -3
  15. package/templates/qoder/hooks/session-start.py +365 -384
  16. package/templates/qoder/hooks/stop-eval.py +254 -383
  17. package/templates/qoder/scripts/capability/adapters/mcp.py +314 -322
  18. package/templates/qoder/scripts/capability/registry.py +248 -245
  19. package/templates/qoder/scripts/capability/registry_mcp.py +4 -10
  20. package/templates/qoder/scripts/capability/smoke_test_report.json.new +94 -0
  21. package/templates/qoder/scripts/deployment/setup/init_doctor.py +2 -6
  22. package/templates/qoder/scripts/deployment/setup/install_qoderwork.py +14 -0
  23. package/templates/qoder/scripts/deployment/setup/setup.py +2 -16
  24. package/templates/qoder/scripts/domain/integration/return_to_platform.py +2 -4
  25. package/templates/qoder/scripts/domain/integration/spec_upload.py +4 -4
  26. package/templates/qoder/scripts/domain/kg/build/build_entity_registry.py +12 -12
  27. package/templates/qoder/scripts/domain/kg/build/build_relations.py +12 -12
  28. package/templates/qoder/scripts/domain/kg/extract/asset/__init__.py +10 -0
  29. package/templates/qoder/scripts/domain/kg/extract/asset/asset_tree.py +57 -0
  30. package/templates/qoder/scripts/domain/kg/extract/asset/discussion_importer.py +62 -0
  31. package/templates/qoder/scripts/domain/kg/extract/asset/prd_importer.py +146 -0
  32. package/templates/qoder/scripts/domain/kg/extract/asset/prototype_importer.py +64 -0
  33. package/templates/qoder/scripts/domain/kg/extract/asset/returns_importer.py +52 -0
  34. package/templates/qoder/scripts/domain/kg/extract/build_goal3.py +104 -0
  35. package/templates/qoder/scripts/domain/kg/extract/build_goal4.py +55 -0
  36. package/templates/qoder/scripts/domain/kg/extract/build_goal5.py +95 -0
  37. package/templates/qoder/scripts/domain/kg/extract/db/__init__.py +8 -0
  38. package/templates/qoder/scripts/domain/kg/extract/db/data_profile.py +22 -0
  39. package/templates/qoder/scripts/domain/kg/extract/db/fk_extractor.py +55 -0
  40. package/templates/qoder/scripts/domain/kg/extract/db/schema_extractor.py +90 -0
  41. package/templates/qoder/scripts/domain/kg/extract/inference/__init__.py +9 -0
  42. package/templates/qoder/scripts/domain/kg/extract/inference/community_summarizer.py +206 -0
  43. package/templates/qoder/scripts/domain/kg/extract/inference/embed_builder.py +132 -0
  44. package/templates/qoder/scripts/domain/kg/extract/inference/naming_matcher.py +80 -0
  45. package/templates/qoder/scripts/domain/kg/extract/inference/promote.py +59 -0
  46. package/templates/qoder/scripts/domain/kg/extract/inference/recompute.py +93 -0
  47. package/templates/qoder/scripts/domain/kg/extract/inference/weak_link.py +421 -0
  48. package/templates/qoder/scripts/domain/kg/extract/mybatis/__init__.py +9 -0
  49. package/templates/qoder/scripts/domain/kg/extract/mybatis/all.py +79 -0
  50. package/templates/qoder/scripts/domain/kg/extract/mybatis/mapper_parser.py +99 -0
  51. package/templates/qoder/scripts/domain/kg/extract/mybatis/relation_builder.py +69 -0
  52. package/templates/qoder/scripts/domain/kg/extract/mybatis/sql_extractor.py +78 -0
  53. package/templates/qoder/scripts/domain/kg/extract/prd/__init__.py +8 -0
  54. package/templates/qoder/scripts/domain/kg/extract/prd/prd_chunk_embed.py +105 -0
  55. package/templates/qoder/scripts/domain/kg/extract/prd/prd_llm_extract.py +153 -0
  56. package/templates/qoder/scripts/domain/kg/extract/prd/req_anchor.py +120 -0
  57. package/templates/qoder/scripts/domain/kg/graph/kg_link_db.py +235 -0
  58. package/templates/qoder/scripts/domain/kg/graph/kg_semantic.py +5 -41
  59. package/templates/qoder/scripts/domain/kg/kg.py +0 -14
  60. package/templates/qoder/scripts/domain/kg/search/enrich_prompt.py +238 -0
  61. package/templates/qoder/scripts/domain/kg/server/perf_bench.py +197 -0
  62. package/templates/qoder/scripts/domain/kg/switch_project.py +2 -2
  63. package/templates/qoder/scripts/domain/kg/sync_repowiki.py +109 -0
  64. package/templates/qoder/scripts/domain/task/wlkj_panel.py +1348 -1503
  65. package/templates/qoder/scripts/engine/poller.py +219 -0
  66. package/templates/qoder/scripts/foundation/bootstrap.py +145 -145
  67. package/templates/qoder/scripts/orchestration/wlkj.py +189 -306
  68. package/templates/qoder/settings.json +0 -8
  69. package/templates/qoder/skills/wl-spec/SKILL.md +1 -1
  70. package/templates/qoder/commands/wl-data.md +0 -46
  71. package/templates/qoder/commands/wl-fix.md +0 -47
  72. package/templates/qoder/commands/wl-knowledge.md +0 -41
  73. package/templates/qoder/commands/wl-review.md +0 -49
  74. package/templates/qoder/contracts/insight.md +0 -55
  75. package/templates/qoder/hooks/pre-tool-use-commit.py +0 -124
  76. package/templates/qoder/scripts/foundation/io/context_cache.py +0 -94
  77. package/templates/qoder/scripts/tool_guide.md +0 -70
  78. package/templates/qoder/scripts/validation/eval/alignment_matrix.py +0 -176
  79. package/templates/qoder/scripts/validation/eval/bf2_content_fidelity.py +0 -110
  80. package/templates/qoder/scripts/validation/eval/bf2_llmjudge.py +0 -104
  81. package/templates/qoder/scripts/validation/eval/bf_score.py +0 -218
  82. package/templates/qoder/scripts/validation/eval/code_flywheel.py +0 -150
  83. package/templates/qoder/scripts/validation/eval/dispatcher_ab.py +0 -156
  84. package/templates/qoder/scripts/validation/eval/dispatcher_ab_2026-07-21.json +0 -23
  85. package/templates/qoder/scripts/validation/eval/feature_fidelity_flywheel.py +0 -143
  86. package/templates/qoder/scripts/validation/eval/gradient_matrix.py +0 -261
  87. package/templates/qoder/scripts/validation/eval/gradient_matrix_baseline_2026-07-21.json +0 -33
  88. package/templates/qoder/scripts/validation/eval/metrics_dashboard.py +0 -105
  89. package/templates/qoder/scripts/validation/eval/multi_turn_flywheel.py +0 -118
  90. package/templates/qoder/scripts/validation/eval/prd_fidelity_flywheel.py +0 -128
  91. package/templates/qoder/scripts/validation/eval/prd_flywheel.py +0 -148
  92. package/templates/qoder/scripts/validation/eval/prototype_fidelity_flywheel.py +0 -166
  93. package/templates/qoder/scripts/validation/eval/recall_flywheel.py +0 -148
  94. package/templates/qoder/scripts/validation/eval/robustness_flywheel.py +0 -139
  95. package/templates/qoder/scripts/validation/eval/speed_accuracy_flywheel.py +0 -188
  96. package/templates/qoder/scripts/validation/eval/task_flywheel.py +0 -124
  97. package/templates/qoder/scripts/validation/eval/token_flywheel.py +0 -88
  98. package/templates/qoder/scripts/validation/metrics/eval_code_ac.py +0 -177
  99. package/templates/qoder/scripts/validation/metrics/lint_cases.py +0 -170
  100. package/templates/qoder/scripts/validation/test/test_context_cache.py +0 -78
  101. package/templates/qoder/scripts/validation/test/test_lint_cases.py +0 -60
  102. package/templates/qoder/scripts/validation/test/test_pre_tool_use_commit.py +0 -70
  103. package/templates/workspace/specs/REQ-TEST-CTX/.context-cache.json +0 -1
@@ -0,0 +1,219 @@
1
+ """引擎轮询器:后台轮询平台 pending 触发 → 认领 → 执行 → 回流 → 回填结果。
2
+
3
+ 这是"平台→引擎→平台"真闭环的引擎侧执行器:
4
+ 1. 轮询 GET /trigger/pending(带项目令牌)
5
+ 2. 认领 POST /trigger/{tid}/claim(原子锁)
6
+ 3. 执行:根据 command 调 MCP 取知识 + 生成产出(简化版:rag_search + 模板拼接)
7
+ 4. 回流:create_prd/submit_return
8
+ 5. 回填:PATCH /trigger/{tid}(mcp_tools_called/knowledge_used)
9
+
10
+ 用法:
11
+ python poller.py --config ../mcp_config.json --interval 10
12
+
13
+ 注:完整版需要接 LLM(qwen-plus)生成 PRD/原型。当前是简化版(rag_search + 模板),
14
+ 验证闭环链路。接 LLM 后把 _execute 里的模板拼接换成 cap.mcp.call("ask_corpus",...) 生成。
15
+ """
16
+ import json
17
+ import time
18
+ import urllib.request
19
+ import urllib.error
20
+ import argparse
21
+ import os
22
+ import sys
23
+ from pathlib import Path
24
+
25
+
26
+ def _load_config(config_path: str) -> dict:
27
+ with open(config_path, encoding="utf-8") as f:
28
+ return json.load(f)
29
+
30
+
31
+ def _get_token(cfg: dict) -> str:
32
+ for srv in (cfg.get("mcpServers") or {}).values():
33
+ return srv.get("token", "")
34
+ return ""
35
+
36
+
37
+ def _get_pid(cfg: dict) -> str:
38
+ for srv in (cfg.get("mcpServers") or {}).values():
39
+ env = srv.get("env") or {}
40
+ return env.get("WLKJ_PROJECT_ID", "")
41
+ return ""
42
+
43
+
44
+ def _get_base(cfg: dict) -> str:
45
+ ep = cfg.get("return_endpoint") or {}
46
+ url = ep.get("url", "")
47
+ # 从 return_endpoint.url 提取 base(/api/projects 前面那段)
48
+ if "/api/" in url:
49
+ return url.split("/api/")[0]
50
+ for srv in (cfg.get("mcpServers") or {}).values():
51
+ u = srv.get("url", "")
52
+ if "/mcp" in u:
53
+ return u.split("/mcp")[0]
54
+ return "http://127.0.0.1:10010"
55
+
56
+
57
+ def _api(base, method, path, token, body=None):
58
+ url = base + path
59
+ data = json.dumps(body).encode() if body else None
60
+ req = urllib.request.Request(url, data=data, method=method)
61
+ req.add_header("Content-Type", "application/json")
62
+ if token:
63
+ req.add_header("X-Engine-Token", token)
64
+ try:
65
+ with urllib.request.urlopen(req, timeout=30) as resp:
66
+ return resp.status, json.loads(resp.read().decode())
67
+ except urllib.error.HTTPError as e:
68
+ return e.code, json.loads(e.read().decode() or "{}")
69
+ except Exception as e:
70
+ return 0, {"error": str(e)}
71
+
72
+
73
+ def _mcp_call(base, token, tool, args):
74
+ """通过 MCP JSON-RPC 调工具。"""
75
+ body = {"jsonrpc": "2.0", "id": 1, "method": "tools/call",
76
+ "params": {"name": tool, "arguments": args}}
77
+ data = json.dumps(body).encode()
78
+ req = urllib.request.Request(base + "/mcp", data=data, method="POST")
79
+ req.add_header("Content-Type", "application/json")
80
+ req.add_header("X-MCP-Token", token)
81
+ try:
82
+ with urllib.request.urlopen(req, timeout=60) as resp:
83
+ r = json.loads(resp.read().decode())
84
+ text = r.get("result", {}).get("content", [{}])[0].get("text", "")
85
+ return json.loads(text) if text else {}
86
+ except Exception as e:
87
+ return {"error": str(e)}
88
+
89
+
90
+ def _execute(base, token, pid, command, args):
91
+ """执行触发的工作流(简化版:rag_search 取知识 + 模板拼 PRD + learn 写回)。
92
+ 完整版应接 LLM(ask_corpus)生成自然语言 PRD。"""
93
+ query = (args or {}).get("query", "")
94
+ tools_called = []
95
+ knowledge_used = []
96
+
97
+ # 1) rag_search 取知识
98
+ if query:
99
+ r = _mcp_call(base, token, "rag_search", {"query": query, "top_k": 8})
100
+ tools_called.append("rag_search")
101
+ knowledge_used = [it.get("entity_id", "") for it in r.get("items", [])[:5]]
102
+ knowledge_text = "\n".join(
103
+ "- %s (%s)" % (it.get("text", "")[:60], it.get("entity_id", "")[:30])
104
+ for it in r.get("items", [])[:8]
105
+ )
106
+ # 1b) get_learnings 查之前学到的(反哺)
107
+ lr = _mcp_call(base, token, "get_learnings", {"project_id": pid, "limit": 5})
108
+ tools_called.append("get_learnings")
109
+ learnings_text = "\n".join(
110
+ "- [%s] %s" % (l.get("category",""), (l.get("pattern","")[:60]))
111
+ for l in (lr.get("items") or [])[:5]
112
+ ) or "(暂无学习记录)"
113
+ else:
114
+ knowledge_text = "(无关键词)"
115
+ learnings_text = ""
116
+
117
+ # 2) 根据 command 生成产出
118
+ if "prd" in command.lower():
119
+ prd_md = f"# {query} PRD(引擎自动生成)\n\n## 概述\n基于知识层自动生成的「{query}」需求文档。\n\n## 相关知识\n{knowledge_text}\n\n## 历史学习\n{learnings_text}\n\n## 功能\n(根据上述知识补充)"
120
+ # 回流
121
+ r = _mcp_call(base, token, "create_prd", {
122
+ "project_id": pid, "title": f"{query}PRD(引擎触发)", "content_md": prd_md, "status": "planning"
123
+ })
124
+ tools_called.append("create_prd")
125
+ # 学习写回:把这次的关键认知沉淀
126
+ _mcp_call(base, token, "learn", {"project_id": pid, "category": "rule",
127
+ "pattern": f"{query}: PRD已生成,涉及知识{len(knowledge_used)}条", "source": "wlkj-poller"})
128
+ tools_called.append("learn")
129
+ output = f"生成PRD: {query}PRD(引擎触发) + 学习沉淀"
130
+ elif "design" in command.lower():
131
+ html = f"<html><body><h1>{query}</h1><p>引擎自动生成原型</p></body></html>"
132
+ r = _mcp_call(base, token, "create_prototype", {
133
+ "project_id": pid, "feature": f"{query}(引擎触发)", "platform": "web", "html": html
134
+ })
135
+ tools_called.append("create_prototype")
136
+ output = f"生成原型: {query}(引擎触发)"
137
+ else:
138
+ # learn 写回
139
+ _mcp_call(base, token, "learn", {
140
+ "project_id": pid, "category": "rule", "pattern": f"{query}相关规则(引擎触发)",
141
+ "source": "wlkj-poller"
142
+ })
143
+ tools_called.append("learn")
144
+ output = f"执行命令: {command}"
145
+
146
+ return {"output": output, "tools": tools_called, "knowledge": knowledge_used}
147
+
148
+
149
+ def poll_once(cfg):
150
+ """轮询一次:取 pending → 认领 → 执行 → 回填。"""
151
+ token = _get_token(cfg)
152
+ pid = _get_pid(cfg)
153
+ base = _get_base(cfg)
154
+ if not token or not pid:
155
+ print("[poller] 配置缺 token/project_id,跳过")
156
+ return
157
+
158
+ # 1) 查 pending
159
+ _, pending = _api(base, "GET", f"/api/projects/{pid}/trigger/pending", token)
160
+ items = pending.get("pending", [])
161
+ if not items:
162
+ return # 无待执行
163
+
164
+ for item in items[:1]: # 一次只处理 1 条
165
+ tid = item.get("triggerId")
166
+ cmd = item.get("command", "")
167
+ args = item.get("args", {})
168
+ print(f"[poller] 认领: {tid} {cmd} {args}")
169
+
170
+ # 2) claim
171
+ code, resp = _api(base, "POST", f"/api/projects/{pid}/trigger/workflow/{tid}/claim",
172
+ token, {"engine_id": "poller-001"})
173
+ if code != 200:
174
+ print(f"[poller] 认领失败({code}): {resp}")
175
+ continue
176
+
177
+ # 3) 执行
178
+ t0 = time.time()
179
+ try:
180
+ result = _execute(base, token, pid, cmd, args)
181
+ status = "success"
182
+ except Exception as e:
183
+ result = {"output": str(e), "tools": [], "knowledge": []}
184
+ status = "failed"
185
+
186
+ # 4) patch 回填
187
+ _api(base, "PATCH", f"/api/projects/{pid}/trigger/workflow/{tid}", token, {
188
+ "status": status,
189
+ "output_summary": result["output"],
190
+ "knowledge_used": result["knowledge"],
191
+ "mcp_tools_called": result["tools"],
192
+ "duration_sec": int(time.time() - t0),
193
+ })
194
+ print(f"[poller] 完成: {status} → {result['output']} (tools={result['tools']})")
195
+
196
+
197
+ def main():
198
+ parser = argparse.ArgumentParser(description="wlkj 引擎轮询器")
199
+ parser.add_argument("--config", default=str(Path(__file__).parent.parent.parent / "mcp_config.json"))
200
+ parser.add_argument("--interval", type=int, default=10, help="轮询间隔(秒)")
201
+ parser.add_argument("--once", action="store_true", help="只跑一次")
202
+ args = parser.parse_args()
203
+
204
+ cfg = _load_config(args.config)
205
+ print(f"[poller] 启动: project={_get_pid(cfg)} interval={args.interval}s")
206
+ print(f"[poller] 按 Ctrl+C 停止")
207
+
208
+ while True:
209
+ try:
210
+ poll_once(cfg)
211
+ except Exception as e:
212
+ print(f"[poller] 异常: {e}")
213
+ if args.once:
214
+ break
215
+ time.sleep(args.interval)
216
+
217
+
218
+ if __name__ == "__main__":
219
+ main()
@@ -1,145 +1,145 @@
1
- # -*- coding: utf-8 -*-
2
- """
3
- bootstrap.py - 统一 CLI 启动样板 (DRY) + 子目录化路径自举 (v3.0)
4
-
5
- 治"70处 sys.path + 50处 reconfigure" + 子目录化后的路径定位。
6
-
7
- 子目录化后, 脚本在 scripts/domain/kg/kg.py, 它的 dirname(__file__)=scripts/domain/kg/,
8
- 直接 sys.path.insert(scripts/domain/kg/) 后 `from foundation.x` 会断 (foundation 在 scripts/foundation/)。
9
-
10
- 本模块的 setup() 解决: 无论脚本在 scripts/ 根还是 scripts/某子包/,
11
- 都能正确定位 scripts/ 根并注入, 让 `from foundation.x` 和跨包 import 都能用。
12
-
13
- 核心: _find_scripts_root() 向上找含 'foundation/' 子目录的那个目录 = scripts/ 根。
14
-
15
- Usage (两种姿势):
16
- # 姿势1: 子包脚本开头 (推荐)
17
- import os, sys
18
- _S = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # 子包→scripts
19
- if _S not in sys.path: sys.path.insert(0, _S)
20
- from bootstrap import setup; setup()
21
-
22
- # 姿势2: scripts/ 根脚本 (老姿势仍可用)
23
- from bootstrap import setup; setup()
24
- """
25
-
26
- import os
27
- import sys
28
-
29
- __all__ = ["setup", "SCRIPTS_DIR", "REPO_ROOT", "find_scripts_root", "ensure_scripts_path"]
30
-
31
-
32
- def find_scripts_root(start=None):
33
- """定位 .qoder/scripts/ 根目录。
34
-
35
- 策略: 从 start (默认 __file__ 的目录) 向上找, 直到某目录含 'common/' 子目录
36
- 且该 common/ 下有 bootstrap.py (即本文件) → 那就是 scripts/ 根。
37
- 兜底: 找含 '.qoder' 的目录 + '/scripts'。
38
- """
39
- start = start or os.path.dirname(os.path.abspath(__file__))
40
- cur = start
41
- for _ in range(10):
42
- # scripts/ 根的标志: 含 common/bootstrap.py
43
- if os.path.isfile(os.path.join(cur, "foundation", "bootstrap.py")):
44
- return cur
45
- parent = os.path.dirname(cur)
46
- if parent == cur:
47
- break
48
- cur = parent
49
- # 兜底: cwd 向上找 .qoder/scripts
50
- cwd = os.getcwd()
51
- for _ in range(10):
52
- cand = os.path.join(cwd, ".qoder", "scripts")
53
- if os.path.isdir(cand):
54
- return cand
55
- parent = os.path.dirname(cwd)
56
- if parent == cwd:
57
- break
58
- cwd = parent
59
- return start
60
-
61
-
62
- def ensure_scripts_path(start=None):
63
- """确保 scripts/ 根在 sys.path 最前。幂等。
64
-
65
- 子包脚本调用此函数后, `from foundation.x` 即可用。
66
- """
67
- root = find_scripts_root(start)
68
- if root not in sys.path:
69
- sys.path.insert(0, root)
70
- return root
71
-
72
-
73
- def _find_scripts_dir() -> str:
74
- """[内部] 定位 scripts/ 根 (兼容旧名)。"""
75
- return find_scripts_root()
76
-
77
-
78
- def ensure_utf8_stdio():
79
- """UTF-8 stdio 配置 (v3.0: 从 utf8.py 并入)。
80
-
81
- 把 stdout/stderr 切到 UTF-8, 失败则忽略 (不崩溃)。
82
- 幂等: 多次调用无害。
83
- """
84
- for stream_name in ("stdout", "stderr"):
85
- stream = getattr(sys, stream_name, None)
86
- if stream is None:
87
- continue
88
- reconfigure = getattr(stream, "reconfigure", None)
89
- if reconfigure is None:
90
- continue
91
- try:
92
- reconfigure(encoding="utf-8", errors="replace")
93
- except (TypeError, ValueError, OSError, IOError):
94
- try:
95
- reconfigure(encoding="utf-8")
96
- except Exception:
97
- pass
98
-
99
-
100
- def setup():
101
- """统一 CLI 启动初始化。幂等。
102
-
103
- 做: ① ensure_utf8_stdio ② scripts/ 根注入 sys.path ③ scripts/common/ 注入
104
- Returns: (SCRIPTS_DIR, REPO_ROOT)
105
- """
106
- # 先确保 scripts/ 根在 path (本文件可能在 common/ 或被引用)
107
- scripts_dir = ensure_scripts_root = find_scripts_root()
108
- if scripts_dir not in sys.path:
109
- sys.path.insert(0, scripts_dir)
110
- common_dir = os.path.join(scripts_dir, "foundation")
111
- if os.path.isdir(common_dir) and common_dir not in sys.path:
112
- sys.path.insert(0, common_dir)
113
- # 注入所有子包目录 (kg/build/mcp/test/setup/carriers/task/search/report)
114
- # 让跨包 import (旧式不带前缀, 如 import search_index) 仍可用
115
- try:
116
- for _name in os.listdir(scripts_dir):
117
- _subdir = os.path.join(scripts_dir, _name)
118
- if os.path.isdir(_subdir) and _subdir not in sys.path:
119
- sys.path.insert(0, _subdir)
120
- except OSError:
121
- pass
122
-
123
- # 1. UTF-8 stdio (v3.0: utf8.py 已并入 bootstrap, 内联实现)
124
- try:
125
- ensure_utf8_stdio()
126
- except Exception:
127
- for sname in ("stdout", "stderr"):
128
- s = getattr(sys, sname, None)
129
- rc = getattr(s, "reconfigure", None) if s else None
130
- if rc:
131
- try:
132
- rc(encoding="utf-8", errors="replace")
133
- except Exception:
134
- try:
135
- rc(encoding="utf-8")
136
- except Exception:
137
- pass
138
-
139
- repo_root = os.path.dirname(os.path.dirname(scripts_dir))
140
- return scripts_dir, repo_root
141
-
142
-
143
- # 模块级常量
144
- SCRIPTS_DIR = find_scripts_root()
145
- REPO_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(SCRIPTS_DIR))))
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ bootstrap.py - 统一 CLI 启动样板 (DRY) + 子目录化路径自举 (v3.0)
4
+
5
+ 治"70处 sys.path + 50处 reconfigure" + 子目录化后的路径定位。
6
+
7
+ 子目录化后, 脚本在 scripts/domain/kg/kg.py, 它的 dirname(__file__)=scripts/domain/kg/,
8
+ 直接 sys.path.insert(scripts/domain/kg/) 后 `from foundation.x` 会断 (foundation 在 scripts/foundation/)。
9
+
10
+ 本模块的 setup() 解决: 无论脚本在 scripts/ 根还是 scripts/某子包/,
11
+ 都能正确定位 scripts/ 根并注入, 让 `from foundation.x` 和跨包 import 都能用。
12
+
13
+ 核心: _find_scripts_root() 向上找含 'foundation/' 子目录的那个目录 = scripts/ 根。
14
+
15
+ Usage (两种姿势):
16
+ # 姿势1: 子包脚本开头 (推荐)
17
+ import os, sys
18
+ _S = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # 子包→scripts
19
+ if _S not in sys.path: sys.path.insert(0, _S)
20
+ from bootstrap import setup; setup()
21
+
22
+ # 姿势2: scripts/ 根脚本 (老姿势仍可用)
23
+ from bootstrap import setup; setup()
24
+ """
25
+
26
+ import os
27
+ import sys
28
+
29
+ __all__ = ["setup", "SCRIPTS_DIR", "REPO_ROOT", "find_scripts_root", "ensure_scripts_path"]
30
+
31
+
32
+ def find_scripts_root(start=None):
33
+ """定位 .qoder/scripts/ 根目录。
34
+
35
+ 策略: 从 start (默认 __file__ 的目录) 向上找, 直到某目录含 'common/' 子目录
36
+ 且该 common/ 下有 bootstrap.py (即本文件) → 那就是 scripts/ 根。
37
+ 兜底: 找含 '.qoder' 的目录 + '/scripts'。
38
+ """
39
+ start = start or os.path.dirname(os.path.abspath(__file__))
40
+ cur = start
41
+ for _ in range(10):
42
+ # scripts/ 根的标志: 含 common/bootstrap.py
43
+ if os.path.isfile(os.path.join(cur, "foundation", "bootstrap.py")):
44
+ return cur
45
+ parent = os.path.dirname(cur)
46
+ if parent == cur:
47
+ break
48
+ cur = parent
49
+ # 兜底: cwd 向上找 .qoder/scripts
50
+ cwd = os.getcwd()
51
+ for _ in range(10):
52
+ cand = os.path.join(cwd, ".qoder", "scripts")
53
+ if os.path.isdir(cand):
54
+ return cand
55
+ parent = os.path.dirname(cwd)
56
+ if parent == cwd:
57
+ break
58
+ cwd = parent
59
+ return start
60
+
61
+
62
+ def ensure_scripts_path(start=None):
63
+ """确保 scripts/ 根在 sys.path 最前。幂等。
64
+
65
+ 子包脚本调用此函数后, `from foundation.x` 即可用。
66
+ """
67
+ root = find_scripts_root(start)
68
+ if root not in sys.path:
69
+ sys.path.insert(0, root)
70
+ return root
71
+
72
+
73
+ def _find_scripts_dir() -> str:
74
+ """[内部] 定位 scripts/ 根 (兼容旧名)。"""
75
+ return find_scripts_root()
76
+
77
+
78
+ def ensure_utf8_stdio():
79
+ """UTF-8 stdio 配置 (v3.0: 从 utf8.py 并入)。
80
+
81
+ 把 stdout/stderr 切到 UTF-8, 失败则忽略 (不崩溃)。
82
+ 幂等: 多次调用无害。
83
+ """
84
+ for stream_name in ("stdout", "stderr"):
85
+ stream = getattr(sys, stream_name, None)
86
+ if stream is None:
87
+ continue
88
+ reconfigure = getattr(stream, "reconfigure", None)
89
+ if reconfigure is None:
90
+ continue
91
+ try:
92
+ reconfigure(encoding="utf-8", errors="replace")
93
+ except (TypeError, ValueError, OSError, IOError):
94
+ try:
95
+ reconfigure(encoding="utf-8")
96
+ except Exception:
97
+ pass
98
+
99
+
100
+ def setup():
101
+ """统一 CLI 启动初始化。幂等。
102
+
103
+ 做: ① ensure_utf8_stdio ② scripts/ 根注入 sys.path ③ scripts/common/ 注入
104
+ Returns: (SCRIPTS_DIR, REPO_ROOT)
105
+ """
106
+ # 先确保 scripts/ 根在 path (本文件可能在 common/ 或被引用)
107
+ scripts_dir = ensure_scripts_root = find_scripts_root()
108
+ if scripts_dir not in sys.path:
109
+ sys.path.insert(0, scripts_dir)
110
+ common_dir = os.path.join(scripts_dir, "foundation")
111
+ if os.path.isdir(common_dir) and common_dir not in sys.path:
112
+ sys.path.insert(0, common_dir)
113
+ # 注入所有子包目录 (kg/build/mcp/test/setup/carriers/task/search/report)
114
+ # 让跨包 import (旧式不带前缀, 如 import search_index) 仍可用
115
+ try:
116
+ for _name in os.listdir(scripts_dir):
117
+ _subdir = os.path.join(scripts_dir, _name)
118
+ if os.path.isdir(_subdir) and _subdir not in sys.path:
119
+ sys.path.insert(0, _subdir)
120
+ except OSError:
121
+ pass
122
+
123
+ # 1. UTF-8 stdio (v3.0: utf8.py 已并入 bootstrap, 内联实现)
124
+ try:
125
+ ensure_utf8_stdio()
126
+ except Exception:
127
+ for sname in ("stdout", "stderr"):
128
+ s = getattr(sys, sname, None)
129
+ rc = getattr(s, "reconfigure", None) if s else None
130
+ if rc:
131
+ try:
132
+ rc(encoding="utf-8", errors="replace")
133
+ except Exception:
134
+ try:
135
+ rc(encoding="utf-8")
136
+ except Exception:
137
+ pass
138
+
139
+ repo_root = os.path.dirname(os.path.dirname(scripts_dir))
140
+ return scripts_dir, repo_root
141
+
142
+
143
+ # 模块级常量
144
+ SCRIPTS_DIR = find_scripts_root()
145
+ REPO_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(SCRIPTS_DIR))))