@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
|
-
"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
|
-
"""
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
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
|
-
|
|
366
|
-
|
|
367
|
-
|
|
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': '
|
|
528
|
+
return {'ok': False, 'error': '本机连不上禅道内网: %s' % msg}, 5
|
|
377
529
|
|
|
378
|
-
# ③
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
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
|