@hupan56/wlkj 3.1.30 → 3.1.32
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
|
@@ -352,11 +352,11 @@ def configure_git_identity(name):
|
|
|
352
352
|
return False
|
|
353
353
|
|
|
354
354
|
# 5. 交互式收集 git 账号
|
|
355
|
-
print('\n
|
|
355
|
+
print('\n 配置 git 账号 (仅用于标识谁提交了代码, 不需要注册/登录):')
|
|
356
356
|
while True:
|
|
357
357
|
try:
|
|
358
358
|
git_name = input(f' git 用户名 [{existing_name or name}]: ').strip() or existing_name or name
|
|
359
|
-
git_email = input(' git 邮箱 (
|
|
359
|
+
git_email = input(f' git 邮箱 (填你的邮箱即可, 不需要注册): ').strip()
|
|
360
360
|
if not git_email or '@' not in git_email:
|
|
361
361
|
print(' 邮箱格式不对 (需含 @), 重试。')
|
|
362
362
|
continue
|
|
@@ -664,6 +664,73 @@ def offer_cron(skip=False):
|
|
|
664
664
|
print(' 跳过。以后可手动跑: bash .qoder/scripts/setup_weekly_cron.sh')
|
|
665
665
|
|
|
666
666
|
|
|
667
|
+
# ============================================================
|
|
668
|
+
# Step 3.6: 禅道凭据配置 (每个开发者配自己的禅道账号)
|
|
669
|
+
# ============================================================
|
|
670
|
+
|
|
671
|
+
def offer_zentao(name):
|
|
672
|
+
"""引导禅道凭据配置。
|
|
673
|
+
|
|
674
|
+
禅道 URL 团队共享(配在 config.yaml), 但用户名/密码是个人凭据,
|
|
675
|
+
存 workspace/members/{dev}/.private/secrets/zentao.env (gitignored)。
|
|
676
|
+
不配也能用 (kg/search/PRD 不依赖禅道), 但 /wl-task 禅道同步和
|
|
677
|
+
/wl-report 禅道查询需要。
|
|
678
|
+
"""
|
|
679
|
+
print('\n--- 禅道凭据 (可选, 不配也能用核心功能) ---')
|
|
680
|
+
|
|
681
|
+
# 检查 config.yaml 有没有禅道 URL
|
|
682
|
+
zentao_url = ''
|
|
683
|
+
try:
|
|
684
|
+
from foundation.core.config import load_config_section, SECTIONS
|
|
685
|
+
cfg = load_config_section(SECTIONS.ZENTAO)
|
|
686
|
+
zentao_url = (cfg.get('url') or '').strip()
|
|
687
|
+
except Exception:
|
|
688
|
+
pass
|
|
689
|
+
|
|
690
|
+
if not zentao_url:
|
|
691
|
+
print(' 团队未配置禅道 URL (config.yaml 无 zentao.url)。')
|
|
692
|
+
print(' 管理员配: 在 config.yaml 加 zentao.url: http://你的禅道地址')
|
|
693
|
+
print(' 跳过禅道配置。配好 URL 后重跑 init 即可。')
|
|
694
|
+
return
|
|
695
|
+
|
|
696
|
+
print(f' 禅道地址: {zentao_url}')
|
|
697
|
+
|
|
698
|
+
# 检查是否已配
|
|
699
|
+
try:
|
|
700
|
+
from foundation.core.paths import get_secrets_dir
|
|
701
|
+
secrets_dir = get_secrets_dir(name)
|
|
702
|
+
env_file = os.path.join(secrets_dir, 'zentao.env')
|
|
703
|
+
if os.path.isfile(env_file):
|
|
704
|
+
ok('禅道凭据已配置 (zentao.env 存在)')
|
|
705
|
+
return
|
|
706
|
+
except Exception:
|
|
707
|
+
pass
|
|
708
|
+
|
|
709
|
+
if not ask('配置禅道账号? (用于同步任务/需求/Bug)', 'n'):
|
|
710
|
+
print(' 跳过。以后需要: npx @hupan56/wlkj init <名字> <角色>')
|
|
711
|
+
return
|
|
712
|
+
|
|
713
|
+
# 交互式收集禅道凭据
|
|
714
|
+
try:
|
|
715
|
+
zentao_user = input(' 禅道用户名: ').strip()
|
|
716
|
+
zentao_pwd = input(' 禅道密码: ').strip()
|
|
717
|
+
if not zentao_user or not zentao_pwd:
|
|
718
|
+
warn('用户名/密码为空, 跳过')
|
|
719
|
+
return
|
|
720
|
+
|
|
721
|
+
# 写入 zentao.env
|
|
722
|
+
secrets_dir = os.path.join(os.getcwd(), 'workspace', 'members', name, '.private', 'secrets')
|
|
723
|
+
os.makedirs(secrets_dir, exist_ok=True)
|
|
724
|
+
env_file = os.path.join(secrets_dir, 'zentao.env')
|
|
725
|
+
with open(env_file, 'w', encoding='utf-8') as f:
|
|
726
|
+
f.write(f'ZENTAO_USER={zentao_user}\n')
|
|
727
|
+
f.write(f'ZENTAO_PASSWORD={zentao_pwd}\n')
|
|
728
|
+
ok(f'禅道凭据已保存: {env_file}')
|
|
729
|
+
print(' (个人凭据, 已 gitignored, 不会 push 给团队)')
|
|
730
|
+
except (EOFError, KeyboardInterrupt):
|
|
731
|
+
print(' 跳过')
|
|
732
|
+
|
|
733
|
+
|
|
667
734
|
# ============================================================
|
|
668
735
|
# Step 4.5: 蓝湖 MCP (设计师直读蓝湖设计稿)
|
|
669
736
|
# ============================================================
|
|
@@ -873,6 +940,9 @@ def main():
|
|
|
873
940
|
# Step 3.5: 团队协作仓库 remote (workspace/ 产出 push 到这里)
|
|
874
941
|
configure_team_remote()
|
|
875
942
|
|
|
943
|
+
# Step 3.6: 禅道凭据配置 (每个开发者都要配自己的禅道账号)
|
|
944
|
+
offer_zentao(name)
|
|
945
|
+
|
|
876
946
|
# Step 4
|
|
877
947
|
offer_cron(skip=args.skip_cron)
|
|
878
948
|
|
|
@@ -300,6 +300,106 @@ def fetch_workbench(include_closed=False, kind=None):
|
|
|
300
300
|
return {'ok': True, 'data': data, 'meta': meta}, 0
|
|
301
301
|
|
|
302
302
|
|
|
303
|
+
# ============================================================
|
|
304
|
+
# 写操作 (v0.3): 面板按钮触发的禅道状态流转
|
|
305
|
+
# 设计对齐 MCP server 的 _assert_owner: 写前校验归属, 防误改队友。
|
|
306
|
+
# 任务: start/pause/finish/log_effort; Bug: confirm/resolve
|
|
307
|
+
# ============================================================
|
|
308
|
+
|
|
309
|
+
def _check_owner(client, my_account, kind, item_id):
|
|
310
|
+
"""写前归属校验 (对齐 mcp server _assert_owner)。
|
|
311
|
+
task 只认 assignedTo==我; bug 认 assignedTo==我 OR openedBy==我。
|
|
312
|
+
返回 None=放行, 字符串=拒绝原因。"""
|
|
313
|
+
r = client.api('GET', '/%ss/%s' % (kind, item_id))
|
|
314
|
+
if r.status_code not in (200, 201):
|
|
315
|
+
return None # 查询失败不阻塞 (禅道权限兜底)
|
|
316
|
+
data = r.json()
|
|
317
|
+
asg = ZentaoClient.assignee_account(data)
|
|
318
|
+
allow = (asg == my_account)
|
|
319
|
+
if kind == 'bug' and not allow:
|
|
320
|
+
allow = (ZentaoClient.openedby_account(data) == my_account)
|
|
321
|
+
if not allow:
|
|
322
|
+
return '这条%s指派给 %s, 不是你(%s), 已拒绝改。' % (kind, asg or '空', my_account)
|
|
323
|
+
return None
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
# 禅道写操作的 HTTP 调度表: action -> (method, path_tpl, payload_builder, ok_msg)
|
|
327
|
+
# payload_builder(args_dict) -> payload_dict
|
|
328
|
+
def _do_action(client, my_account, action, item_id, params):
|
|
329
|
+
"""执行单个写操作。返回 (result_dict, exit_code)。"""
|
|
330
|
+
developer = get_developer()
|
|
331
|
+
my_account = my_account or (_resolve_my_account(developer)[0] if developer else None)
|
|
332
|
+
if not my_account:
|
|
333
|
+
return {'ok': False, 'error': '未识别禅道身份, 无法校验归属。请配 member.json 的 zentao_account。'}, 3
|
|
334
|
+
|
|
335
|
+
# action 分派
|
|
336
|
+
ACTIONS = {
|
|
337
|
+
# 任务类 (kind=task)
|
|
338
|
+
'start_task': ('task', 'POST', '/tasks/%s/start', {'left': 'left'}),
|
|
339
|
+
'pause_task': ('task', 'POST', '/tasks/%s/pause', {}),
|
|
340
|
+
'finish_task': ('task', 'POST', '/tasks/%s/finish', {'left': 'left', 'consumed': 'consumed'}),
|
|
341
|
+
'log_effort': ('task', 'POST', '/tasks/%s/estimate', {'consumed': 'consumed', 'left': 'left'}),
|
|
342
|
+
# Bug 类 (kind=bug)
|
|
343
|
+
'confirm_bug': ('bug', 'POST', '/bugs/%s/confirm', {}),
|
|
344
|
+
'resolve_bug': ('bug', 'POST', '/bugs/%s/resolve', {'resolution': 'resolution', 'comment': 'comment'}),
|
|
345
|
+
}
|
|
346
|
+
if action not in ACTIONS:
|
|
347
|
+
return {'ok': False, 'error': '未知操作: %s。支持: %s' % (action, ', '.join(ACTIONS.keys()))}, 3
|
|
348
|
+
|
|
349
|
+
kind, method, path_tpl, fields = ACTIONS[action]
|
|
350
|
+
path = path_tpl % item_id
|
|
351
|
+
|
|
352
|
+
# ① 归属校验 (防误改队友)
|
|
353
|
+
guard = _check_owner(client, my_account, kind, item_id)
|
|
354
|
+
if guard:
|
|
355
|
+
return {'ok': False, 'error': guard}, 4
|
|
356
|
+
|
|
357
|
+
# ② 拼 payload (只取该操作关心的字段, 缺省给合理默认)
|
|
358
|
+
payload = {}
|
|
359
|
+
for pkey, fkey in fields.items():
|
|
360
|
+
v = params.get(fkey)
|
|
361
|
+
if v is not None and v != '':
|
|
362
|
+
try:
|
|
363
|
+
payload[pkey] = float(v) if fkey in ('left', 'consumed') else v
|
|
364
|
+
except (ValueError, TypeError):
|
|
365
|
+
payload[pkey] = v
|
|
366
|
+
# finish 强制 left=0 (完成=没剩余工时); log_effort 的 consumed 必填兜底
|
|
367
|
+
if action == 'finish_task' and 'left' not in payload:
|
|
368
|
+
payload['left'] = 0
|
|
369
|
+
if action == 'resolve_bug' and 'resolution' not in payload:
|
|
370
|
+
payload['resolution'] = 'fixed'
|
|
371
|
+
|
|
372
|
+
# ③ 调禅道
|
|
373
|
+
r = client.api(method, path, payload)
|
|
374
|
+
if r.status_code in (200, 201):
|
|
375
|
+
label = {
|
|
376
|
+
'start_task': '已开始', 'pause_task': '已暂停', 'finish_task': '已完成',
|
|
377
|
+
'log_effort': '已记工时', 'confirm_bug': '已确认', 'resolve_bug': '已解决',
|
|
378
|
+
}[action]
|
|
379
|
+
return {'ok': True, 'data': {'action': action, 'id': item_id, 'message': '%s #%s %s' % (kind, item_id, label)}}, 0
|
|
380
|
+
return {'ok': False, 'error': '操作失败 (HTTP %s): %s。可能权限不足或当前状态不允许。' % (
|
|
381
|
+
r.status_code, ZentaoClient.err_text(r))}, 5
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def perform_action(action, item_id, params=None):
|
|
385
|
+
"""写操作入口。返回 (result_dict, exit_code)。"""
|
|
386
|
+
params = params or {}
|
|
387
|
+
client = _zentao_creds()
|
|
388
|
+
if client is None:
|
|
389
|
+
return {'ok': False, 'error': '禅道凭据未配置。'}, 3
|
|
390
|
+
ok, msg = client.credentials_ok()
|
|
391
|
+
if not ok:
|
|
392
|
+
return {'ok': False, 'error': '禅道凭据不完整: %s' % msg}, 3
|
|
393
|
+
ok, msg = client.check_intranet()
|
|
394
|
+
if not ok:
|
|
395
|
+
return {'ok': False, 'error': '连不上禅道内网: %s' % msg}, 5
|
|
396
|
+
developer = get_developer()
|
|
397
|
+
my_account, err = _resolve_my_account(developer) if developer else (None, '未识别开发者身份')
|
|
398
|
+
if err and not my_account:
|
|
399
|
+
return {'ok': False, 'error': err}, 3
|
|
400
|
+
return _do_action(client, my_account, action, item_id, params)
|
|
401
|
+
|
|
402
|
+
|
|
303
403
|
def main():
|
|
304
404
|
parser = argparse.ArgumentParser(description='禅道任务面板数据接口 (输出 JSON 供 VS Code 扩展消费)')
|
|
305
405
|
parser.add_argument('--json', action='store_true', help='JSON 输出 (面板默认用这个)')
|
|
@@ -307,21 +407,41 @@ def main():
|
|
|
307
407
|
parser.add_argument('--kind', choices=['tasks', 'bugs', 'stories'],
|
|
308
408
|
help='只取某一域 (默认三域全取)')
|
|
309
409
|
parser.add_argument('--pretty', action='store_true', help='JSON 美化缩进 (默认紧凑, 面板解析更快)')
|
|
410
|
+
# 写操作子命令 (v0.3)
|
|
411
|
+
parser.add_argument('--action', choices=['start_task', 'pause_task', 'finish_task',
|
|
412
|
+
'log_effort', 'confirm_bug', 'resolve_bug'],
|
|
413
|
+
help='执行禅道写操作 (面板按钮触发)')
|
|
414
|
+
parser.add_argument('--id', type=int, help='写操作的目标 task/bug ID')
|
|
415
|
+
parser.add_argument('--left', help='剩余工时 (start/finish/log_effort)')
|
|
416
|
+
parser.add_argument('--consumed', help='消耗工时 (finish/log_effort)')
|
|
417
|
+
parser.add_argument('--resolution', help='Bug 解决方式 (resolve_bug): fixed/bydesign/duplicate/notrepro/postpone')
|
|
418
|
+
parser.add_argument('--comment', help='Bug 解决备注 (resolve_bug)')
|
|
310
419
|
args = parser.parse_args()
|
|
311
420
|
|
|
312
|
-
|
|
421
|
+
# 写操作分支
|
|
422
|
+
if args.action:
|
|
423
|
+
if not args.id:
|
|
424
|
+
result = {'ok': False, 'error': '写操作必须提供 --id'}
|
|
425
|
+
code = 3
|
|
426
|
+
else:
|
|
427
|
+
params = {'left': args.left, 'consumed': args.consumed,
|
|
428
|
+
'resolution': args.resolution, 'comment': args.comment}
|
|
429
|
+
result, code = perform_action(args.action, args.id, params)
|
|
430
|
+
else:
|
|
431
|
+
result, code = fetch_workbench(include_closed=args.include_closed, kind=args.kind)
|
|
313
432
|
|
|
314
433
|
if args.json or not sys.stdout.isatty():
|
|
315
|
-
# 面板/管道场景: 一律 JSON (成功/失败统一结构, 扩展端好解析)
|
|
316
434
|
indent = 2 if args.pretty else None
|
|
317
435
|
print(json.dumps(result, ensure_ascii=False, default=str, indent=indent))
|
|
318
436
|
else:
|
|
319
|
-
# 人类可读 (终端直接跑时, 兜底; 正常都用 --json)
|
|
320
437
|
if result.get('ok'):
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
m['
|
|
438
|
+
if args.action:
|
|
439
|
+
print('✅ %s' % result['data'].get('message', '操作成功'))
|
|
440
|
+
else:
|
|
441
|
+
d, m = result['data'], result['meta']
|
|
442
|
+
print('📋 %s 的禅道工作台 — 任务 %d · 需求 %d · Bug %d (更新于 %s)' % (
|
|
443
|
+
d['account'], m['counts']['tasks'], m['counts']['stories'], m['counts']['bugs'],
|
|
444
|
+
m['fetched_at']))
|
|
325
445
|
else:
|
|
326
446
|
print('❌ %s' % result.get('error', '未知错误'))
|
|
327
447
|
sys.exit(code)
|