@hupan56/wlkj 3.3.14 → 3.3.15

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 (94) hide show
  1. package/bin/cli.js +16 -1
  2. package/package.json +1 -1
  3. package/templates/qoder/commands/optional/wl-spec.md +4 -2
  4. package/templates/qoder/commands/wl-commit.md +3 -1
  5. package/templates/qoder/commands/wl-init.md +2 -2
  6. package/templates/qoder/commands/wl-prd.md +24 -0
  7. package/templates/qoder/commands/wl-search.md +18 -11
  8. package/templates/qoder/commands/wl-task.md +3 -1
  9. package/templates/qoder/config.yaml +0 -6
  10. package/templates/qoder/contracts/insight.md +55 -0
  11. package/templates/qoder/hooks/pre-tool-use-commit.py +124 -0
  12. package/templates/qoder/hooks/session-start.py +20 -1
  13. package/templates/qoder/hooks/stop-eval.py +129 -0
  14. package/templates/qoder/scripts/capability/adapters/mcp.py +9 -1
  15. package/templates/qoder/scripts/capability/registry.py +0 -3
  16. package/templates/qoder/scripts/capability/registry_mcp.py +10 -4
  17. package/templates/qoder/scripts/deployment/setup/init_doctor.py +6 -2
  18. package/templates/qoder/scripts/deployment/setup/install_qoderwork.py +7 -7
  19. package/templates/qoder/scripts/deployment/setup/setup.py +5 -2
  20. package/templates/qoder/scripts/domain/integration/return_to_platform.py +4 -2
  21. package/templates/qoder/scripts/domain/integration/spec_upload.py +4 -4
  22. package/templates/qoder/scripts/domain/kg/build/build_entity_registry.py +12 -12
  23. package/templates/qoder/scripts/domain/kg/build/build_relations.py +12 -12
  24. package/templates/qoder/scripts/domain/kg/graph/kg_semantic.py +41 -5
  25. package/templates/qoder/scripts/domain/kg/kg.py +14 -0
  26. package/templates/qoder/scripts/domain/kg/switch_project.py +2 -2
  27. package/templates/qoder/scripts/domain/task/wlkj_panel.py +169 -14
  28. package/templates/qoder/scripts/foundation/io/context_cache.py +94 -0
  29. package/templates/qoder/scripts/orchestration/wlkj.py +106 -0
  30. package/templates/qoder/scripts/tool_guide.md +70 -0
  31. package/templates/qoder/scripts/validation/eval/alignment_matrix.py +176 -0
  32. package/templates/qoder/scripts/validation/eval/bf2_content_fidelity.py +110 -0
  33. package/templates/qoder/scripts/validation/eval/bf2_llmjudge.py +104 -0
  34. package/templates/qoder/scripts/validation/eval/bf_score.py +218 -0
  35. package/templates/qoder/scripts/validation/eval/code_flywheel.py +150 -0
  36. package/templates/qoder/scripts/validation/eval/dispatcher_ab.py +156 -0
  37. package/templates/qoder/scripts/validation/eval/dispatcher_ab_2026-07-21.json +23 -0
  38. package/templates/qoder/scripts/validation/eval/feature_fidelity_flywheel.py +143 -0
  39. package/templates/qoder/scripts/validation/eval/gradient_matrix.py +261 -0
  40. package/templates/qoder/scripts/validation/eval/gradient_matrix_baseline_2026-07-21.json +33 -0
  41. package/templates/qoder/scripts/validation/eval/metrics_dashboard.py +105 -0
  42. package/templates/qoder/scripts/validation/eval/multi_turn_flywheel.py +118 -0
  43. package/templates/qoder/scripts/validation/eval/prd_fidelity_flywheel.py +128 -0
  44. package/templates/qoder/scripts/validation/eval/prd_flywheel.py +148 -0
  45. package/templates/qoder/scripts/validation/eval/prototype_fidelity_flywheel.py +166 -0
  46. package/templates/qoder/scripts/validation/eval/recall_flywheel.py +148 -0
  47. package/templates/qoder/scripts/validation/eval/robustness_flywheel.py +139 -0
  48. package/templates/qoder/scripts/validation/eval/speed_accuracy_flywheel.py +188 -0
  49. package/templates/qoder/scripts/validation/eval/task_flywheel.py +124 -0
  50. package/templates/qoder/scripts/validation/eval/token_flywheel.py +88 -0
  51. package/templates/qoder/scripts/validation/metrics/eval_code_ac.py +177 -0
  52. package/templates/qoder/scripts/validation/metrics/lint_cases.py +170 -0
  53. package/templates/qoder/scripts/validation/test/test_context_cache.py +78 -0
  54. package/templates/qoder/scripts/validation/test/test_lint_cases.py +60 -0
  55. package/templates/qoder/scripts/validation/test/test_pre_tool_use_commit.py +70 -0
  56. package/templates/qoder/settings.json +8 -0
  57. package/templates/qoder/skills/wl-spec/SKILL.md +1 -1
  58. package/templates/qoder/agents/design-agent.md +0 -20
  59. package/templates/qoder/agents/spec-generator.md +0 -21
  60. package/templates/qoder/scripts/capability/smoke_test_report.json.new +0 -94
  61. package/templates/qoder/scripts/domain/kg/extract/asset/__init__.py +0 -10
  62. package/templates/qoder/scripts/domain/kg/extract/asset/asset_tree.py +0 -57
  63. package/templates/qoder/scripts/domain/kg/extract/asset/discussion_importer.py +0 -62
  64. package/templates/qoder/scripts/domain/kg/extract/asset/prd_importer.py +0 -146
  65. package/templates/qoder/scripts/domain/kg/extract/asset/prototype_importer.py +0 -64
  66. package/templates/qoder/scripts/domain/kg/extract/asset/returns_importer.py +0 -52
  67. package/templates/qoder/scripts/domain/kg/extract/build_goal3.py +0 -104
  68. package/templates/qoder/scripts/domain/kg/extract/build_goal4.py +0 -55
  69. package/templates/qoder/scripts/domain/kg/extract/build_goal5.py +0 -95
  70. package/templates/qoder/scripts/domain/kg/extract/db/__init__.py +0 -8
  71. package/templates/qoder/scripts/domain/kg/extract/db/data_profile.py +0 -22
  72. package/templates/qoder/scripts/domain/kg/extract/db/fk_extractor.py +0 -55
  73. package/templates/qoder/scripts/domain/kg/extract/db/schema_extractor.py +0 -90
  74. package/templates/qoder/scripts/domain/kg/extract/inference/__init__.py +0 -9
  75. package/templates/qoder/scripts/domain/kg/extract/inference/community_summarizer.py +0 -206
  76. package/templates/qoder/scripts/domain/kg/extract/inference/embed_builder.py +0 -132
  77. package/templates/qoder/scripts/domain/kg/extract/inference/naming_matcher.py +0 -80
  78. package/templates/qoder/scripts/domain/kg/extract/inference/promote.py +0 -59
  79. package/templates/qoder/scripts/domain/kg/extract/inference/recompute.py +0 -93
  80. package/templates/qoder/scripts/domain/kg/extract/inference/weak_link.py +0 -421
  81. package/templates/qoder/scripts/domain/kg/extract/mybatis/__init__.py +0 -9
  82. package/templates/qoder/scripts/domain/kg/extract/mybatis/all.py +0 -79
  83. package/templates/qoder/scripts/domain/kg/extract/mybatis/mapper_parser.py +0 -99
  84. package/templates/qoder/scripts/domain/kg/extract/mybatis/relation_builder.py +0 -69
  85. package/templates/qoder/scripts/domain/kg/extract/mybatis/sql_extractor.py +0 -78
  86. package/templates/qoder/scripts/domain/kg/extract/prd/__init__.py +0 -8
  87. package/templates/qoder/scripts/domain/kg/extract/prd/prd_chunk_embed.py +0 -105
  88. package/templates/qoder/scripts/domain/kg/extract/prd/prd_llm_extract.py +0 -153
  89. package/templates/qoder/scripts/domain/kg/extract/prd/req_anchor.py +0 -120
  90. package/templates/qoder/scripts/domain/kg/graph/kg_link_db.py +0 -235
  91. package/templates/qoder/scripts/domain/kg/search/enrich_prompt.py +0 -238
  92. package/templates/qoder/scripts/domain/kg/server/perf_bench.py +0 -197
  93. package/templates/qoder/scripts/domain/kg/sync_repowiki.py +0 -109
  94. package/templates/qoder/scripts/engine/poller.py +0 -219
@@ -834,6 +834,7 @@ def check_lanhu():
834
834
  def main():
835
835
  args = [a for a in sys.argv[1:]]
836
836
  fix = '--fix' in args
837
+ skip_code = '--skip-code' in args or '--cloud' in args
837
838
  pos = [a for a in args if not a.startswith('--')]
838
839
  name = pos[0] if pos else None
839
840
  role = pos[1] if len(pos) > 1 else None
@@ -848,8 +849,11 @@ def main():
848
849
  check_dependencies(fix)
849
850
  check_developer(fix, name, role)
850
851
  check_team_sync(fix)
851
- check_source_repos(fix, config)
852
- check_index(fix)
852
+ if skip_code:
853
+ print('\n--- 4. 源码/索引: 云模式跳过(KG 在云平台,引擎调云 MCP)---')
854
+ else:
855
+ check_source_repos(fix, config)
856
+ check_index(fix)
853
857
  check_style(fix)
854
858
  check_prd_templates()
855
859
  check_weekly(fix)
@@ -438,20 +438,20 @@ def rewrite_mcp_json_for_launcher():
438
438
  }
439
439
  changed = False
440
440
  for srv, sub in name_map.items():
441
- # 共识:kg 走云平台(读 config platform_mcp.url → SSE url),不连本地 kg.duckdb(空)
441
+ # 共识:kg 走云平台(默认 10.89.7.120:10010/mcp,config platform_mcp.url 可覆盖)
442
442
  if srv == "qoder-knowledge-graph":
443
+ _pm = "http://10.89.7.120:10010/mcp"
443
444
  try:
444
445
  import yaml as _y
445
446
  _cfg_p = get_repo_root() / ".qoder" / "config.yaml"
446
447
  if _cfg_p.is_file():
447
448
  _cfg = _y.safe_load(_cfg_p.read_text(encoding="utf-8")) or {}
448
- _pm = (_cfg.get("platform_mcp") or {}).get("url", "")
449
- if _pm:
450
- servers[srv] = {"url": _pm, "type": "sse", "enabled": True}
451
- changed = True
452
- continue
449
+ _pm = (_cfg.get("platform_mcp") or {}).get("url", "") or _pm
453
450
  except Exception:
454
- pass # config 无 platform_mcp → 回退 launcher
451
+ pass
452
+ servers[srv] = {"url": _pm, "type": "sse", "enabled": True}
453
+ changed = True
454
+ continue
455
455
  entry = servers.get(srv)
456
456
  # 全新机器: kg/mysql/lanhu 条目可能不存在 → 创建它 (launcher 模式)
457
457
  if not entry:
@@ -242,9 +242,11 @@ def detect_name(explicit=None):
242
242
  # Step 2: 调 init_doctor --fix (主体)
243
243
  # ============================================================
244
244
 
245
- def run_doctor(name, role):
245
+ def run_doctor(name, role, skip_code=False):
246
246
  print('\n--- 初始化 (init_doctor) ---')
247
247
  cmd = [sys.executable, str(THIS_DIR / 'init_doctor.py'), '--fix', name, role]
248
+ if skip_code:
249
+ cmd.append('--skip-code')
248
250
  # 透传 stdio (用户能看到实时输出) + 强制 UTF-8 环境 (解决 cmd GBK 乱码)
249
251
  env = dict(os.environ)
250
252
  env['PYTHONIOENCODING'] = 'utf-8'
@@ -911,6 +913,7 @@ def main():
911
913
  parser.add_argument('--skip-cron', action='store_true', help='跳过 cron 注册')
912
914
  parser.add_argument('--skip-qoderwork', action='store_true', help='跳过 QoderWork 安装')
913
915
  parser.add_argument('--skip-lanhu', action='store_true', help='跳过蓝湖 MCP 引导')
916
+ parser.add_argument('--skip-code', '--cloud', action='store_true', help='云模式:跳过本地源码 clone/索引(KG 在云平台)')
914
917
  args = parser.parse_args()
915
918
 
916
919
  print('=' * 56)
@@ -931,7 +934,7 @@ def main():
931
934
  configure_git_identity(name)
932
935
 
933
936
  # Step 2
934
- if not run_doctor(name, args.role):
937
+ if not run_doctor(name, args.role, args.skip_code):
935
938
  warn('init_doctor 未完全成功, 继续 setup 后续步骤...')
936
939
 
937
940
  # Step 3
@@ -20,6 +20,7 @@ integration.return_to_platform —— 引擎产出回流平台的统一通道。
20
20
  """
21
21
  from __future__ import annotations
22
22
 
23
+ import os
23
24
  from typing import Optional
24
25
 
25
26
 
@@ -34,6 +35,7 @@ def _load_mcp_config() -> dict:
34
35
  except Exception:
35
36
  repo = Path(__file__).resolve().parent.parent.parent.parent.parent
36
37
  for cand in [
38
+ repo / ".qoder" / "mcp_config.json", # ★ switch_project 写这里,优先查(否则读不到 token → 回流误报"无 X-Engine-Token")
37
39
  repo / "mcp_config.json",
38
40
  ]:
39
41
  try:
@@ -121,7 +123,7 @@ def _via_http(payload: dict) -> tuple[bool, str]:
121
123
 
122
124
 
123
125
  def _platform_base() -> str:
124
- """平台 base urlhttp://127.0.0.1:10010),从 mcp_config server url 推导。"""
126
+ """平台 base url(默认 http://10.89.7.120 云),从 mcp_config server url 推导。"""
125
127
  from urllib.parse import urlparse
126
128
  cfg = _load_mcp_config()
127
129
  for srv in (cfg.get("mcpServers") or {}).values():
@@ -133,7 +135,7 @@ def _platform_base() -> str:
133
135
  if ep:
134
136
  p = urlparse(ep)
135
137
  return "%s://%s" % (p.scheme, p.netloc)
136
- return "http://127.0.0.1:10010"
138
+ return os.environ.get("WLKJ_BASE_URL", "http://10.89.7.120")
137
139
 
138
140
 
139
141
  def _engine_token() -> Optional[str]:
@@ -5,7 +5,7 @@
5
5
  契约: wlinkj-workspace/docs/SPEC-MCP-CONTRACT.md v1(create/confirm/update/get_spec)
6
6
 
7
7
  流程: 解析契约头 → 有platform_spec_id走update/无走create → confirm → 回写幂等锚
8
- 传输: POST /mcp direct JSON-RPC + X-MCP-Token(同 engine/poller.py:_mcp_call)
8
+ 传输: POST /mcp direct JSON-RPC + X-MCP-Token
9
9
  """
10
10
  from __future__ import annotations
11
11
  import json, os, re, sys, urllib.request, urllib.error
@@ -24,8 +24,8 @@ def _load_mcp_config() -> dict:
24
24
  repo = get_repo_root()
25
25
  except Exception:
26
26
  repo = Path(__file__).resolve().parent.parent.parent.parent.parent
27
- for cand in [repo / "mcp_config.json",
28
- repo / ".qoder" / "mcp_config.json"]:
27
+ for cand in [repo / ".qoder" / "mcp_config.json", # ★ switch_project 写这里,优先(同 return_to_platform,否则可能读到 repo/ 旧文件)
28
+ repo / "mcp_config.json"]:
29
29
  try:
30
30
  if cand.is_file():
31
31
  with open(cand, encoding="utf-8") as f:
@@ -48,7 +48,7 @@ def _resolve_endpoint() -> Tuple[str, str, str]:
48
48
  u = srv.get("url", "")
49
49
  if "/mcp" in u:
50
50
  base = u.split("/mcp")[0]; break
51
- base = (base or "http://127.0.0.1:10010").rstrip("/")
51
+ base = (base or os.environ.get("WLKJ_BASE_URL", "http://10.89.7.120")).rstrip("/")
52
52
  token = os.environ.get("WLKJ_MCP_TOKEN", "")
53
53
  if not token:
54
54
  for srv in (cfg.get("mcpServers") or {}).values():
@@ -1,17 +1,6 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  """build_entity_registry.py — 从 ui-modules + code-api 构建 entity-registry.json。
3
3
 
4
- # v3.0 路径自举: 引导到 common/bootstrap, 统一 sys.path 逻辑
5
- import os as _o, sys as _s
6
- _f = _o.path.abspath(__file__)
7
- for _ in range(10):
8
- _f = _o.path.dirname(_f)
9
- _cp = _o.path.join(_f, 'foundation')
10
- if _o.path.isfile(_o.path.join(_cp, 'bootstrap.py')):
11
- break
12
- if _cp not in _s.path: _s.path.insert(0, _cp)
13
- from bootstrap import setup; setup()
14
-
15
4
  产出 data/index/_build_cache/entity-registry.json, 供 kg_duckdb 导入 entities/aliases 表,
16
5
  让 kg.py feature <功能名> 能返回功能画像。
17
6
 
@@ -34,6 +23,18 @@ from bootstrap import setup; setup()
34
23
  业务流程) 由 build_relations.py/build_features.py/build_workflows.py 补充,
35
24
  那些脚本未实现时不影响本脚本的 entity 基础层。
36
25
  """
26
+ # v3.0 路径自举: 引导到 foundation/bootstrap, 统一 sys.path 逻辑
27
+ # ⚠ 必须在 module docstring 之外! 旧版把这块包进 docstring → bootstrap 永不执行 → 跑死。
28
+ import os as _o, sys as _s
29
+ _f = _o.path.abspath(__file__)
30
+ for _ in range(10):
31
+ _f = _o.path.dirname(_f)
32
+ _cp = _o.path.join(_f, 'foundation')
33
+ if _o.path.isfile(_o.path.join(_cp, 'bootstrap.py')):
34
+ break
35
+ if _cp not in _s.path: _s.path.insert(0, _cp)
36
+ from bootstrap import setup; setup()
37
+
37
38
  import json
38
39
  import os
39
40
  import sys
@@ -41,7 +42,6 @@ from datetime import datetime
41
42
 
42
43
  # 路径自适应 (和兄弟脚本一致)
43
44
  THIS_DIR = os.path.dirname(os.path.abspath(__file__))
44
- sys.path.insert(0, os.path.join(THIS_DIR, '..'))
45
45
  from foundation.core.paths import DATA_INDEX_DIR, BUILD_CACHE_DIR
46
46
 
47
47
 
@@ -1,17 +1,6 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  """build_relations.py — 从 trace-chain + code-api 推导关系边, 生成 relations.json。
3
3
 
4
- # v3.0 路径自举: 引导到 common/bootstrap, 统一 sys.path 逻辑
5
- import os as _o, sys as _s
6
- _f = _o.path.abspath(__file__)
7
- for _ in range(10):
8
- _f = _o.path.dirname(_f)
9
- _cp = _o.path.join(_f, 'foundation')
10
- if _o.path.isfile(_o.path.join(_cp, 'bootstrap.py')):
11
- break
12
- if _cp not in _s.path: _s.path.insert(0, _cp)
13
- from bootstrap import setup; setup()
14
-
15
4
  产出 data/index/_build_cache/relations.json, 供 kg_duckdb 导入 edges 表,
16
5
  让 kg.py feature 的端点/按钮数据非零, kg.py hop 多跳遍历有边可走。
17
6
 
@@ -32,13 +21,24 @@ from bootstrap import setup; setup()
32
21
  - code-api.json: {endpoint: controller_file} (api_to_controller 直接用)
33
22
  - test-assertions.json: tests (controller_to_tests 推导)
34
23
  """
24
+ # v3.0 路径自举: 引导到 foundation/bootstrap, 统一 sys.path 逻辑
25
+ # ⚠ 必须在 module docstring 之外! 旧版把这块包进 docstring → bootstrap 永不执行 → 跑死。
26
+ import os as _o, sys as _s
27
+ _f = _o.path.abspath(__file__)
28
+ for _ in range(10):
29
+ _f = _o.path.dirname(_f)
30
+ _cp = _o.path.join(_f, 'foundation')
31
+ if _o.path.isfile(_o.path.join(_cp, 'bootstrap.py')):
32
+ break
33
+ if _cp not in _s.path: _s.path.insert(0, _cp)
34
+ from bootstrap import setup; setup()
35
+
35
36
  import json
36
37
  import os
37
38
  import sys
38
39
  from datetime import datetime
39
40
 
40
41
  THIS_DIR = os.path.dirname(os.path.abspath(__file__))
41
- sys.path.insert(0, os.path.join(THIS_DIR, '..'))
42
42
  from foundation.core.paths import DATA_INDEX_DIR, BUILD_CACHE_DIR
43
43
 
44
44
 
@@ -278,6 +278,31 @@ def build_embeddings(batch_size=64):
278
278
  con.close()
279
279
 
280
280
 
281
+ def _cloud_rag_search(query, top_k=10):
282
+ """云优先语义搜索:调工作台 MCP rag_search(本地无 KG/embedding,全迁云)。
283
+
284
+ Returns:
285
+ [(entity_id, text, score), ...] 或 None(云不可用时返回 None,让调用方退本地)。
286
+ """
287
+ try:
288
+ from domain.kg.search._remote import call_remote
289
+ data = call_remote("rag_search", {"query": query, "top_k": top_k})
290
+ except Exception:
291
+ return None
292
+ items = data.get("items") or []
293
+ out = []
294
+ for it in items[:top_k]:
295
+ eid = it.get("entity_id") or it.get("id") or ""
296
+ text = it.get("text") or it.get("name") or ""
297
+ score = it.get("score")
298
+ try:
299
+ score = float(score) if score is not None else 0.0
300
+ except Exception:
301
+ score = 0.0
302
+ out.append((eid, text, score))
303
+ return out
304
+
305
+
281
306
  def semantic_search(query, top_k=10):
282
307
  """语义搜索: query→向量→余弦相似度→top_k 实体。
283
308
 
@@ -656,13 +681,24 @@ if __name__ == '__main__':
656
681
  n = build_embeddings()
657
682
  print('Built %d embeddings' % n)
658
683
 
659
- elif len(sys.argv) > 2 and sys.argv[1] == 'search':
660
- results = semantic_search(sys.argv[2])
684
+ elif (len(sys.argv) > 2 and sys.argv[1] == 'search') or \
685
+ (len(sys.argv) > 1 and sys.argv[1] not in ('--check', '--label', 'build-embeddings', '--status', '--backfill-cn')):
686
+ # 兼容两种姿势: `search <词>` 和裸词 `<词>`(wlkj.py semantic 保险异常 透传过来)。
687
+ # ★ 云优先:本地无 KG/embedding(全迁云),先调云 rag_search;云不可用才退本地 semantic_search。
688
+ query = sys.argv[2] if sys.argv[1] == 'search' else sys.argv[1]
689
+ results = _cloud_rag_search(query)
690
+ if results is None:
691
+ # 云不可用 → 退本地(开发期有本地 embedding 时仍可用)
692
+ results = semantic_search(query)
661
693
  if results:
662
- for eid, text, score in results:
663
- print(' %.3f %s' % (score, text[:70]))
694
+ for row in results:
695
+ if len(row) == 3:
696
+ eid, text, score = row
697
+ print(' %.3f %s' % (score, text[:70]))
698
+ else:
699
+ print(' %s' % row)
664
700
  else:
665
- print('(无结果或 embedding 未构建)')
701
+ print('(无结果或云 rag_search 不可用 + 本地 embedding 未构建)')
666
702
 
667
703
  elif len(sys.argv) > 1 and sys.argv[1] == '--status':
668
704
  st = embedding_status()
@@ -872,6 +872,20 @@ def main(argv=None):
872
872
  return 0
873
873
  cmd = argv[0]
874
874
  rest = argv[1:]
875
+ # ★ MCP 名兼容:AI 常把 MCP 工具名(search_code)当 kg.py 子命令调 → "未知子命令"。
876
+ # 在此统一归一化,AI 猜哪个名(kg.py 子命令 / MCP 工具名)都能命中(治工作流混名)。
877
+ _MCP_ALIASES = {
878
+ 'search_code': 'search', 'search_api': 'api', 'search_prds': 'prd',
879
+ 'search_field': 'search', 'search_style': 'search', 'search_wiki': 'wiki',
880
+ 'rag_search': 'semantic', 'ask_corpus': 'semantic',
881
+ 'context_pack': 'context', 'context_360': 'context360',
882
+ 'get_impact': 'impact', 'multi_hop': 'hop',
883
+ 'coverage_matrix': 'coverage', 'feature_overview': 'feature',
884
+ 'get_workflow': 'workflow', 'get_design_system': 'design-system',
885
+ 'fill_prototype': 'fill-prototype',
886
+ }
887
+ if cmd in _MCP_ALIASES:
888
+ cmd = _MCP_ALIASES[cmd]
875
889
  # 查找子命令
876
890
  handler = None
877
891
  for name, fn, _ in COMMANDS:
@@ -6,7 +6,7 @@
6
6
  python switch_project.py --email a@b.c --pw xxx # 非交互:直接指定
7
7
  python switch_project.py --project-uuid <UUID> # 非交互:直接指定项目
8
8
 
9
- 需工作台后端在跑(默认 http://127.0.0.1:10010)。
9
+ 需工作台后端在跑(默认 http://10.89.7.120 云)。
10
10
  """
11
11
  import json
12
12
  import os
@@ -33,7 +33,7 @@ def _find_config_path():
33
33
  CONFIG_PATH = _find_config_path()
34
34
 
35
35
  # 平台地址(默认 10010,可 env 覆盖)
36
- BASE_URL = os.environ.get("WLKJ_BASE_URL", "http://127.0.0.1:10010")
36
+ BASE_URL = os.environ.get("WLKJ_BASE_URL", "http://10.89.7.120")
37
37
 
38
38
 
39
39
  def bind(email, password, base_url=BASE_URL):
@@ -51,9 +51,118 @@ from foundation.integrations.zentao_client import ZentaoClient
51
51
  # ============================================================
52
52
 
53
53
  def _zentao_creds():
54
- """构建禅道客户端。凭据不全返回 None
55
- url 来自 .qoder/config.yaml (团队共享); user/password 来自 secrets/zentao.env (个人)。"""
56
- return ZentaoClient.from_config()
54
+ """构建禅道客户端。返回 (client, preset_account)
55
+
56
+ 凭据源优先级:
57
+ ① 云平台 per-user 凭据(Option A:get_zentao_creds)—— account 已知(preset)
58
+ ② 本地 .qoder/config.yaml + secrets/zentao.env —— account 走 member.json
59
+ 云凭据优先:demo 机器在内网可达禅道,平台凭据比本地文件更可能是最新配的。
60
+ 凭据全无 → (None, '')。
61
+ """
62
+ c = _cloud_zentao_creds()
63
+ if c and c.get('ok') and c.get('url') and c.get('account') and c.get('password'):
64
+ try:
65
+ return ZentaoClient(c['url'], c['account'], c['password']), c['account']
66
+ except Exception:
67
+ pass
68
+ return ZentaoClient.from_config(), ''
69
+
70
+
71
+ def _cloud_my_panel():
72
+ """云优先:调平台 get_my_zentao_panel(凭据走平台 per-user 配置,不出平台)。
73
+
74
+ 直连云 /mcp JSON-RPC(读 .qoder/mcp_config.json 的 token+url)。
75
+ Returns: {ok:True, tasks, bugs, stories, account} 或 None(云不可达/工具失败)。
76
+ 云返 {ok:False,error}(未配置/登录失败)→ 返该 dict(调用方据此提示+回退本地)。
77
+ """
78
+ import json as _json
79
+ import urllib.request as _u
80
+ import urllib.error as _ue
81
+ # 定位 mcp_config.json(switch_project 写在 .qoder/)
82
+ cfg_candidates = []
83
+ try:
84
+ from foundation.core.paths import get_repo_root
85
+ cfg_candidates.append(os.path.join(str(get_repo_root()), '.qoder', 'mcp_config.json'))
86
+ except Exception:
87
+ pass
88
+ cfg_candidates.append(os.path.join(_SCRIPTS, '..', 'mcp_config.json'))
89
+ token, url = '', ''
90
+ for cp in cfg_candidates:
91
+ try:
92
+ with open(cp, encoding='utf-8') as f:
93
+ cfg = _json.load(f)
94
+ srv = (cfg.get('mcpServers') or {}).get('wlinkj-knowledge') or {}
95
+ token = srv.get('token', '')
96
+ url = (srv.get('url') or '').rstrip('/')
97
+ if token and url:
98
+ break
99
+ except Exception:
100
+ continue
101
+ if not token or not url:
102
+ return None # 云未绑定(switch_project 没跑)→ 回退本地
103
+ endpoint = url if url.endswith('/mcp') else url + '/mcp'
104
+ body = _json.dumps({"jsonrpc": "2.0", "id": 1, "method": "tools/call",
105
+ "params": {"name": "get_my_zentao_panel", "arguments": {}}}).encode('utf-8')
106
+ req = _u.Request(endpoint, data=body, method='POST')
107
+ req.add_header('Content-Type', 'application/json')
108
+ req.add_header('X-MCP-Token', token)
109
+ try:
110
+ with _u.urlopen(req, timeout=30) as resp:
111
+ r = _json.loads(resp.read().decode('utf-8', errors='replace'))
112
+ except Exception:
113
+ return None # 网络不通 → 回退本地
114
+ try:
115
+ text = r.get('result', {}).get('content', [{}])[0].get('text', '')
116
+ data = _json.loads(text) if text else {}
117
+ except Exception:
118
+ return None
119
+ return data # {ok:True/False, ...}
120
+
121
+
122
+ def _cloud_zentao_creds():
123
+ """Option A:从平台取禅道凭据(url/account/password 解密)。
124
+
125
+ 用途:云到禅道被防火墙挡(my-panel 登录失败)时,本机(demo 机器,内网可达)
126
+ 用平台凭据直连禅道拉真实数据。凭据随绑定 token 取本人配置(同信任级)。
127
+ Returns: {ok:True, url, account, password} 或 None(云不可达/未配置)。
128
+ """
129
+ import json as _json
130
+ import urllib.request as _u
131
+ cfg_candidates = []
132
+ try:
133
+ from foundation.core.paths import get_repo_root
134
+ cfg_candidates.append(os.path.join(str(get_repo_root()), '.qoder', 'mcp_config.json'))
135
+ except Exception:
136
+ pass
137
+ cfg_candidates.append(os.path.join(_SCRIPTS, '..', 'mcp_config.json'))
138
+ token, url = '', ''
139
+ for cp in cfg_candidates:
140
+ try:
141
+ with open(cp, encoding='utf-8') as f:
142
+ cfg = _json.load(f)
143
+ srv = (cfg.get('mcpServers') or {}).get('wlinkj-knowledge') or {}
144
+ token = srv.get('token', '')
145
+ url = (srv.get('url') or '').rstrip('/')
146
+ if token and url:
147
+ break
148
+ except Exception:
149
+ continue
150
+ if not token or not url:
151
+ return None
152
+ endpoint = url if url.endswith('/mcp') else url + '/mcp'
153
+ body = _json.dumps({"jsonrpc": "2.0", "id": 1, "method": "tools/call",
154
+ "params": {"name": "get_zentao_creds", "arguments": {}}}).encode('utf-8')
155
+ req = _u.Request(endpoint, data=body, method='POST')
156
+ req.add_header('Content-Type', 'application/json')
157
+ req.add_header('X-MCP-Token', token)
158
+ try:
159
+ with _u.urlopen(req, timeout=15) as resp:
160
+ r = _json.loads(resp.read().decode('utf-8', errors='replace'))
161
+ text = r.get('result', {}).get('content', [{}])[0].get('text', '')
162
+ data = _json.loads(text) if text else {}
163
+ return data if data.get('ok') else None
164
+ except Exception:
165
+ return None
57
166
 
58
167
 
59
168
  def _resolve_my_account(developer):
@@ -359,26 +468,72 @@ def fetch_workbench(include_closed=False, kind=None):
359
468
  """
360
469
  developer = get_developer()
361
470
 
362
- # 凭据
363
- client = _zentao_creds()
471
+ # 云优先:调平台 get_my_zentao_panel(凭据走平台 per-user,不出平台)。
472
+ # 成功 → 直接用云数据,跳过本地凭据/连通/归属/收集(① ~ ④)。
473
+ # 未配置/登录失败 → 返回 {ok:False} 提示用户去平台配置,并回退本地(本地配了仍可用)。
474
+ # 云不可达(返回 None) → 静默回退本地全流程。
475
+ cloud = _cloud_my_panel()
476
+ if isinstance(cloud, dict):
477
+ # 云可达 → 云的判断权威(平台是凭据源)。
478
+ if cloud.get('ok'):
479
+ tasks = cloud.get('tasks') or []
480
+ bugs = cloud.get('bugs') or []
481
+ stories = cloud.get('stories') or []
482
+ # 按 kind 裁剪(与本地一致)
483
+ if kind == 'tasks':
484
+ bugs, stories = [], []
485
+ elif kind == 'bugs':
486
+ tasks, stories = [], []
487
+ elif kind == 'stories':
488
+ tasks, bugs = [], []
489
+ tasks, bugs, stories = _filter_live(tasks, bugs, stories, include_closed)
490
+ # 云路径:数据已平台聚合,归一化即可(进度检测需本地 client,云模式跳过给默认值)
491
+ norm_tasks = [_norm_task(t) for t in tasks]
492
+ norm_bugs = [_norm_bug(b) for b in bugs]
493
+ norm_stories = [_norm_story(s) for s in stories]
494
+ for it in norm_tasks + norm_bugs + norm_stories:
495
+ it['progress'] = {'prd': False, 'spec': 'none', 'code': False}
496
+ data = {'account': cloud.get('account', ''), 'developer': developer,
497
+ 'current_task': None, 'tasks': norm_tasks,
498
+ 'bugs': norm_bugs, 'stories': norm_stories}
499
+ meta = {'fetched_at': datetime.now().isoformat(timespec='seconds'),
500
+ 'source': 'cloud',
501
+ 'counts': {'tasks': len(norm_tasks), 'bugs': len(norm_bugs),
502
+ 'stories': len(norm_stories)},
503
+ 'include_closed': include_closed}
504
+ return {'ok': True, 'data': data, 'meta': meta}, 0
505
+ # 云可达但失败(未配置/登录失败/防火墙)→ 记下云诊断,回退本地。
506
+ # 回退价值:云到禅道常被防火墙挡,但本机(demo 机器,内网)可能直连可达。
507
+ # 本地也失败时,把云诊断一起带出(平台有凭据但云连不上 + 本地无凭据)。
508
+ cloud_error = cloud.get('error', '禅道不可用')
509
+ else:
510
+ cloud_error = None # 云不可达(switch_project 没跑/平台宕机)→ 静默回退本地
511
+
512
+ # ① 凭据(云平台优先 Option A,回退本地文件)
513
+ client, preset_account = _zentao_creds()
364
514
  if client is None:
365
- return {'ok': False,
366
- 'error': '禅道凭据未配置。请检查 .qoder/config.yaml 的 zentao.url, '
367
- '及 workspace/members/<我>/.private/secrets/zentao.env 的账号密码。'}, 3
515
+ base_msg = ('禅道凭据未配置。推荐在【平台设置 → 禅道】配置(panel 自动走云取凭据);'
516
+ '或本地 .qoder/config.yaml 的 zentao.url + secrets/zentao.env 账号密码。')
517
+ if cloud_error:
518
+ base_msg += '(云 my-panel 失败:%s)' % cloud_error
519
+ return {'ok': False, 'error': base_msg}, 3
368
520
 
369
521
  ok, msg = client.credentials_ok()
370
522
  if not ok:
371
523
  return {'ok': False, 'error': '禅道凭据不完整: %s' % msg}, 3
372
524
 
373
- # ② 内网连通
525
+ # ② 内网连通(本机能否直连禅道——demo 机器在内网可达)
374
526
  ok, msg = client.check_intranet()
375
527
  if not ok:
376
- return {'ok': False, 'error': '连不上禅道内网: %s' % msg}, 5
528
+ return {'ok': False, 'error': '本机连不上禅道内网: %s' % msg}, 5
377
529
 
378
- # ③ 归属
379
- my_account, err = _resolve_my_account(developer) if developer else (None, '未识别当前开发者身份。')
380
- if not my_account:
381
- return {'ok': False, 'error': err}, 3
530
+ # ③ 归属(云凭据带的 account 优先;否则走 member.json)
531
+ if preset_account:
532
+ my_account = preset_account
533
+ else:
534
+ my_account, err = _resolve_my_account(developer) if developer else (None, '未识别当前开发者身份。')
535
+ if not my_account:
536
+ return {'ok': False, 'error': err}, 3
382
537
 
383
538
  # ④ 收集 (三域并行, 按 kind 决定取哪几域)
384
539
  from concurrent.futures import ThreadPoolExecutor
@@ -0,0 +1,94 @@
1
+ """context_cache.py — 按 REQ-id 缓存 context_pack/rag_search 结果(T1.3 跨命令上下文缓存)。
2
+
3
+ PRD→spec→code 链路里同一 REQ 反复拉同一份上下文(3 次 KG/MCP 往返)→ 缓存复用,省 token + 减反复搜。
4
+
5
+ 存储: workspace/specs/{REQ}/.context-cache.json
6
+ 结构: {query_hash: {"query":..., "text":..., "ts":...}}
7
+ TTL 默认 24h(PRD/spec 改动后用 invalidate(req) 显式失效)。
8
+
9
+ 纯模块,无外部依赖;任何异常 fail-open(缓存坏了不影响主流程,调用方降级直拉)。
10
+ """
11
+ import os
12
+ import json
13
+ import time
14
+ import hashlib
15
+
16
+ DEFAULT_TTL = 24 * 3600 # 24h
17
+
18
+
19
+ def _repo_root():
20
+ """从本文件向上找含 workspace/ 或 .qoder/ 的目录作仓库根。"""
21
+ d = os.path.dirname(os.path.abspath(__file__))
22
+ for _ in range(7):
23
+ if os.path.isdir(os.path.join(d, "workspace")) or os.path.isdir(os.path.join(d, ".qoder")):
24
+ return d
25
+ parent = os.path.dirname(d)
26
+ if parent == d:
27
+ break
28
+ d = parent
29
+ return os.getcwd()
30
+
31
+
32
+ def _sanitize(req_id):
33
+ return "".join(c for c in str(req_id) if c.isalnum() or c in "-_") or "unknown"
34
+
35
+
36
+ def cache_path(req_id, base=None):
37
+ base = base or _repo_root()
38
+ return os.path.join(base, "workspace", "specs", _sanitize(req_id), ".context-cache.json")
39
+
40
+
41
+ def _load(path):
42
+ try:
43
+ with open(path, encoding="utf-8") as f:
44
+ data = json.load(f)
45
+ return data if isinstance(data, dict) else {}
46
+ except Exception:
47
+ return {}
48
+
49
+
50
+ def _save(path, data):
51
+ try:
52
+ os.makedirs(os.path.dirname(path), exist_ok=True)
53
+ tmp = path + ".tmp"
54
+ with open(tmp, "w", encoding="utf-8") as f:
55
+ json.dump(data, f, ensure_ascii=False, indent=2)
56
+ os.replace(tmp, path) # 原子写
57
+ return True
58
+ except Exception:
59
+ return False
60
+
61
+
62
+ def _qhash(query):
63
+ return hashlib.md5(query.strip().lower().encode("utf-8")).hexdigest()[:12]
64
+
65
+
66
+ def get(req_id, query, ttl=DEFAULT_TTL, base=None):
67
+ """命中且未过期 → 返回 text;否则 None。"""
68
+ data = _load(cache_path(req_id, base))
69
+ entry = data.get(_qhash(query))
70
+ if not entry:
71
+ return None
72
+ if ttl and (time.time() - float(entry.get("ts", 0))) > ttl:
73
+ return None
74
+ return entry.get("text")
75
+
76
+
77
+ def put(req_id, query, text, base=None):
78
+ """写一条缓存(覆盖同 query 旧值)。"""
79
+ path = cache_path(req_id, base)
80
+ data = _load(path)
81
+ data[_qhash(query)] = {"query": query, "text": text, "ts": time.time()}
82
+ return _save(path, data)
83
+
84
+
85
+ def invalidate(req_id, base=None):
86
+ """清整个 REQ 的缓存(PRD/spec 改动后调)。"""
87
+ return _save(cache_path(req_id, base), {})
88
+
89
+
90
+ def stats(req_id, base=None):
91
+ """缓存条数 + query 摘要(调试/观测命中率用)。"""
92
+ data = _load(cache_path(req_id, base))
93
+ return {"entries": len(data),
94
+ "queries": [str(e.get("query", ""))[:40] for e in data.values()]}