@hupan56/wlkj 3.3.3 → 3.3.5

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 (36) hide show
  1. package/package.json +29 -29
  2. package/templates/qoder/scripts/capability/adapters/mcp.py +8 -1
  3. package/templates/qoder/scripts/capability/registry_mcp.py +9 -3
  4. package/templates/qoder/scripts/domain/kg/graph/kg_semantic.py +41 -5
  5. package/templates/qoder/scripts/domain/task/wlkj_panel.py +98 -4
  6. package/templates/qoder/scripts/capability/__pycache__/__init__.cpython-39.pyc +0 -0
  7. package/templates/qoder/scripts/capability/__pycache__/present_html.cpython-39.pyc +0 -0
  8. package/templates/qoder/scripts/capability/__pycache__/registry.cpython-39.pyc +0 -0
  9. package/templates/qoder/scripts/capability/__pycache__/registry_mcp.cpython-39.pyc +0 -0
  10. package/templates/qoder/scripts/capability/adapters/__pycache__/__init__.cpython-39.pyc +0 -0
  11. package/templates/qoder/scripts/capability/adapters/__pycache__/cli.cpython-39.pyc +0 -0
  12. package/templates/qoder/scripts/capability/adapters/__pycache__/mcp.cpython-39.pyc +0 -0
  13. package/templates/qoder/scripts/capability/adapters/__pycache__/qw.cpython-39.pyc +0 -0
  14. package/templates/qoder/scripts/capability/caps/__pycache__/__init__.cpython-39.pyc +0 -0
  15. package/templates/qoder/scripts/capability/caps/__pycache__/context.cpython-39.pyc +0 -0
  16. package/templates/qoder/scripts/capability/caps/__pycache__/cron.cpython-39.pyc +0 -0
  17. package/templates/qoder/scripts/capability/caps/__pycache__/identity.cpython-39.pyc +0 -0
  18. package/templates/qoder/scripts/capability/caps/__pycache__/memory.cpython-39.pyc +0 -0
  19. package/templates/qoder/scripts/capability/caps/__pycache__/notify.cpython-39.pyc +0 -0
  20. package/templates/qoder/scripts/capability/caps/__pycache__/present.cpython-39.pyc +0 -0
  21. package/templates/qoder/scripts/capability/caps/__pycache__/repo.cpython-39.pyc +0 -0
  22. package/templates/qoder/scripts/capability/caps/__pycache__/sandbox.cpython-39.pyc +0 -0
  23. package/templates/qoder/scripts/domain/__pycache__/__init__.cpython-39.pyc +0 -0
  24. package/templates/qoder/scripts/domain/integration/__pycache__/__init__.cpython-39.pyc +0 -0
  25. package/templates/qoder/scripts/domain/integration/__pycache__/return_to_platform.cpython-39.pyc +0 -0
  26. package/templates/qoder/scripts/domain/task/__pycache__/wlkj_panel.cpython-39.pyc +0 -0
  27. package/templates/qoder/scripts/foundation/__pycache__/__init__.cpython-39.pyc +0 -0
  28. package/templates/qoder/scripts/foundation/core/__pycache__/__init__.cpython-39.pyc +0 -0
  29. package/templates/qoder/scripts/foundation/core/__pycache__/paths.cpython-39.pyc +0 -0
  30. package/templates/qoder/scripts/protocol/__pycache__/__init__.cpython-39.pyc +0 -0
  31. package/templates/qoder/scripts/protocol/transports/__pycache__/__init__.cpython-39.pyc +0 -0
  32. package/templates/qoder/scripts/protocol/transports/__pycache__/base.cpython-39.pyc +0 -0
  33. package/templates/qoder/scripts/protocol/transports/__pycache__/cli.cpython-39.pyc +0 -0
  34. package/templates/qoder/scripts/protocol/transports/__pycache__/http.cpython-39.pyc +0 -0
  35. package/templates/qoder/scripts/protocol/transports/__pycache__/stdio.cpython-39.pyc +0 -0
  36. package/templates/qoder/scripts/validation/metrics/__pycache__/present_board.cpython-39.pyc +0 -0
package/package.json CHANGED
@@ -1,29 +1,29 @@
1
- {
2
- "name": "@hupan56/wlkj",
3
- "version": "3.3.3",
4
- "description": "AI Product R&D Workflow - PRD/Prototype/Search/Task/Report",
5
- "bin": {
6
- "wlkj": "bin/cli.js"
7
- },
8
- "files": [
9
- "bin/",
10
- "templates/",
11
- "MAC-VERIFY.md"
12
- ],
13
- "keywords": [
14
- "workflow",
15
- "ai",
16
- "prd",
17
- "pipeline",
18
- "qoder",
19
- "product",
20
- "team"
21
- ],
22
- "license": "MIT",
23
- "publishConfig": {
24
- "access": "public"
25
- },
26
- "engines": {
27
- "node": ">=16"
28
- }
29
- }
1
+ {
2
+ "name": "@hupan56/wlkj",
3
+ "version": "3.3.5",
4
+ "description": "AI Product R&D Workflow - PRD/Prototype/Search/Task/Report",
5
+ "bin": {
6
+ "wlkj": "bin/cli.js"
7
+ },
8
+ "files": [
9
+ "bin/",
10
+ "templates/",
11
+ "MAC-VERIFY.md"
12
+ ],
13
+ "keywords": [
14
+ "workflow",
15
+ "ai",
16
+ "prd",
17
+ "pipeline",
18
+ "qoder",
19
+ "product",
20
+ "team"
21
+ ],
22
+ "license": "MIT",
23
+ "publishConfig": {
24
+ "access": "public"
25
+ },
26
+ "engines": {
27
+ "node": ">=16"
28
+ }
29
+ }
@@ -132,6 +132,11 @@ class McpCap:
132
132
 
133
133
  def _cli_fallback(self, tool_name: str, args: dict) -> Optional[str]:
134
134
  """MCP 工具名 → wlkj.py 执行。无映射返回 None。"""
135
+ # ★ 递归守卫:search_code→kg search→search_index→call_remote→call→_cli_fallback 会无限套娃。
136
+ # 已在降级子进程内(_run_wlkj 设了 _WLKJ_IN_CLI_FALLBACK=1)则直接放弃降级,返回原始错误。
137
+ import os as _os
138
+ if _os.environ.get("_WLKJ_IN_CLI_FALLBACK"):
139
+ return None
135
140
  entry = self._TOOL_TO_WLKJ.get(tool_name)
136
141
  if entry is None:
137
142
  return None
@@ -183,10 +188,12 @@ class McpCap:
183
188
  repo = os.getcwd()
184
189
  wlkj = os.path.join(repo, ".qoder", "scripts", "orchestration", "wlkj.py")
185
190
  try:
191
+ # 递归守卫:子进程内 _WLKJ_IN_CLI_FALLBACK=1,其 McpCap 不再降级(否则套娃)
192
+ env = dict(os.environ, _WLKJ_IN_CLI_FALLBACK="1")
186
193
  r = subprocess.run(
187
194
  [sys.executable, wlkj, cmd] + args,
188
195
  capture_output=True, text=True, timeout=60,
189
- encoding="utf-8", errors="replace",
196
+ encoding="utf-8", errors="replace", env=env,
190
197
  )
191
198
  if r.returncode == 0:
192
199
  return r.stdout.strip()
@@ -73,16 +73,22 @@ SERVERS: dict[str, dict] = {
73
73
 
74
74
 
75
75
  def _mcp_config_path() -> Path:
76
- """定位 mcp_config.json(在仓库根/宿主下)。"""
76
+ """定位 mcp_config.json(在仓库根/宿主下)。
77
+
78
+ ★ switch_project.py 实际写入位置是 {repo}/.qoder/mcp_config.json(云模式连云 bind 后),
79
+ 必须优先查这里——否则读不到 token,transport 用空 token 调云 MCP 鉴权失败(历史 bug:
80
+ 引擎 search 全返回"MCP返回非JSON"+递归降级套娃)。老形态 repo/mcp_config.json 兼容。
81
+ """
77
82
  repo = _get_repo_root()
78
83
  # registry_mcp.py 在 .../scripts/capability/, mcp_config.json 在宿主根(.qoder)。
79
84
  for cand in [
80
- repo / "mcp_config.json",
85
+ repo / ".qoder" / "mcp_config.json", # ★ 云模式实际写入位置(switch_project)
86
+ repo / "mcp_config.json", # 老形态:仓库根
81
87
  repo / "registry_mcp_config.json",
82
88
  ]:
83
89
  if cand.is_file():
84
90
  return cand
85
- return repo / "mcp_config.json"
91
+ return repo / ".qoder" / "mcp_config.json"
86
92
 
87
93
 
88
94
  def _load_servers() -> dict[str, dict]:
@@ -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()
@@ -56,6 +56,57 @@ def _zentao_creds():
56
56
  return ZentaoClient.from_config()
57
57
 
58
58
 
59
+ def _cloud_my_panel():
60
+ """云优先:调平台 get_my_zentao_panel(凭据走平台 per-user 配置,不出平台)。
61
+
62
+ 直连云 /mcp JSON-RPC(读 .qoder/mcp_config.json 的 token+url)。
63
+ Returns: {ok:True, tasks, bugs, stories, account} 或 None(云不可达/工具失败)。
64
+ 云返 {ok:False,error}(未配置/登录失败)→ 返该 dict(调用方据此提示+回退本地)。
65
+ """
66
+ import json as _json
67
+ import urllib.request as _u
68
+ import urllib.error as _ue
69
+ # 定位 mcp_config.json(switch_project 写在 .qoder/)
70
+ cfg_candidates = []
71
+ try:
72
+ from foundation.core.paths import get_repo_root
73
+ cfg_candidates.append(os.path.join(str(get_repo_root()), '.qoder', 'mcp_config.json'))
74
+ except Exception:
75
+ pass
76
+ cfg_candidates.append(os.path.join(_SCRIPTS, '..', 'mcp_config.json'))
77
+ token, url = '', ''
78
+ for cp in cfg_candidates:
79
+ try:
80
+ with open(cp, encoding='utf-8') as f:
81
+ cfg = _json.load(f)
82
+ srv = (cfg.get('mcpServers') or {}).get('wlinkj-knowledge') or {}
83
+ token = srv.get('token', '')
84
+ url = (srv.get('url') or '').rstrip('/')
85
+ if token and url:
86
+ break
87
+ except Exception:
88
+ continue
89
+ if not token or not url:
90
+ return None # 云未绑定(switch_project 没跑)→ 回退本地
91
+ endpoint = url if url.endswith('/mcp') else url + '/mcp'
92
+ body = _json.dumps({"jsonrpc": "2.0", "id": 1, "method": "tools/call",
93
+ "params": {"name": "get_my_zentao_panel", "arguments": {}}}).encode('utf-8')
94
+ req = _u.Request(endpoint, data=body, method='POST')
95
+ req.add_header('Content-Type', 'application/json')
96
+ req.add_header('X-MCP-Token', token)
97
+ try:
98
+ with _u.urlopen(req, timeout=30) as resp:
99
+ r = _json.loads(resp.read().decode('utf-8', errors='replace'))
100
+ except Exception:
101
+ return None # 网络不通 → 回退本地
102
+ try:
103
+ text = r.get('result', {}).get('content', [{}])[0].get('text', '')
104
+ data = _json.loads(text) if text else {}
105
+ except Exception:
106
+ return None
107
+ return data # {ok:True/False, ...}
108
+
109
+
59
110
  def _resolve_my_account(developer):
60
111
  """解析当前 developer → 禅道 account。返回 (account, 错误原因)。"""
61
112
  mem = get_member(developer)
@@ -359,12 +410,55 @@ def fetch_workbench(include_closed=False, kind=None):
359
410
  """
360
411
  developer = get_developer()
361
412
 
362
- # 凭据
413
+ # 云优先:调平台 get_my_zentao_panel(凭据走平台 per-user,不出平台)。
414
+ # 成功 → 直接用云数据,跳过本地凭据/连通/归属/收集(① ~ ④)。
415
+ # 未配置/登录失败 → 返回 {ok:False} 提示用户去平台配置,并回退本地(本地配了仍可用)。
416
+ # 云不可达(返回 None) → 静默回退本地全流程。
417
+ cloud = _cloud_my_panel()
418
+ if isinstance(cloud, dict):
419
+ # 云可达 → 云的判断权威(平台是凭据源)。
420
+ if cloud.get('ok'):
421
+ tasks = cloud.get('tasks') or []
422
+ bugs = cloud.get('bugs') or []
423
+ stories = cloud.get('stories') or []
424
+ # 按 kind 裁剪(与本地一致)
425
+ if kind == 'tasks':
426
+ bugs, stories = [], []
427
+ elif kind == 'bugs':
428
+ tasks, stories = [], []
429
+ elif kind == 'stories':
430
+ tasks, bugs = [], []
431
+ tasks, bugs, stories = _filter_live(tasks, bugs, stories, include_closed)
432
+ # 云路径:数据已平台聚合,归一化即可(进度检测需本地 client,云模式跳过给默认值)
433
+ norm_tasks = [_norm_task(t) for t in tasks]
434
+ norm_bugs = [_norm_bug(b) for b in bugs]
435
+ norm_stories = [_norm_story(s) for s in stories]
436
+ for it in norm_tasks + norm_bugs + norm_stories:
437
+ it['progress'] = {'prd': False, 'spec': 'none', 'code': False}
438
+ data = {'account': cloud.get('account', ''), 'developer': developer,
439
+ 'current_task': None, 'tasks': norm_tasks,
440
+ 'bugs': norm_bugs, 'stories': norm_stories}
441
+ meta = {'fetched_at': datetime.now().isoformat(timespec='seconds'),
442
+ 'source': 'cloud',
443
+ 'counts': {'tasks': len(norm_tasks), 'bugs': len(norm_bugs),
444
+ 'stories': len(norm_stories)},
445
+ 'include_closed': include_closed}
446
+ return {'ok': True, 'data': data, 'meta': meta}, 0
447
+ # 云可达但失败(未配置/登录失败/防火墙)→ 记下云诊断,回退本地。
448
+ # 回退价值:云到禅道常被防火墙挡,但本机(demo 机器,内网)可能直连可达。
449
+ # 本地也失败时,把云诊断一起带出(平台有凭据但云连不上 + 本地无凭据)。
450
+ cloud_error = cloud.get('error', '禅道不可用')
451
+ else:
452
+ cloud_error = None # 云不可达(switch_project 没跑/平台宕机)→ 静默回退本地
453
+
454
+ # ① 凭据(本地回退)
363
455
  client = _zentao_creds()
364
456
  if client is None:
365
- return {'ok': False,
366
- 'error': '禅道凭据未配置。请检查 .qoder/config.yaml 的 zentao.url, '
367
- '及 workspace/members/<我>/.private/secrets/zentao.env 的账号密码。'}, 3
457
+ base_msg = ('禅道凭据未配置。推荐在【平台设置 → 禅道】配置(panel 自动走云);'
458
+ '或本地 .qoder/config.yaml 的 zentao.url + secrets/zentao.env 账号密码。')
459
+ if cloud_error:
460
+ base_msg += '(注:平台已配凭据但云端连不上禅道:%s;本机若在内网可配本地凭据直连。)' % cloud_error
461
+ return {'ok': False, 'error': base_msg}, 3
368
462
 
369
463
  ok, msg = client.credentials_ok()
370
464
  if not ok: