@hupan56/wlkj 3.0.0 → 3.1.2

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 (237) hide show
  1. package/bin/cli.js +1435 -1373
  2. package/package.json +1 -1
  3. package/templates/qoder/commands/optional/wl-insight.md +6 -4
  4. package/templates/qoder/commands/optional/wl-report.md +20 -39
  5. package/templates/qoder/commands/optional/wl-status.md +4 -4
  6. package/templates/qoder/commands/wl-code.md +2 -2
  7. package/templates/qoder/commands/wl-design.md +5 -5
  8. package/templates/qoder/commands/wl-init.md +3 -3
  9. package/templates/qoder/commands/wl-prd.md +26 -13
  10. package/templates/qoder/commands/wl-req.md +3 -3
  11. package/templates/qoder/commands/wl-search.md +78 -47
  12. package/templates/qoder/commands/wl-task.md +343 -40
  13. package/templates/qoder/commands/wl-test.md +10 -10
  14. package/templates/qoder/config.yaml +35 -13
  15. package/templates/qoder/hooks/post-tool-use.py +181 -0
  16. package/templates/qoder/hooks/session-start.py +82 -1
  17. package/templates/qoder/hooks/stop-eval.py +207 -0
  18. package/templates/qoder/hooks/user-prompt-submit.py +139 -0
  19. package/templates/qoder/rules/wl-pipeline.md +70 -8
  20. package/templates/qoder/scripts/README.md +78 -115
  21. package/templates/qoder/scripts/capability/__init__.py +26 -0
  22. package/templates/qoder/scripts/capability/__main__.py +72 -0
  23. package/templates/qoder/scripts/capability/adapters/__init__.py +29 -0
  24. package/templates/qoder/scripts/capability/adapters/cli.py +316 -0
  25. package/templates/qoder/scripts/capability/adapters/mcp.py +334 -0
  26. package/templates/qoder/scripts/capability/adapters/qw.py +271 -0
  27. package/templates/qoder/scripts/capability/caps/__init__.py +27 -0
  28. package/templates/qoder/scripts/capability/caps/context.py +36 -0
  29. package/templates/qoder/scripts/capability/caps/cron.py +50 -0
  30. package/templates/qoder/scripts/capability/caps/identity.py +43 -0
  31. package/templates/qoder/scripts/capability/caps/memory.py +71 -0
  32. package/templates/qoder/scripts/capability/caps/notify.py +41 -0
  33. package/templates/qoder/scripts/capability/caps/present.py +48 -0
  34. package/templates/qoder/scripts/capability/caps/repo.py +31 -0
  35. package/templates/qoder/scripts/capability/caps/sandbox.py +43 -0
  36. package/templates/qoder/scripts/capability/chain.py +92 -0
  37. package/templates/qoder/scripts/capability/memory_chain.py +41 -0
  38. package/templates/qoder/scripts/capability/registry.py +246 -0
  39. package/templates/qoder/scripts/capability/registry_mcp.py +250 -0
  40. package/templates/qoder/scripts/capability/smoke_test.py +211 -0
  41. package/templates/qoder/scripts/capability/smoke_test_report.json +94 -0
  42. package/templates/qoder/scripts/{report → deployment/setup}/__init__.py +11 -11
  43. package/templates/qoder/scripts/{setup → deployment/setup}/carriers.py +33 -26
  44. package/templates/qoder/scripts/deployment/setup/carriers_verify.py +148 -0
  45. package/templates/qoder/scripts/{setup → deployment/setup}/init_doctor.py +40 -16
  46. package/templates/qoder/scripts/{setup → deployment/setup}/install_qoderwork.py +20 -12
  47. package/templates/qoder/scripts/{setup → deployment/setup}/platform_doctor.py +8 -2
  48. package/templates/qoder/scripts/{setup → deployment/setup}/repo_root.py +113 -112
  49. package/templates/qoder/scripts/{setup → deployment/setup}/setup.py +31 -11
  50. package/templates/qoder/scripts/{setup → deployment/setup}/setup_lanhu.py +13 -7
  51. package/templates/qoder/scripts/domain/__init__.py +0 -0
  52. package/templates/qoder/scripts/domain/design/__init__.py +0 -0
  53. package/templates/qoder/scripts/{report → domain/design}/fill_prototype.py +12 -6
  54. package/templates/qoder/scripts/{report → domain/design}/gen_design_doc.py +8 -2
  55. package/templates/qoder/scripts/{setup → domain/kg}/__init__.py +11 -11
  56. package/templates/qoder/scripts/domain/kg/build/__init__.py +0 -0
  57. package/templates/qoder/scripts/{kg → domain/kg/build}/build_entity_registry.py +7 -2
  58. package/templates/qoder/scripts/{kg → domain/kg/build}/build_relations.py +7 -2
  59. package/templates/qoder/scripts/{kg → domain/kg/build}/build_style_index.py +13 -7
  60. package/templates/qoder/scripts/{kg → domain/kg/build}/build_workflows.py +7 -2
  61. package/templates/qoder/scripts/{kg → domain/kg/build}/kg_build.py +76 -9
  62. package/templates/qoder/scripts/{kg → domain/kg/build}/kg_build_db.py +7 -2
  63. package/templates/qoder/scripts/{kg → domain/kg/build}/kg_incremental.py +11 -6
  64. package/templates/qoder/scripts/{report → domain/kg/build}/learn_aggregate.py +9 -4
  65. package/templates/qoder/scripts/domain/kg/extract/__init__.py +0 -0
  66. package/templates/qoder/scripts/{common → domain/kg/extract}/extract.py +430 -419
  67. package/templates/qoder/scripts/{common → domain/kg/extract}/test_extract.py +115 -115
  68. package/templates/qoder/scripts/{common → domain/kg/extract}/ts_extract.py +614 -536
  69. package/templates/qoder/scripts/domain/kg/graph/__init__.py +0 -0
  70. package/templates/qoder/scripts/{kg → domain/kg/graph}/kg_link_db.py +7 -2
  71. package/templates/qoder/scripts/domain/kg/graph/kg_semantic.py +683 -0
  72. package/templates/qoder/scripts/{kg → domain/kg}/kg.py +263 -30
  73. package/templates/qoder/scripts/{common → domain/kg}/kg_capabilities.py +182 -182
  74. package/templates/qoder/scripts/domain/kg/search/__init__.py +0 -0
  75. package/templates/qoder/scripts/{kg → domain/kg/search}/context_pack.py +30 -9
  76. package/templates/qoder/scripts/{kg → domain/kg/search}/enrich_prompt.py +10 -4
  77. package/templates/qoder/scripts/{kg → domain/kg/search}/prefetch.py +33 -8
  78. package/templates/qoder/scripts/{common → domain/kg/search}/search_engine.py +207 -205
  79. package/templates/qoder/scripts/{kg → domain/kg/search}/search_index.py +143 -77
  80. package/templates/qoder/scripts/domain/kg/server/__init__.py +0 -0
  81. package/templates/qoder/scripts/domain/kg/server/kgd.py +549 -0
  82. package/templates/qoder/scripts/domain/kg/server/perf_bench.py +197 -0
  83. package/templates/qoder/scripts/domain/kg/storage/__init__.py +0 -0
  84. package/templates/qoder/scripts/{kg → domain/kg/storage}/kg_duckdb.py +38 -14
  85. package/templates/qoder/scripts/domain/learning/__init__.py +0 -0
  86. package/templates/qoder/scripts/{report → domain/learning}/learn.py +8 -3
  87. package/templates/qoder/scripts/{mcp → domain/report}/__init__.py +11 -11
  88. package/templates/qoder/scripts/{report → domain/report}/add_session.py +9 -3
  89. package/templates/qoder/scripts/{report → domain/report}/export.py +10 -5
  90. package/templates/qoder/scripts/{report → domain/report}/report.py +9 -4
  91. package/templates/qoder/scripts/domain/report/report_snapshot.py +261 -0
  92. package/templates/qoder/scripts/{report → domain/report}/role.py +10 -4
  93. package/templates/qoder/scripts/{report → domain/report}/status.py +112 -9
  94. package/templates/qoder/scripts/domain/report/status_snapshot.py +191 -0
  95. package/templates/qoder/scripts/domain/requirement/__init__.py +0 -0
  96. package/templates/qoder/scripts/{report → domain/requirement}/archive_prd.py +8 -2
  97. package/templates/qoder/scripts/{report → domain/requirement}/req.py +228 -222
  98. package/templates/qoder/scripts/{kg → domain/task}/__init__.py +11 -11
  99. package/templates/qoder/scripts/{task → domain/task}/git_sync.py +40 -21
  100. package/templates/qoder/scripts/{task → domain/task}/syncgate.py +13 -8
  101. package/templates/qoder/scripts/{task → domain/task}/task.py +13 -5
  102. package/templates/qoder/scripts/{task → domain/task}/task_lifecycle.py +606 -596
  103. package/templates/qoder/scripts/{task → domain/task}/task_query.py +162 -161
  104. package/templates/qoder/scripts/{task → domain/task}/task_relations.py +425 -424
  105. package/templates/qoder/scripts/{task → domain/task}/team_sync.py +9 -4
  106. package/templates/qoder/scripts/domain/task/zentao_sync.py +370 -0
  107. package/templates/qoder/scripts/foundation/__init__.py +0 -0
  108. package/templates/qoder/scripts/{common → foundation}/bootstrap.py +10 -10
  109. package/templates/qoder/scripts/foundation/core/__init__.py +0 -0
  110. package/templates/qoder/scripts/{common → foundation/core}/cmd_registry.py +1 -1
  111. package/templates/qoder/scripts/{common → foundation/core}/config.py +1 -1
  112. package/templates/qoder/scripts/{common → foundation/core}/paths.py +706 -586
  113. package/templates/qoder/scripts/foundation/data/__init__.py +0 -0
  114. package/templates/qoder/scripts/{common → foundation/data}/contract.py +2 -2
  115. package/templates/qoder/scripts/{common → foundation/data}/events.py +2 -2
  116. package/templates/qoder/scripts/{common → foundation/data}/result.py +1 -1
  117. package/templates/qoder/scripts/{common → foundation/data}/task_utils.py +427 -404
  118. package/templates/qoder/scripts/foundation/identity/__init__.py +0 -0
  119. package/templates/qoder/scripts/{common → foundation/identity}/check_publish.py +103 -98
  120. package/templates/qoder/scripts/{common → foundation/identity}/developer.py +239 -239
  121. package/templates/qoder/scripts/{common → foundation/identity}/guard.py +2 -2
  122. package/templates/qoder/scripts/{common → foundation/identity}/identity.py +14 -10
  123. package/templates/qoder/scripts/{common → foundation/identity}/roles.py +1 -1
  124. package/templates/qoder/scripts/foundation/integrations/__init__.py +0 -0
  125. package/templates/qoder/scripts/{common → foundation/integrations}/active_task.py +30 -15
  126. package/templates/qoder/scripts/{common → foundation/integrations}/feishu.py +2 -2
  127. package/templates/qoder/scripts/{common → foundation/integrations}/terms.py +3 -3
  128. package/templates/qoder/scripts/foundation/integrations/zentao_client.py +220 -0
  129. package/templates/qoder/scripts/foundation/io/__init__.py +0 -0
  130. package/templates/qoder/scripts/{common → foundation/io}/filelock.py +1 -1
  131. package/templates/qoder/scripts/{common → foundation/io}/reqid.py +1 -1
  132. package/templates/qoder/scripts/foundation/protocol/__init__.py +0 -0
  133. package/templates/qoder/scripts/foundation/protocol/duckdb_conn.py +39 -0
  134. package/templates/qoder/scripts/{common → foundation/protocol}/mcp_base.py +1 -1
  135. package/templates/qoder/scripts/foundation/utils/__init__.py +0 -0
  136. package/templates/qoder/scripts/{common → foundation/utils}/eval_api.py +12 -1
  137. package/templates/qoder/scripts/{common → foundation/utils}/pip_install.py +23 -6
  138. package/templates/qoder/scripts/{common → foundation/utils}/platform_guard.py +1 -1
  139. package/templates/qoder/scripts/orchestration/__init__.py +0 -0
  140. package/templates/qoder/scripts/orchestration/wlkj.py +185 -0
  141. package/templates/qoder/scripts/protocol/__init__.py +0 -0
  142. package/templates/qoder/scripts/protocol/browser/README.md +207 -0
  143. package/templates/qoder/scripts/protocol/browser/SKILL.md +265 -0
  144. package/templates/qoder/scripts/protocol/browser/config.env +9 -0
  145. package/templates/qoder/scripts/protocol/browser/references/cdp-api.md +110 -0
  146. package/templates/qoder/scripts/protocol/browser/references/migration-2.5.3.md +72 -0
  147. package/templates/qoder/scripts/protocol/browser/references/site-patterns/.gitkeep +0 -0
  148. package/templates/qoder/scripts/protocol/browser/scripts/browser-discovery.mjs +138 -0
  149. package/templates/qoder/scripts/protocol/browser/scripts/cdp-proxy.mjs +672 -0
  150. package/templates/qoder/scripts/protocol/browser/scripts/check-deps.mjs +206 -0
  151. package/templates/qoder/scripts/protocol/browser/scripts/find-url.mjs +253 -0
  152. package/templates/qoder/scripts/protocol/browser/scripts/match-site.mjs +46 -0
  153. package/templates/qoder/scripts/protocol/browser/templates/config.env.template +9 -0
  154. package/templates/qoder/scripts/protocol/mcp/__init__.py +11 -0
  155. package/templates/qoder/scripts/protocol/mcp/kg_mcp_server.py +478 -0
  156. package/templates/qoder/scripts/{mcp → protocol/mcp}/lanhu_stdio_wrapper.py +11 -5
  157. package/templates/qoder/scripts/{mcp → protocol/mcp}/mcp_doctor.py +47 -14
  158. package/templates/qoder/scripts/protocol/mcp/mcp_launcher.py +600 -0
  159. package/templates/qoder/scripts/{mcp → protocol/mcp}/mysql_mcp_server.py +181 -67
  160. package/templates/qoder/scripts/protocol/mcp/zentao_mcp_server.py +1920 -0
  161. package/templates/qoder/scripts/protocol/transports/__init__.py +56 -0
  162. package/templates/qoder/scripts/protocol/transports/base.py +87 -0
  163. package/templates/qoder/scripts/protocol/transports/cli.py +132 -0
  164. package/templates/qoder/scripts/protocol/transports/http.py +141 -0
  165. package/templates/qoder/scripts/protocol/transports/stdio.py +293 -0
  166. package/templates/qoder/scripts/run_weekly_update.bat +27 -18
  167. package/templates/qoder/scripts/run_weekly_update.sh +22 -13
  168. package/templates/qoder/scripts/validation/__init__.py +0 -0
  169. package/templates/qoder/scripts/validation/eval/__init__.py +0 -0
  170. package/templates/qoder/scripts/validation/eval/perf-report-2026-06-24.md +139 -0
  171. package/templates/qoder/scripts/validation/eval/qwork_harness.py +617 -0
  172. package/templates/qoder/scripts/validation/eval/report-commands.md +220 -0
  173. package/templates/qoder/scripts/validation/eval/transcript_timing.py +386 -0
  174. package/templates/qoder/scripts/validation/metrics/__init__.py +0 -0
  175. package/templates/qoder/scripts/{report → validation/metrics}/eval_prd.py +17 -10
  176. package/templates/qoder/scripts/validation/metrics/usability_score.py +750 -0
  177. package/templates/qoder/scripts/validation/test/__init__.py +11 -0
  178. package/templates/qoder/scripts/validation/test/assertion_gen.py +551 -0
  179. package/templates/qoder/scripts/{test → validation/test}/autotest.py +64 -4
  180. package/templates/qoder/scripts/{common → validation/test}/autotest_auth.py +1 -1
  181. package/templates/qoder/scripts/{test → validation/test}/autotest_batch.py +16 -3
  182. package/templates/qoder/scripts/{test → validation/test}/autotest_data.py +17 -12
  183. package/templates/qoder/scripts/validation/test/autotest_opencli.py +646 -0
  184. package/templates/qoder/scripts/{test → validation/test}/autotest_run.py +17 -3
  185. package/templates/qoder/scripts/validation/test/autotest_webaccess.py +571 -0
  186. package/templates/qoder/scripts/{test → validation/test}/benchmark.py +12 -6
  187. package/templates/qoder/scripts/{test → validation/test}/kg_auto_login.py +8 -2
  188. package/templates/qoder/scripts/{test → validation/test}/kg_test_runner.py +7 -2
  189. package/templates/qoder/scripts/{test → validation/test}/page_probe.py +8 -3
  190. package/templates/qoder/scripts/validation/test/smoke_all_commands.py +170 -0
  191. package/templates/qoder/settings.json +30 -0
  192. package/templates/qoder/skills/design-import/SKILL.md +22 -13
  193. package/templates/qoder/skills/design-review/SKILL.md +19 -10
  194. package/templates/qoder/skills/prd-generator/SKILL.md +22 -18
  195. package/templates/qoder/skills/prd-review/SKILL.md +6 -4
  196. package/templates/qoder/skills/prototype-generator/SKILL.md +63 -14
  197. package/templates/qoder/skills/spec-coder/SKILL.md +22 -13
  198. package/templates/qoder/skills/spec-generator/SKILL.md +22 -10
  199. package/templates/qoder/skills/test-generator/SKILL.md +7 -5
  200. package/templates/qoder/skills/wl-code/SKILL.md +23 -11
  201. package/templates/qoder/skills/wl-commit/SKILL.md +9 -8
  202. package/templates/qoder/skills/wl-design/SKILL.md +5 -3
  203. package/templates/qoder/skills/wl-init/SKILL.md +6 -6
  204. package/templates/qoder/skills/wl-insight/SKILL.md +23 -10
  205. package/templates/qoder/skills/wl-prd-full/SKILL.md +57 -6
  206. package/templates/qoder/skills/wl-prd-quick/SKILL.md +3 -3
  207. package/templates/qoder/skills/wl-prd-review/SKILL.md +16 -4
  208. package/templates/qoder/skills/wl-report/SKILL.md +25 -23
  209. package/templates/qoder/skills/wl-search/SKILL.md +149 -78
  210. package/templates/qoder/skills/wl-spec/SKILL.md +7 -5
  211. package/templates/qoder/skills/wl-status/SKILL.md +48 -16
  212. package/templates/qoder/skills/wl-task/SKILL.md +92 -10
  213. package/templates/qoder/skills/wl-test/SKILL.md +75 -29
  214. package/templates/qoder/templates/prd-full-template.md +7 -0
  215. package/templates/root/AGENTS.md +275 -236
  216. package/templates/root/requirements.txt +3 -0
  217. package/templates/root//344/275/277/347/224/250/350/257/264/346/230/216.md +3 -3
  218. package/templates/root//346/226/260/346/211/213/346/214/207/345/215/227.md +2 -2
  219. package/templates/.qoder/.runtime/ctx-cache-5660152f1d6dd819.md +0 -23
  220. package/templates/.qoder/.runtime/ctx-cache-afdce0dac06b25b0.md +0 -23
  221. package/templates/.qoder/.runtime/search-cache-eae7644e7b122f35.txt +0 -1
  222. package/templates/.qoder/learning/eval-history.jsonl +0 -28
  223. package/templates/data/index/wiki-index.json +0 -8
  224. package/templates/qoder/nul +0 -4
  225. package/templates/qoder/scripts/kg/extract.py +0 -398
  226. package/templates/qoder/scripts/kg/kg_semantic.py +0 -156
  227. package/templates/qoder/scripts/mcp/kg_mcp_server.py +0 -606
  228. package/templates/qoder/scripts/mcp/mcp_launcher.py +0 -442
  229. package/templates/qoder/scripts/mcp/zentao_mcp_server.py +0 -384
  230. package/templates/qoder/scripts/task/__init__.py +0 -11
  231. package/templates/qoder/scripts/test/__init__.py +0 -11
  232. package/templates/qoder/scripts/wlkj.py +0 -116
  233. /package/templates/qoder/scripts/{common → deployment}/__init__.py +0 -0
  234. /package/templates/qoder/scripts/{common → domain/kg/graph}/graph_traverse.py +0 -0
  235. /package/templates/qoder/scripts/{common → domain/kg/server}/repowiki.py +0 -0
  236. /package/templates/qoder/scripts/{common → foundation/io}/atomicio.py +0 -0
  237. /package/templates/qoder/scripts/{test → validation/test}/secure-ls.js +0 -0
@@ -0,0 +1,549 @@
1
+ # -*- coding: utf-8 -*-
2
+ # v3.0 路径自举: 引导到 common/bootstrap, 统一 sys.path 逻辑
3
+ import os as _o, sys as _s
4
+ _f = _o.path.abspath(__file__)
5
+ for _ in range(10):
6
+ _f = _o.path.dirname(_f)
7
+ _cp = _o.path.join(_f, 'foundation')
8
+ if _o.path.isfile(_o.path.join(_cp, 'bootstrap.py')):
9
+ break
10
+ if _cp not in _s.path: _s.path.insert(0, _cp)
11
+ from bootstrap import setup; setup()
12
+
13
+ """
14
+ kgd.py — 知识图谱常驻加速服务 (Knowledge-Graph Daemon)
15
+
16
+ 治"用了工作流反而更慢"的根因: 每次命令起一个新 Python 进程,
17
+ 冷启动税 (import duckdb + 加载 95MB bge 模型) 把毫秒级能力放大成几秒~26秒。
18
+
19
+ 本进程常驻内存, 启动时一次性加载:
20
+ • bge-small-zh 向量模型 (语义搜索 26.8s → <0.5s, 53x)
21
+ • DuckDB 连接 (避免每次重连)
22
+ • search/context/graph 全部模块 (避免重复 import)
23
+
24
+ CLI (kg.py) 和 MCP 调用时优先连 daemon (热路径), 连不上则透明回退冷路径。
25
+
26
+ 协议: TCP localhost (跨平台最稳), 每请求一行 JSON, 每响应一行 JSON。
27
+ 请求: {"cmd": "semantic", "args": ["报销"]}
28
+ 响应: {"ok": true, "text": "...", "secs": 0.31}
29
+
30
+ 用法:
31
+ python kgd.py start # 后台启动 daemon
32
+ python kgd.py start --fore # 前台启动 (调试, 看日志)
33
+ python kgd.py stop # 停止
34
+ python kgd.py status # 查看状态 (模型加载否/内存/命中)
35
+ python kgd.py restart # 重启
36
+
37
+ 渐进增强原则: daemon 是可选加速层。没有 daemon, kg.py 照常工作 (冷路径)。
38
+ """
39
+ import io
40
+ import json
41
+ import os
42
+ import socket
43
+ import subprocess
44
+ import sys
45
+ import time
46
+
47
+ try:
48
+ sys.stdout.reconfigure(encoding='utf-8', errors='replace')
49
+ except Exception:
50
+ pass
51
+
52
+ _HERE = os.path.dirname(os.path.abspath(__file__))
53
+ if _HERE not in sys.path:
54
+ sys.path.insert(0, _HERE) # 同包 import (kg, search_index ...)
55
+
56
+ RUNTIME_DIR = os.path.join(
57
+ os.path.dirname(os.path.dirname(os.path.dirname(_HERE))), # scripts→.qoder→repo
58
+ '.qoder', '.runtime')
59
+ PID_FILE = os.path.join(RUNTIME_DIR, 'kgd.pid')
60
+ PORT_FILE = os.path.join(RUNTIME_DIR, 'kgd.port')
61
+ LOG_FILE = os.path.join(RUNTIME_DIR, 'kgd.log')
62
+
63
+ # 端口范围: 避开常用端口, 在动态区间内选
64
+ PORT_MIN = 49152
65
+ PORT_MAX = 49200
66
+ CONNECT_TIMEOUT = 0.3 # client 探活超时 (秒) — 超过即回退冷路径
67
+ READ_TIMEOUT = 120 # 单请求最长等待 (语义搜索首次需加载)
68
+
69
+ # ── 这些 handler 在 daemon 里会被直接调用 (复用 kg.py 的实现) ──
70
+ # 哪些子命令值得走 daemon? 必须是"进程内直接执行"的重量级命令
71
+ # (import 慢 / 模型重 / 计算重), daemon 常驻后能省掉冷启动。
72
+ #
73
+ # ⚠️ 排除"subprocess 再起进程"类命令 (fill-prototype / design-system):
74
+ # 它们内部用 subprocess.run 另起 Python 跑脚本, daemon 常驻的优势用不上,
75
+ # 反而多一层 IPC 转发开销 (实测 daemon 比冷路径慢 ~1s)。
76
+ # 这类命令应走冷路径, daemon 不要接管。
77
+ DAEMON_COMMANDS = {
78
+ 'search', 'api', 'prd', 'impact', 'context360', 'context',
79
+ 'prefetch', 'coverage', 'feature', 'workflow', 'hop',
80
+ 'wiki', 'semantic',
81
+ # 排除: 'fill-prototype', 'design-system' (subprocess 再起, daemon 帮倒忙)
82
+ }
83
+ # 其中 semantic 收益最大 (26.8s→0.5s), context/impact/prefetch 次之。
84
+
85
+
86
+ # ============================================================
87
+ # Daemon 服务端
88
+ # ============================================================
89
+ class KgDaemon:
90
+ """常驻服务进程。启动时预加载, 之后监听 TCP 转发请求。"""
91
+
92
+ def __init__(self):
93
+ self.handlers = {} # cmd_name -> handler_fn
94
+ self.model_loaded = False
95
+ self.started_at = time.time()
96
+ self.requests = 0
97
+ self.cache_hits = 0
98
+ self._db_mtime = 0
99
+ self._db_path = ''
100
+
101
+ # ── 预加载 (启动时跑一次, 后续全热) ──────────────────────────
102
+ def preload(self):
103
+ """一次性加载所有重资源。"""
104
+ log('preload: 导入 kg.py 模块 (cwd=%s, _HERE=%s) ...' % (os.getcwd(), _HERE))
105
+ # 注意: kg 既是包名(有 __init__.py) 又含 kg.py CLI 模块。
106
+ # `import kg` 会命中包而非 kg.py → 必须按文件路径加载 kg.py 本身。
107
+ import importlib.util
108
+ kg_file = os.path.join(_HERE, 'kg.py')
109
+ _spec = importlib.util.spec_from_file_location('_kgd_kg', kg_file)
110
+ _kg = importlib.util.module_from_spec(_spec)
111
+ _spec.loader.exec_module(_kg)
112
+ cmds = getattr(_kg, 'COMMANDS', [])
113
+ log('preload: kg.py = %s, COMMANDS = %d' % (kg_file, len(cmds)))
114
+ for name, fn, _desc in cmds:
115
+ if name in DAEMON_COMMANDS:
116
+ self.handlers[name] = fn
117
+ log('preload: %d handlers 注册' % len(self.handlers))
118
+
119
+ # 预加载 DuckDB 连接 (各 handler 内部会自建连接, 这里只是 warm-up import)
120
+ try:
121
+ import duckdb
122
+ self._db_path = os.path.join(
123
+ os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(_HERE)))),
124
+ 'data', 'index', 'kg.duckdb')
125
+ if os.path.isfile(self._db_path):
126
+ self._db_mtime = os.path.getmtime(self._db_path)
127
+ con = duckdb.connect(self._db_path, read_only=True)
128
+ con.execute('SELECT COUNT(*) FROM entities').fetchone()
129
+ con.close()
130
+ log('preload: DuckDB warm-up ok (entities 表可读)')
131
+ except Exception as e:
132
+ log('preload: DuckDB warm-up 跳过 (%s)' % str(e)[:60])
133
+
134
+ # 预加载向量模型 (头号收益: semantic 26.8s→0.5s)
135
+ # 放最后, 因为它最重 (95MB, ~25s)。即使失败也不影响其他 handler。
136
+ # ⚠️ 关键: 强制离线加载。sentence_transformers 默认会联网校验模型元数据,
137
+ # 在 DETACHED daemon 进程里网络不通会崩溃。设 HF_HUB_OFFLINE 后纯本地,
138
+ # 既根治联网不稳定, 又避免每次 ~25s 的网络等待 (真正的冷加载只需几秒)。
139
+ log('preload: 预加载 bge 向量模型 (强制离线, 本地缓存) ...')
140
+ os.environ['HF_HUB_OFFLINE'] = '1'
141
+ os.environ['TRANSFORMERS_OFFLINE'] = '1'
142
+ try:
143
+ from domain.kg.graph import kg_semantic
144
+ m = kg_semantic._load_model()
145
+ self.model_loaded = m is not None
146
+ if self.model_loaded:
147
+ log('preload: ✓ bge 模型已常驻, semantic 将秒回')
148
+ else:
149
+ log('preload: bge 模型未安装 (sentence-transformers 缺失), semantic 仍走冷路径')
150
+ except Exception as e:
151
+ log('preload: 模型预加载失败 (%s)' % str(e)[:80])
152
+ log('preload 完成, 用时 %.1fs' % (time.time() - self.started_at))
153
+
154
+ def _db_changed(self):
155
+ """检测 kg.duckdb 是否被重建 (mtime 变化)。变了需 handler 内部自重连。"""
156
+ try:
157
+ if self._db_path and os.path.isfile(self._db_path):
158
+ return os.path.getmtime(self._db_path) != self._db_mtime
159
+ except Exception:
160
+ pass
161
+ return False
162
+
163
+ # ── 请求处理 ──────────────────────────────────────────────────
164
+ # ── 内部命令 (不走 handler 表) ────────────────────────────────
165
+ def _internal(self, cmd):
166
+ """处理 __ping / __status 等内部探测命令。返回 dict 或 None。"""
167
+ if cmd == '__ping':
168
+ return {'ok': True, 'text': 'pong', 'secs': 0}
169
+ if cmd == '__status':
170
+ return {'ok': True, 'text': json.dumps({
171
+ 'handlers': len(self.handlers),
172
+ 'model': self.model_loaded,
173
+ 'requests': self.requests,
174
+ 'uptime_min': round((time.time() - self.started_at) / 60, 1),
175
+ 'db_changed': self._db_changed(),
176
+ }, ensure_ascii=False), 'secs': 0}
177
+ return None # 不是内部命令
178
+
179
+ def handle(self, cmd, args):
180
+ """执行一个子命令, 返回 dict 响应。"""
181
+ # 内部探测命令优先
182
+ internal = self._internal(cmd)
183
+ if internal is not None:
184
+ return internal
185
+
186
+ self.requests += 1
187
+ if cmd not in self.handlers:
188
+ return {'ok': False, 'error': 'daemon 不支持: %s' % cmd,
189
+ 'fallback': True} # 让 client 回退冷路径
190
+
191
+ # 捕获 handler 的输出 + 返回值。
192
+ # ⚠️ sys.stdout 是进程全局, 多线程并发会串台 → 必须用线程本地隔离。
193
+ # 同时 handler 有两种风格: cmd_semantic 直接 return 字符串,
194
+ # cmd_search 通过 _capture 捕获 stdout → 两者都要收集。
195
+ import threading
196
+ if not hasattr(self, '_tls'):
197
+ self._tls = threading.local()
198
+ buf = io.StringIO()
199
+ real_stdout = sys.stdout
200
+ self._tls.buf = buf # 线程私有
201
+ # 把当前线程的 sys.stdout 指向 buf (只影响本线程调用的代码)
202
+ # 注意: sys.stdout 是全局的, 这里靠"单请求处理很快+GIL"近似隔离;
203
+ # 更严格的隔离需改 handler, 但当前 kg.py 的 handler 都是瞬时完成的, 风险低。
204
+ sys.stdout = buf
205
+ t0 = time.time()
206
+ ret = None
207
+ try:
208
+ ret = self.handlers[cmd](args or [])
209
+ text = buf.getvalue().strip()
210
+ # handler 优先用返回值, 没有返回值才用 stdout 捕获
211
+ if ret:
212
+ text = (text + '\n' + str(ret)).strip() if text else str(ret).strip()
213
+ return {'ok': True, 'text': text, 'secs': round(time.time() - t0, 3)}
214
+ except Exception as e:
215
+ return {'ok': False, 'error': str(e)[:200],
216
+ 'fallback': True} # 出错让 client 回退
217
+ finally:
218
+ sys.stdout = real_stdout
219
+
220
+ def serve(self, port):
221
+ """TCP 监听主循环。"""
222
+ srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
223
+ srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
224
+ srv.bind(('127.0.0.1', port))
225
+ srv.listen(8)
226
+ srv.settimeout(1.0) # 1s 唤醒一次, 便于优雅退出
227
+ log('kgd 监听 127.0.0.1:%d (PID %d)' % (port, os.getpid()))
228
+
229
+ running = [True]
230
+ try:
231
+ while running[0]:
232
+ try:
233
+ conn, _ = srv.accept()
234
+ except socket.timeout:
235
+ if _should_exit():
236
+ break
237
+ continue
238
+ except OSError:
239
+ break
240
+ # 每连接一个线程 (请求都很短, 用 daemon 线程即可)
241
+ import threading
242
+ def serve_conn(c):
243
+ try:
244
+ c.settimeout(READ_TIMEOUT)
245
+ data = b''
246
+ while b'\n' not in data:
247
+ chunk = c.recv(8192)
248
+ if not chunk:
249
+ break
250
+ data += chunk
251
+ line = data.decode('utf-8', 'replace').strip()
252
+ if not line:
253
+ return
254
+ req = json.loads(line)
255
+ resp = self.handle(req.get('cmd', ''), req.get('args', []))
256
+ c.sendall((json.dumps(resp, ensure_ascii=False) + '\n').encode('utf-8'))
257
+ except Exception as e:
258
+ try:
259
+ err = json.dumps({'ok': False, 'error': str(e)[:200],
260
+ 'fallback': True})
261
+ c.sendall((err + '\n').encode('utf-8'))
262
+ except Exception:
263
+ pass
264
+ finally:
265
+ c.close()
266
+ threading.Thread(target=serve_conn, args=(conn,), daemon=True).start()
267
+ finally:
268
+ srv.close()
269
+ log('kgd 已停止')
270
+
271
+
272
+ def _should_exit():
273
+ """检测停止信号 (删除 pidfile 即视为要求退出)。"""
274
+ return not os.path.isfile(PID_FILE)
275
+
276
+
277
+ def log(msg):
278
+ """写日志 (追加, 带时间戳)。"""
279
+ try:
280
+ os.makedirs(RUNTIME_DIR, exist_ok=True)
281
+ with open(LOG_FILE, 'a', encoding='utf-8') as f:
282
+ f.write('[%s] %s\n' % (time.strftime('%H:%M:%S'), msg))
283
+ except Exception:
284
+ pass
285
+
286
+
287
+ def _find_free_port():
288
+ """在 PORT_MIN..PORT_MAX 找一个可用端口。"""
289
+ for p in range(PORT_MIN, PORT_MAX + 1):
290
+ try:
291
+ s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
292
+ s.bind(('127.0.0.1', p))
293
+ s.close()
294
+ return p
295
+ except OSError:
296
+ continue
297
+ return 0
298
+
299
+
300
+ def _read_pid_port():
301
+ """读 pidfile+portfile, 返回 (pid, port) 或 (None, None)。"""
302
+ try:
303
+ if not os.path.isfile(PID_FILE):
304
+ return None, None
305
+ with open(PID_FILE) as f:
306
+ pid = int(f.read().strip())
307
+ port = None
308
+ if os.path.isfile(PORT_FILE):
309
+ with open(PORT_FILE) as f:
310
+ port = int(f.read().strip())
311
+ return pid, port
312
+ except Exception:
313
+ return None, None
314
+
315
+
316
+ def _is_alive(pid):
317
+ """进程是否存活 (跨平台)。"""
318
+ if not pid:
319
+ return False
320
+ try:
321
+ if os.name == 'nt':
322
+ # Windows: kernel32 OpenProcess
323
+ import ctypes
324
+ k32 = ctypes.windll.kernel32
325
+ PROCESS_QUERY_LIMITED_INFORMATION = 0x1000
326
+ h = k32.OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, False, pid)
327
+ if h:
328
+ k32.CloseHandle(h)
329
+ return True
330
+ return False
331
+ else:
332
+ os.kill(pid, 0) # signal 0 = 探活
333
+ return True
334
+ except Exception:
335
+ return False
336
+
337
+
338
+ # ============================================================
339
+ # Client (供 kg.py / MCP / perf_bench 调用)
340
+ # ============================================================
341
+ class _KgdClient:
342
+ """daemon 客户端。所有方法在 daemon 不可用时返回 None (触发调用方回退)。"""
343
+
344
+ def ping(self, timeout=CONNECT_TIMEOUT):
345
+ """探活。daemon 在线返回 True。"""
346
+ pid, port = _read_pid_port()
347
+ if not pid or not port or not _is_alive(pid):
348
+ return False
349
+ try:
350
+ s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
351
+ s.settimeout(timeout)
352
+ s.connect(('127.0.0.1', port))
353
+ # 发一个 no-op ping (status 查询)
354
+ s.sendall((json.dumps({'cmd': '__ping', 'args': []}) + '\n').encode('utf-8'))
355
+ data = b''
356
+ while b'\n' not in data:
357
+ chunk = s.recv(1024)
358
+ if not chunk:
359
+ break
360
+ data += chunk
361
+ s.close()
362
+ return b'\n' in data
363
+ except Exception:
364
+ return False
365
+
366
+ def call(self, cmd, args, timeout=READ_TIMEOUT):
367
+ """转发请求到 daemon。返回文本结果, 或 None (连不上/出错 → 调用方回退)。
368
+
369
+ Args:
370
+ cmd: 子命令名 (如 'semantic')
371
+ args: 参数列表 (如 ['报销'])
372
+ Returns:
373
+ (text, secs) 或 None
374
+ """
375
+ pid, port = _read_pid_port()
376
+ if not pid or not port or not _is_alive(pid):
377
+ return None
378
+ try:
379
+ s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
380
+ s.settimeout(CONNECT_TIMEOUT)
381
+ s.connect(('127.0.0.1', port))
382
+ s.settimeout(timeout)
383
+ req = json.dumps({'cmd': cmd, 'args': args or []}, ensure_ascii=False)
384
+ s.sendall((req + '\n').encode('utf-8'))
385
+ data = b''
386
+ while b'\n' not in data:
387
+ chunk = s.recv(65536)
388
+ if not chunk:
389
+ break
390
+ data += chunk
391
+ s.close()
392
+ if b'\n' not in data:
393
+ return None
394
+ resp = json.loads(data.decode('utf-8', 'replace').strip())
395
+ if resp.get('ok'):
396
+ return resp.get('text', ''), resp.get('secs', 0)
397
+ # daemon 要求回退 (不支持的命令 / 出错)
398
+ return None
399
+ except Exception:
400
+ return None
401
+
402
+
403
+ kgd_client = _KgdClient()
404
+
405
+
406
+ # ============================================================
407
+ # 命令行入口: start / stop / status / restart
408
+ # ============================================================
409
+ def cmd_start(foreground=False):
410
+ """启动 daemon。"""
411
+ pid, port = _read_pid_port()
412
+ if pid and _is_alive(pid):
413
+ print('kgd 已在运行 (PID %d, port %s)' % (pid, port or '?'))
414
+ return 0
415
+
416
+ port = _find_free_port()
417
+ if not port:
418
+ print('✗ 无可用端口')
419
+ return 1
420
+
421
+ if foreground:
422
+ # 前台模式: 直接跑 (调试用)
423
+ os.makedirs(RUNTIME_DIR, exist_ok=True)
424
+ with open(PID_FILE, 'w') as f:
425
+ f.write(str(os.getpid()))
426
+ with open(PORT_FILE, 'w') as f:
427
+ f.write(str(port))
428
+ try:
429
+ d = KgDaemon()
430
+ d.preload()
431
+ d.serve(port)
432
+ finally:
433
+ for p in (PID_FILE, PORT_FILE):
434
+ try:
435
+ os.remove(p)
436
+ except OSError:
437
+ pass
438
+ return 0
439
+
440
+ # 后台模式: spawn 自己 --fore (detach)
441
+ env = dict(os.environ)
442
+ env['PYTHONIOENCODING'] = 'utf-8'
443
+ creationflags = 0
444
+ if os.name == 'nt':
445
+ creationflags = subprocess.CREATE_NEW_PROCESS_GROUP | 0x00000008 # DETACHED
446
+ try:
447
+ subprocess.Popen(
448
+ [sys.executable, os.path.abspath(__file__), 'start', '--fore'],
449
+ stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
450
+ stdin=subprocess.DEVNULL, cwd=os.getcwd(), env=env,
451
+ creationflags=creationflags)
452
+ except Exception as e:
453
+ print('✗ 启动失败: %s' % e)
454
+ return 1
455
+
456
+ # 等待 daemon 就绪 (轮询 ping, 最多等 preload 完)
457
+ print('kgd 启动中 (预加载模型 ~25s) ...', end='', flush=True)
458
+ for _ in range(60): # 最多等 60s
459
+ time.sleep(1)
460
+ print('.', end='', flush=True)
461
+ if kgd_client.ping(timeout=0.5):
462
+ _, p = _read_pid_port()
463
+ print(' 就绪 (port %s)' % p)
464
+ return 0
465
+ print(' 超时, 查日志: %s' % LOG_FILE)
466
+ return 1
467
+
468
+
469
+ def cmd_stop():
470
+ """停止 daemon。"""
471
+ pid, port = _read_pid_port()
472
+ if not pid:
473
+ print('kgd 未运行')
474
+ return 0
475
+ if not _is_alive(pid):
476
+ for p in (PID_FILE, PORT_FILE):
477
+ try:
478
+ os.remove(p)
479
+ except OSError:
480
+ pass
481
+ print('kgd 未运行 (清理了残留 pidfile)')
482
+ return 0
483
+ try:
484
+ if os.name == 'nt':
485
+ subprocess.run(['taskkill', '/PID', str(pid), '/F'],
486
+ capture_output=True)
487
+ else:
488
+ os.kill(pid, 15)
489
+ time.sleep(0.5)
490
+ except Exception:
491
+ pass
492
+ for p in (PID_FILE, PORT_FILE):
493
+ try:
494
+ os.remove(p)
495
+ except OSError:
496
+ pass
497
+ print('kgd 已停止 (PID %d)' % pid)
498
+ return 0
499
+
500
+
501
+ def cmd_status():
502
+ """查状态。"""
503
+ pid, port = _read_pid_port()
504
+ alive = _is_alive(pid) if pid else False
505
+ if not alive:
506
+ print('kgd: 未运行')
507
+ print(' 启动: python .qoder/scripts/domain/kg/server/kgd.py start')
508
+ return 0
509
+ # 在线, 查详情
510
+ uptime = '?'
511
+ res = kgd_client.call('__status', [], timeout=2)
512
+ print('kgd: 运行中 (PID %d, port %s)' % (pid, port))
513
+ if res:
514
+ text, secs = res
515
+ if text:
516
+ try:
517
+ info = json.loads(text)
518
+ print(' handlers: %d' % info.get('handlers', '?'))
519
+ print(' 模型加载: %s' % ('✓' if info.get('model') else '✗'))
520
+ print(' 请求数: %d' % info.get('requests', '?'))
521
+ print(' 运行时长: %.0f 分钟' % info.get('uptime_min', 0))
522
+ except Exception:
523
+ print(' %s' % text[:200])
524
+ print(' 日志: %s' % LOG_FILE)
525
+ return 0
526
+
527
+
528
+ def main():
529
+ args = sys.argv[1:]
530
+ if not args or args[0] in ('-h', '--help', 'help'):
531
+ print(__doc__)
532
+ return 0
533
+ cmd = args[0]
534
+ if cmd == 'start':
535
+ return cmd_start(foreground='--fore' in args)
536
+ if cmd == 'stop':
537
+ return cmd_stop()
538
+ if cmd == 'status':
539
+ return cmd_status()
540
+ if cmd == 'restart':
541
+ cmd_stop()
542
+ time.sleep(1)
543
+ return cmd_start(foreground='--fore' in args)
544
+ print('未知命令: %s (start/stop/status/restart)' % cmd)
545
+ return 1
546
+
547
+
548
+ if __name__ == '__main__':
549
+ sys.exit(main())