@hupan56/wlkj 3.3.4 → 3.3.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,29 +1,29 @@
1
- {
2
- "name": "@hupan56/wlkj",
3
- "version": "3.3.4",
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.6",
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
+ }
@@ -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